%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/includes/api/ |
Current File : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/includes/api/ApiMobileView.php |
<?php /** * Extends Api of MediaWiki with actions for mobile devices. For further information see * https://www.mediawiki.org/wiki/Extension:MobileFrontend#API */ class ApiMobileView extends ApiBase { /** * Increment this when changing the format of cached data */ const CACHE_VERSION = 9; /** @var boolean Saves whether redirects has to be followed or not */ private $followRedirects; /** @var boolean Saves whether sections have the header name or not */ private $noHeadings; /** @var boolean Saves whether the requested page is the main page */ private $mainPage; /** @var boolean Saves whether the output is formatted or not */ private $noTransform; /** @var boolean Saves whether page images should be added or not */ protected $usePageImages; /** @var string Saves in which language the content should be output */ private $variant; /** @var Integer Saves at which character the section content start at */ private $offset; /** @var Integer Saves value to specify the max length of a sections content */ private $maxlen; /** @var file|boolean Saves a File Object, or false if no file exist */ private $file; /** * Run constructor of ApiBase * @param ApiMain $main Instance of class ApiMain * @param string $action Name of this module */ public function __construct( $main, $action ) { $this->usePageImages = ExtensionRegistry::getInstance()->isLoaded( 'PageImages' ); parent::__construct( $main, $action ); } /** * Obtain the requested page properties. * @param string $propNames requested list of pageprops separated by '|'. If '*' * all page props will be returned. * @param array $data data available as returned by getData * @return Array associative */ public function getMobileViewPageProps( $propNames, $data ) { if ( array_key_exists( 'pageprops', $data ) ) { if ( $propNames == '*' ) { $pageProps = $data['pageprops']; } else { $pageProps = array_intersect_key( $data['pageprops'], array_flip( explode( '|', $propNames ) ) ); } } else { $pageProps = []; } return $pageProps; } /** * 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' ); // Don't strip srcset on renderings for mobileview api; the // app below it will decide how to use them. MobileContext::singleton()->setStripResponsiveImages( false ); // 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']; $resultObj = $this->getResult(); $moduleName = $this->getModuleName(); if ( $this->offset === 0 && $this->maxlen === 0 ) { // Disable text splitting $this->offset = -1; } elseif ( $this->maxlen === 0 ) { $this->maxlen = PHP_INT_MAX; } $title = $this->makeTitle( $params['page'] ); RequestContext::getMain()->setTitle( $title ); $namespace = $title->getNamespace(); $this->addXAnalyticsItem( 'ns', (string)$namespace ); // 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->addWarning( 'apiwarn-mobilefrontend-ignoringnoheadings', 'ignoringnoheadings' ); } if ( isset( $prop['normalizedtitle'] ) && $title->getPrefixedText() != $params['page'] ) { $resultObj->addValue( null, $moduleName, [ 'normalizedtitle' => $title->getPageLanguage()->convert( $title->getPrefixedText() ) ] ); } if ( isset( $prop['namespace'] ) ) { $resultObj->addValue( null, $moduleName, [ 'ns' => $namespace, ] ); } $data = $this->getData( $title, $params['noimages'], $params['revision'] ); $plainData = [ 'lastmodified', 'lastmodifiedby', 'revision', 'languagecount', 'hasvariants', 'displaytitle', 'id', 'contentmodel' ]; foreach ( $plainData as $name ) { // 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[$name] ) && isset( $data[$name] ) ) { $resultObj->addValue( null, $moduleName, [ $name => $data[$name] ] ); } } if ( isset( $data['id'] ) ) { $this->addXAnalyticsItem( 'page_id', (string)$data['id'] ); } if ( isset( $prop['pageprops'] ) ) { $mvPageProps = $this->getMobileViewPageProps( $params['pageprops'], $data ); ApiResult::setArrayType( $mvPageProps, 'assoc' ); $resultObj->addValue( null, $moduleName, [ 'pageprops' => $mvPageProps ] ); } if ( isset( $prop['description'] ) && array_key_exists( 'pageprops', $data ) && is_array( $data['pageprops'] ) ) { $this->addDescriptionToResult( $resultObj, $data['pageprops'], $moduleName ); } if ( $this->usePageImages ) { $this->addPageImage( $data, $params, $prop ); } $result = []; $missingSections = []; if ( $this->mainPage ) { if ( $onlyRequestedSections ) { $requestedSections = self::getRequestedSectionIds( $params['sections'], $data, $missingSections ); } else { $requestedSections = [ 0 ]; } $resultObj->addValue( null, $moduleName, [ 'mainpage' => true ] ); } elseif ( isset( $params['sections'] ) ) { $requestedSections = self::getRequestedSectionIds( $params['sections'], $data, $missingSections ); } else { $requestedSections = []; } 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 = []; 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'] = true; } $result[] = $section; } $missingSections = array_keys( $requestedSections ); } else { foreach ( array_keys( $requestedSections ) as $index ) { $section = [ 'id' => $index ]; if ( isset( $data['text'][$index] ) ) { $section[$textElement] = $this->stringSplitter( $this->prepareSection( $data['text'][$index] ) ); } else { $missingSections[] = $index; } $result[] = $section; } } $resultObj->setIndexedTagName( $result, 'section' ); $resultObj->addValue( null, $moduleName, [ '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 ); } $resultObj->addValue( null, $moduleName, [ 'editable' => $editable, ApiResult::META_BC_BOOLS => [ '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::class ) && \LqtDispatch::isLqtPage( $title ) ) { $resultObj->addValue( null, $moduleName, [ 'liquidthreads' => true ] ); } if ( count( $missingSections ) && isset( $prop['text'] ) ) { $this->addWarning( [ 'apiwarn-mobilefrontend-sectionsnotfound', Message::listParam( $missingSections ), count( $missingSections ) ], 'sectionsnotfound' ); } if ( $this->maxlen < 0 ) { // There is more data available $resultObj->addValue( null, $moduleName, [ 'continue-offset' => $params['offset'] + $params['maxlen'] ] ); } } /** * Adds the short description of the page to the result if available. * @param ApiResult $resultObj API result object * @param array $pageprops page props * @param string $moduleName name of the module being executed by this instance */ private function addDescriptionToResult( ApiResult $resultObj, array $pageprops, $moduleName ) { if ( array_key_exists( 'wikibase-shortdesc', $pageprops ) ) { // wikibase-shortdesc is unfortunately the pageprop name for the local description $resultObj->addValue( null, $moduleName, [ 'description' => $pageprops['wikibase-shortdesc'], 'descriptionsource' => 'local', ] ); } elseif ( array_key_exists( 'wikibase_item', $pageprops ) ) { // try wikibase for the central description $desc = ExtMobileFrontend::getWikibaseDescription( $pageprops['wikibase_item'] ); if ( $desc ) { $resultObj->addValue( null, $moduleName, [ 'description' => $desc, 'descriptionsource' => 'central', ] ); } } } /** * Small wrapper around XAnalytics extension * * @see \XAnalytics::addItem * @param string $name * @param string $value */ private function addXAnalyticsItem( $name, $value ) { if ( is_callable( [ \XAnalytics::class, 'addItem' ] ) ) { \XAnalytics::addItem( $name, $value ); } } /** * Creates and validates a title * @param string $name Title content * @return Title */ protected function makeTitle( $name ) { global $wgContLang; $title = Title::newFromText( $name ); if ( !$title ) { $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $name ) ] ); } $unconvertedTitle = $title->getPrefixedText(); $wgContLang->findVariantLink( $name, $title ); if ( $unconvertedTitle !== $title->getPrefixedText() ) { $values = [ 'from' => $unconvertedTitle, 'to' => $title->getPrefixedText() ]; $this->getResult()->addValue( 'mobileview', 'converted', $values ); } if ( $title->inNamespace( NS_FILE ) ) { $this->file = $this->findFile( $title ); } if ( !$title->exists() && !$this->file ) { $this->dieWithError( [ 'apierror-missingtitle' ] ); } return $title; } /** * Wrapper that returns a page image for a given title * * @param Title $title Page title * @return bool|File */ protected function getPageImage( Title $title ) { return PageImages::getPageImage( $title ); } /** * Wrapper for wfFindFile * * @param Title|string $title Page title * @param array $options Options for wfFindFile (see RepoGroup::findFile) * @return bool|File */ protected function findFile( $title, $options = [] ) { return wfFindFile( $title, $options ); } /** * Check if page is the main page after follow redirect when followRedirects is true. * * @param Title $title Title object to check * @return bool */ protected function isMainPage( $title ) { if ( $title->isRedirect() && $this->followRedirects ) { $wikiPage = $this->makeWikiPage( $title ); $target = $wikiPage->getRedirectTarget(); if ( $target ) { return $target->isMainPage(); } } return $title->isMainPage(); } /** * Splits a string (using $offset and $maxlen) * @param string $text The text to split * @return string */ private function stringSplitter( $text ) { if ( $this->offset < 0 ) { // NOOP - string splitting mode is off return $text; } elseif ( $this->maxlen < 0 ) { // Limit exceeded return ''; } $textLen = mb_strlen( $text ); $start = $this->offset; $len = $textLen - $start; if ( $len > 0 ) { // At least part of the $text should be included if ( $len > $this->maxlen ) { $len = $this->maxlen; $this->maxlen = -1; } else { $this->maxlen -= $len; } $this->offset = 0; return mb_substr( $text, $start, $len ); } $this->offset -= $textLen; return ''; } /** * Delete headings from page html * @param string $html Page content * @return string */ private function prepareSection( $html ) { if ( $this->noHeadings ) { $html = preg_replace( '#<(h[1-6])\b.*?<\s*/\s*\\1>#', '', $html ); } return trim( $html ); } /** * Parses requested sections string into a list of sections * @param string $str String to parse * @param array $data Processed parser output * @param array &$missingSections Upon return, contains the list of sections that were * requested but are not present in parser output (passed by reference) * @return array */ public static function getRequestedSectionIds( $str, $data, &$missingSections ) { $str = trim( $str ); if ( !isset( $data['sections'] ) ) { return []; } $sectionCount = count( $data['sections'] ); if ( $str === 'all' ) { return range( 0, $sectionCount ); } $sections = array_map( 'trim', explode( '|', $str ) ); $ret = []; foreach ( $sections as $sec ) { if ( $sec === '' ) { continue; } if ( $sec === 'references' ) { $ret = array_merge( $ret, array_keys( $data['refsections'] ) ); continue; } $val = intval( $sec ); if ( strval( $val ) === $sec ) { if ( $val >= 0 && $val <= $sectionCount ) { $ret[] = $val; continue; } } else { $parts = explode( '-', $sec ); if ( count( $parts ) === 2 ) { $from = intval( $parts[0] ); if ( strval( $from ) === $parts[0] && $from >= 0 && $from <= $sectionCount ) { if ( $parts[1] === '' ) { $ret = array_merge( $ret, range( $from, $sectionCount ) ); continue; } $to = intval( $parts[1] ); if ( strval( $to ) === $parts[1] ) { $ret = array_merge( $ret, range( $from, $to ) ); continue; } } } } $missingSections[] = $sec; } $ret = array_unique( $ret ); sort( $ret ); return array_flip( $ret ); } /** * Performs a page parse * @param WikiPage $wikiPage * @param ParserOptions $parserOptions * @param null|int $oldid Revision ID to get the text from, passing null or 0 will * get the current revision (default value) * @return ParserOutput|bool */ protected function getParserOutput( WikiPage $wikiPage, ParserOptions $parserOptions, $oldid = null ) { $parserOutput = $wikiPage->getParserOutput( $parserOptions, $oldid ); if ( $parserOutput && !defined( 'ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) ) { $parserOutput->setTOCEnabled( false ); } return $parserOutput; } /** * Creates a WikiPage from title * @param Title $title Page title * @return WikiPage */ protected function makeWikiPage( Title $title ) { return WikiPage::factory( $title ); } /** * Call makeParserOptions on a WikiPage with the wrapper output class disabled. * @param WikiPage $wikiPage to call makeParserOptions on. * @return ParserOptions */ protected function makeParserOptions( WikiPage $wikiPage ) { $popt = $wikiPage->makeParserOptions( $this ); return $popt; } /** * Parses section data * @param string $html representing the entire page * @param Title $title Page title * @param ParserOutput $parserOutput * @param int|null $revId this is a temporary parameter to avoid debug log warnings. * Long term the call to wfDebugLog should be moved outside this method (optional) * @return array structure representing the list of sections and their properties: * - refsections: [] where all keys are section ids of sections with refs * that contain references * - sections: [] a structured array of all the sections inside the page * - text: [] of the text of each individual section. length === same as sections * or of length 1 when there is a mismatch. */ protected function parseSectionsData( $html, Title $title, ParserOutput $parserOutput, $revId = null ) { $data = []; $data['sections'] = $parserOutput->getSections(); $sectionCount = count( $data['sections'] ); for ( $i = 0; $i < $sectionCount; $i++ ) { $data['sections'][$i]['line'] = $title->getPageLanguage()->convert( $data['sections'][$i]['line'] ); } $chunks = preg_split( '/<h(?=[1-6]\b)/i', $html ); if ( count( $chunks ) != count( $data['sections'] ) + 1 ) { wfDebugLog( 'mobile', __METHOD__ . "(): mismatching number of " . "sections from parser and split on page {$title->getPrefixedText()}, oldid=$revId" ); // We can't be sure about anything here, return all page HTML as one big section $chunks = [ $html ]; $data['sections'] = []; } $data['text'] = []; $data['refsections'] = []; foreach ( $chunks as $chunk ) { if ( count( $data['text'] ) ) { $chunk = "<h$chunk"; } if ( preg_match( '/<ol\b[^>]*?class="references"/', $chunk ) ) { $data['refsections'][count( $data['text'] )] = true; } $data['text'][] = $chunk; } return $data; } /** * Get data of requested article. * @param Title $title * @param boolean $noImages * @param null|int $oldid Revision ID to get the text from, passing null or 0 will * get the current revision (default value) * @suppress SecurityCheck-XSS (T203490) * @return array */ private function getData( Title $title, $noImages, $oldid = null ) { global $wgMemc; $mfConfig = MobileContext::singleton()->getMFConfig(); $mfMinCachedPageSize = $mfConfig->get( 'MFMinCachedPageSize' ); $mfSpecialCaseMainPage = $mfConfig->get( 'MFSpecialCaseMainPage' ); $result = $this->getResult(); $wikiPage = $this->makeWikiPage( $title ); if ( $this->followRedirects && $wikiPage->isRedirect() ) { $newTitle = $wikiPage->getRedirectTarget(); if ( $newTitle ) { $title = $newTitle; $textTitle = $title->getPrefixedText(); if ( $title->hasFragment() ) { $textTitle .= $title->getFragmentForUrl(); } $result->addValue( null, $this->getModuleName(), [ 'redirected' => $textTitle ] ); if ( $title->getNamespace() < 0 ) { $result->addValue( null, $this->getModuleName(), [ 'viewable' => 'no' ] ); return []; } $wikiPage = $this->makeWikiPage( $title ); } } $latest = $wikiPage->getLatest(); // Use page_touched so template updates invalidate cache $touched = $wikiPage->getTouched(); $revId = $oldid ? $oldid : $title->getLatestRevID(); if ( $this->file ) { $key = $wgMemc->makeKey( 'mf', 'mobileview', self::CACHE_VERSION, $noImages, $touched, $this->noTransform, $this->file->getSha1(), $this->variant ); $cacheExpiry = 3600; } else { if ( !$latest ) { // https://bugzilla.wikimedia.org/show_bug.cgi?id=53378 // Title::exists() above doesn't seem to always catch recently deleted pages $this->dieWithError( [ 'apierror-missingtitle' ] ); } $parserOptions = $this->makeParserOptions( $wikiPage ); $parserCache = \MediaWiki\MediaWikiServices::getInstance()->getParserCache(); $parserCacheKey = $parserCache->getKey( $wikiPage, $parserOptions ); $key = $wgMemc->makeKey( 'mf', 'mobileview', self::CACHE_VERSION, $noImages, $touched, $revId, $this->noTransform, $parserCacheKey ); } $data = $wgMemc->get( $key ); if ( $data ) { wfIncrStats( 'mobile.view.cache-hit' ); return $data; } wfIncrStats( 'mobile.view.cache-miss' ); if ( $this->file ) { $html = $this->getFilePage( $title ); } else { $parserOutput = $this->getParserOutput( $wikiPage, $parserOptions, $oldid ); if ( $parserOutput === false ) { $this->dieWithError( 'apierror-mobilefrontend-badidtitle', 'invalidparams' ); return; } $html = $parserOutput->getText( [ 'allowTOC' => false, 'unwrap' => true, 'deduplicateStyles' => false ] ); $cacheExpiry = $parserOutput->getCacheExpiry(); } if ( !$this->noTransform ) { $mf = new MobileFormatter( MobileFormatter::wrapHTML( $html ), $title ); $mf->setRemoveMedia( $noImages ); $mf->setIsMainPage( $this->mainPage && $mfSpecialCaseMainPage ); $mf->filterContent(); $html = $mf->getText(); } if ( $this->mainPage || $this->file ) { $data = [ 'sections' => [], 'text' => [ $html ], 'refsections' => [], ]; } else { $data = $this->parseSectionsData( $html, $title, $parserOutput, $latest ); if ( $this->usePageImages ) { $image = $this->getPageImage( $title ); if ( $image ) { $data['image'] = $image->getTitle()->getText(); } } } $data['lastmodified'] = wfTimestamp( TS_ISO_8601, $wikiPage->getTimestamp() ); // Page id $data['id'] = $wikiPage->getId(); $user = User::newFromId( $wikiPage->getUser() ); if ( !$user->isAnon() ) { $data['lastmodifiedby'] = [ 'name' => $wikiPage->getUserText(), 'gender' => $user->getOption( 'gender' ), ]; } else { $data['lastmodifiedby'] = null; } $data['revision'] = $revId; if ( isset( $parserOutput ) ) { $languages = $parserOutput->getLanguageLinks(); $data['languagecount'] = count( $languages ); $data['displaytitle'] = $parserOutput->getDisplayTitle(); // @fixme: Does no work for some extension properties that get added in LinksUpdate $data['pageprops'] = $parserOutput->getProperties(); } else { $data['languagecount'] = 0; $data['displaytitle'] = htmlspecialchars( $title->getPrefixedText() ); $data['pageprops'] = []; } $data['contentmodel'] = $title->getContentModel(); if ( $title->getPageLanguage()->hasVariants() ) { $data['hasvariants'] = true; } // Don't store small pages to decrease cache size requirements if ( strlen( $html ) >= $mfMinCachedPageSize ) { // store for the same time as original parser output $wgMemc->set( $key, $data, $cacheExpiry ); } return $data; } /** * Get a Filepage as parsed HTML * @param Title $title * @return string HTML * @suppress SecurityCheck-XSS OutputPage::getHtml is a hack, but safe html */ private function getFilePage( Title $title ) { // HACK: HACK: HACK: $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); $context->setOutput( new OutputPage( $context ) ); $page = new ImagePage( $title ); $page->setContext( $context ); // T123821: Without setting the wiki page on the derivative context, // DerivativeContext#getWikiPage will (eventually) fall back to // RequestContext#getWikiPage. Here, the request context is distinct from the // derivative context and deliberately constructed with a bad title in the prelude // of api.php. $context->setWikiPage( $page->getPage() ); $page->view(); $html = $context->getOutput()->getHTML(); return $html; } /** * Adds Image information to Api result. * @param array $data whatever getData() returned * @param array $params parameters to this API module * @param array $prop prop parameter value */ private function addPageImage( array $data, array $params, array $prop ) { if ( !isset( $prop['image'] ) && !isset( $prop['thumb'] ) ) { return; } if ( !isset( $data['image'] ) ) { return; } if ( isset( $params['thumbsize'] ) && ( isset( $params['thumbwidth'] ) || isset( $params['thumbheight'] ) ) ) { $this->dieWithError( 'apierror-mobilefrontend-toomanysizeparams', 'toomanysizeparams' ); } $file = $this->findFile( $data['image'] ); if ( !$file ) { return; } $result = $this->getResult(); if ( isset( $prop['image'] ) ) { $result->addValue( null, $this->getModuleName(), [ 'image' => [ 'file' => $data['image'], 'width' => $file->getWidth(), 'height' => $file->getHeight(), ] ] ); } if ( isset( $prop['thumb'] ) ) { $resize = []; if ( isset( $params['thumbsize'] ) ) { $resize['width'] = $resize['height'] = $params['thumbsize']; } if ( isset( $params['thumbwidth'] ) ) { $resize['width'] = $params['thumbwidth']; } if ( isset( $params['thumbheight'] ) ) { $resize['height'] = $params['thumbheight']; } if ( isset( $resize['width'] ) && !isset( $resize['height'] ) ) { $resize['height'] = $this->isSVG( $file->getMimeType() ) ? $this->getScaledDimen( $file->getWidth(), $file->getHeight(), $resize['width'] ) : $file->getHeight(); } if ( !isset( $resize['width'] ) && isset( $resize['height'] ) ) { $resize['width'] = $this->isSVG( $file->getMimeType() ) ? $this->getScaledDimen( $file->getHeight(), $file->getWidth(), $resize['height'] ) : $file->getWidth(); } if ( !$resize ) { // Default $resize['width'] = $resize['height'] = 50; } $thumb = $file->transform( $resize ); if ( !$thumb ) { return; } $result->addValue( null, $this->getModuleName(), [ 'thumb' => [ 'url' => $thumb->getUrl(), 'width' => $thumb->getWidth(), 'height' => $thumb->getHeight(), ] ] ); } } /** * When only one dimension is given in a thumbnail request, scale the other proportionally * with respect to the original file dimensions. * * @param int $srcX image width * @param int $srcY image height * @param int $dstX target image width * @return int */ private function getScaledDimen( $srcX, $srcY, $dstX ) { return $srcX === 0 ? 0 : (int)round( $srcY * $dstX / $srcX ); } /** * Verify if mime type is SVG * @param string $typeStr mime type * @return bool */ private function isSVG( $typeStr ) { return strpos( $typeStr, 'image/svg' ) === 0; } /** * Adds protection information to the Api result * @param Title $title */ private function addProtection( Title $title ) { $result = $this->getResult(); $protection = []; ApiResult::setArrayType( $protection, 'assoc' ); foreach ( $title->getRestrictionTypes() as $type ) { $levels = $title->getRestrictions( $type ); if ( $levels ) { $protection[$type] = $levels; ApiResult::setIndexedTagName( $protection[$type], 'level' ); } } $result->addValue( null, $this->getModuleName(), [ 'protection' => $protection ] ); } /** * Get allowed Api parameters. * @return array */ public function getAllowedParams() { $res = [ 'page' => [ ApiBase::PARAM_REQUIRED => true, ], 'redirect' => [ ApiBase::PARAM_TYPE => [ 'yes', 'no' ], ApiBase::PARAM_DFLT => 'yes', ], 'sections' => null, 'prop' => [ ApiBase::PARAM_DFLT => 'text|sections|normalizedtitle', ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => [ 'id', 'text', 'sections', 'normalizedtitle', 'lastmodified', 'lastmodifiedby', 'revision', 'protection', 'editable', 'languagecount', 'hasvariants', 'displaytitle', 'pageprops', 'description', 'contentmodel', 'namespace', ] ], 'sectionprop' => [ ApiBase::PARAM_TYPE => [ 'toclevel', 'level', 'line', 'number', 'index', 'fromtitle', 'anchor', ], ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_DFLT => 'toclevel|line', ], 'pageprops' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_DFLT => 'notoc|noeditsection|wikibase_item' ], 'variant' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_DFLT => '', ], 'noimages' => false, 'noheadings' => false, 'notransform' => false, 'onlyrequestedsections' => false, 'offset' => [ ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_MIN => 0, ApiBase::PARAM_DFLT => 0, ], 'maxlen' => [ ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_MIN => 0, ApiBase::PARAM_DFLT => 0, ], 'revision' => [ ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_MIN => 0, ApiBase::PARAM_DFLT => 0, ], ]; if ( $this->usePageImages ) { $res['prop'][ApiBase::PARAM_TYPE][] = 'image'; $res['prop'][ApiBase::PARAM_TYPE][] = 'thumb'; $res['thumbsize'] = $res['thumbwidth'] = $res['thumbheight'] = [ ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_MIN => 0, ]; } return $res; } /** * Returns usage examples for this module. * @see ApiBase::getExamplesMessages() * @return array */ protected function getExamplesMessages() { return [ 'action=mobileview&page=Doom_metal§ions=0' => 'apihelp-mobileview-example-1', 'action=mobileview&page=Candlemass§ions=0|references' => 'apihelp-mobileview-example-2', 'action=mobileview&page=Candlemass§ions=1-|references' => 'apihelp-mobileview-example-3', ]; } /** * Returns the Help URL for this Api * @return string */ public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/Extension:MobileFrontend#action.3Dmobileview'; } }