Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.19% covered (success)
99.19%
367 / 370
90.62% covered (success)
90.62%
29 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectUtils
99.19% covered (success)
99.19%
367 / 370
90.62% covered (success)
90.62%
29 / 32
164
0.00% covered (danger)
0.00%
0 / 1
 wrapBCP47CodeInFakeCodexChip
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 isValidSerialisedZObject
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 isValidZObject
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 isValidZObjectList
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
 isValidZObjectResolver
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 isValidZObjectRecord
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 canonicalize
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
17
 orderZKeyIDs
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
11
 canonicalizeZRecord
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 comparableString
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 filterZMultilingualStringsToLanguage
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
10
 getPreferredMonolingualObject
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 isTypeEqualTo
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 isValidZObjectReference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isNullReference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValidOrNullZObjectReference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isValidId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValidZObjectKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValidZObjectGlobalKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getZObjectReferenceFromKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getIterativeList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRequiredZids
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 getLabelOfReference
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLabelOfGlobalKey
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getLabelOfLocalKey
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 getLabelOfErrorTypeKey
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getLabelOfTypeKey
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getLabelOfFunctionArgument
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 extractHumanReadableZObject
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
11
 isCompatibleType
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getZid
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 makeCacheKeyFromZObject
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
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
11namespace MediaWiki\Extension\WikiLambda;
12
13use JsonException;
14use Language;
15use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZFunction;
18use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZObject;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZPersistentObject;
21use MediaWiki\Extension\WikiLambda\ZObjects\ZReference;
22use MediaWiki\Extension\WikiLambda\ZObjects\ZType;
23use MediaWiki\Extension\WikiLambda\ZObjects\ZTypedList;
24use MediaWiki\Html\Html;
25use MediaWiki\Title\Title;
26use Normalizer;
27use stdClass;
28use Transliterator;
29
30class 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}