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