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