/** * Transforms content to be mobile friendly version. * Filters out various elements and runs the MobileFormatter. * @param OutputPage $out * @param string $mode mobile mode, i.e. stable or beta * * @return string */ public static function DOMParse(OutputPage $out, $text = null, $isBeta = false) { $html = $text ? $text : $out->getHTML(); $context = MobileContext::singleton(); $formatter = MobileFormatter::newFromContext($context, $html); Hooks::run('MobileFrontendBeforeDOM', array($context, $formatter)); $title = $out->getTitle(); $isSpecialPage = $title->isSpecialPage(); $formatter->enableExpandableSections($out->canUseWikiPage() && $out->getWikiPage()->getContentModel() == CONTENT_MODEL_WIKITEXT && array_search($title->getNamespace(), $context->getMFConfig()->get('MFNamespacesWithoutCollapsibleSections')) === false && $context->getRequest()->getText('action', 'view') == 'view'); if ($context->getContentTransformations()) { // Remove images if they're disabled from special pages, but don't transform otherwise $formatter->filterContent(!$isSpecialPage); } $contentHtml = $formatter->getText(); // If the page is a user page which has not been created, then let the // user know about it with pretty graphics and different texts depending // on whether the user is the owner of the page or not. if ($isBeta && $title->inNamespace(NS_USER) && !$title->isSubpage()) { $pageUserId = User::idFromName($title->getText()); if ($pageUserId && !$title->exists()) { $pageUser = User::newFromId($pageUserId); $contentHtml = ExtMobileFrontend::getUserPageContent($out, $pageUser); } } return $contentHtml; }
/** * initialize various variables and generate the template * @return QuickTemplate */ protected function prepareQuickTemplate() { $appleTouchIcon = $this->getConfig()->get('AppleTouchIcon'); $out = $this->getOutput(); // add head items if ($appleTouchIcon !== false) { $out->addHeadItem('touchicon', Html::element('link', array('rel' => 'apple-touch-icon', 'href' => $appleTouchIcon))); } $out->addHeadItem('viewport', Html::element('meta', array('name' => 'viewport', 'content' => 'initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, ' . 'maximum-scale=5.0, width=device-width'))); if ($this->isMobileMode) { // Customize page content for mobile view, e.g. add togglable sections, filter // out various elements. // We do this before executing parent::prepareQuickTemplate() since the parent // overwrites $out->mBodytext, adding an mw-content-text div which is // redundant to our own content div. By defining the bodytext HTML before // $out->mBodytext is overwritten, we avoid including the mw-content-text div. // FIXME: Git rid of our content div and consolidate this line with the other // isMobileMode lines below. This will bring us more in line with core DOM. $html = ExtMobileFrontend::DOMParse($out); } // Generate skin template $tpl = parent::prepareQuickTemplate(); // Set whether or not the page content should be wrapped in div.content (for // example, on a special page) $tpl->set('unstyledContent', $out->getProperty('unstyledContent')); // Set the links for the main menu $tpl->set('menu_data', $this->getMenuData()); // Set the links for page secondary actions $tpl->set('secondary_actions', $this->getSecondaryActions($tpl)); // Construct various Minerva-specific interface elements $this->preparePageContent($tpl); $this->prepareHeaderAndFooter($tpl); $this->prepareMenuButton($tpl); $this->prepareBanners($tpl); $this->prepareWarnings($tpl); $this->preparePageActions($tpl); $this->prepareUserButton($tpl); $this->prepareLanguages($tpl); // Perform a few extra changes if we are in mobile mode if ($this->isMobileMode) { // Set our own bodytext that has been filtered by MobileFormatter $tpl->set('bodytext', $html); // Construct mobile-friendly footer $this->prepareMobileFooterLinks($tpl); } return $tpl; }
/** * OutputPageParserOutput hook handler * Disables TOC in output before it grabs HTML * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput * * @param OutputPage $outputPage * @param ParserOutput $po * @return bool */ public static function onOutputPageParserOutput($outputPage, ParserOutput $po) { global $wgMFWikibaseImageCategory; $context = MobileContext::singleton(); $isBeta = $context->isBetaGroupMember(); $mfUseWikibaseDescription = $context->getMFConfig()->get('MFUseWikibaseDescription'); if ($context->shouldDisplayMobileView()) { $outputPage->enableTOC(false); $outputPage->setProperty('MinervaTOC', $po->getTOCHTML() !== ''); if ($mfUseWikibaseDescription && $isBeta) { $item = $po->getProperty('wikibase_item'); if ($item) { $desc = ExtMobileFrontend::getWikibaseDescription($item); $category = ExtMobileFrontend::getWikibasePropertyValue($item, $wgMFWikibaseImageCategory); if ($desc) { $outputPage->setProperty('wgMFDescription', $desc); } if ($category) { $outputPage->setProperty('wgMFImagesCategory', $category); } } } // Enable wrapped sections $po->setText(ExtMobileFrontend::DOMParse($outputPage, $po->getText(), $isBeta)); } return true; }
/** * Saves the settings submitted by the settings form. Redirects the user to the destination * of returnto or, if not set, back to this special page */ private function submitSettingsForm() { $schema = 'MobileOptionsTracking'; $schemaRevision = 14003392; $schemaData = array('action' => 'success', 'images' => "nochange", 'beta' => "nochange"); $context = MobileContext::singleton(); $request = $this->getRequest(); $user = $this->getUser(); if ($user->isLoggedIn() && !$user->matchEditToken($request->getVal('token'))) { $errorText = __METHOD__ . '(): token mismatch'; wfIncrStats('mobile.options.errors'); wfDebugLog('mobile', $errorText); $this->getOutput()->addHTML('<div class="error">' . $this->msg("mobile-frontend-save-error")->parse() . '</div>'); $schemaData['action'] = 'error'; $schemaData['errorText'] = $errorText; ExtMobileFrontend::eventLog($schema, $schemaRevision, $schemaData); $this->getSettingsForm(); return; } wfIncrStats('mobile.options.saves'); if ($request->getBool('enableBeta')) { $group = 'beta'; if (!$context->isBetaGroupMember()) { // The request was to turn on beta $schemaData['beta'] = "on"; } } else { $group = ''; if ($context->isBetaGroupMember()) { // beta was turned off $schemaData['beta'] = "off"; } } $context->setMobileMode($group); $imagesDisabled = !$request->getBool('enableImages'); if ($context->imagesDisabled() !== $imagesDisabled) { // Only record when the state has changed $schemaData['images'] = $imagesDisabled ? "off" : "on"; } $context->setDisableImagesCookie($imagesDisabled); $returnToTitle = Title::newFromText($request->getText('returnto')); if ($returnToTitle) { $url = $returnToTitle->getFullURL(); } else { $url = $this->getPageTitle()->getFullURL('success'); } ExtMobileFrontend::eventLog($schema, $schemaRevision, $schemaData); $context->getOutput()->redirect(MobileContext::singleton()->getMobileUrl($url)); }
/** * OutputPageParserOutput hook handler * Disables TOC in output before it grabs HTML * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput * * @param OutputPage $outputPage * @param ParserOutput $po * @return bool */ public static function onOutputPageParserOutput($outputPage, ParserOutput $po) { $context = MobileContext::singleton(); $mfUseWikibaseDescription = $context->getMFConfig()->get('MFUseWikibaseDescription'); if ($context->shouldDisplayMobileView()) { $outputPage->enableTOC(false); $outputPage->setProperty('MinervaTOC', $po->getTOCHTML() !== ''); if ($mfUseWikibaseDescription && $context->isBetaGroupMember()) { $item = $po->getProperty('wikibase_item'); if ($item) { $desc = ExtMobileFrontend::getWikibaseDescription($item); if ($desc) { $outputPage->setProperty('wgMFDescription', $desc); } } } } return true; }
/** * @param $html string * @return string */ public function DOMParse($html) { global $wgScript; wfProfileIn(__METHOD__); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); libxml_use_internal_errors(true); $this->doc = new DOMDocument(); $this->doc->loadHTML('<?xml encoding="UTF-8">' . $html); libxml_use_internal_errors(false); $this->doc->preserveWhiteSpace = false; $this->doc->strictErrorChecking = false; $this->doc->encoding = 'UTF-8'; $itemToRemoveRecords = $this->parseItemsToRemove(); $zeroRatedBannerElement = $this->doc->getElementById('zero-rated-banner'); if (!$zeroRatedBannerElement) { $zeroRatedBannerElement = $this->doc->getElementById('zero-rated-banner-red'); } if ($zeroRatedBannerElement) { self::$zeroRatedBanner = $this->doc->saveXML($zeroRatedBannerElement, LIBXML_NOEMPTYTAG); } if (self::$isBetaGroupMember) { $ptLogout = $this->doc->getElementById('pt-logout'); if ($ptLogout) { $ptLogoutLink = $ptLogout->firstChild; self::$logoutHtml = $this->doc->saveXML($ptLogoutLink, LIBXML_NOEMPTYTAG); } $ptAnonLogin = $this->doc->getElementById('pt-anonlogin'); if (!$ptAnonLogin) { $ptAnonLogin = $this->doc->getElementById('pt-login'); } if ($ptAnonLogin) { $ptAnonLoginLink = $ptAnonLogin->firstChild; if ($ptAnonLoginLink && $ptAnonLoginLink->hasAttributes()) { $ptAnonLoginLinkHref = $ptAnonLoginLink->getAttributeNode('href'); $ptAnonLoginLinkTitle = $ptAnonLoginLink->getAttributeNode('title'); if ($ptAnonLoginLinkTitle) { $ptAnonLoginLinkTitle->nodeValue = self::$messages['mobile-frontend-login']; } if ($ptAnonLoginLinkHref) { $ptAnonLoginLinkHref->nodeValue = str_replace("&", "&", $ptAnonLoginLinkHref->nodeValue); } $ptAnonLoginLinkText = $ptAnonLoginLink->firstChild; if ($ptAnonLoginLinkText) { $ptAnonLoginLinkText->nodeValue = self::$messages['mobile-frontend-login']; } } self::$loginHtml = $this->doc->saveXML($ptAnonLoginLink, LIBXML_NOEMPTYTAG); } } if (self::$title->isSpecial('Userlogin') && self::$isBetaGroupMember) { $userlogin = $this->doc->getElementById('userloginForm'); if ($userlogin && get_class($userlogin) === 'DOMElement') { $firstHeading = $this->doc->getElementById('firstHeading'); if ($firstHeading) { $firstHeading->nodeValue = ''; } } } // Tags // You can't remove DOMNodes from a DOMNodeList as you're iterating // over them in a foreach loop. It will seemingly leave the internal // iterator on the foreach out of wack and results will be quite // strange. Though, making a queue of items to remove seems to work. // For example: if (self::$disableImages == 1) { $itemToRemoveRecords['TAG'][] = "img"; $itemToRemoveRecords['TAG'][] = "audio"; $itemToRemoveRecords['TAG'][] = "video"; $itemToRemoveRecords['CLASS'][] = "thumb tright"; $itemToRemoveRecords['CLASS'][] = "thumb tleft"; $itemToRemoveRecords['CLASS'][] = "thumbcaption"; $itemToRemoveRecords['CLASS'][] = "gallery"; } $tagToRemoveNodeIdAttributeValues = array('zero-language-search'); $domElemsToRemove = array(); foreach ($itemToRemoveRecords['TAG'] as $tagToRemove) { $tagToRemoveNodes = $this->doc->getElementsByTagName($tagToRemove); foreach ($tagToRemoveNodes as $tagToRemoveNode) { $tagToRemoveNodeIdAttributeValue = ''; if ($tagToRemoveNode) { $tagToRemoveNodeIdAttribute = $tagToRemoveNode->getAttributeNode('id'); if ($tagToRemoveNodeIdAttribute) { $tagToRemoveNodeIdAttributeValue = $tagToRemoveNodeIdAttribute->value; } if (!in_array($tagToRemoveNodeIdAttributeValue, $tagToRemoveNodeIdAttributeValues)) { $domElemsToRemove[] = $tagToRemoveNode; } } } } foreach ($domElemsToRemove as $domElement) { $domElement->parentNode->removeChild($domElement); } // Elements with named IDs foreach ($itemToRemoveRecords['ID'] as $itemToRemove) { $itemToRemoveNode = $this->doc->getElementById($itemToRemove); if ($itemToRemoveNode) { $itemToRemoveNode->parentNode->removeChild($itemToRemoveNode); } } // CSS Classes $xpath = new DOMXpath($this->doc); foreach ($itemToRemoveRecords['CLASS'] as $classToRemove) { $elements = $xpath->query('//*[@class="' . $classToRemove . '"]'); foreach ($elements as $element) { $element->parentNode->removeChild($element); } } // Tags with CSS Classes foreach ($itemToRemoveRecords['TAG_CLASS'] as $classToRemove) { $parts = explode('.', $classToRemove); $elements = $xpath->query('//' . $parts[0] . '[@class="' . $parts[1] . '"]'); foreach ($elements as $element) { $removedElement = $element->parentNode->removeChild($element); } } // Handle red links with action equal to edit $redLinks = $xpath->query('//a[@class="new"]'); foreach ($redLinks as $redLink) { // PHP Bug #36795 — Inappropriate "unterminated entity reference" $spanNode = $this->doc->createElement("span", str_replace("&", "&", $redLink->nodeValue)); if ($redLink->hasAttributes()) { $attributes = $redLink->attributes; foreach ($attributes as $i => $attribute) { if ($attribute->name != 'href') { $spanNode->setAttribute($attribute->name, $attribute->value); } } } $redLink->parentNode->replaceChild($spanNode, $redLink); } if (self::$title->isSpecial('Userlogin') && self::$isBetaGroupMember) { if ($userlogin && get_class($userlogin) === 'DOMElement') { $login = $this->renderLogin(); $loginNode = $this->doc->importNode($login, true); $userlogin->appendChild($loginNode); } } $content = $this->doc->getElementById('content'); $contentHtml = $this->doc->saveXML($content, LIBXML_NOEMPTYTAG); if (self::$isMainPage) { $contentHtml = $this->DOMParseMainPage($contentHtml); } $title = htmlspecialchars(self::$title->getText()); $htmlTitle = htmlspecialchars(self::$htmlTitle); if (strlen($contentHtml) > 4000 && $this->contentFormat == 'XHTML' && self::$device['supports_javascript'] === true && empty(self::$search) && !self::$isMainPage) { $contentHtml = $this->headingTransform($contentHtml); } elseif ($this->contentFormat == 'WML') { header('Content-Type: text/vnd.wap.wml'); $contentHtml = $this->headingTransform($contentHtml); // Content removal for WML rendering $elements = array('span', 'div', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'sup', 'sub'); foreach ($elements as $element) { $contentHtml = preg_replace('#</?' . $element . '[^>]*>#is', '', $contentHtml); } // Wml for searching $searchWml = '<p><input emptyok="true" format="*M" type="text" name="search" value="" size="16" />' . '<do type="accept" label="' . self::$messages['mobile-frontend-search-submit'] . '">' . '<go href="' . $wgScript . '?title=Special%3ASearch&search=$(search)"></go></do></p>'; $contentHtml = $searchWml . $contentHtml; // Content wrapping $contentHtml = $this->createWMLCard($contentHtml); $applicationWmlTemplate = new ApplicationWmlTemplate(); $options = array('mainPageUrl' => self::$mainPageUrl, 'randomPageUrl' => self::$randomPageUrl, 'dir' => self::$dir, 'code' => self::$code, 'contentHtml' => $contentHtml, 'homeButton' => self::$messages['mobile-frontend-home-button'], 'randomButton' => self::$messages['mobile-frontend-random-button']); $applicationWmlTemplate->setByArray($options); $applicationHtml = $applicationWmlTemplate->getHTML(); } if ($this->contentFormat == 'XHTML' && self::$format != 'json') { if (!empty(self::$displayNoticeId)) { if (intval(self::$displayNoticeId) === 1) { $thanksNoticeTemplate = new ThanksNoticeTemplate(); $thanksNoticeTemplate->set('messages', self::$messages); $noticeHtml = $thanksNoticeTemplate->getHTML(); } } // header( 'Content-Type: application/xhtml+xml; charset=utf-8' ); $searchTemplate = $this->getSearchTemplate(); $searchWebkitHtml = $searchTemplate->getHTML(); $footerTemplate = $this->getFooterTemplate(); $footerHtml = $footerTemplate->getHTML(); $noticeHtml = !empty($noticeHtml) ? $noticeHtml : ''; $applicationTemplate = $this->getApplicationTemplate(); $options = array('noticeHtml' => $noticeHtml, 'htmlTitle' => $htmlTitle, 'searchWebkitHtml' => $searchWebkitHtml, 'contentHtml' => $contentHtml, 'footerHtml' => $footerHtml); $applicationTemplate->setByArray($options); $applicationHtml = $applicationTemplate->getHTML(); } if (self::$format === 'json') { header('Content-Type: application/javascript'); header('Content-Disposition: attachment; filename="data.js";'); $json_data = array(); $json_data['title'] = htmlspecialchars(self::$title->getText()); $json_data['html'] = $contentHtml; $json = FormatJson::encode($json_data); if (!empty(self::$callback)) { $json = urlencode(htmlspecialchars(self::$callback)) . '(' . $json . ')'; } wfProfileOut(__METHOD__); return $json; } wfProfileOut(__METHOD__); return $applicationHtml; }
/** * Execute the requested Api actions. * @todo: Write some unit tests for API results */ public function execute() { // Logged-in users' parser options depend on preferences $this->getMain()->setCacheMode('anon-public-user-private'); // Enough '*' keys in JSON!!! $isXml = $this->getMain()->isInternalMode() || $this->getMain()->getPrinter()->getFormat() == 'XML'; $textElement = $isXml ? '*' : 'text'; $params = $this->extractRequestParams(); $prop = array_flip($params['prop']); $sectionProp = array_flip($params['sectionprop']); $this->variant = $params['variant']; $this->followRedirects = $params['redirect'] == 'yes'; $this->noHeadings = $params['noheadings']; $this->noTransform = $params['notransform']; $onlyRequestedSections = $params['onlyrequestedsections']; $this->offset = $params['offset']; $this->maxlen = $params['maxlen']; if ($this->offset === 0 && $this->maxlen === 0) { $this->offset = -1; // Disable text splitting } elseif ($this->maxlen === 0) { $this->maxlen = PHP_INT_MAX; } $title = $this->makeTitle($params['page']); // See whether the actual page (or if enabled, the redirect target) is the main page $this->mainPage = $this->isMainPage($title); if ($this->mainPage && $this->noHeadings) { $this->noHeadings = false; $this->setWarning("``noheadings'' makes no sense on the main page, ignoring"); } if (isset($prop['normalizedtitle']) && $title->getPrefixedText() != $params['page']) { $this->getResult()->addValue(null, $this->getModuleName(), array('normalizedtitle' => $title->getPageLanguage()->convert($title->getPrefixedText()))); } $data = $this->getData($title, $params['noimages']); // Bug 73109: #getData will return an empty array if the title redirects to // a page in a virtual namespace (NS_SPECIAL, NS_MEDIA), so make sure that // the requested data exists too. if (isset($prop['lastmodified']) && isset($data['lastmodified'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('lastmodified' => $data['lastmodified'])); } if (isset($prop['lastmodifiedby']) && isset($data['lastmodifiedby'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('lastmodifiedby' => $data['lastmodifiedby'])); } if (isset($prop['revision']) && isset($data['revision'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('revision' => $data['revision'])); } if (isset($prop['id']) && isset($data['id'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('id' => $data['id'])); } if (isset($prop['languagecount']) && isset($data['languagecount'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('languagecount' => $data['languagecount'])); } if (isset($prop['hasvariants']) && isset($data['hasvariants'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('hasvariants' => $data['hasvariants'])); } if (isset($prop['displaytitle']) && isset($data['displaytitle'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('displaytitle' => $data['displaytitle'])); } if (isset($prop['pageprops'])) { $propNames = $params['pageprops']; if ($propNames == '*' && isset($data['pageprops'])) { $pageProps = $data['pageprops']; } else { $propNames = explode('|', $propNames); $pageProps = array_intersect_key($data['pageprops'], array_flip($propNames)); } $this->getResult()->addValue(null, $this->getModuleName(), array('pageprops' => $pageProps)); } if (isset($prop['description']) && isset($data['pageprops']['wikibase_item'])) { $desc = ExtMobileFrontend::getWikibaseDescription($data['pageprops']['wikibase_item']); if ($desc) { $this->getResult()->addValue(null, $this->getModuleName(), array('description' => $desc)); } } if ($this->usePageImages) { $this->addPageImage($data, $params, $prop); } $result = array(); $missingSections = array(); if ($this->mainPage) { if ($onlyRequestedSections) { $requestedSections = self::parseSections($params['sections'], $data, $missingSections); } else { $requestedSections = array(0); } $this->getResult()->addValue(null, $this->getModuleName(), array('mainpage' => '')); } elseif (isset($params['sections'])) { $requestedSections = self::parseSections($params['sections'], $data, $missingSections); } else { $requestedSections = array(); } if (isset($data['sections'])) { if (isset($prop['sections'])) { $sectionCount = count($data['sections']); for ($i = 0; $i <= $sectionCount; $i++) { if (!isset($requestedSections[$i]) && $onlyRequestedSections) { continue; } $section = array(); if ($i > 0) { $section = array_intersect_key($data['sections'][$i - 1], $sectionProp); } $section['id'] = $i; if (isset($prop['text']) && isset($requestedSections[$i]) && isset($data['text'][$i])) { $section[$textElement] = $this->stringSplitter($this->prepareSection($data['text'][$i])); unset($requestedSections[$i]); } if (isset($data['refsections'][$i])) { $section['references'] = ''; } $result[] = $section; } $missingSections = array_keys($requestedSections); } else { foreach (array_keys($requestedSections) as $index) { $section = array('id' => $index); if (isset($data['text'][$index])) { $section[$textElement] = $this->stringSplitter($this->prepareSection($data['text'][$index])); } else { $missingSections[] = $index; } $result[] = $section; } } $this->getResult()->setIndexedTagName($result, 'section'); $this->getResult()->addValue(null, $this->getModuleName(), array('sections' => $result)); } if (isset($prop['protection'])) { $this->addProtection($title); } if (isset($prop['editable'])) { $user = $this->getUser(); if ($user->isAnon()) { // HACK: Anons receive cached information, so don't check blocked status for them // to avoid them receiving false positives. Currently there is no way to check // all permissions except blocked status from the Title class. $req = new FauxRequest(); $req->setIP('127.0.0.1'); $user = User::newFromSession($req); } $editable = $title->quickUserCan('edit', $user); if ($isXml) { $editable = intval($editable); } $this->getResult()->addValue(null, $this->getModuleName(), array('editable' => $editable)); } // https://bugzilla.wikimedia.org/show_bug.cgi?id=51586 // Inform ppl if the page is infested with LiquidThreads but that's the // only thing we support about it. if (class_exists('LqtDispatch') && LqtDispatch::isLqtPage($title)) { $this->getResult()->addValue(null, $this->getModuleName(), array('liquidthreads' => '')); } if (count($missingSections) && isset($prop['text'])) { $this->setWarning('Section(s) ' . implode(', ', $missingSections) . ' not found'); } if ($this->maxlen < 0) { // There is more data available $this->getResult()->addValue(null, $this->getModuleName(), array('continue-offset' => $params['offset'] + $params['maxlen'])); } }