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