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