Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.17% covered (danger)
11.17%
21 / 188
7.55% covered (danger)
7.55%
4 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
MockSiteConfig
11.17% covered (danger)
11.17%
21 / 188
7.55% covered (danger)
7.55%
4 / 53
3912.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
4.11
 getLinterSiteConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allowedExternalImagePrefixes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 baseURI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exportMetadataToHeadBcp47
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 redirectRegexp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 categoryRegexp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bswRegexp
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 canonicalNamespaceId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 namespaceId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 namespaceName
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 namespaceHasSubpages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 namespaceCase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 specialPageLocalName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setInterwikiMagic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 interwikiMagic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 interwikiMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 iwp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 legalTitleChars
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 linkPrefixRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkTrail
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 linkTrailRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 langBcp47
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mainPageLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMWConfigValue
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 rtl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 langConverterEnabledBcp47
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 script
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scriptpath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 server
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 timezoneOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 variantsFor
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 widthOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableIDs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 haveComputedFunctionSynonyms
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updateFunctionSynonym
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMagicWords
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getMagicWordMatcher
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getParameterizedAliasMatcher
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 getNonNativeExtensionTags
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxTemplateDepth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpecialPageAliases
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialNSAliases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProtocols
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fakeTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFakeTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimezoneOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scrubBidiChars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNoFollowConfig
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 metrics
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 incrementCounter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 observeTiming
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Mocks;
5
6use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
7use Monolog\Formatter\LineFormatter;
8use Monolog\Handler\ErrorLogHandler;
9use Monolog\Logger;
10use Wikimedia\Bcp47Code\Bcp47Code;
11use Wikimedia\Bcp47Code\Bcp47CodeValue;
12use Wikimedia\Parsoid\Config\SiteConfig;
13use Wikimedia\Parsoid\Config\StubMetadataCollector;
14use Wikimedia\Parsoid\Core\ContentMetadataCollector;
15use Wikimedia\Parsoid\Core\LinkTarget;
16use Wikimedia\Parsoid\DOM\Document;
17use Wikimedia\Parsoid\Utils\Title;
18use Wikimedia\Parsoid\Utils\Utils;
19
20class MockSiteConfig extends SiteConfig {
21
22    /** @var ?int Unix timestamp */
23    private $fakeTimestamp = 946782245; // 2000-01-02T03:04:05Z
24
25    /** @var int */
26    private $timezoneOffset = 0; // UTC
27
28    /** @var bool */
29    private $interwikiMagic = true;
30
31    /** @var array */
32    private $linterOverrides = [];
33
34    /** If set, generate experimental Parsoid HTML v3 parser function output */
35    private bool $v3pf;
36
37    private const NAMESPACE_MAP = [
38        'media' => -2,
39        'special' => -1,
40        '' => 0,
41        'talk' => 1,
42        'user' => 2,
43        'user_talk' => 3,
44        // Last one will be used by namespaceName
45        'project' => 4, 'wp' => 4, 'wikipedia' => 4,
46        'project_talk' => 5, 'wt' => 5, 'wikipedia_talk' => 5,
47        'file' => 6,
48        'file_talk' => 7,
49        'template' => 10,
50        'template_talk' => 11,
51        'help' => 12,
52        'category' => 14,
53        'category_talk' => 15,
54    ];
55
56    /** @var array<int,bool> */
57    protected $namespacesWithSubpages = [];
58
59    /** @var array */
60    protected $interwikiMap = [];
61
62    /** @var int */
63    private $maxDepth = 40;
64
65    /** @var string|null */
66    private $linkPrefixRegex = null;
67
68    /** @var string|bool */
69    private $externalLinkTarget;
70
71    public function __construct( array $opts ) {
72        parent::__construct();
73
74        if ( isset( $opts['linting'] ) ) {
75            $this->linterEnabled = $opts['linting'];
76        }
77        if ( isset( $opts['maxDepth'] ) ) {
78            $this->maxDepth = $opts['maxDepth'];
79        }
80        if ( isset( $opts['linterOverrides'] ) ) {
81            $this->linterOverrides = $opts['linterOverrides'];
82        }
83        $this->linkPrefixRegex = $opts['linkPrefixRegex'] ?? null;
84        $this->linkTrailRegex = $opts['linkTrailRegex'] ?? '/^([a-z]+)/sD'; // enwiki default
85        $this->externalLinkTarget = $opts['externallinktarget'] ?? false;
86        $this->v3pf = $opts['v3pf'] ?? false;
87
88        // Use Monolog's PHP console handler
89        $logger = new Logger( "Parsoid CLI" );
90        $handler = new ErrorLogHandler();
91        $handler->setFormatter( new LineFormatter( '%message%' ) );
92        $logger->pushHandler( $handler );
93        $this->setLogger( $logger );
94    }
95
96    public function getLinterSiteConfig(): array {
97        return $this->linterOverrides + parent::getLinterSiteConfig();
98    }
99
100    public function allowedExternalImagePrefixes(): array {
101        return [];
102    }
103
104    public function baseURI(): string {
105        return '//my.wiki.example/wikix/';
106    }
107
108    /**
109     * @inheritDoc
110     */
111    public function exportMetadataToHeadBcp47(
112        Document $document,
113        ContentMetadataCollector $metadata,
114        string $defaultTitle,
115        Bcp47Code $lang
116    ): void {
117        '@phan-var StubMetadataCollector $metadata'; // @var StubMetadataCollector $metadata
118        $moduleLoadURI = $this->server() . $this->scriptpath() . '/load.php';
119        // Look for a displaytitle.
120        $displayTitle = $metadata->getPageProperty( 'displaytitle' ) ??
121            // Use the default title, properly escaped
122            Utils::escapeHtml( $defaultTitle );
123        $this->exportMetadataHelper(
124            $document,
125            $moduleLoadURI,
126            $metadata->getModules(),
127            $metadata->getModuleStyles(),
128            $metadata->getJsConfigVars(),
129            $displayTitle,
130            $lang
131        );
132    }
133
134    public function redirectRegexp(): string {
135        return '/(?i:#REDIRECT)/';
136    }
137
138    public function categoryRegexp(): string {
139        return '/Category/';
140    }
141
142    public function bswRegexp(): string {
143        return '/' .
144                'NOGLOBAL|DISAMBIG|NOCOLLABORATIONHUBTOC|nocollaborationhubtoc|NOTOC|notoc|' .
145                'NOGALLERY|nogallery|FORCETOC|forcetoc|TOC|toc|NOEDITSECTION|noeditsection|' .
146                'NOTITLECONVERT|notitleconvert|NOTC|notc|NOCONTENTCONVERT|nocontentconvert|' .
147                'NOCC|nocc|NEWSECTIONLINK|NONEWSECTIONLINK|HIDDENCAT|INDEX|NOINDEX|STATICREDIRECT' .
148            '/';
149    }
150
151    /** @inheritDoc */
152    public function canonicalNamespaceId( string $name ): ?int {
153        return self::NAMESPACE_MAP[$name] ?? null;
154    }
155
156    /** @inheritDoc */
157    public function namespaceId( string $name ): ?int {
158        $name = Utils::normalizeNamespaceName( $name );
159        return self::NAMESPACE_MAP[$name] ?? null;
160    }
161
162    /** @inheritDoc */
163    public function namespaceName( int $ns ): ?string {
164        static $map = null;
165        if ( $map === null ) {
166            $map = array_flip( self::NAMESPACE_MAP );
167        }
168        if ( !isset( $map[$ns] ) ) {
169            return null;
170        }
171        return ucwords( strtr( $map[$ns], '_', ' ' ) );
172    }
173
174    /** @inheritDoc */
175    public function namespaceHasSubpages( int $ns ): bool {
176        return !empty( $this->namespacesWithSubpages[$ns] );
177    }
178
179    /** @inheritDoc */
180    public function namespaceCase( int $ns ): string {
181        return 'first-letter';
182    }
183
184    /** @inheritDoc */
185    public function specialPageLocalName( string $alias ): ?string {
186        return null;
187    }
188
189    public function setInterwikiMagic( bool $val ): void {
190        $this->interwikiMagic = $val;
191    }
192
193    public function interwikiMagic(): bool {
194        return $this->interwikiMagic;
195    }
196
197    public function interwikiMap(): array {
198        return $this->interwikiMap;
199    }
200
201    public function iwp(): string {
202        return 'mywiki';
203    }
204
205    public function legalTitleChars(): string {
206        return ' %!"$&\'()*,\-.\/0-9:;=?@A-Z\\\\^_`a-z~\x80-\xFF+';
207    }
208
209    public function linkPrefixRegex(): ?string {
210        return $this->linkPrefixRegex;
211    }
212
213    protected function linkTrail(): string {
214        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
215        throw new \BadMethodCallException(
216            'Should not be used. linkTrailRegex() is overridden here.' );
217    }
218
219    public function linkTrailRegex(): ?string {
220        return $this->linkTrailRegex;
221    }
222
223    public function langBcp47(): Bcp47Code {
224        return new Bcp47CodeValue( 'en' );
225    }
226
227    public function mainPageLinkTarget(): LinkTarget {
228        return Title::newFromText( 'Main Page', $this );
229    }
230
231    /** @inheritDoc */
232    public function getMWConfigValue( string $key ) {
233        switch ( $key ) {
234            case 'CiteResponsiveReferences':
235                return true;
236            case 'CiteResponsiveReferencesThreshold':
237                return 10;
238            case 'ParsoidFragmentInput':
239                return false;
240            case 'ParsoidExperimentalParserFunctionOutput':
241                return $this->v3pf;
242            default:
243                return null;
244        }
245    }
246
247    public function rtl(): bool {
248        return false;
249    }
250
251    /** @inheritDoc */
252    public function langConverterEnabledBcp47( Bcp47Code $lang ): bool {
253        return $lang->toBcp47Code() === 'sr';
254    }
255
256    public function script(): string {
257        return '/wx/index.php';
258    }
259
260    public function scriptpath(): string {
261        return '/wx';
262    }
263
264    public function server(): string {
265        return '//my.wiki.example';
266    }
267
268    public function timezoneOffset(): int {
269        return $this->timezoneOffset;
270    }
271
272    /** @inheritDoc */
273    public function variantsFor( Bcp47Code $lang ): ?array {
274        switch ( $lang->toBcp47Code() ) {
275            case 'sr':
276                return [
277                'base' => new Bcp47CodeValue( 'sr' ),
278                'fallbacks' => [
279                    new Bcp47CodeValue( 'sr-Cyrl' )
280                ]
281            ];
282            case 'sr-Cyrl':
283                return [
284                'base' => new Bcp47CodeValue( 'sr' ),
285                'fallbacks' => [
286                    new Bcp47CodeValue( 'sr' )
287                ]
288            ];
289            case 'sr-Latn':
290                return [
291                'base' => new Bcp47CodeValue( 'sr' ),
292                'fallbacks' => [
293                    new Bcp47CodeValue( 'sr' )
294                ]
295            ];
296            default:
297                return null;
298        }
299    }
300
301    public function widthOption(): int {
302        return 220;
303    }
304
305    /** @inheritDoc */
306    protected function getVariableIDs(): array {
307        return []; // None for now
308    }
309
310    /** @inheritDoc */
311    protected function haveComputedFunctionSynonyms(): bool {
312        return false;
313    }
314
315    /** @inheritDoc */
316    protected function updateFunctionSynonym( string $func, string $magicword, bool $caseSensitive ): void {
317        /* Nothing for now. Look at src/Config/Api/SiteConfig when mocking is needed. */
318    }
319
320    /** @inheritDoc */
321    protected function getMagicWords(): array {
322        return [
323            'toc'             => [ 0, '__TOC__' ],
324            'img_thumbnail'   => [ 1, 'thumb' ],
325            'img_framed'      => [ 1, 'frame', 'framed' ],
326            'img_frameless'   => [ 1, 'frameless' ],
327            'img_manualthumb' => [ 1, 'thumbnail=$1', 'thumb=$1' ],
328            'img_none'        => [ 1, 'none' ],
329            'img_left'        => [ 1, 'left' ],
330            'img_right'       => [ 1, 'right' ],
331            // T345026: 'sub' should follow 'img_sub' to match dewikivoyage
332            'img_sub'         => [ 1, 'sub' ],
333            'sub'             => [ 0, 'sub' ],
334            'notoc'           => [ 0, '__NOTOC__' ],
335            'timedmedia_loop' => [ 0, 'loop' ],
336            'timedmedia_muted' => [ 0, 'muted' ],
337        ];
338    }
339
340    /** @inheritDoc */
341    public function getMagicWordMatcher( string $id ): string {
342        if ( $id === 'toc' ) {
343            return '/^TOC$/';
344        } else {
345            return '/(?!)/';
346        }
347    }
348
349    /** @inheritDoc */
350    public function getParameterizedAliasMatcher( array $words ): callable {
351        $paramMWs = [
352            'img_lossy' => "/^(?:(?i:lossy\=(.*?)))$/uS",
353            'timedmedia_thumbtime' => "/^(?:(?i:thumbtime\=(.*?)))$/uS",
354            'timedmedia_starttime' => "/^(?:(?i:start\=(.*?)))$/uS",
355            'timedmedia_endtime' => "/^(?:(?i:end\=(.*?)))$/uS",
356            'timedmedia_disablecontrols' => "/^(?:(?i:disablecontrols\=(.*?)))$/uS",
357            'img_manualthumb' => "/^(?:(?:thumbnail\=(.*?)|thumb\=(.*?)))$/uS",
358            'img_width' => "/^(?:(?:(.*?)px))$/uS",
359            'img_lang' => "/^(?:(?:lang\=(.*?)))$/uS",
360            'img_page' => "/^(?:(?:page\=(.*?)|page (.*?)))$/uS",
361            'img_upright' => "/^(?:(?:upright\=(.*?)|upright (.*?)))$/uS",
362            'img_link' => "/^(?:(?:link\=(.*?)))$/uS",
363            'img_alt' => "/^(?:(?:alt\=(.*?)))$/uS",
364            'img_class' => "/^(?:(?:class\=(.*?)))$/uS"
365        ];
366        $regexes = array_intersect_key( $paramMWs, array_flip( $words ) );
367        return static function ( $text ) use ( $regexes ) {
368            /**
369             * $name is the canonical magic word name
370             * $re has patterns for matching aliases
371             */
372            foreach ( $regexes as $name => $re ) {
373                if ( preg_match( $re, $text, $m ) ) {
374                    unset( $m[0] );
375
376                    // Ex. regexp here is, /^(?:(?:|vinculo\=(.*?)|enlace\=(.*?)|link\=(.*?)))$/uS
377                    // Check all the capture groups for a value, if not, it's safe to return an
378                    // empty string since we did get a match.
379                    foreach ( $m as $v ) {
380                        if ( $v !== '' ) {
381                            return [ 'k' => $name, 'v' => $v ];
382                        }
383                    }
384                    return [ 'k' => $name, 'v' => '' ];
385                }
386            }
387            return null;
388        };
389    }
390
391    /** @inheritDoc */
392    protected function getNonNativeExtensionTags(): array {
393        return [
394            'indicator' => true,
395            'timeline' => true,
396            'hiero' => true,
397            'charinsert' => true,
398            'inputbox' => true,
399            'source' => true,
400            'syntaxhighlight' => true,
401            'section' => true,
402            'score' => true,
403            'templatedata' => true,
404            'math' => true,
405            'ce' => true,
406            'chem' => true,
407            'graph' => true,
408            'maplink' => true,
409            'categorytree' => true,
410            'templatestyles' => true
411        ];
412    }
413
414    /** @inheritDoc */
415    public function getMaxTemplateDepth(): int {
416        return $this->maxDepth;
417    }
418
419    /** @inheritDoc */
420    protected function getSpecialPageAliases( string $specialPage ): array {
421        if ( $specialPage === 'Booksources' ) {
422            return [ 'Booksources', 'BookSources' ]; // Mock value
423        } else {
424            throw new \BadMethodCallException( 'Not implemented' );
425        }
426    }
427
428    /** @inheritDoc */
429    protected function getSpecialNSAliases(): array {
430        return [ "Special", "special" ]; // Mock value
431    }
432
433    /** @inheritDoc */
434    protected function getProtocols(): array {
435        return [ "http:", "https:", "irc:", "ircs:", "news:", "ftp:", "mailto:", "gopher:", "//" ];
436    }
437
438    public function fakeTimestamp(): ?int {
439        return $this->fakeTimestamp;
440    }
441
442    /**
443     * Set the fake timestamp for testing
444     * @param ?int $ts Unix timestamp
445     */
446    public function setFakeTimestamp( ?int $ts ): void {
447        $this->fakeTimestamp = $ts;
448    }
449
450    /**
451     * Set the timezone offset for testing
452     * @param int $offset Offset from UTC
453     */
454    public function setTimezoneOffset( int $offset ): void {
455        $this->timezoneOffset = $offset;
456    }
457
458    public function scrubBidiChars(): bool {
459        return true;
460    }
461
462    /** @inheritDoc */
463    public function getNoFollowConfig(): array {
464        return [
465            'nofollow' => true,
466            'nsexceptions' => [ 1 ],
467            'domainexceptions' => [ 'www.example.com' ]
468        ];
469    }
470
471    /** @inheritDoc */
472    public function getExternalLinkTarget() {
473        return $this->externalLinkTarget;
474    }
475
476    /** @var ?MockMetrics */
477    private $metrics;
478
479    /** @inheritDoc */
480    public function metrics(): ?StatsdDataFactoryInterface {
481        if ( $this->metrics === null ) {
482            $this->metrics = new MockMetrics();
483        }
484        return $this->metrics;
485    }
486
487    /**
488     * Increment a counter metric
489     * @param string $name
490     * @param array $labels
491     * @param float $amount
492     * @return void
493     */
494    public function incrementCounter( string $name, array $labels, float $amount = 1 ): void {
495        // We don't use the labels for now, using MockMetrics instead
496        $this->metrics->increment( $name );
497    }
498
499    /**
500     * Record a timing metric
501     * @param string $name
502     * @param float $value
503     * @param array $labels
504     * @return void
505     */
506    public function observeTiming( string $name, float $value, array $labels ): void {
507        // We don't use the labels for now, using MockMetrics instead
508        $this->metrics->timing( $name, $value );
509    }
510}