Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
10.00% |
1 / 10 |
CRAP | |
2.00% |
3 / 150 |
MockDataAccess | |
0.00% |
0 / 1 |
|
10.00% |
1 / 10 |
3448.29 | |
2.00% |
3 / 150 |
normTitle | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
getPageInfo | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 11 |
|||
getFileInfo | |
0.00% |
0 / 1 |
1260 | |
0.00% |
0 / 91 |
|||
doPst | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
parseWikitext | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 14 |
|||
preprocessWikitext | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 17 |
|||
fetchTemplateSource | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 8 |
|||
fetchTemplateData | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
logLinterData | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
<?php | |
declare( strict_types = 1 ); | |
namespace Wikimedia\Parsoid\Mocks; | |
use Error; | |
use Wikimedia\Parsoid\Config\DataAccess; | |
use Wikimedia\Parsoid\Config\PageConfig; | |
use Wikimedia\Parsoid\Config\PageContent; | |
use Wikimedia\Parsoid\Core\ContentMetadataCollector; | |
use Wikimedia\Parsoid\Utils\PHPUtils; | |
/** | |
* This implements some of the functionality that the tests/ParserTests/MockAPIHelper.php | |
* provides. While originally implemented to support ParserTests, this is no longer used | |
* by parser tests. | |
*/ | |
class MockDataAccess extends DataAccess { | |
private static $PAGE_DATA = [ | |
"Main_Page" => [ | |
"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 | |
'*' => "<strong>MediaWiki has been successfully installed.</strong>\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!" | |
] | |
], | |
"linkclasses" => [ | |
"mw-disambig", | |
] | |
], | |
"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', | |
'File:Bad.jpg' => 'Bad.jpg', | |
]; | |
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 | |
// Note that parserTests use a MockLocalRepo with | |
// url=>'http://example.com/images' although $wgServer="http://example.org" | |
private const IMAGE_BASE_URL = 'http://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' | |
], | |
'Bad.jpg' => [ | |
'size' => 12345, | |
'width' => 320, | |
'height' => 240, | |
'bits' => 24, | |
'mime' => 'image/jpeg', | |
], | |
'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 comes from | |
# TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength() | |
'duration' => 4.3666666666667, | |
'mime' => 'video/ogg; codecs="theora"', | |
'mediatype' => 'VIDEO', | |
'thumbtimes' => [ | |
'1.2' => 'seek%3D1.2', | |
'85' => 'seek%3D3.3666666666667', # hard limited by duration | |
], | |
], | |
'Audio.oga' => [ | |
'size' => 12345, | |
'width' => 0, | |
'height' => 0, | |
'bits' => 0, | |
# duration comes from | |
# TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength() | |
'duration' => 0.99875, | |
'mime' => 'audio/ogg; codecs="vorbis"', | |
'mediatype' => 'AUDIO', | |
'title' => 'Original Ogg file (41 kbps)', | |
'shorttitle' => 'Ogg source', | |
] | |
]; | |
/** | |
* @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, | |
'linkclasses' => $pageData['linkclasses'] ?? [], | |
]; | |
} | |
return $ret; | |
} | |
/** @inheritDoc */ | |
public function getFileInfo( PageConfig $pageConfig, array $files ): array { | |
$ret = []; | |
foreach ( $files as $f ) { | |
$name = $f[0]; | |
$dims = $f[1]; | |
// From mockAPI.js | |
$normFileName = self::FNAMES[$name] ?? $name; | |
$props = self::FILE_PROPS[$normFileName] ?? null; | |
if ( $props === null ) { | |
// We don't have info for this file | |
$ret[] = null; | |
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; | |
$mediatype = $props['mediatype'] ?? | |
( $props['mime'] === 'image/svg+xml' ? 'DRAWING' : 'BITMAP' ); | |
$info = [ | |
'size' => $props['size'] ?? 12345, | |
'height' => $height, | |
'width' => $width, | |
'url' => $baseurl, | |
'descriptionurl' => $durl, | |
'mediatype' => $mediatype, | |
'mime' => $props['mime'], | |
'badFile' => ( $normFileName === 'Bad.jpg' ), | |
]; | |
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['lang'] ) ) { | |
$txopts['lang'] = $dims['lang']; | |
} | |
} | |
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-'; | |
if ( $mediatype === 'VIDEO' ) { | |
// Hack in a 'seek' option, if provided (T258767) | |
if ( isset( $txopts['thumbtime'] ) ) { | |
$turl .= $props['thumbtimes'][strval( $txopts['thumbtime'] )] ?? ''; | |
} | |
$turl .= '-'; | |
} | |
$turl .= $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; | |
} | |
// Make this look like a TMH response | |
if ( isset( $props['title'] ) || isset( $props['shorttitle'] ) ) { | |
$info['derivatives'] = [ | |
[ | |
'src' => $info['url'], | |
'type' => $info['mime'], | |
'width' => strval( $info['width'] ), | |
'height' => strval( $info['height'] ), | |
] | |
]; | |
if ( isset( $props['title'] ) ) { | |
$info['derivatives'][0]['title'] = $props['title']; | |
} | |
if ( isset( $props['shorttitle'] ) ) { | |
$info['derivatives'][0]['shorttitle'] = $props['shorttitle']; | |
} | |
} | |
$ret[] = $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, | |
ContentMetadataCollector $metadata, | |
string $wikitext | |
): string { | |
// 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 = "<style data-mw-deduplicate='TemplateStyles:r123456'>" . | |
"small { font-size: 120% } big { font-size: 80% }</style>"; | |
break; | |
case 'translate': | |
$html = $wikitext; | |
break; | |
case 'indicator': | |
case 'section': | |
$html = ""; | |
break; | |
default: | |
throw new Error( 'Unhandled extension type encountered in: ' . $wikitext ); | |
} | |
return $html; | |
} | |
/** @inheritDoc */ | |
public function preprocessWikitext( | |
PageConfig $pageConfig, | |
ContentMetadataCollector $metadata, | |
string $wikitext | |
): string { | |
$revid = $pageConfig->getRevisionId(); | |
$expanded = str_replace( '{{!}}', '|', $wikitext ); | |
preg_match( '/{{1x\|(.*?)}}/s', $expanded, $match1 ); | |
preg_match( '/{{#tag:ref\|(.*?)\|(.*?)}}/s', $expanded, $match2 ); | |
if ( $match1 ) { | |
$ret = $match1[1]; | |
} elseif ( $match2 ) { | |
$ret = "<ref {$match2[2]}>{$match2[1]}</ref>"; | |
} elseif ( $wikitext === '{{colours of the rainbow}}' ) { | |
$ret = 'purple'; | |
} elseif ( $wikitext === '{{REVISIONID}}' ) { | |
$ret = (string)$revid; | |
} elseif ( $wikitext === '{{mangle}}' ) { | |
$ret = 'hi'; | |
$metadata->addCategory( 'Mangle', 'ho' ); | |
} else { | |
$ret = ''; | |
} | |
return $ret; | |
} | |
/** @inheritDoc */ | |
public function fetchTemplateSource( | |
PageConfig $pageConfig, string $title | |
): ?PageContent { | |
$normTitle = $this->normTitle( $title ); | |
$pageData = self::$PAGE_DATA[$normTitle] ?? null; | |
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 ) ); | |
} | |
} | |
} |