[ "title" => "Main Page", "pageid" => 1, "ns" => 0, "revid" => 1, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', // phpcs:ignore Generic.Files.LineLength.TooLong '*' => "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]" ] ] ], "Junk_Page" => [ "title" => "Junk Page", "pageid" => 2, "ns" => 0, "revid" => 2, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => '2. This is just some junk. See the comment above.' ] ] ], "Large_Page" => [ "title" => "Large_Page", "pageid" => 3, "ns" => 0, "revid" => 3, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => '', // Will be fixed up in the constructor ] ] ], "Reuse_Page" => [ "title" => "Reuse_Page", "pageid" => 100, "ns" => 0, "revid" => 100, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => '{{colours of the rainbow}}' ] ] ], "JSON_page" => [ "title" => "JSON_Page", "pageid" => 101, "ns" => 0, "revid" => 101, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'json', 'contentformat' => 'text/json', '*' => '[1]' ] ] ], "Lint_Page" => [ "title" => "Lint Page", "pageid" => 102, "ns" => 0, "revid" => 102, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "{|\nhi\n|ho\n|}" ] ] ], "Redlinks_Page" => [ "title" => "Redlinks Page", "pageid" => 103, "ns" => 0, "revid" => 103, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => '[[Special:Version]] [[Doesnotexist]] [[Redirected]]' ] ] ], "Variant_Page" => [ "title" => "Variant Page", "pageid" => 104, "ns" => 0, "revid" => 104, "parentid" => 0, 'pagelanguage' => 'sr', 'pagelanguagedir' => 'ltr', 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "абвг abcd" ] ] ], "No_Variant_Page" => [ "title" => "No Variant Page", "pageid" => 105, "ns" => 0, "revid" => 105, "parentid" => 0, 'pagelanguage' => 'sr', 'pagelanguagedir' => 'ltr', 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "абвг abcd\n__NOCONTENTCONVERT__" ] ] ], "Revision_ID" => [ "title" => "Revision ID", "pageid" => 63, "ns" => 0, "revid" => 63, "parentid" => 0, 'pagelanguage' => 'sr', 'pagelanguagedir' => 'ltr', 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => '{{REVISIONID}}' ] ] ], "Redirected" => [ "title" => "Revision ID", "pageid" => 63, "ns" => 0, "revid" => 64, "parentid" => 0, "redirect" => true, ], "Disambiguation" => [ "title" => "Disambiguation Page", "pageid" => 106, "ns" => 0, "revid" => 106, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "This is a mock disambiguation page with no more info!" ] ], "pageprops" => [ "disambiguation" => "", ] ], "Special:Version" => [ "title" => "Version", "pageid" => 107, "ns" => -1, "revid" => 107, "parentid" => 0, 'slots' => [ 'main' => [ 'contentmodel' => 'wikitext', 'contentformat' => 'text/x-wiki', '*' => "This is a mock special page." ] ], ] ]; // 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 const TEMPLATE_DATA = [ '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 _____ = _}}" ] ]; private const FNAMES = [ 'Image: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' ]; // configuration to match PHP parserTests // although protocol-relative; see T235217 and // If52d21b50cdbb466395ca64ac9877d992e19ce40 private const IMAGE_BASE_URL = '//example.com/images'; private const IMAGE_DESC_URL = self::IMAGE_BASE_URL; private const FILE_PROPS = [ 'Foobar.jpg' => [ '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' ] ]; /** * @param string $title * @return string */ private function normTitle( string $title ): string { return strtr( $title, ' ', '_' ); } /** * @param array $opts */ public function __construct( array $opts ) { // Update data of the large page $mainSlot = &self::$PAGE_DATA['Large_Page']['slots']['main']; $mainSlot['*'] = str_repeat( 'a', $opts['maxWikitextSize'] ?? 1000000 ); } /** @inheritDoc */ public function getPageInfo( PageConfig $pageConfig, array $titles ): array { $ret = []; foreach ( $titles as $title ) { $normTitle = $this->normTitle( $title ); $pageData = self::$PAGE_DATA[$normTitle] ?? null; $ret[$title] = [ 'pageId' => $pageData['pageid'] ?? null, 'revId' => $pageData['revid'] ?? null, 'missing' => $pageData === null, 'known' => $pageData !== null || ( $pageData['known'] ?? false ), 'redirect' => $pageData['redirect'] ?? false, 'disambiguation' => ( $pageData['pageprops']['disambiguation'] ?? false ) !== false, ]; } return $ret; } /** @inheritDoc */ public function getFileInfo( PageConfig $pageConfig, array $files ): array { $ret = []; foreach ( $files as $name => $dims ) { // From mockAPI.js $normPageName = self::PNAMES[$name] ?? $name; $normFileName = self::FNAMES[$name] ?? $name; $props = self::FILE_PROPS[$normFileName] ?? null; if ( $props === null ) { // We don't have info for this file continue; } $md5 = md5( $normFileName ); $md5prefix = $md5[0] . '/' . $md5[0] . $md5[1] . '/'; $baseurl = self::IMAGE_BASE_URL . '/' . $md5prefix . $normFileName; $height = $props['height'] ?? 220; $width = $props['width'] ?? 1941; $turl = self::IMAGE_BASE_URL . '/thumb/' . $md5prefix . $normFileName; $durl = self::IMAGE_DESC_URL . '/' . $normFileName; if ( isset( $props['mediatype'] ) ) { $mediatype = $props['mime'] === 'image/svg+xml' ? 'DRAWING' : 'BITMAP'; } else { $mediatype = null; } $info = [ 'size' => $props['size'] ?? 12345, 'height' => $height, 'width' => $width, 'url' => $baseurl, 'descriptionurl' => $durl, 'mediatype' => $mediatype, 'mime' => $props['mime'] ]; if ( isset( $props['duration'] ) ) { $info['duration'] = $props['duration']; } // See Config/Api/DataAccess.php $txopts = [ 'width' => null, 'height' => null, ]; if ( isset( $dims['width'] ) && $dims['width'] !== null ) { $txopts['width'] = $dims['width']; if ( isset( $dims['page'] ) ) { $txopts['page'] = $dims['page']; } } if ( isset( $dims['height'] ) && $dims['height'] !== null ) { $txopts['height'] = $dims['height']; } if ( isset( $dims['seek'] ) ) { $txopts['thumbtime'] = $dims['seek']; } // From mockAPI.js if ( $mediatype === 'VIDEO' && empty( $txopts['height'] ) && empty( $txopts['width'] ) ) { $txopts['width'] = $width; $txopts['height'] = $height; } if ( !empty( $txopts['height'] ) || !empty( $txopts['width'] ) ) { if ( $txopts['height'] === null ) { // File::scaleHeight in PHP $txopts['height'] = round( $height * $txopts['width'] / $width ); } elseif ( $txopts['width'] === null ) { // MediaHandler::fitBoxWidth in PHP // This is crazy! $idealWidth = $width * $txopts['height'] / $height; $roundedUp = ceil( $idealWidth ); if ( round( $roundedUp * $height / $width ) > $txopts['height'] ) { $txopts['width'] = floor( $idealWidth ); } else { $txopts['width'] = $roundedUp; } } else { if ( round( $height * $txopts['width'] / $width ) > $txopts['height'] ) { $txopts['width'] = ceil( $width * $txopts['height'] / $height ); } else { $txopts['height'] = round( $height * $txopts['width'] / $width ); } } $urlWidth = $txopts['width']; if ( $txopts['width'] > $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'] = $txopts['width']; $info['thumbheight'] = $txopts['height']; $info['thumburl'] = $turl; } $ret = array_merge( $ret, [ $normFileName => $info ] ); } return $ret; } /** @inheritDoc */ public function doPst( PageConfig $pageConfig, string $wikitext ): string { // FIXME: This is all mockAPI does return preg_replace( '/\{\{subst:1x\|([^}]+)\}\}/', '$1', $wikitext, 1 ); } /** @inheritDoc */ public function parseWikitext( PageConfig $pageConfig, string $wikitext ): array { // Render to html the contents of known extension tags preg_match( '#<([A-Za-z][^\t\n\v />\0]*)#', $wikitext, $match ); switch ( $match[1] ) { case 'templatestyles': // Silliness $html = ""; break; case 'translate': $html = $wikitext; break; case 'indicator': case 'section': $html = "\n"; break; default: throw new Error( 'Unhandled extension type encountered in: ' . $wikitext ); } return [ 'html' => $html, 'modules' => [], 'modulescripts' => [], 'modulestyles' => [], 'categories' => [], ]; } /** @inheritDoc */ public function preprocessWikitext( PageConfig $pageConfig, string $wikitext ): array { $revid = $pageConfig->getRevisionId(); $ret = [ 'modules' => [], 'modulescripts' => [], 'modulestyles' => [], 'categories' => [], 'properties' => [], ]; $expanded = str_replace( '{{!}}', '|', $wikitext ); preg_match( '/{{1x\|(.*?)}}/s', $expanded, $match ); if ( $match ) { $ret['wikitext'] = $match[1]; } elseif ( $wikitext === '{{colours of the rainbow}}' ) { $ret['wikitext'] = 'purple'; } elseif ( $wikitext === '{{REVISIONID}}' ) { $ret['wikitext'] = (string)$revid; } else { $ret['wikitext'] = ''; } return $ret; } /** @inheritDoc */ public function fetchPageContent( PageConfig $pageConfig, string $title, int $oldid = 0 ): ?PageContent { $normTitle = $this->normTitle( $title ); $pageData = self::$PAGE_DATA[$normTitle] ?? null; // FIXME: Ignoring revid / oldid checks if ( $pageData ) { $content = []; foreach ( $pageData['slots'] as $role => $data ) { $content['role'] = $data['*']; } return new MockPageContent( $content ); } else { return null; } } /** @inheritDoc */ public function fetchTemplateData( PageConfig $pageConfig, string $title ): ?array { return self::TEMPLATE_DATA[$this->normTitle( $title )] ?? null; } /** @inheritDoc */ public function logLinterData( PageConfig $pageConfig, array $lints ): void { foreach ( $lints as $l ) { error_log( PHPUtils::jsonEncode( $l ) ); } } }