Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.19% |
367 / 370 |
|
90.62% |
29 / 32 |
CRAP | |
0.00% |
0 / 1 |
ZObjectUtils | |
99.19% |
367 / 370 |
|
90.62% |
29 / 32 |
164 | |
0.00% |
0 / 1 |
wrapBCP47CodeInFakeCodexChip | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
isValidSerialisedZObject | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
isValidZObject | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
isValidZObjectList | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
5 | |||
isValidZObjectResolver | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
isValidZObjectRecord | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
5 | |||
canonicalize | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
17 | |||
orderZKeyIDs | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
11 | |||
canonicalizeZRecord | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
comparableString | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
filterZMultilingualStringsToLanguage | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
10 | |||
getPreferredMonolingualObject | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
isTypeEqualTo | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
7.02 | |||
isValidZObjectReference | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isNullReference | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isValidOrNullZObjectReference | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
isValidId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isValidZObjectKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isValidZObjectGlobalKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getZObjectReferenceFromKey | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getIterativeList | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRequiredZids | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
8 | |||
getLabelOfReference | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getLabelOfGlobalKey | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getLabelOfLocalKey | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
10 | |||
getLabelOfErrorTypeKey | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
getLabelOfTypeKey | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
getLabelOfFunctionArgument | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
5.01 | |||
extractHumanReadableZObject | |
96.97% |
32 / 33 |
|
0.00% |
0 / 1 |
11 | |||
isCompatibleType | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getZid | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
makeCacheKeyFromZObject | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * WikiLambda ZObject utilities class |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\WikiLambda; |
12 | |
13 | use JsonException; |
14 | use Language; |
15 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
17 | use MediaWiki\Extension\WikiLambda\ZObjects\ZFunction; |
18 | use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall; |
19 | use MediaWiki\Extension\WikiLambda\ZObjects\ZObject; |
20 | use MediaWiki\Extension\WikiLambda\ZObjects\ZPersistentObject; |
21 | use MediaWiki\Extension\WikiLambda\ZObjects\ZReference; |
22 | use MediaWiki\Extension\WikiLambda\ZObjects\ZType; |
23 | use MediaWiki\Extension\WikiLambda\ZObjects\ZTypedList; |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\Title\Title; |
26 | use Normalizer; |
27 | use stdClass; |
28 | use Transliterator; |
29 | |
30 | class ZObjectUtils { |
31 | /** |
32 | * Render a language 'Chip' of a language code with a hover-title of the language's label. |
33 | * |
34 | * TODO (T309039): use the chip component and ZID language object here instead |
35 | * |
36 | * @param string $code The BCP47 language code, e.g. 'fr' or 'en-US'. |
37 | * @param string $label The plain text label of the language, e.g. 'français' or 'American English' |
38 | * @param string $class The name of the class for the HTML element in which to wrap the label |
39 | * @return string The HTML of the element to be rendered |
40 | */ |
41 | public static function wrapBCP47CodeInFakeCodexChip( string $code, string $label, string $class ) { |
42 | return Html::element( |
43 | 'span', |
44 | [ |
45 | 'data-title' => $label, |
46 | 'class' => $class |
47 | ], |
48 | $code |
49 | ); |
50 | } |
51 | |
52 | /** |
53 | * @param string $input |
54 | * @return bool |
55 | */ |
56 | public static function isValidSerialisedZObject( string $input ): bool { |
57 | // ZObject := String | List | Record |
58 | // String := "Character*" // to be specific, as in JSON / ECMA-404 |
59 | // List := [(ZObject(,ZObject)*)] |
60 | // Record := { "Z1K1": ZObject(, "Key": ZObject)* } |
61 | // Key := ZNumberKNumber | KNumber |
62 | $status = true; |
63 | |
64 | // Encoded inputs which don't start with {, or [, are instead read as strings. |
65 | if ( $input !== '' && ( $input[0] === '{' || $input[0] === '[' ) ) { |
66 | try { |
67 | $evaluatedInput = json_decode( $input, false, 512, JSON_THROW_ON_ERROR ); |
68 | } catch ( JsonException $e ) { |
69 | return false; |
70 | } |
71 | |
72 | try { |
73 | $status = self::isValidZObject( $evaluatedInput ); |
74 | } catch ( ZErrorException $e ) { |
75 | $status = false; |
76 | } |
77 | } |
78 | |
79 | return $status; |
80 | } |
81 | |
82 | /** |
83 | * @param string|array|stdClass $input |
84 | * @return bool |
85 | * @throws ZErrorException |
86 | */ |
87 | public static function isValidZObject( $input ): bool { |
88 | if ( is_string( $input ) ) { |
89 | return true; |
90 | } |
91 | |
92 | if ( is_array( $input ) ) { |
93 | return self::isValidZObjectList( $input ); |
94 | } |
95 | |
96 | if ( is_object( $input ) ) { |
97 | return self::isValidZObjectRecord( $input ); |
98 | } |
99 | |
100 | // Fall through: invalid format error |
101 | throw new ZErrorException( |
102 | ZErrorFactory::createZErrorInstance( |
103 | ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT, |
104 | [ |
105 | 'data' => $input |
106 | ] |
107 | ) |
108 | ); |
109 | } |
110 | |
111 | /** |
112 | * @param array $input |
113 | * @return bool |
114 | * @throws ZErrorException |
115 | */ |
116 | public static function isValidZObjectList( array $input ): bool { |
117 | if ( count( $input ) === 0 ) { |
118 | throw new ZErrorException( |
119 | ZErrorFactory::createZErrorInstance( |
120 | ZErrorTypeRegistry::Z_ERROR_UNDEFINED_LIST_TYPE, |
121 | [ |
122 | 'data' => $input |
123 | ] |
124 | ) |
125 | ); |
126 | } |
127 | |
128 | $listType = array_shift( $input ); |
129 | |
130 | if ( !self::isValidZObjectResolver( $listType ) ) { |
131 | throw new ZErrorException( |
132 | ZErrorFactory::createZErrorInstance( |
133 | ZErrorTypeRegistry::Z_ERROR_WRONG_LIST_TYPE, |
134 | [ |
135 | 'data' => $listType |
136 | ] |
137 | ) |
138 | ); |
139 | } |
140 | |
141 | foreach ( $input as $index => $value ) { |
142 | try { |
143 | self::isValidZObject( $value ); |
144 | } catch ( ZErrorException $e ) { |
145 | throw new ZErrorException( |
146 | ZErrorFactory::createArrayElementZError( (string)$index, $e->getZError() ) |
147 | ); |
148 | } |
149 | } |
150 | return true; |
151 | } |
152 | |
153 | /** |
154 | * @param mixed $input |
155 | * @return bool |
156 | * @throws ZErrorException |
157 | */ |
158 | public static function isValidZObjectResolver( $input ): bool { |
159 | if ( is_string( $input ) ) { |
160 | return self::isValidZObjectReference( $input ); |
161 | } |
162 | |
163 | if ( is_object( $input ) ) { |
164 | try { |
165 | self::isValidZObjectRecord( $input ); |
166 | } catch ( ZErrorException $e ) { |
167 | return false; |
168 | } |
169 | $resolverType = $input->{ ZTypeRegistry::Z_OBJECT_TYPE }; |
170 | if ( ( $resolverType === ZTypeRegistry::Z_REFERENCE ) || |
171 | ( $resolverType === ZTypeRegistry::Z_FUNCTIONCALL ) || |
172 | ( $resolverType === ZTypeRegistry::Z_ARGUMENTREFERENCE ) ) { |
173 | return true; |
174 | } |
175 | } |
176 | |
177 | return false; |
178 | } |
179 | |
180 | /** |
181 | * @param stdClass $input |
182 | * @return bool |
183 | * @throws ZErrorException |
184 | */ |
185 | public static function isValidZObjectRecord( stdClass $input ): bool { |
186 | $objectVars = get_object_vars( $input ); |
187 | |
188 | if ( !array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $objectVars ) ) { |
189 | // Each ZObject must define its type. |
190 | throw new ZErrorException( |
191 | ZErrorFactory::createZErrorInstance( |
192 | ZErrorTypeRegistry::Z_ERROR_MISSING_TYPE, |
193 | [ |
194 | 'data' => $input |
195 | ] |
196 | ) |
197 | ); |
198 | } |
199 | |
200 | foreach ( $input as $key => $value ) { |
201 | // Check wellformedness of the key |
202 | if ( !self::isValidZObjectKey( $key ) ) { |
203 | throw new ZErrorException( |
204 | ZErrorFactory::createZErrorInstance( |
205 | ZErrorTypeRegistry::Z_ERROR_INVALID_KEY, |
206 | [ |
207 | 'dataPointer' => [ $key ] |
208 | ] |
209 | ) |
210 | ); |
211 | } |
212 | // Check general wellformedness of the value |
213 | try { |
214 | self::isValidZObject( $value ); |
215 | } catch ( ZErrorException $e ) { |
216 | throw new ZErrorException( ZErrorFactory::createKeyValueZError( $key, $e->getZError() ) ); |
217 | } |
218 | } |
219 | return true; |
220 | } |
221 | |
222 | /** |
223 | * Canonicalizes a ZObject. |
224 | * |
225 | * @param string|array|stdClass $input decoded JSON object for a valid ZObject |
226 | * @return string|array|stdClass canonical decoded JSON object of same ZObject |
227 | */ |
228 | public static function canonicalize( $input ) { |
229 | if ( is_array( $input ) ) { |
230 | return array_map( [ self::class, 'canonicalize' ], $input ); |
231 | } |
232 | |
233 | if ( is_object( $input ) ) { |
234 | $outputObj = self::canonicalizeZRecord( $input ); |
235 | $output = get_object_vars( $outputObj ); |
236 | |
237 | if ( array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $output ) ) { |
238 | $type = self::canonicalize( $output[ ZTypeRegistry::Z_OBJECT_TYPE ] ); |
239 | |
240 | if ( is_string( $type ) ) { |
241 | // Type is ZString |
242 | if ( $type === ZTypeRegistry::Z_STRING |
243 | && array_key_exists( ZTypeRegistry::Z_STRING_VALUE, $output ) |
244 | && !self::isValidId( $output[ ZTypeRegistry::Z_STRING_VALUE ] ) ) { |
245 | // FIXME: what if it is a valid ID? what are we returning here? |
246 | return self::canonicalize( $output[ ZTypeRegistry::Z_STRING_VALUE ] ); |
247 | } |
248 | |
249 | // Type is a ZReference |
250 | if ( $type === ZTypeRegistry::Z_REFERENCE |
251 | && array_key_exists( ZTypeRegistry::Z_REFERENCE_VALUE, $output ) |
252 | && self::isValidId( $output[ ZTypeRegistry::Z_REFERENCE_VALUE ] ) ) { |
253 | return self::canonicalize( $output[ ZTypeRegistry::Z_REFERENCE_VALUE ] ); |
254 | } |
255 | } |
256 | |
257 | // Type is a Typed list |
258 | if ( is_object( $type ) ) { |
259 | $typeVars = get_object_vars( $type ); |
260 | if ( |
261 | array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $typeVars ) |
262 | && $typeVars[ ZTypeRegistry::Z_OBJECT_TYPE ] == ZTypeRegistry::Z_FUNCTIONCALL |
263 | && array_key_exists( ZTypeRegistry::Z_FUNCTIONCALL_FUNCTION, $typeVars ) |
264 | && $typeVars[ ZTypeRegistry::Z_FUNCTIONCALL_FUNCTION ] == ZTypeRegistry::Z_FUNCTION_TYPED_LIST |
265 | ) { |
266 | $itemType = $typeVars[ ZTypeRegistry::Z_FUNCTION_TYPED_LIST_TYPE ]; |
267 | $typedListArray = [ $itemType ]; |
268 | if ( array_key_exists( 'K1', $output ) ) { |
269 | array_push( $typedListArray, $output['K1'], ...array_slice( $output['K2'], 1 ) ); |
270 | } |
271 | return $typedListArray; |
272 | } |
273 | } |
274 | } |
275 | return $outputObj; |
276 | } |
277 | |
278 | return $input; |
279 | } |
280 | |
281 | /** |
282 | * Compares IDs of ZKeys in an order. |
283 | * |
284 | * First come global ZIDs, then local ones. The globals are sorted first |
285 | * numerically by the Z-Number, and then by the K-Number. |
286 | * |
287 | * @param string $left left key for comparision |
288 | * @param string $right right key for comparision |
289 | * @return int whether left is smaller (-1) than right or not (+1) |
290 | */ |
291 | public static function orderZKeyIDs( string $left, string $right ): int { |
292 | if ( $left == $right ) { |
293 | return 0; |
294 | } |
295 | if ( $left[0] == 'Z' && $right[0] == 'K' ) { |
296 | return -1; |
297 | } |
298 | if ( $left[0] == 'K' && $right[0] == 'Z' ) { |
299 | return 1; |
300 | } |
301 | $leftkpos = strpos( $left, 'K' ); |
302 | $rightkpos = strpos( $right, 'K' ); |
303 | if ( $leftkpos == 0 ) { |
304 | $leftzid = 0; |
305 | } else { |
306 | $leftzid = intval( substr( $left, 1, $leftkpos - 1 ) ); |
307 | } |
308 | if ( $rightkpos == 0 ) { |
309 | $rightzid = 0; |
310 | } else { |
311 | $rightzid = intval( substr( $right, 1, $rightkpos - 1 ) ); |
312 | } |
313 | if ( $leftzid < $rightzid ) { |
314 | return -1; |
315 | } |
316 | if ( $leftzid > $rightzid ) { |
317 | return 1; |
318 | } |
319 | $leftkid = intval( substr( $left, $leftkpos + 1 ) ); |
320 | $rightkid = intval( substr( $right, $rightkpos + 1 ) ); |
321 | if ( $leftkid < $rightkid ) { |
322 | return -1; |
323 | } |
324 | return 1; |
325 | } |
326 | |
327 | /** |
328 | * Canonicalizes a record-like ZObject. |
329 | * |
330 | * This trims and sorts the keys. |
331 | * |
332 | * @param stdClass $input The decoded JSON object of a well-formed ZObject |
333 | * @return stdClass Canonical decoded JSON object representing the same ZObject |
334 | */ |
335 | public static function canonicalizeZRecord( stdClass $input ): stdClass { |
336 | $record = get_object_vars( $input ); |
337 | $record = array_combine( array_map( 'trim', array_keys( $record ) ), $record ); |
338 | |
339 | $type = self::canonicalize( $record[ ZTypeRegistry::Z_OBJECT_TYPE ] ?? null ); |
340 | if ( is_string( $type ) ) { |
341 | foreach ( $record as $key => $value ) { |
342 | if ( preg_match( '/^K[1-9]\d*$/', $key ) ) { |
343 | // $key is guaranteed to be unique, so $globalKey is unique as well |
344 | $globalKey = $type . $key; |
345 | if ( !array_key_exists( $globalKey, $record ) ) { |
346 | $record[$globalKey] = $value; |
347 | unset( $record[$key] ); |
348 | } |
349 | } |
350 | } |
351 | } |
352 | |
353 | uksort( $record, [ self::class, 'orderZKeyIDs' ] ); |
354 | $record = array_map( [ self::class, 'canonicalize' ], $record ); |
355 | return (object)$record; |
356 | } |
357 | |
358 | /** |
359 | * Normalise and down-cast a label for database comparison by normalising Unicode, lower-casing, |
360 | * and collapsing accents. |
361 | * |
362 | * TODO: To consider further changes. |
363 | * |
364 | * @param string $input The input |
365 | * @return string |
366 | */ |
367 | public static function comparableString( string $input ): string { |
368 | // First, lower-case the input (in a multi-byte-aware manner) |
369 | $output = mb_strtolower( $input ); |
370 | |
371 | // This Transliterator removes Latin accents but e.g. retains Han characters as-is. |
372 | // Specifically, it does canonical decomposition (NFD); removes non-spacing marks like accents; |
373 | // then recomposes, e.g. for Korean Hangul syllables. |
374 | // TODO: Replace with a language-aware transliterator? |
375 | $transliterator = Transliterator::create( 'NFD; [:Nonspacing Mark:] Remove; NFC;' ); |
376 | $output = $transliterator->transliterate( mb_strtolower( Normalizer::normalize( $output ) ) ); |
377 | |
378 | return $output; |
379 | } |
380 | |
381 | /** |
382 | * Filters ZObject to preferred language. |
383 | * |
384 | * Given a ZObject, reduces all its ZMultilingualStrings to |
385 | * only the preferred language or fallbacks. |
386 | * |
387 | * @param array|stdClass|string $input decoded JSON object for a ZObject |
388 | * @param string[] $languages array of language Zids |
389 | * @return string|array|stdClass same ZObject with only selected Monolingual |
390 | * string for each of its Multilingual strings |
391 | */ |
392 | public static function filterZMultilingualStringsToLanguage( $input, array $languages = [] ) { |
393 | if ( is_string( $input ) ) { |
394 | return $input; |
395 | } |
396 | |
397 | if ( is_array( $input ) ) { |
398 | return array_map( function ( $item ) use ( $languages ) { |
399 | return self::filterZMultilingualStringsToLanguage( $item, $languages ); |
400 | }, $input ); |
401 | } |
402 | |
403 | // For each key of the input ZObject |
404 | foreach ( $input as $index => $value ) { |
405 | // Apply language filter to every item of the array or object |
406 | $input->$index = self::filterZMultilingualStringsToLanguage( $value, $languages ); |
407 | |
408 | // If the value is a string, and the type is ZMonolingualString, |
409 | // select the preferred language out of the available ZMonolingualStrings |
410 | if ( |
411 | is_string( $value ) && |
412 | $index === ZTypeRegistry::Z_OBJECT_TYPE && |
413 | $value === ZTypeRegistry::Z_MULTILINGUALSTRING |
414 | ) { |
415 | $input->{ZTypeRegistry::Z_MULTILINGUALSTRING_VALUE} = self::getPreferredMonolingualObject( |
416 | $input->{ZTypeRegistry::Z_MULTILINGUALSTRING_VALUE}, |
417 | $languages, |
418 | ZTypeRegistry::Z_MONOLINGUALSTRING_LANGUAGE |
419 | ); |
420 | break; |
421 | } |
422 | |
423 | // If the value is a string, and the type is ZMonolingualStringSet, |
424 | // select the preferred language out of the available ZMonolingualStringSets |
425 | if ( |
426 | is_string( $value ) && |
427 | $index === ZTypeRegistry::Z_OBJECT_TYPE && |
428 | $value === ZTypeRegistry::Z_MULTILINGUALSTRINGSET |
429 | ) { |
430 | $input->{ZTypeRegistry::Z_MULTILINGUALSTRINGSET_VALUE} = self::getPreferredMonolingualObject( |
431 | $input->{ZTypeRegistry::Z_MULTILINGUALSTRINGSET_VALUE}, |
432 | $languages, |
433 | ZTypeRegistry::Z_MONOLINGUALSTRINGSET_LANGUAGE |
434 | ); |
435 | break; |
436 | } |
437 | } |
438 | return $input; |
439 | } |
440 | |
441 | /** |
442 | * Filters Monolingual Strings and Stringsets to the preferred language. |
443 | * |
444 | * Returns the preferred Monolingual String/Stringset of a Multilingual |
445 | * String/Stringset given an array of preferred languages. |
446 | * |
447 | * @param array $multilingual decoded JSON for a Multilingual String/Stringset value |
448 | * @param string[] $languages array of language Zids |
449 | * @param string $key Identifies the key that contains the language in the monolingual object, Z11K1 or Z31K1 |
450 | * @return array same Multilingual String/Stringset value with only one item of the preferred language |
451 | */ |
452 | private static function getPreferredMonolingualObject( array $multilingual, array $languages, string $key ): array { |
453 | // Ignore first item in the canonical form array; this is a string representing the type |
454 | $itemType = array_shift( $multilingual ); |
455 | |
456 | $availableLangs = []; |
457 | $selectedIndex = 0; |
458 | |
459 | if ( count( $multilingual ) == 0 ) { |
460 | return [ $itemType ]; |
461 | } |
462 | |
463 | foreach ( $multilingual as $value ) { |
464 | $availableLangs[] = $value->{$key}; |
465 | } |
466 | |
467 | foreach ( $languages as $lang ) { |
468 | $index = array_search( $lang, $availableLangs ); |
469 | if ( $index !== false ) { |
470 | $selectedIndex = $index; |
471 | break; |
472 | } |
473 | } |
474 | |
475 | return [ $itemType, $multilingual[ $selectedIndex ] ]; |
476 | } |
477 | |
478 | /** |
479 | * Asserts whether two types are equivalent |
480 | * |
481 | * @param stdClass|string $type1 |
482 | * @param stdClass|string $type2 |
483 | * @return bool |
484 | */ |
485 | public static function isTypeEqualTo( $type1, $type2 ) { |
486 | // Not the same type |
487 | if ( gettype( $type1 ) !== gettype( $type2 ) ) { |
488 | return false; |
489 | } |
490 | |
491 | // If they are both strings, return identity |
492 | if ( is_string( $type1 ) ) { |
493 | return $type1 === $type2; |
494 | } |
495 | |
496 | // If they are both objects, compare their keys |
497 | $typeArr1 = (array)$type1; |
498 | $typeArr2 = (array)$type2; |
499 | if ( count( $typeArr1 ) !== count( $typeArr2 ) ) { |
500 | return false; |
501 | } |
502 | foreach ( $typeArr1 as $key => $value ) { |
503 | if ( !array_key_exists( $key, $typeArr2 ) ) { |
504 | return false; |
505 | } |
506 | if ( !self::isTypeEqualTo( $value, $typeArr2[ $key ] ) ) { |
507 | return false; |
508 | } |
509 | } |
510 | return true; |
511 | } |
512 | |
513 | /** |
514 | * Is the input a ZObject reference key (e.g. Z1 or Z12345)? |
515 | * |
516 | * @param string $input |
517 | * @return bool |
518 | */ |
519 | public static function isValidZObjectReference( string $input ): bool { |
520 | return preg_match( "/^\s*Z[1-9]\d*\s*$/", $input ); |
521 | } |
522 | |
523 | /** |
524 | * Is the input a null reference (Z0)? |
525 | * |
526 | * @param string $input |
527 | * @return bool |
528 | */ |
529 | public static function isNullReference( string $input ): bool { |
530 | return ( $input === ZTypeRegistry::Z_NULL_REFERENCE ); |
531 | } |
532 | |
533 | /** |
534 | * Is the input a ZObject reference key (e.g. Z1 or Z12345)? |
535 | * |
536 | * @param string $input |
537 | * @return bool |
538 | */ |
539 | public static function isValidOrNullZObjectReference( string $input ): bool { |
540 | return ( self::isValidZObjectReference( $input ) || self::isNullReference( $input ) ); |
541 | } |
542 | |
543 | /** |
544 | * Is the input a valid possible identifier across WMF projects? |
545 | * |
546 | * @param string $input |
547 | * @return bool |
548 | */ |
549 | public static function isValidId( string $input ): bool { |
550 | return preg_match( "/^[A-Z][1-9]\d*$/", $input ); |
551 | } |
552 | |
553 | /** |
554 | * Is the input a ZObject reference key (e.g. Z1K1 or K12345)? |
555 | * |
556 | * @param string $input |
557 | * @return bool |
558 | */ |
559 | public static function isValidZObjectKey( string $input ): bool { |
560 | return preg_match( "/^\s*(Z[1-9]\d*)?K\d+\s*$/", $input ); |
561 | } |
562 | |
563 | /** |
564 | * Is the input a global ZObject reference key (e.g. Z1K1)? |
565 | * |
566 | * @param string $input |
567 | * @return bool |
568 | */ |
569 | public static function isValidZObjectGlobalKey( string $input ): bool { |
570 | return preg_match( "/^\s*Z[1-9]\d*K\d+\s*$/", $input ); |
571 | } |
572 | |
573 | /** |
574 | * Split out the ZObject reference from a given global reference key (e.g. 'Z1' from 'Z1K1'). |
575 | * |
576 | * @param string $input |
577 | * @return string |
578 | */ |
579 | public static function getZObjectReferenceFromKey( string $input ): string { |
580 | preg_match( "/^\s*(Z[1-9]\d*)?(K\d+)\s*$/", $input, $matches ); |
581 | return $matches[1] ?? ''; |
582 | } |
583 | |
584 | /** |
585 | * Given an array or a ZTypedList, returns an array that can be iterated over |
586 | * |
587 | * @param array|ZTypedList $list |
588 | * @return array |
589 | */ |
590 | public static function getIterativeList( $list ): array { |
591 | if ( $list instanceof ZTypedList ) { |
592 | return $list->getAsArray(); |
593 | } |
594 | return $list; |
595 | } |
596 | |
597 | /** |
598 | * @param string|array|\stdClass $zobject |
599 | * @return array |
600 | */ |
601 | public static function getRequiredZids( $zobject ): array { |
602 | $zids = []; |
603 | |
604 | // If $zobject is a reference, add to the array |
605 | if ( is_string( $zobject ) ) { |
606 | if ( self::isValidZObjectReference( $zobject ) ) { |
607 | $zids[] = $zobject; |
608 | } |
609 | } |
610 | |
611 | // If $zobject is an array, get required Zids from each element |
612 | if ( is_array( $zobject ) ) { |
613 | foreach ( $zobject as $item ) { |
614 | $zids = array_merge( $zids, self::getRequiredZids( $item ) ); |
615 | } |
616 | } |
617 | |
618 | // If $zobject is an object, get required Zids from keys and values |
619 | if ( is_object( $zobject ) ) { |
620 | foreach ( $zobject as $key => $value ) { |
621 | // Add the reference part of the key. Do not add empty string if local key. |
622 | $ref = self::getZObjectReferenceFromKey( $key ); |
623 | if ( $ref ) { |
624 | $zids[] = $ref; |
625 | } |
626 | // Recursively add other references in the value |
627 | $zids = array_merge( $zids, self::getRequiredZids( $value ) ); |
628 | } |
629 | } |
630 | |
631 | return array_values( array_unique( $zids ) ); |
632 | } |
633 | |
634 | /** |
635 | * Returns the natural language label of a given Zid in the language |
636 | * passed as parameter or available fallback languages. If not available, |
637 | * returns the non-translated Zid. |
638 | * |
639 | * @param string $zid |
640 | * @param ZPersistentObject $zobject |
641 | * @param Language $lang |
642 | * @return string |
643 | */ |
644 | public static function getLabelOfReference( $zid, $zobject, $lang ): string { |
645 | $labels = $zobject->getLabels(); |
646 | $label = $labels->buildStringForLanguage( $lang )->fallbackWithEnglish()->getString(); |
647 | |
648 | if ( $label === null ) { |
649 | return $zid; |
650 | } |
651 | |
652 | return $label; |
653 | } |
654 | |
655 | /** |
656 | * Returns the natural language label of a given type key, function argument or error key |
657 | * in the language passed as parameter or available fallback languages. If not available, |
658 | * returns the untranslated key Id. |
659 | * |
660 | * @param string $key |
661 | * @param ZPersistentObject $zobject |
662 | * @param Language $lang |
663 | * @return string |
664 | */ |
665 | public static function getLabelOfGlobalKey( $key, $zobject, $lang ): string { |
666 | $ztype = $zobject->getInternalZType(); |
667 | |
668 | if ( $ztype === ZTypeRegistry::Z_TYPE ) { |
669 | return self::getLabelOfTypeKey( $key, $zobject, $lang ); |
670 | } |
671 | |
672 | if ( $ztype === ZTypeRegistry::Z_FUNCTION ) { |
673 | return self::getLabelOfFunctionArgument( $key, $zobject, $lang ); |
674 | } |
675 | |
676 | if ( $ztype === ZTypeRegistry::Z_ERRORTYPE ) { |
677 | return self::getLabelOfErrorTypeKey( $key, $zobject, $lang ); |
678 | } |
679 | |
680 | // Not a type nor an error type, return untranslated key Id |
681 | return $key; |
682 | } |
683 | |
684 | /** |
685 | * |
686 | * @param string $key |
687 | * @param \stdClass $zobject |
688 | * @param ZPersistentObject[] $data |
689 | * @param Language $lang |
690 | * @return string |
691 | */ |
692 | public static function getLabelOfLocalKey( $key, $zobject, $data, $lang ): string { |
693 | $type = $zobject->{ ZTypeRegistry::Z_OBJECT_TYPE }; |
694 | |
695 | // If type is a reference, desambiguate and find the key in its type definition |
696 | if ( is_string( $type ) && ( array_key_exists( $type, $data ) ) ) { |
697 | $globalKey = "$type$key"; |
698 | $translatedKey = self::getLabelOfGlobalKey( $globalKey, $data[ $type ], $lang ); |
699 | if ( $translatedKey !== $globalKey ) { |
700 | return $translatedKey; |
701 | } |
702 | } |
703 | |
704 | // If type is a function call, |
705 | if ( is_object( $type ) && property_exists( $type, ZTypeRegistry::Z_FUNCTIONCALL_FUNCTION ) ) { |
706 | $function = $type->{ ZTypeRegistry::Z_FUNCTIONCALL_FUNCTION }; |
707 | |
708 | // Builtin Z885: we can build the global keys with error type Zid |
709 | if ( is_string( $function ) && ( $function === ZTypeRegistry::Z_FUNCTION_ERRORTYPE_TO_TYPE ) ) { |
710 | $errorType = $type->{ ZTypeRegistry::Z_FUNCTION_ERRORTYPE_TYPE }; |
711 | if ( array_key_exists( $errorType, $data ) ) { |
712 | $globalKey = "$errorType$key"; |
713 | $translatedKey = self::getLabelOfErrorTypeKey( $globalKey, $data[ $errorType ], $lang ); |
714 | if ( $translatedKey !== $globalKey ) { |
715 | return $translatedKey; |
716 | } |
717 | } |
718 | } |
719 | |
720 | // Builtin Z881: we don't need to translate keys |
721 | // TODO (T301451): Non builtins, request function call execution from the orchestrator |
722 | } |
723 | |
724 | return $key; |
725 | } |
726 | |
727 | /** |
728 | * Returns the natural language label of a given ZKey in the language |
729 | * passed as parameter or available fallback languages. If not available, |
730 | * returns the non-translated ZKey. |
731 | * |
732 | * @param string $key |
733 | * @param ZPersistentObject $zobject |
734 | * @param Language $lang |
735 | * @return string |
736 | */ |
737 | public static function getLabelOfErrorTypeKey( $key, $zobject, $lang ): string { |
738 | $keys = $zobject->getInnerZObject()->getValueByKey( ZTypeRegistry::Z_ERRORTYPE_KEYS ); |
739 | |
740 | if ( !is_array( $keys ) && !( $keys instanceof ZTypedList ) ) { |
741 | return $key; |
742 | } |
743 | |
744 | foreach ( self::getIterativeList( $keys ) as $zkey ) { |
745 | if ( $zkey->getKeyId() === $key ) { |
746 | $labels = $zkey->getKeyLabel(); |
747 | |
748 | if ( $labels === null ) { |
749 | return $key; |
750 | } |
751 | |
752 | $label = $labels->buildStringForLanguage( $lang )->fallbackWithEnglish()->getString(); |
753 | if ( $label === null ) { |
754 | return $key; |
755 | } |
756 | |
757 | return $label; |
758 | } |
759 | } |
760 | |
761 | // Key not found |
762 | return $key; |
763 | } |
764 | |
765 | /** |
766 | * Returns the natural language label of a given ZKey in the language |
767 | * passed as parameter or available fallback languages. If not available, |
768 | * returns the non-translated ZKey. |
769 | * |
770 | * @param string $key |
771 | * @param ZPersistentObject $zobject |
772 | * @param Language $lang |
773 | * @return string |
774 | */ |
775 | public static function getLabelOfTypeKey( $key, $zobject, $lang ): string { |
776 | $ztype = $zobject->getInnerZObject(); |
777 | if ( !( $ztype instanceof ZType ) ) { |
778 | return $key; |
779 | } |
780 | |
781 | $zkey = $ztype->getZKey( $key ); |
782 | if ( $zkey === null ) { |
783 | return $key; |
784 | } |
785 | |
786 | $labels = $zkey->getKeyLabel(); |
787 | if ( $labels === null ) { |
788 | return $key; |
789 | } |
790 | |
791 | $label = $labels->buildStringForLanguage( $lang )->fallbackWithEnglish()->getString(); |
792 | if ( $label === null ) { |
793 | return $key; |
794 | } |
795 | |
796 | return $label; |
797 | } |
798 | |
799 | /** |
800 | * Returns the natural language label of a given ZArgument in the language |
801 | * passed as parameter or available fallback languages. If not available, |
802 | * returns the non-translated ZKey. |
803 | * |
804 | * |
805 | * @param string $key |
806 | * @param ZPersistentObject $zobject |
807 | * @param Language $lang |
808 | * @return string |
809 | */ |
810 | public static function getLabelOfFunctionArgument( $key, $zobject, $lang ): string { |
811 | $zfunction = $zobject->getInnerZObject(); |
812 | if ( !( $zfunction instanceof ZFunction ) ) { |
813 | return $key; |
814 | } |
815 | |
816 | $zargs = $zfunction->getArgumentDeclarations(); |
817 | foreach ( $zargs as $zarg ) { |
818 | $zargid = $zarg->getValueByKey( ZTypeRegistry::Z_ARGUMENTDECLARATION_ID ); |
819 | |
820 | if ( $key === $zargid->getZValue() ) { |
821 | $labels = $zarg->getValueByKey( ZTypeRegistry::Z_ARGUMENTDECLARATION_LABEL ); |
822 | $label = $labels->buildStringForLanguage( $lang )->fallbackWithEnglish()->getString(); |
823 | if ( $label === null ) { |
824 | return $key; |
825 | } |
826 | return $label; |
827 | } |
828 | } |
829 | |
830 | // key not found in argument list |
831 | return $key; |
832 | } |
833 | |
834 | /** |
835 | * Translates a serialized ZObject from Zids and ZKeys to natural language in the |
836 | * language passed as parameter or available fallback languages. |
837 | * |
838 | * @param stdClass|array|string $zobject |
839 | * @param ZPersistentObject[] $data |
840 | * @param Language $lang |
841 | * @return stdClass|array|string |
842 | */ |
843 | public static function extractHumanReadableZObject( $zobject, $data, $lang ) { |
844 | if ( is_string( $zobject ) ) { |
845 | if ( self::isValidZObjectReference( $zobject ) ) { |
846 | if ( array_key_exists( $zobject, $data ) ) { |
847 | return self::getLabelOfReference( $zobject, $data[ $zobject ], $lang ); |
848 | } |
849 | } |
850 | return $zobject; |
851 | } |
852 | |
853 | if ( is_array( $zobject ) ) { |
854 | return array_map( function ( $item ) use ( $data, $lang ) { |
855 | return self::extractHumanReadableZObject( $item, $data, $lang ); |
856 | }, $zobject ); |
857 | } |
858 | |
859 | if ( !is_object( $zobject ) ) { |
860 | // Fall through: invalid syntax error |
861 | throw new ZErrorException( |
862 | ZErrorFactory::createZErrorInstance( |
863 | ZErrorTypeRegistry::Z_ERROR_INVALID_SYNTAX, |
864 | [ |
865 | 'data' => $zobject |
866 | ] |
867 | ) |
868 | ); |
869 | } |
870 | |
871 | $labelized = []; |
872 | foreach ( $zobject as $key => $value ) { |
873 | // Labelize key: |
874 | if ( self::isValidZObjectGlobalKey( $key ) ) { |
875 | // If $key is a global key (ZnK1, ZnK2...), typeZid contains the |
876 | // Zid where we can find the key definition. |
877 | $typeZid = self::getZObjectReferenceFromKey( $key ); |
878 | if ( array_key_exists( $typeZid, $data ) ) { |
879 | $labelizedKey = self::getLabelOfGlobalKey( $key, $data[ $typeZid ], $lang ); |
880 | } else { |
881 | $labelizedKey = $key; |
882 | } |
883 | } else { |
884 | // If $key is local, get the type from $zobject[ Z1K1 ] |
885 | $labelizedKey = self::getLabelOfLocalKey( $key, $zobject, $data, $lang ); |
886 | } |
887 | |
888 | // Labelize value: |
889 | $labelizedValue = in_array( $key, ZTypeRegistry::IGNORE_KEY_VALUES_FOR_LABELLING ) |
890 | ? $value |
891 | : self::extractHumanReadableZObject( $value, $data, $lang ); |
892 | |
893 | // Exception: labelized key already exists |
894 | if ( array_key_exists( $labelizedKey, $labelized ) ) { |
895 | $labelized[ "$labelizedKey ($key)" ] = $labelizedValue; |
896 | } else { |
897 | $labelized[ $labelizedKey ] = $labelizedValue; |
898 | } |
899 | |
900 | } |
901 | return (object)$labelized; |
902 | } |
903 | |
904 | /** |
905 | * @param ZObject $accepted The ZObject we accept (typically a ZReference) |
906 | * @param ZObject $input A ZObject we're looking to evaluate whether it's compatible |
907 | * @return bool True if the types are compatible |
908 | */ |
909 | public static function isCompatibleType( ZObject $accepted, ZObject $input ): bool { |
910 | // Do we accept anything? If so, go ahead. |
911 | if ( $accepted->getZValue() === 'Z1' ) { |
912 | return true; |
913 | } |
914 | |
915 | // Are we being given a reference? If so, go ahead. |
916 | // TODO (T318588): Dereference this to see if it is actually to an allowed object? |
917 | if ( $input instanceof ZReference ) { |
918 | return true; |
919 | } |
920 | |
921 | // Are we being given a function call? If so, go ahead. |
922 | // TODO (T318588): Execute this to see if it is actually to an allowed object? |
923 | if ( $input instanceof ZFunctionCall ) { |
924 | return true; |
925 | } |
926 | |
927 | // Do we exactly match by type what's accepted? If so, go ahead. |
928 | if ( $accepted->getZValue() === $input->getZTypeObject()->getZValue() ) { |
929 | return true; |
930 | } |
931 | |
932 | // Otherwise, no. |
933 | return false; |
934 | } |
935 | |
936 | /** |
937 | * Get the ZID of the input if it's a persistent ZObject or a reference to one. |
938 | * |
939 | * @param mixed $zobject The ZObject to examine for |
940 | * @return string The ZID of the given ZObject, or Z0 |
941 | */ |
942 | public static function getZid( $zobject ): string { |
943 | if ( $zobject instanceof ZObject ) { |
944 | if ( $zobject instanceof ZReference || $zobject->getZType() === ZTypeRegistry::Z_REFERENCE ) { |
945 | return $zobject->getValueByKey( ZTypeRegistry::Z_REFERENCE_VALUE ); |
946 | } |
947 | if ( $zobject instanceof ZPersistentObject || $zobject->getZType() === ZTypeRegistry::Z_PERSISTENTOBJECT ) { |
948 | return $zobject |
949 | ->getValueByKey( ZTypeRegistry::Z_PERSISTENTOBJECT_ID ) |
950 | ->getValueByKey( ZTypeRegistry::Z_STRING_VALUE ); |
951 | } |
952 | } |
953 | |
954 | // Use placeholder ZID for non-persisted objects. |
955 | return ZTypeRegistry::Z_NULL_REFERENCE; |
956 | } |
957 | |
958 | /** |
959 | * Walk a given input ZObject, and make a cache key constructed of its keys and values, with any |
960 | * ZObject referenced being expanded to also include its revision ID. |
961 | * |
962 | * E.g. { "Z1K1": "Z7", "Z7K1": "Z801", "Z801K1": "Hey" } => 'Z1K1|Z7#1,Z7K1|Z801#2,Z801K1|Hey' |
963 | * |
964 | * TODO (T338245): Is this cache key too broad? Can we simplify? |
965 | * |
966 | * TODO (T338246): When a Z7/Function call, we also need to poison the key with the revision ID of the |
967 | * relevant implementation, but we don't know which was selected, as that's the call of the |
968 | * function orchestrator. |
969 | * |
970 | * @param \stdClass|array $query |
971 | * @return string response object returned by orchestrator |
972 | */ |
973 | public static function makeCacheKeyFromZObject( $query ): string { |
974 | $accumulator = ''; |
975 | |
976 | foreach ( $query as $key => $value ) { |
977 | $accumulator .= $key . '|'; |
978 | if ( is_array( $value ) || is_object( $value ) ) { |
979 | $accumulator .= self::makeCacheKeyFromZObject( $value ); |
980 | } elseif ( is_scalar( $value ) ) { |
981 | $accumulator .= $value; |
982 | // Special-case: If this is a ZObject reference, also append the object's revision ID to cache-bust |
983 | if ( is_string( $value ) && self::isValidZObjectReference( $value ) ) { |
984 | $accumulator .= '#' . Title::newFromDBkey( $value )->getLatestRevID(); |
985 | } |
986 | } |
987 | $accumulator .= ','; |
988 | } |
989 | |
990 | return $accumulator; |
991 | } |
992 | |
993 | } |