/**
  * 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("&", "&amp;", $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("&", "&amp;", $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&amp;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']));
     }
 }