[ 'size' => 7881, 'width' => 1941, 'height' => 220, 'bits' => 8, 'mime' => 'image/jpeg' ], 'Thumb.png' => [ 'size' => 22589, 'width' => 135, 'height' => 135, 'bits' => 8, 'mime' => 'image/png' ], 'Foobar.svg' => [ 'size' => 12345, 'width' => 240, 'height' => 180, 'bits' => 24, 'mime' => 'image/svg+xml' ], 'LoremIpsum.djvu' => [ 'size' => 3249, 'width' => 2480, 'height' => 3508, 'bits' => 8, 'mime' => 'image/vnd.djvu' ], 'Video.ogv' => [ 'size' => 12345, 'width' => 320, 'height' => 240, 'bits' => 0, 'duration' => 160.733333333333, 'mime' => 'application/ogg', 'mediatype' => 'VIDEO' ], 'Audio.oga' => [ 'size' => 12345, 'width' => 0, 'height' => 0, 'bits' => 0, 'duration' => 160.733333333333, 'mime' => 'application/ogg', 'mediatype' => 'AUDIO' ] ]; private $cachedConfigs = []; private static $MAIN_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 1, 'ns' => 0, 'title' => 'Main Page', 'revisions' => [ [ 'revid' => 1, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => "MediaWiki has been successfully installed.\n\nConsult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]" ] ] ] ] ] ] ] ]; // Old response structure, pre-mcr private static $OLD_RESPONSE = [ 'query' => [ 'pages' => [ [ 'pageid' => 999, 'ns' => 0, 'title' => 'Old Response', 'revisions' => [ [ 'revid' => 999, 'parentid' => 0, 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "MediaWiki was successfully installed.\n\nConsult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]" ] ] ] ] ] ]; private static $JUNK_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 2, 'ns' => 0, 'title' => 'Junk Page', 'revisions' => [ [ 'revid' => 2, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => '2. This is just some junk. See the comment above.' ] ] ] ] ] ] ] ]; private static $LARGE_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 3, 'ns' => 0, 'title' => 'Large_Page', 'revisions' => [ [ 'revid' => 3, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', /* content will be set separately */ ] ] ] ] ] ] ] ]; private static $REUSE_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 100, 'ns' => 0, 'title' => 'Reuse_Page', 'revisions' => [ [ 'revid' => 100, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => '{{colours of the rainbow}}' ] ] ] ] ] ] ] ]; private static $JSON_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 101, 'ns' => 0, 'title' => 'JSON_Page', 'revisions' => [ [ 'revid' => 101, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'json', 'contentformat' => 'text/json', 'content' => '[1]' ] ] ] ] ] ] ] ]; private static $LINT_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 102, 'ns' => 0, 'title' => 'Lint Page', 'revisions' => [ [ 'revid' => 102, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => "{|\nhi\n|ho\n|}" ] ] ] ] ] ] ] ]; private static $REDLINKS_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 103, 'ns' => 0, 'title' => 'Redlinks Page', 'revisions' => [ [ 'revid' => 103, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => '[[Special:Version]] [[Doesnotexist]] [[Redirected]]' ] ] ] ] ] ] ] ]; private static $VARIANT_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 104, 'ns' => 0, 'pagelanguage' => 'sr', 'pagelanguagedir' => 'ltr', 'title' => 'Variant Page', 'revisions' => [ [ 'revid' => 104, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => "абвг abcd" ] ] ] ] ] ] ] ]; private static $NOVARIANT_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 105, 'ns' => 0, 'pagelanguage' => 'sr', 'pagelanguagedir' => 'ltr', 'title' => 'No Variant Page', 'revisions' => [ [ 'revid' => 105, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => "абвг abcd\n__NOCONTENTCONVERT__" ] ] ] ] ] ] ] ]; private static $REVISION_PAGE = [ 'query' => [ 'pages' => [ [ 'pageid' => 63, 'ns' => 0, 'title' => 'Revision ID', 'revisions' => [ [ 'revid' => 63, 'parentid' => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', 'content' => '{{REVISIONID}}' ] ] ] ] ] ] ] ]; private static $missingTitles = [ 'Doesnotexist' ]; private static $specialTitles = [ 'Special:Version' ]; private static $redirectTitles = [ 'Redirected' ]; private static $disambigTitles = [ 'Disambiguation' ]; private const FNAMES = [ 'Image:Foobar.jpg' => 'Foobar.jpg', 'Datei:Foobar.jpg' => 'Foobar.jpg', 'File:Foobar.jpg' => 'Foobar.jpg', 'Archivo:Foobar.jpg' => 'Foobar.jpg', 'Mynd:Foobar.jpg' => 'Foobar.jpg', "Датотека:Foobar.jpg" => 'Foobar.jpg', 'Image:Foobar.svg' => 'Foobar.svg', 'File:Foobar.svg' => 'Foobar.svg', 'Image:Thumb.png' => 'Thumb.png', 'File:Thumb.png' => 'Thumb.png', 'File:LoremIpsum.djvu' => 'LoremIpsum.djvu', 'File:Video.ogv' => 'Video.ogv', 'File:Audio.oga' => 'Audio.oga' ]; private const PNAMES = [ 'Image:Foobar.jpg' => 'File:Foobar.jpg', 'Image:Foobar.svg' => 'File:Foobar.svg', 'Image:Thumb.png' => 'File:Thumb.png' ]; // This templatedata description only provides a subset of fields // that mediawiki API returns. Parsoid only uses the format and // paramOrder fields at this point, so keeping these lean. private static $templateData = [ 'Template:NoFormatWithParamOrder' => [ 'paramOrder' => [ 'f0', 'f1', 'unused2', 'f2', 'unused3' ] ], 'Template:InlineTplNoParamOrder' => [ 'format' => 'inline' ], 'Template:BlockTplNoParamOrder' => [ 'format' => 'block' ], 'Template:InlineTplWithParamOrder' => [ 'format' => 'inline', 'paramOrder' => [ 'f1', 'f2' ] ], 'Template:BlockTplWithParamOrder' => [ 'format' => 'block', 'paramOrder' => [ 'f1', 'f2' ] ], 'Template:WithParamOrderAndAliases' => [ 'params' => [ 'f1' => [ 'aliases' => [ 'f4', 'f3' ] ] ], 'paramOrder' => [ 'f1', 'f2' ] ], 'Template:InlineFormattedTpl_1' => [ 'format' => '{{_|_=_}}' ], 'Template:InlineFormattedTpl_2' => [ 'format' => "\n{{_ | _ = _}}" ], 'Template:InlineFormattedTpl_3' => [ 'format' => '{{_| _____ = _}}' ], 'Template:BlockFormattedTpl_1' => [ 'format' => "{{_\n| _ = _\n}}" ], 'Template:BlockFormattedTpl_2' => [ 'format' => "\n{{_\n| _ = _\n}}\n" ], 'Template:BlockFormattedTpl_3' => [ 'format' => "{{_|\n _____ = _}}" ] ]; /** @var string wiki prefix for which we are mocking the api access */ private $prefix = 'enwiki'; /** * @param ?string $prefix */ public function __construct( ?string $prefix = null ) { if ( $prefix ) { $this->prefix = $prefix; } // PORT-FIXME: Need to get this value // $wtSizeLimit = $parsoidOptions->limits->wt2html->maxWikitextSize; $wtSizeLimit = 1000000; $mainSlot = &self::$LARGE_PAGE['query']['pages'][0]['revisions'][0]['slots']['main']; $mainSlot['content'] = str_repeat( 'a', $wtSizeLimit + 1 ); } /** * Update prefix * @param string $prefix */ public function setApiPrefix( string $prefix ): void { $this->prefix = $prefix; } /** * @param array $params * @return array */ public function makeRequest( array $params ): array { switch ( $params['action'] ?? null ) { case 'query': return $this->processQuery( $params ); case 'parse': return $this->parse( $params['text'], !empty( $params['onlypst'] ) ); case 'templatedata': return $this->fetchTemplateData( $params ); case 'expandtemplates': $ret = $this->preProcess( $params['titles'] ?? $params['title'], $params['text'], $params['revid'] ?? null ); if ( $ret ) { $ret = $ret + [ 'categories' => [], 'modules' => [], 'modulescripts' => [], 'modulestyles' => [] ]; } return $ret; default: return []; // FIXME: Maybe some error } } /** * @param string $filename * @param ?int $twidth * @param ?int $theight * @return ?array */ private function imageInfo( string $filename, ?int $twidth, ?int $theight ) : ?array { $normPagename = self::PNAMES[$filename] ?? $filename; $normFilename = self::FNAMES[$filename] ?? $filename; $props = self::FILE_PROPS[$normFilename] ?? null; if ( $props === null ) { // We don't have info for this file return null; } $md5 = md5( $normFilename ); $md5prefix = $md5[0] . '/' . $md5[0] . $md5[1] . '/'; $baseurl = self::IMAGE_BASE_URL . '/' . $md5prefix . $normFilename; $height = $props['height']; $width = $props['width']; $turl = self::IMAGE_BASE_URL . '/thumb/' . $md5prefix . $normFilename; $durl = self::IMAGE_DESC_URL . '/' . $normFilename; $mediatype = $props['mediatype'] ?? ( $props['mime'] === 'image/svg+xml' ? 'DRAWING' : 'BITMAP' ); $info = [ 'size' => $props['size'], 'height' => $height, 'width' => $width, 'url' => $baseurl, 'descriptionurl' => $durl, 'mediatype' => $mediatype, 'mime' => $props['mime'] ]; if ( isset( $props['duration'] ) ) { $info['duration'] = $props['duration']; } if ( $mediatype === 'VIDEO' && !$twidth && !$theight ) { $twidth = $width; $theight = $height; } if ( $theight || $twidth ) { if ( $theight === null ) { // File::scaleHeight in PHP $theight = round( $height * $twidth / $width ); } elseif ( $twidth === null ) { // MediaHandler::fitBoxWidth in PHP // This is crazy! $idealWidth = $width * $theight / $height; $roundedUp = ceil( $idealWidth ); if ( round( $roundedUp * $height / $width ) > $theight ) { $twidth = floor( $idealWidth ); } else { $twidth = $roundedUp; } } else { if ( round( $height * $twidth / $width ) > $theight ) { $twidth = ceil( $width * $theight / $height ); } else { $theight = round( $height * $twidth / $width ); } } $urlWidth = $twidth; if ( $twidth > $width ) { // The PHP api won't enlarge a bitmap ... but the batch api will. // But, to match the PHP sections, don't scale. if ( $mediatype !== 'DRAWING' ) { $urlWidth = $width; } } if ( $urlWidth !== $width || $mediatype === 'AUDIO' || $mediatype === 'VIDEO' ) { $turl .= '/' . $urlWidth . 'px-' . $normFilename; switch ( $mediatype ) { case 'AUDIO': // No thumbs are generated for audio $turl = self::IMAGE_BASE_URL . '/w/resources/assets/file-type-icons/fileicon-ogg.png'; break; case 'VIDEO': $turl .= '.jpg'; break; case 'DRAWING': $turl .= '.png'; break; } } else { $turl = $baseurl; } $info['thumbwidth'] = $twidth; $info['thumbheight'] = $theight; $info['thumburl'] = $turl; } return [ 'result' => $info, 'normPagename' => $normPagename ]; } /** * @param array $params * @return array */ private function processQuery( array $params ): array { if ( ( $params['meta'] ?? null ) === 'siteinfo' ) { if ( !isset( $this->cachedConfigs[$this->prefix] ) ) { $this->cachedConfigs[$this->prefix] = json_decode( file_get_contents( __DIR__ . "/../../baseconfig/2/$this->prefix.json" ), true ); } return $this->cachedConfigs[$this->prefix]; } $revid = $params['revids'] ?? null; if ( ( $params['prop'] ?? null ) === 'revisions' ) { if ( $revid === '1' || $params['titles'] === 'Main_Page' ) { return self::$MAIN_PAGE; } elseif ( $revid === '2' || $params['titles'] === 'Junk_Page' ) { return self::$JUNK_PAGE; } elseif ( $revid === '3' || $params['titles'] === 'Large_Page' ) { return self::$LARGE_PAGE; } elseif ( $revid === '63' || $params['titles'] === 'Revision_ID' ) { return self::$REVISION_PAGE; } elseif ( $revid === '100' || $params['titles'] === 'Reuse_Page' ) { return self::$REUSE_PAGE; } elseif ( $revid === '101' || $params['titles'] === 'JSON_Page' ) { return self::$JSON_PAGE; } elseif ( $revid === '102' || $params['titles'] === 'Lint_Page' ) { return self::$LINT_PAGE; } elseif ( $revid === '103' || $params['titles'] === 'Redlinks_Page' ) { return self::$REDLINKS_PAGE; } elseif ( $revid === '104' || $params['titles'] === 'Variant_Page' ) { return self::$VARIANT_PAGE; } elseif ( $revid === '105' || $params['titles'] === 'No_Variant_Page' ) { return self::$NOVARIANT_PAGE; } elseif ( $revid === '999' || $params['titles'] === 'Old_Response' ) { return self::$OLD_RESPONSE; } else { return [ 'query' => [ 'pages' => [ [ 'ns' => 6, 'title' => json_encode( $params['titles'] ), 'missing' => '', 'imagerepository' => '' ] ] ] ]; } } if ( ( $params['prop'] ?? null ) === 'info|pageprops' ) { $ret = []; $titles = preg_split( '/\|/', $params['titles'] ); foreach ( $titles as $t ) { $props = [ 'title' => $t ]; if ( in_array( $t, self::$missingTitles, true ) ) { $props['missing'] = ''; } if ( in_array( $t, self::$specialTitles, true ) ) { $props['special'] = ''; } if ( in_array( $t, self::$redirectTitles, true ) ) { $props['redirect'] = ''; } if ( in_array( $t, self::$disambigTitles, true ) ) { $props['pageprops'] = [ 'disambiguation' => '' ]; } $ret[] = $props; } return [ 'query' => [ 'pages' => $ret ] ]; } if ( ( $params['prop'] ?? null ) === 'imageinfo' ) { $response = [ 'query' => [] ]; $filename = $params['titles']; // assumes this is a single file $tonum = function ( $x ) { return $x ? (int)$x : null; }; $ii = self::imageInfo( $filename, isset( $params['iiurlwidth'] ) ? $tonum( $params['iiurlwidth'] ) : null, isset( $params['iiurlheight'] ) ? $tonum( $params['iiurlheight'] ) : null ); if ( $ii === null ) { $p = [ 'ns' => 6, 'title' => $filename, 'missing' => '', 'imagerepository' => '', 'imageinfo' => [ [ 'size' => 0, 'width' => 0, 'height' => 0, 'filemissing' => '', 'mime' => null, 'mediatype' => null ] ] ]; $p['missing'] = $p['imageinfo']['filemissing'] = true; $p['badfile'] = false; } else { if ( $filename !== $ii['normPagename'] ) { $response['query']['normalized'] = [ [ 'from' => $filename, 'to' => $ii['normPagename'] ] ]; } $p = [ 'pageid' => 1, 'ns' => 6, 'title' => $ii['normPagename'], 'imageinfo' => [ $ii['result'] ] ]; $p['badfile'] = false; } $response['query']['pages'] = [ $p ]; return $response; } return [ "error" => new Error( 'Uh oh!' ) ]; } /** * @param string $text * @param bool $onlypst * @return array */ private function parse( string $text, bool $onlypst ): array { // We're performing a subst if ( $onlypst ) { return [ 'text' => preg_replace( '/\{\{subst:1x\|([^}]+)\}\}/', '$1', $text, 1 ) ]; } $res = null; // Render to html the contents of known extension tags // These are the only known extensions (besides native extensions) // used in parser tests currently. This would need to be updated // as more templates are added OR we need to rely on true parsing. preg_match( '#<([A-Za-z][^\t\n\v />\0]*)#', $text, $match ); switch ( $match[1] ?? '' ) { // FIXME: this isn't really used by the mocha tests // since some mocha tests hit the production db, but // when we fix that, they should go through this. case 'templatestyles': $res = ""; // Silliness break; case 'translate': $res = $text; break; case 'indicator': case 'section': $res = "\n"; break; default: throw new Error( 'Unhandled extension type encountered in: ' . $text ); } $parse = [ 'text' => $res, 'categories' => [], 'modules' => [], 'modulescripts' => [], 'modulestyles' => [] ]; return [ 'parse' => $parse ]; } /** * @param string $title * @param string $text * @param ?int $revid * @return ?array */ private function preProcess( string $title, string $text, ?int $revid ): ?array { // These are the only known templates in current parser tests. // This would need to be updated as more templates are added OR we need // to rely on true (instead of mock) preprocessing. preg_match( '/{{1x\|(.*?)}}/', $text, $match ); if ( $match ) { return [ 'wikitext' => $match[1] ]; } elseif ( $text === '{{colours of the rainbow}}' ) { return [ 'wikitext' => 'purple' ]; } elseif ( $text === '{{REVISIONID}}' ) { return [ 'wikitext' => (string)$revid ]; } else { error_log( "UNKNOWN TEMPLATE: $text for $title\n" ); return null; } } /** * @param array $params * @return array */ private function fetchTemplateData( array $params ): array { return [ // Assumes that titles is a single title // (which is how Parsoid uses this) 'pages' => [ '1' => self::$templateData[$params['titles'] ?? ''] ?? [] ] ]; } }