Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 99 |
|
0.00% |
0 / 35 |
CRAP | |
0.00% |
0 / 1 |
StubMetadataCollector | |
0.00% |
0 / 99 |
|
0.00% |
0 / 35 |
3192 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addCategory | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
addWarningMsg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExternalLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setOutputFlag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendOutputStrings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
setPageProperty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setJsConfigVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendJsConfigVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addModules | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
addModuleStyles | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
setLimitReportData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTOCData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addImage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addLanguageLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLanguageLinks | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
collect | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
110 | |||
get | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getModuleStyles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJsConfigVars | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getCategoryNames | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCategorySortKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPageProperty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOutputFlags | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getTOCData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIndicator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIndicators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getImages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
linkToString | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
stringToLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikimedia\Parsoid\Config; |
6 | |
7 | use Psr\Log\LoggerInterface; |
8 | use Psr\Log\LogLevel; |
9 | use Wikimedia\Parsoid\Core\ContentMetadataCollector; |
10 | use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat; |
11 | use Wikimedia\Parsoid\Core\LinkTarget; |
12 | use Wikimedia\Parsoid\Core\TOCData; |
13 | use 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 | */ |
20 | class 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 | } |