Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 321
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MockApiHelper
0.00% covered (danger)
0.00%
0 / 321
0.00% covered (danger)
0.00%
0 / 10
13572
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 setApiPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addArticle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 makeRequest
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 transformHelper
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 imageInfo
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 1
2256
 processQuery
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 1
1722
 parse
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 preProcess
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 fetchTemplateData
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4// phpcs:disable Generic.Files.LineLength.TooLong
5
6namespace Wikimedia\Parsoid\ParserTests;
7
8use Error;
9use Wikimedia\Parsoid\Config\Api\ApiHelper;
10
11/**
12 * This class supports the implementation of Parser Tests in a standalone mode
13 * and without network access.
14 *
15 * In standalone mode, the config and data transformations needed by Parsoid
16 * cannot come from MediaWiki's database or its core classes.
17 *
18 * Without network access, we cannot fetch site configs or do data transformations
19 * on a remote wiki. This class supports this by intercepting network requests
20 * and returning mock responses based on cached site configs, hardcoded network
21 * responses and config,
22 *
23 * So, this API helper should be used with the Parsoid\Config\Api* set of config classes
24 * (and any subclasses derived from them).
25 *
26 * A lot of the responses here are tuned to what ParserTests needed. But, presumably
27 * this can be used by PHP Unit tests as long as the specific set of mocked responses
28 * satisfies the needs of those tests. Ideally, this class should NOT be updated for
29 * anything but the needs of running parser tests.
30 *
31 * Alternatively, PHP Unit tests could bypass the Api* classes altogether and use
32 * a (sub)set of mocked classes (Env, SiteConfig, PageConfig, DataAccess) if those
33 * classes and the data they provide satisfies the needs of those tests.
34 */
35class MockApiHelper extends ApiHelper {
36    // configuration to match PHP parserTests
37    private const IMAGE_BASE_URL = 'http://example.com/images';
38    private const IMAGE_DESC_URL = self::IMAGE_BASE_URL;
39    private const FILE_PROPS = [
40        'Foobar.jpg' => [
41            'size' => 7881,
42            'width' => 1941,
43            'height' => 220,
44            'bits' => 8,
45            'mime' => 'image/jpeg'
46        ],
47        'Thumb.png' => [
48            'size' => 22589,
49            'width' => 135,
50            'height' => 135,
51            'bits' => 8,
52            'mime' => 'image/png'
53        ],
54        'Foobar.svg' => [
55            'size' => 12345,
56            'width' => 240,
57            'height' => 180,
58            'bits' => 24,
59            'mime' => 'image/svg+xml'
60        ],
61        'Bad.jpg' => [
62            'size' => 12345,
63            'width' => 320,
64            'height' => 240,
65            'bits' => 24,
66            'mime' => 'image/jpeg',
67        ],
68        'LoremIpsum.djvu' => [
69            'size' => 3249,
70            'width' => 2480,
71            'height' => 3508,
72            'bits' => 8,
73            'mime' => 'image/vnd.djvu',
74            'mediatype' => 'OFFICE',
75            'pagecount' => 5,
76        ],
77        'Video.ogv' => [
78            'size' => 12345,
79            'width' => 320,
80            'height' => 240,
81            'bits' => 0,
82            # duration comes from
83            # TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength()
84            'duration' => 4.3666666666667,
85            'mime' => 'video/ogg; codecs="theora"',
86            'mediatype' => 'VIDEO',
87            # hacky way to get seek parameters to return the correct info
88            'extraParams' => [
89                'seek=1.2' => 'seek%3D1.2',
90                'seek=85' => 'seek%3D3.3666666666667', # hard limited by duration
91            ],
92        ],
93        'Transcode.webm' => [
94            'size' => 12345,
95            'width' => 492,
96            'height' => 360,
97            'bits' => 0,
98            'duration' => 4,
99            'mime' => 'video/webm; codecs="vp8, vorbis"',
100            'mediatype' => 'VIDEO',
101            'derivatives' => [
102                [
103                    'type' => 'video/webm; codecs="vp9, opus"',
104                    'transcodekey' => '240p.vp9.webm',
105                    'width' => 328,
106                    'height' => 240,
107                ],
108            ],
109        ],
110        'Audio.oga' => [
111            'size' => 12345,
112            'width' => 0,
113            'height' => 0,
114            'bits' => 0,
115            # duration comes from
116            # TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength()
117            'duration' => 0.99875,
118            'mime' => 'audio/ogg; codecs="vorbis"',
119            'mediatype' => 'AUDIO',
120        ],
121        'Hi-ho.jpg' => [
122            'size' => 7881,
123            'width' => 1941,
124            'height' => 220,
125            'bits' => 8,
126            'mime' => 'image/jpeg'
127        ],
128    ];
129
130    private $articleCache = [];
131    private $cachedConfigs = [];
132
133    private static $MAIN_PAGE = [
134        'query' => [
135            'pages' => [
136                [
137                    'pageid' => 1,
138                    'ns' => 0,
139                    'title' => 'Main Page',
140                    'revisions' => [
141                        [
142                            'revid' => 1,
143                            'parentid' => 0,
144                            'slots' => [
145                                'main' => [
146                                    'contentmodel' => 'wikitext',
147                                    'contentformat' => 'text/x-wiki',
148                                    'content' => "<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]"
149                                ]
150                            ]
151                        ]
152                    ]
153                ]
154            ]
155        ]
156    ];
157
158    // Old response structure, pre-mcr
159    private static $OLD_RESPONSE = [
160        'query' => [
161            'pages' => [
162                [
163                    'pageid' => 999,
164                    'ns' => 0,
165                    'title' => 'Old Response',
166                    'revisions' => [
167                        [
168                            'revid' => 999,
169                            'parentid' => 0,
170                            'contentmodel' => 'wikitext',
171                            'contentformat' => 'text/x-wiki',
172                            '*' => "<strong>MediaWiki was 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]"
173                        ]
174                    ]
175                ]
176            ]
177        ]
178    ];
179
180    private static $JUNK_PAGE = [
181        'query' => [
182            'pages' => [
183                [
184                    'pageid' => 2,
185                    'ns' => 0,
186                    'title' => 'Junk Page',
187                    'revisions' => [
188                        [
189                            'revid' => 2,
190                            'parentid' => 0,
191                            'slots' => [
192                                'main' => [
193                                    'contentmodel' => 'wikitext',
194                                    'contentformat' => 'text/x-wiki',
195                                    'content' => '2. This is just some junk. See the comment above.'
196                                ]
197                            ]
198                        ]
199                    ]
200                ]
201            ]
202        ]
203    ];
204
205    private static $LARGE_PAGE = [
206        'query' => [
207            'pages' => [
208                [
209                    'pageid' => 3,
210                    'ns' => 0,
211                    'title' => 'Large_Page',
212                    'revisions' => [
213                        [
214                            'revid' => 3,
215                            'parentid' => 0,
216                            'slots' => [
217                                'main' => [
218                                    'contentmodel' => 'wikitext',
219                                    'contentformat' => 'text/x-wiki',
220                                    /* content will be set separately */
221                                ]
222                            ]
223                        ]
224                    ]
225                ]
226            ]
227        ]
228    ];
229
230    private static $REUSE_PAGE = [
231        'query' => [
232            'pages' => [
233                [
234                    'pageid' => 100,
235                    'ns' => 0,
236                    'title' => 'Reuse_Page',
237                    'revisions' => [
238                        [
239                            'revid' => 100,
240                            'parentid' => 0,
241                            'slots' => [
242                                'main' => [
243                                    'contentmodel' => 'wikitext',
244                                    'contentformat' => 'text/x-wiki',
245                                    'content' => '{{colours of the rainbow}}'
246                                ]
247                            ]
248                        ]
249                    ]
250                ]
251            ]
252        ]
253    ];
254
255    private static $JSON_PAGE = [
256        'query' => [
257            'pages' => [
258                [
259                    'pageid' => 101,
260                    'ns' => 0,
261                    'title' => 'JSON_Page',
262                    'revisions' => [
263                        [
264                            'revid' => 101,
265                            'parentid' => 0,
266                            'slots' => [
267                                'main' => [
268                                    'contentmodel' => 'json',
269                                    'contentformat' => 'text/json',
270                                    'content' => '[1]'
271                                ]
272                            ]
273                        ]
274                    ]
275                ]
276            ]
277        ]
278    ];
279
280    private static $LINT_PAGE = [
281        'query' => [
282            'pages' => [
283                [
284                    'pageid' => 102,
285                    'ns' => 0,
286                    'title' => 'Lint Page',
287                    'revisions' => [
288                        [
289                            'revid' => 102,
290                            'parentid' => 0,
291                            'slots' => [
292                                'main' => [
293                                    'contentmodel' => 'wikitext',
294                                    'contentformat' => 'text/x-wiki',
295                                    'content' => "{|\nhi\n|ho\n|}"
296                                ]
297                            ]
298                        ]
299                    ]
300                ]
301            ]
302        ]
303    ];
304
305    private static $REDLINKS_PAGE = [
306        'query' => [
307            'pages' => [
308                [
309                    'pageid' => 103,
310                    'ns' => 0,
311                    'title' => 'Redlinks Page',
312                    'revisions' => [
313                        [
314                            'revid' => 103,
315                            'parentid' => 0,
316                            'slots' => [
317                                'main' => [
318                                    'contentmodel' => 'wikitext',
319                                    'contentformat' => 'text/x-wiki',
320                                    'content' => '[[Special:Version]] [[Doesnotexist]] [[Redirected]]'
321                                ]
322                            ]
323                        ]
324                    ]
325                ]
326            ]
327        ]
328    ];
329
330    private static $VARIANT_PAGE = [
331        'query' => [
332            'pages' => [
333                [
334                    'pageid' => 104,
335                    'ns' => 0,
336                    'pagelanguage' => 'sr',
337                    'pagelanguagedir' => 'ltr',
338                    'title' => 'Variant Page',
339                    'revisions' => [
340                        [
341                            'revid' => 104,
342                            'parentid' => 0,
343                            'slots' => [
344                                'main' => [
345                                    'contentmodel' => 'wikitext',
346                                    'contentformat' => 'text/x-wiki',
347                                    'content' => "абвг abcd"
348                                ]
349                            ]
350                        ]
351                    ]
352                ]
353            ]
354        ]
355    ];
356
357    private static $NOVARIANT_PAGE = [
358        'query' => [
359            'pages' => [
360                [
361                    'pageid' => 105,
362                    'ns' => 0,
363                    'pagelanguage' => 'sr',
364                    'pagelanguagedir' => 'ltr',
365                    'title' => 'No Variant Page',
366                    'revisions' => [
367                        [
368                            'revid' => 105,
369                            'parentid' => 0,
370                            'slots' => [
371                                'main' => [
372                                    'contentmodel' => 'wikitext',
373                                    'contentformat' => 'text/x-wiki',
374                                    'content' => "абвг abcd\n__NOCONTENTCONVERT__"
375                                ]
376                            ]
377                        ]
378                    ]
379                ]
380            ]
381        ]
382    ];
383
384    private static $REVISION_PAGE = [
385        'query' => [
386            'pages' => [
387                [
388                    'pageid' => 63,
389                    'ns' => 0,
390                    'title' => 'Revision ID',
391                    'revisions' => [
392                        [
393                            'revid' => 63,
394                            'parentid' => 0,
395                            'slots' => [
396                                'main' => [
397                                    'contentmodel' => 'wikitext',
398                                    'contentformat' => 'text/x-wiki',
399                                    'content' => '{{REVISIONID}}'
400                                ]
401                            ]
402                        ]
403                    ]
404                ]
405            ]
406        ]
407    ];
408
409    private static $missingTitles = [ 'Doesnotexist' ];
410    private static $specialTitles = [
411        'Special:Version',
412        'Special:BookSources',
413        'Special:BookSources/isbn=4-00-026157-6',
414        'Special:BookSources/0978739256',
415    ];
416    private static $redirectTitles = [ 'Redirected' ];
417    private static $disambigTitles = [ 'Disambiguation' ];
418
419    private const FNAMES = [
420        'Image:Foobar.jpg' => 'Foobar.jpg',
421        'Datei:Foobar.jpg' => 'Foobar.jpg',
422        'File:Foobar.jpg' => 'Foobar.jpg',
423        'Archivo:Foobar.jpg' => 'Foobar.jpg',
424        'Mynd:Foobar.jpg' => 'Foobar.jpg',
425        "Датотека:Foobar.jpg" => 'Foobar.jpg',
426        'Dosiero:Foobar.jpg' => 'Foobar.jpg',
427        'Image:Foobar.svg' => 'Foobar.svg',
428        'File:Foobar.svg' => 'Foobar.svg',
429        'Файл:Foobar.svg' => 'Foobar.svg',
430        'Datei:Foobar.svg' => 'Foobar.svg',
431        'Image:Thumb.png' => 'Thumb.png',
432        'File:Thumb.png' => 'Thumb.png',
433        'File:LoremIpsum.djvu' => 'LoremIpsum.djvu',
434        'File:Video.ogv' => 'Video.ogv',
435        'File:Transcode.webm' => 'Transcode.webm',
436        'File:Audio.oga' => 'Audio.oga',
437        'File:Bad.jpg' => 'Bad.jpg',
438        'File:Hi-ho.jpg' => 'Hi-ho.jpg',
439    ];
440
441    private const PNAMES = [
442        'Image:Foobar.jpg' => 'File:Foobar.jpg',
443        'Image:Foobar.svg' => 'File:Foobar.svg',
444        'Image:Thumb.png' => 'File:Thumb.png'
445    ];
446
447    // FIXME: Get this info from pagelanguage of a revision for these pages
448    private const PAGELANGS = [
449        'Rupage' => 'ru',
450        'Depage' => 'de',
451    ];
452
453    // File is present in these langs
454    private const FILELANGS = [
455        'Foobar.svg' => [ 'en', 'ru' ],
456    ];
457
458    // This templatedata description only provides a subset of fields
459    // that mediawiki API returns. Parsoid only uses the format and
460    // paramOrder fields at this point, so keeping these lean.
461    private static $templateData = [
462        'Template:NoFormatWithParamOrder' => [
463            'paramOrder' => [ 'f0', 'f1', 'unused2', 'f2', 'unused3' ]
464        ],
465        'Template:InlineTplNoParamOrder' => [
466            'format' => 'inline'
467        ],
468        'Template:BlockTplNoParamOrder' => [
469            'format' => 'block'
470        ],
471        'Template:InlineTplWithParamOrder' => [
472            'format' => 'inline',
473            'paramOrder' => [ 'f1', 'f2' ]
474        ],
475        'Template:BlockTplWithParamOrder' => [
476            'format' => 'block',
477            'paramOrder' => [ 'f1', 'f2' ]
478        ],
479        'Template:WithParamOrderAndAliases' => [
480            'params' => [
481                'f1' => [ 'aliases' => [ 'f4', 'f3' ] ]
482            ],
483            'paramOrder' => [ 'f1', 'f2' ]
484        ],
485        'Template:InlineFormattedTpl_1' => [
486            'format' => '{{_|_=_}}'
487        ],
488        'Template:InlineFormattedTpl_2' => [
489            'format' => "\n{{_ | _ = _}}"
490        ],
491        'Template:InlineFormattedTpl_3' => [
492            'format' => '{{_| _____ = _}}'
493        ],
494        'Template:BlockFormattedTpl_1' => [
495            'format' => "{{_\n| _ = _\n}}"
496        ],
497        'Template:BlockFormattedTpl_2' => [
498            'format' => "\n{{_\n| _ = _\n}}\n"
499        ],
500        'Template:BlockFormattedTpl_3' => [
501            'format' => "{{_|\n _____ = _}}"
502        ]
503    ];
504
505    /** @var string wiki prefix for which we are mocking the api access */
506    private $prefix = 'enwiki';
507
508    /** @var callable(string):string A helper to normalize titles. */
509    private $normalizeTitle = null;
510
511    public function __construct( ?string $prefix = null, ?callable $normalizeTitleFunc = null ) {
512        $this->prefix = $prefix ?? $this->prefix;
513        $this->normalizeTitle = $normalizeTitleFunc ??
514            // poor man's normalization
515            ( fn ( $t ) => str_replace( ' ', '_', $t ) );
516
517        // PORT-FIXME: Need to get this value
518        // $wtSizeLimit = $parsoidOptions->limits->wt2html->maxWikitextSize;
519        $wtSizeLimit = 1000000;
520        $mainSlot = &self::$LARGE_PAGE['query']['pages'][0]['revisions'][0]['slots']['main'];
521        $mainSlot['content'] = str_repeat( 'a', $wtSizeLimit + 1 );
522    }
523
524    /**
525     * Update prefix
526     * @param string $prefix
527     */
528    public function setApiPrefix( string $prefix ): void {
529        $this->prefix = $prefix;
530    }
531
532    /**
533     * Register an article defined in parsertests so that we can return
534     * the proper known/missing information about that title.
535     * @param string $key The normalized title of the article
536     * @param Article $article The contents of the article
537     * @return callable
538     */
539    public function addArticle( string $key, Article $article ): callable {
540        $oldVal = $this->articleCache[$key] ?? null;
541        $this->articleCache[$key] = $article;
542        return function () use ( $key, $oldVal ) {
543            $this->articleCache[$key] = $oldVal;
544        };
545    }
546
547    public function makeRequest( array $params ): array {
548        switch ( $params['action'] ?? null ) {
549            case 'query':
550                return $this->processQuery( $params );
551
552            case 'parse':
553                return $this->parse( $params['text'], !empty( $params['onlypst'] ) );
554
555            case 'templatedata':
556                return $this->fetchTemplateData( $params );
557
558            case 'expandtemplates':
559                $ret = $this->preProcess( $params['titles'] ?? $params['title'], $params['text'], $params['revid'] ?? null );
560                if ( $ret ) {
561                    $ret += [
562                        'categories' => [],
563                        'modules' => [],
564                        'modulestyles' => []
565                    ];
566                }
567                return $ret;
568
569            default:
570                return []; // FIXME: Maybe some error
571        }
572    }
573
574    /**
575     * Image scaling computation helper.
576     *
577     * Linker.php in core calls File::transform(...) for each dimension (1x,
578     * 1.5x, 2x) which then scales the image dimensions, using round/ceil/floor
579     * as appropriate to yield integer dimensions.  Note that the results
580     * may be unintuitive due to the conversion to integer: eg, a 442px width
581     * image may become 883px in 2x mode.  Resist the temptation to "optimize"
582     * this by computing the transformed size once and then scaling that;
583     * always scale the input dimensions instead.
584     * @see ImageHandler::normaliseParams, MediaHandler::fitBoxWidth,
585     * File::scaleHeight, etc, in core.
586     *
587     * Either $twidth or $theight or both will be set when called; both
588     * will be set when this function returns.
589     *
590     * @param int $width Original image width
591     * @param int $height Original image height
592     * @param int|float|null &$twidth Thumbnail width (inout parameter)
593     * @param int|float|null &$theight Thumbnail height (inout parameter)
594     */
595    public static function transformHelper( $width, $height, &$twidth, &$theight ) {
596        if ( $theight === null ) {
597            // File::scaleHeight in PHP
598            $theight = round( $height * $twidth / $width );
599        } elseif (
600            $twidth === null ||
601            // Match checks in ImageHandler.php::normaliseParams in core
602            ( $twidth * $height > $theight * $width )
603        ) {
604            // MediaHandler::fitBoxWidth in PHP
605            // This is crazy!
606            $idealWidth = $width * $theight / $height;
607            $roundedUp = ceil( $idealWidth );
608            if ( round( $roundedUp * $height / $width ) > $theight ) {
609                $twidth = floor( $idealWidth );
610            } else {
611                $twidth = $roundedUp;
612            }
613        } else {
614            if ( round( $height * $twidth / $width ) > $theight ) {
615                $twidth = ceil( $width * $theight / $height );
616            } else {
617                $theight = round( $height * $twidth / $width );
618            }
619        }
620    }
621
622    /**
623     * @param string $filename
624     * @param ?int $twidth
625     * @param ?int $theight
626     * @param ?string $extraParam optional iiurlparam, used for video/pdf/etc
627     * @param ?string $contexttitle optional iibadfilecontexttitle
628     * @return ?array
629     */
630    private function imageInfo(
631        string $filename, ?int $twidth, ?int $theight, ?string $extraParam,
632        ?string $contexttitle
633    ): ?array {
634        $normPageName = self::PNAMES[$filename] ?? $filename;
635        $normFileName = self::FNAMES[$filename] ?? $filename;
636        $props = self::FILE_PROPS[$normFileName] ?? null;
637        if ( $props === null ) {
638            // We don't have info for this file
639            return null;
640        }
641
642        $md5 = md5( $normFileName );
643        $md5prefix = $md5[0] . '/' . $md5[0] . $md5[1] . '/';
644        $baseurl = self::IMAGE_BASE_URL . '/' . $md5prefix . $normFileName;
645        $height = $props['height'];
646        $width = $props['width'];
647        $turl = self::IMAGE_BASE_URL . '/thumb/' . $md5prefix . $normFileName;
648        $durl = self::IMAGE_DESC_URL . '/' . $normFileName;
649        $mediatype = $props['mediatype'] ??
650            ( $props['mime'] === 'image/svg+xml' ? 'DRAWING' : 'BITMAP' );
651
652        $info = [
653            'size' => $props['size'],
654            'height' => $height,
655            'width' => $width,
656            'url' => $baseurl,
657            'descriptionurl' => $durl,
658            'mediatype' => $mediatype,
659            'mime' => $props['mime']
660        ];
661
662        if ( isset( $props['duration'] ) ) {
663            $info['duration'] = $props['duration'];
664        }
665        if ( isset( $props['pagecount'] ) ) {
666            $info['pagecount'] = $props['pagecount'];
667        }
668
669        if ( ( $mediatype === 'VIDEO' || $mediatype === 'DRAWING' ) && !$twidth && !$theight ) {
670            $twidth = $width;
671            $theight = $height;
672        }
673
674        preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $extraParam ?? '', $matches );
675        $lang = $matches[1] ?? null;
676        $pagelang = self::PAGELANGS[$contexttitle] ?? 'en';
677        $filelangs = self::FILELANGS[$normFileName] ?? [ 'en' ];
678
679        // Set $lang based on the targetlang, if the file is present in that lang
680        if (
681            $lang === null &&
682            $mediatype === 'DRAWING' &&
683            $pagelang !== 'en' &&
684            in_array( $pagelang, $filelangs, true )
685        ) {
686            $lang = $pagelang;
687            $extraParam = "lang{$lang}-{$twidth}px";
688        }
689
690        if ( $theight || $twidth ) {
691
692            // Save $twidth and $theight
693            $origThumbHeight = $theight;
694            $origThumbWidth = $twidth;
695
696            // Set $twidth and $theight
697            self::transformHelper( $width, $height, $twidth, $theight );
698
699            $urlWidth = $twidth;
700            if ( $twidth > $width ) {
701                // The PHP api won't enlarge a bitmap ... but the batch api will.
702                // But, to match the PHP sections, don't scale.
703                if ( $mediatype !== 'DRAWING' ) {
704                    $urlWidth = $width;
705                }
706            }
707            $thumbBaseUrl = $turl;
708            $page = null;
709            if ( $urlWidth !== $width || $mediatype === 'AUDIO' || $mediatype === 'VIDEO' || $mediatype === 'OFFICE' || $mediatype === 'DRAWING' ) {
710                $turl .= '/';
711                if ( preg_match( '/^page(\d+)-(\d+)px$/', $extraParam ?? '', $matches ) ) {
712                    $turl .= $extraParam;
713                    $page = (int)$matches[1];
714                } elseif ( $mediatype === 'OFFICE' ) {
715                    $turl .= 'page1-' . $urlWidth . 'px';
716                    $page = 1;
717                } elseif ( $lang !== null ) {
718                    // Explicit English just gets the default path
719                    if ( $lang === 'en' ) {
720                        $turl .= $urlWidth . 'px';
721                        $lang = null;
722                    } else {
723                        $turl .= $extraParam;
724                    }
725                } else {
726                    $turl .= $urlWidth . 'px';
727                }
728                $turl .= '-';
729                if ( $mediatype === 'VIDEO' ) {
730                    // Hack in a 'seek' option, if provided (T258767)
731                    if ( str_starts_with( $extraParam ?? '', 'seek' ) ) {
732                        $turl .= $props['extraParams'][$extraParam] ?? '';
733                    }
734                    $turl .= '-';
735                }
736                $turl .= $normFileName;
737                switch ( $mediatype ) {
738                    case 'AUDIO':
739                        // No thumbs are generated for audio
740                        $turl = self::IMAGE_BASE_URL . '/w/resources/assets/file-type-icons/fileicon-ogg.png';
741                        break;
742                    case 'VIDEO':
743                    case 'OFFICE':
744                        $turl .= '.jpg';
745                        break;
746                    case 'DRAWING':
747                        $turl .= '.png';
748                        break;
749                }
750            } else {
751                $turl = $baseurl;
752            }
753            $info['thumbwidth'] = $twidth;
754            $info['thumbheight'] = $theight;
755            $info['thumburl'] = $turl;
756            // src set info; added to core API result as part of T226683
757            // See Linker.php::processResponsiveImages() in core
758            foreach ( [ 1.5, 2 ] as $scale ) {
759                $stwidth = $stheight = null;
760                if ( $origThumbWidth !== null ) {
761                    $stwidth = round( $origThumbWidth * $scale );
762                }
763                if ( $origThumbHeight !== null ) {
764                    $stheight = round( $origThumbHeight * $scale );
765                }
766                self::transformHelper( $width, $height, $stwidth, $stheight );
767                $turl = $baseurl;
768                if (
769                    $stwidth < $width ||
770                    $mediatype === 'DRAWING' ||
771                    $mediatype === 'OFFICE'
772                ) {
773                    $turl = $thumbBaseUrl . '/';
774                    if ( $page !== null ) {
775                        $turl .= "page{$page}-";
776                    }
777                    if ( $lang !== null ) {
778                        $turl .= "lang{$lang}-";
779                    }
780                    $turl .= $stwidth . 'px-' . $normFileName;
781                    if ( $mediatype === 'VIDEO' || $mediatype === 'OFFICE' ) {
782                        $turl .= '.jpg';
783                    } elseif ( $mediatype === 'DRAWING' ) {
784                        $turl .= '.png';
785                    }
786                }
787                if ( $info['thumburl'] !== $turl && $mediatype !== 'AUDIO' ) {
788                    $info['responsiveUrls']["$scale"] = $turl;
789                }
790            }
791        }
792
793        if ( isset( $props['derivatives'] ) ) {
794            $info['derivatives'] = [
795                [
796                    'src' => $info['url'],
797                    'type' => $info['mime'],
798                    'width' => strval( $info['width'] ),
799                    'height' => strval( $info['height'] ),
800                ],
801            ];
802            foreach ( $props['derivatives'] as $derivative ) {
803                $info['derivatives'][] = [
804                    'src' => self::IMAGE_BASE_URL . '/transcoded/' .
805                        $md5prefix . $normFileName . '/' .
806                        $normFileName . '.' . $derivative['transcodekey'],
807                    'type' => $derivative['type'],
808                    'transcodekey' => $derivative['transcodekey'],
809                    'width' => strval( $derivative['width'] ),
810                    'height' => strval( $derivative['height'] ),
811                ];
812            }
813        }
814
815        return [
816            'result' => $info,
817            'normPageName' => $normPageName
818        ];
819    }
820
821    private const TRACKING_CATEGORIES = [
822        'broken-file-category' => 'Pages with broken file links',
823        'magiclink-tracking-rfc' => 'Pages using RFC magic links',
824        'magiclink-tracking-isbn' => 'Pages using ISBN magic links',
825        'magiclink-tracking-pmid' => 'Pages using PMID magic links',
826    ];
827
828    private function processQuery( array $params ): array {
829        if ( ( $params['meta'] ?? null ) === 'siteinfo' ) {
830            if ( !isset( $this->cachedConfigs[$this->prefix] ) ) {
831                $this->cachedConfigs[$this->prefix] = json_decode(
832                    file_get_contents( __DIR__ . "/../../baseconfig/$this->prefix.json" ), true );
833            }
834            return $this->cachedConfigs[$this->prefix];
835        }
836
837        if ( ( $params['meta'] ?? null ) === 'allmessages' ) {
838            $allmessages = [];
839            if ( isset( self::TRACKING_CATEGORIES[$params['ammessages']] ) ) {
840                $allmessages[] = [
841                    'content' => self::TRACKING_CATEGORIES[$params['ammessages']]
842                ];
843            } else {
844                $allmessages[] = [ 'missing' => true ];
845            }
846            return [ 'query' => [ 'allmessages' => $allmessages ] ];
847        }
848
849        $revid = $params['revids'] ?? null;
850
851        if ( ( $params['prop'] ?? null ) === 'revisions' ) {
852            if ( $revid === '1' || $params['titles'] === 'Main_Page' ) {
853                return self::$MAIN_PAGE;
854            } elseif ( $revid === '2' || $params['titles'] === 'Junk_Page' ) {
855                return self::$JUNK_PAGE;
856            } elseif ( $revid === '3' || $params['titles'] === 'Large_Page' ) {
857                return self::$LARGE_PAGE;
858            } elseif ( $revid === '63' || $params['titles'] === 'Revision_ID' ) {
859                return self::$REVISION_PAGE;
860            } elseif ( $revid === '100' || $params['titles'] === 'Reuse_Page' ) {
861                return self::$REUSE_PAGE;
862            } elseif ( $revid === '101' || $params['titles'] === 'JSON_Page' ) {
863                return self::$JSON_PAGE;
864            } elseif ( $revid === '102' || $params['titles'] === 'Lint_Page' ) {
865                return self::$LINT_PAGE;
866            } elseif ( $revid === '103' || $params['titles'] === 'Redlinks_Page' ) {
867                return self::$REDLINKS_PAGE;
868            } elseif ( $revid === '104' || $params['titles'] === 'Variant_Page' ) {
869                return self::$VARIANT_PAGE;
870            } elseif ( $revid === '105' || $params['titles'] === 'No_Variant_Page' ) {
871                return self::$NOVARIANT_PAGE;
872            } elseif ( $revid === '999' || $params['titles'] === 'Old_Response' ) {
873                return self::$OLD_RESPONSE;
874            } else {
875                return [ 'query' => [ 'pages' => [
876                            [
877                                'ns' => 6,
878                                'title' => json_encode( $params['titles'] ),
879                                'missing' => true,
880                                'imagerepository' => true
881                            ]
882                        ]
883                    ]
884                ];
885            }
886        }
887
888        if ( ( $params['prop'] ?? null ) === 'info' ) {
889            $ret = [];
890            $titles = preg_split( '/\|/', $params['titles'] );
891            foreach ( $titles as $t ) {
892                $props = [ 'title' => $t ];
893                $normalizeTitle = $this->normalizeTitle;
894                $key = $normalizeTitle( $t );
895                $definedInPt = isset( $this->articleCache[$key] );
896                if ( in_array( $t, self::$missingTitles, true ) ||
897                     !$definedInPt ) {
898                    $props['missing'] = true;
899                }
900                if ( in_array( $t, self::$specialTitles, true ) ) {
901                    $props['special'] = true;
902                    $props['missing'] = false;
903                }
904                if ( in_array( $t, self::$redirectTitles, true ) ) {
905                    $props['redirect'] = true;
906                    $props['missing'] = false;
907                }
908                if ( in_array( $t, self::$disambigTitles, true ) ) {
909                    $props['linkclasses'] = [ 'mw-disambig' ];
910                    $props['missing'] = false;
911                }
912                $ret[] = $props;
913            }
914            return [ 'query' => [ 'pages' => $ret ] ];
915        }
916
917        if ( ( $params['prop'] ?? null ) === 'imageinfo' ) {
918            $response = [ 'query' => [] ];
919            $filename = $params['titles']; // assumes this is a single file
920            $tonum = static function ( $x ) {
921                return $x ? (int)$x : null;
922            };
923            $ii = self::imageInfo(
924                $filename,
925                isset( $params['iiurlwidth'] ) ? $tonum( $params['iiurlwidth'] ) : null,
926                isset( $params['iiurlheight'] ) ? $tonum( $params['iiurlheight'] ) : null,
927                $params['iiurlparam'] ?? null,
928                $params['iibadfilecontexttitle'] ?? null
929            );
930            if ( $ii === null ) {
931                $p = [
932                    'ns' => 6,
933                    'title' => $filename,
934                    'imagerepository' => true,
935                    'imageinfo' => [ [
936                        'size' => 0,
937                        'width' => 0,
938                        'height' => 0,
939                        'filemissing' => true,
940                        'mime' => null,
941                        'mediatype' => null
942                    ] ]
943                ];
944                $p['missing'] = $p['imageinfo']['filemissing'] = true;
945                $p['badfile'] = false;
946            } else {
947                if ( $filename !== $ii['normPageName'] ) {
948                    $response['query']['normalized'] = [
949                        [ 'from' => $filename, 'to' => $ii['normPageName'] ]
950                    ];
951                }
952                $p = [
953                    'pageid' => 1,
954                    'ns' => 6,
955                    'title' => $ii['normPageName'],
956                    'imageinfo' => [ $ii['result'] ]
957                ];
958                $p['badfile'] = ( $filename === 'File:Bad.jpg' );
959            }
960            $response['query']['pages'] = [ $p ];
961
962            return $response;
963        }
964
965        return [ "error" => new Error( 'Uh oh!' ) ];
966    }
967
968    private function parse( string $text, bool $onlypst ): array {
969        // We're performing a subst
970        if ( $onlypst ) {
971            return [ 'text' => preg_replace( '/\{\{subst:1x\|([^}]+)\}\}/', '$1', $text, 1 ) ];
972        }
973
974        $res = null;
975        // Render to html the contents of known extension tags
976        // These are the only known extensions (besides native extensions)
977        // used in parser tests currently. This would need to be updated
978        // as more templates are added OR we need to rely on true parsing.
979        preg_match( '#<([A-Za-z][^\t\n\v />\0]*)#', $text, $match );
980        switch ( $match[1] ?? '' ) {
981            // FIXME: this isn't really used by the mocha tests
982            // since some mocha tests hit the production db, but
983            // when we fix that, they should go through this.
984            case 'templatestyles':
985                $res = "<style data-mw-deduplicate='TemplateStyles:r123456'>small { font-size: 120% } big { font-size: 80% }</style>"; // Silliness
986                break;
987
988            case 'translate':
989                $res = $text;
990                break;
991
992            case 'indicator':
993            case 'section':
994                $res = "";
995                break;
996
997            default:
998                throw new Error( 'Unhandled extension type encountered in: ' . $text );
999        }
1000
1001        $parse = [
1002            'text' => $res,
1003            'categories' => [],
1004            'modules' => [],
1005            'modulestyles' => []
1006        ];
1007        return [ 'parse' => $parse ];
1008    }
1009
1010    private function preProcess(
1011        string $title, string $text, ?int $revid
1012    ): ?array {
1013        // These are the only known templates in current parser tests.
1014        // This would need to be updated as more templates are added OR we need
1015        // to rely on true (instead of mock) preprocessing.
1016        preg_match( '/{{1x\|(.*?)}}/', $text, $match );
1017        if ( $match ) {
1018            return [ 'wikitext' => $match[1] ];
1019        } elseif ( $text === '{{colours of the rainbow}}' ) {
1020            return [ 'wikitext' => 'purple' ];
1021        } elseif ( $text === '{{REVISIONID}}' ) {
1022            return [ 'wikitext' => (string)$revid ];
1023        } else {
1024            error_log( "UNKNOWN TEMPLATE: $text for $title\n" );
1025            return null;
1026        }
1027    }
1028
1029    private function fetchTemplateData( array $params ): array {
1030        return [
1031            // Assumes that titles is a single title
1032            // (which is how Parsoid uses this)
1033            'pages' => [
1034                '1' => self::$templateData[$params['titles'] ?? ''] ?? []
1035            ]
1036        ];
1037    }
1038}