Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
StubMetadataCollector
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 35
3192
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addCategory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 addWarningMsg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addExternalLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOutputFlag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appendOutputStrings
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 setPageProperty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExtensionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setJsConfigVar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appendExtensionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appendJsConfigVar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addModules
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 addModuleStyles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 setLimitReportData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTOCData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addImage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addLanguageLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLanguageLinks
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 collect
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
110
 get
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getModuleStyles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getJsConfigVars
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getCategoryNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCategorySortKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageProperty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtensionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOutputFlags
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getTOCData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIndicator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndicators
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getImages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkToString
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 stringToLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikimedia\Parsoid\Config;
6
7use Psr\Log\LoggerInterface;
8use Psr\Log\LogLevel;
9use Wikimedia\Parsoid\Core\ContentMetadataCollector;
10use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat;
11use Wikimedia\Parsoid\Core\LinkTarget;
12use Wikimedia\Parsoid\Core\TOCData;
13use Wikimedia\Parsoid\Utils\Title;
14
15/**
16 * Minimal implementation of a ContentMetadataCollector which just
17 * records all metadata in an array.  Used for testing or operation
18 * in API mode.
19 */
20class StubMetadataCollector implements ContentMetadataCollector {
21    use ContentMetadataCollectorCompat;
22
23    /** @var SiteConfig */
24    private $siteConfig;
25
26    /** @var LoggerInterface */
27    private $logger;
28
29    /** @var array<string,array> */
30    private $mWarningMsgs = [];
31
32    /** @var array */
33    private $storage = [];
34
35    /** @var string */
36    private const MERGE_STRATEGY_KEY = '_parsoid-strategy_';
37
38    /**
39     * Non-standard merge strategy to use for properties which are *not*
40     * accumulators: "write-once" means that the property should be set
41     * once (although subsequently resetting it to the same value is ok)
42     * and an error will be thrown if there is an attempt to combine
43     * multiple values.
44     *
45     * This strategy is internal to the StubMetadataCollector for now;
46     * ParserOutput implements similar semantics for many of its properties,
47     * but not (yet) in a principled or uniform way.
48     */
49    private const MERGE_STRATEGY_WRITE_ONCE = 'write-once';
50
51    /**
52     * @param SiteConfig $siteConfig Used to resolve title namespaces
53     *  and to log warnings for unsafe metadata updates
54     */
55    public function __construct(
56        SiteConfig $siteConfig
57    ) {
58        $this->siteConfig = $siteConfig;
59        $this->logger = $siteConfig->getLogger();
60    }
61
62    /** @inheritDoc */
63    public function addCategory( $c, $sort = '' ): void {
64        if ( $c instanceof LinkTarget ) {
65            $c = $c->getDBkey();
66        }
67        // Numeric strings often become an `int` when passed to addCategory()
68        $this->collect( 'categories', (string)$c, $sort, self::MERGE_STRATEGY_WRITE_ONCE );
69    }
70
71    /** @inheritDoc */
72    public function addWarningMsg( string $msg, ...$args ): void {
73        $this->mWarningMsgs[$msg] = $args;
74    }
75
76    /** @inheritDoc */
77    public function addExternalLink( string $url ): void {
78        $this->collect( 'externallinks', '', $url );
79    }
80
81    /** @inheritDoc */
82    public function setOutputFlag( string $name, bool $value = true ): void {
83        $this->collect( 'outputflags', $name, (string)$value, self::MERGE_STRATEGY_WRITE_ONCE );
84    }
85
86    /** @inheritDoc */
87    public function appendOutputStrings( string $name, array $value ): void {
88        foreach ( $value as $v ) {
89            $this->collect( 'outputstrings', $name, $v );
90        }
91    }
92
93    /** @inheritDoc */
94    public function setPageProperty( string $name, $value ): void {
95        $this->collect( 'properties', $name, $value, self::MERGE_STRATEGY_WRITE_ONCE );
96    }
97
98    /** @inheritDoc */
99    public function setExtensionData( string $key, $value ): void {
100        $this->collect( 'extensiondata', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE );
101    }
102
103    /** @inheritDoc */
104    public function setJsConfigVar( string $key, $value ): void {
105        $this->collect( 'jsconfigvars', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE );
106    }
107
108    /** @inheritDoc */
109    public function appendExtensionData(
110        string $key,
111        $value,
112        string $strategy = self::MERGE_STRATEGY_UNION
113    ): void {
114        $this->collect( 'extensiondata', $key, $value, $strategy );
115    }
116
117    /** @inheritDoc */
118    public function appendJsConfigVar(
119        string $key,
120        string $value,
121        string $strategy = self::MERGE_STRATEGY_UNION
122    ): void {
123        $this->collect( 'jsconfigvars', $key, $value, $strategy );
124    }
125
126    /** @inheritDoc */
127    public function addModules( array $modules ): void {
128        foreach ( $modules as $module ) {
129            $this->collect( 'modules', '', $module );
130        }
131    }
132
133    /** @inheritDoc */
134    public function addModuleStyles( array $moduleStyles ): void {
135        foreach ( $moduleStyles as $style ) {
136            $this->collect( 'modulestyles', '', $style );
137        }
138    }
139
140    /** @inheritDoc */
141    public function setLimitReportData( string $key, $value ): void {
142        // XXX maybe need to JSON-encode $value
143        $this->collect( 'limitreportdata', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE );
144    }
145
146    /** @inheritDoc */
147    public function setTOCData( TOCData $tocData ): void {
148        $this->collect( 'tocdata', '', $tocData, self::MERGE_STRATEGY_WRITE_ONCE );
149    }
150
151    /** @inheritDoc */
152    public function addLink( LinkTarget $link, $id = null ): void {
153        $this->collect( 'links', '', $this->linkToString( $link ) );
154    }
155
156    /** @inheritDoc */
157    public function addImage( LinkTarget $name, $timestamp = null, $sha1 = null ): void {
158        $title = Title::newFromLinkTarget( $name, $this->siteConfig );
159        $this->collect( 'images', '', $title->getDBkey() );
160    }
161
162    /** @inheritDoc */
163    public function addLanguageLink( LinkTarget $lt ): void {
164        $this->collect( 'language-link', '', $this->linkToString( $lt ) );
165    }
166
167    /** @return LinkTarget[] */
168    public function getLanguageLinks(): array {
169        return array_map( function ( $v ) {
170            return $this->stringToLink( $v );
171        }, $this->get( 'language-link', '' ) );
172    }
173
174    /**
175     * Unified internal implementation of metadata collection.
176     * @param string $which Internal string identifying the type of metadata.
177     * @param string $key Key for storage (or '' if this is not relevant)
178     * @param mixed $value Value to store
179     * @param string $strategy "union" or "write-once"
180     */
181    private function collect(
182        string $which, string $key, $value,
183        string $strategy = self::MERGE_STRATEGY_UNION
184    ): void {
185        if ( !array_key_exists( $which, $this->storage ) ) {
186            $this->storage[$which] = [];
187        }
188        if ( !array_key_exists( $key, $this->storage[$which] ) ) {
189            $this->storage[$which][$key] = [ self::MERGE_STRATEGY_KEY => $strategy ];
190            if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) {
191                $this->storage[$which][$key]['value'] = $value;
192                return;
193            }
194        }
195        if ( $this->storage[$which][$key][self::MERGE_STRATEGY_KEY] !== $strategy ) {
196            $this->logger->log(
197                LogLevel::WARNING,
198                "Conflicting strategies for $which $key"
199            );
200            // Destructive update for compatibility; this is deprecated!
201            unset( $this->storage[$which][$key] );
202            $this->collect( $which, $key, $value, $strategy );
203            return;
204        }
205        if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) {
206            if ( ( $this->storage[$which][$key]['value'] ?? null ) === $value ) {
207                return; // already exists with the desired value
208            }
209            $this->logger->log(
210                LogLevel::WARNING,
211                "Multiple writes to a write-once: $which $key"
212            );
213            // Destructive update for compatibility; this is deprecated!
214            unset( $this->storage[$which][$key] );
215            $this->collect( $which, $key, $value, $strategy );
216            return;
217        } elseif ( $strategy === self::MERGE_STRATEGY_UNION ) {
218            if ( !( is_string( $value ) || is_int( $value ) ) ) {
219                throw new \InvalidArgumentException( "Bad value type for $key" . gettype( $value ) );
220            }
221            $this->storage[$which][$key][$value] = true;
222            return;
223        } else {
224            throw new \InvalidArgumentException( "Unknown strategy: $strategy" );
225        }
226    }
227
228    /**
229     * Retrieve values from the collector.
230     * @param string $which Internal string identifying the type of metadata.
231     * @param string|null $key Key for storage (or '' if this is not relevant)
232     * @param string $defaultStrategy Determines whether to return an empty
233     *  array or null for a missing $key
234     * @return mixed
235     */
236    private function get( string $which, ?string $key = null, string $defaultStrategy = self::MERGE_STRATEGY_UNION ) {
237        if ( $key !== null ) {
238            $result = ( $this->storage[$which] ?? [] )[$key] ?? [];
239            $strategy = $result[self::MERGE_STRATEGY_KEY] ?? $defaultStrategy;
240            unset( $result[self::MERGE_STRATEGY_KEY] );
241            if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) {
242                return $result['value'] ?? null;
243            } else {
244                return array_keys( $result );
245            }
246        }
247        $result = [];
248        foreach ( ( $this->storage[$which] ?? [] ) as $key => $ignore ) {
249            $result[$key] = $this->get( $which, (string)$key );
250        }
251        return $result;
252    }
253
254    // @internal introspection methods
255
256    /** @return string[] */
257    public function getModules(): array {
258        return $this->get( 'modules', '' );
259    }
260
261    /** @return string[] */
262    public function getModuleStyles(): array {
263        return $this->get( 'modulestyles', '' );
264    }
265
266    /** @return string[] */
267    public function getJsConfigVars(): array {
268        // This is somewhat unusual, in that we expose the 'set' represenation
269        // as $key => true, instead of just returning array_keys().
270        $result = $this->storage['jsconfigvars'] ?? [];
271        foreach ( $result as $key => &$value ) {
272            $strategy = $value[self::MERGE_STRATEGY_KEY] ?? null;
273            unset( $value[self::MERGE_STRATEGY_KEY] );
274            if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) {
275                $value = array_keys( $value )[0];
276            }
277        }
278        return $result;
279    }
280
281    /** @return list<string> */
282    public function getCategoryNames(): array {
283        // array keys can get converted to int if numeric, so ensure
284        // return value is all strings.
285        return array_map( 'strval', array_keys( $this->get( 'categories' ) ) );
286    }
287
288    /**
289     * @param string $name Category name
290     * @return ?string Sort key
291     */
292    public function getCategorySortKey( string $name ): ?string {
293        return $this->get( 'categories', $name );
294    }
295
296    /**
297     * @param string $name
298     * @return ?string
299     */
300    public function getPageProperty( string $name ): ?string {
301        return $this->get( 'properties', $name, self::MERGE_STRATEGY_WRITE_ONCE );
302    }
303
304    /**
305     * Return the collected extension data under the given key.
306     * @param string $key
307     * @return mixed|null
308     */
309    public function getExtensionData( string $key ) {
310        return $this->get( 'extensiondata', $key, self::MERGE_STRATEGY_WRITE_ONCE );
311    }
312
313    /**
314     * Return the active output flags.
315     * @return string[]
316     */
317    public function getOutputFlags() {
318        $result = [];
319        foreach ( $this->get( 'outputflags', null ) as $key => $value ) {
320            if ( $value ) {
321                $result[] = $key;
322            }
323        }
324        return $result;
325    }
326
327    /**
328     * Return the collected TOC data, or null if no TOC data was collected.
329     * @return ?TOCData
330     */
331    public function getTOCData(): ?TOCData {
332        return $this->get( 'tocdata', '', self::MERGE_STRATEGY_WRITE_ONCE );
333    }
334
335    /**
336     * Set the content for an indicator.
337     * @param string $name
338     * @param string $content
339     */
340    public function setIndicator( $name, $content ): void {
341        $this->collect( 'indicators', $name, $content, self::MERGE_STRATEGY_WRITE_ONCE );
342    }
343
344    /**
345     * Return a "name" => "content-id" mapping of recorded indicators
346     * @return array
347     */
348    public function getIndicators(): array {
349        return $this->get( 'indicators' );
350    }
351
352    /**
353     * @return array
354     */
355    public function getImages(): array {
356        return $this->get( 'images', '' );
357    }
358
359    // helper functions for recording LinkTarget objects
360
361    /**
362     * Convert a LinkTarget to a string for storing in the collected metadata.
363     * @param LinkTarget $lt
364     * @return string
365     */
366    private function linkToString( LinkTarget $lt ): string {
367        $title = Title::newFromLinkTarget( $lt, $this->siteConfig );
368        $text = $title->getPrefixedText();
369        $fragment = $title->getFragment();
370        if ( $fragment !== '' ) {
371            $text .= '#' . $fragment;
372        }
373        return $text;
374    }
375
376    /**
377     * Convert a string back into a LinkTarget for retrieval from the
378     * collected metadata.
379     * @param string $s
380     * @return LinkTarget
381     */
382    private function stringToLink( string $s ): LinkTarget {
383        return Title::newFromText( $s, $this->siteConfig );
384    }
385}