Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.69% |
392 / 414 |
|
75.00% |
30 / 40 |
CRAP | |
0.00% |
0 / 1 |
ApiResult | |
94.69% |
392 / 414 |
|
75.00% |
30 / 40 |
213.43 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setErrorFormatter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
serializeForApiResult | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reset | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getResultData | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
getSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setValue | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
12 | |||
validateValue | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
15 | |||
addValue | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
unsetValue | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
removeValue | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
setContentValue | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
addContentValue | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
addParsedLimit | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setContentField | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
addContentField | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
setSubelementsList | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addSubelementsList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unsetSubelementsList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
removeSubelementsList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setIndexedTagName | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
addIndexedTagName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setIndexedTagNameRecursive | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
5.12 | |||
addIndexedTagNameRecursive | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setPreserveKeysList | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addPreserveKeysList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unsetPreserveKeysList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
removePreserveKeysList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setArrayType | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
addArrayType | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setArrayTypeRecursive | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
addArrayTypeRecursive | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isMetadataKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
applyTransformations | |
96.48% |
137 / 142 |
|
0.00% |
0 / 1 |
68 | |||
stripMetadata | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
11 | |||
stripMetadataNonRecursive | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
10 | |||
size | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
path | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
8.06 | |||
addMetadataToResultVars | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
10 | |||
formatExpiry | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\MediaWikiServices; |
22 | |
23 | /** |
24 | * This class represents the result of the API operations. |
25 | * It simply wraps a nested array structure, adding some functions to simplify |
26 | * array's modifications. As various modules execute, they add different pieces |
27 | * of information to this result, structuring it as it will be given to the client. |
28 | * |
29 | * Each subarray may either be a dictionary - key-value pairs with unique keys, |
30 | * or lists, where the items are added using $data[] = $value notation. |
31 | * |
32 | * @since 1.25 this is no longer a subclass of ApiBase |
33 | * @ingroup API |
34 | */ |
35 | class ApiResult implements ApiSerializable { |
36 | |
37 | /** |
38 | * Override existing value in addValue(), setValue(), and similar functions |
39 | * @since 1.21 |
40 | */ |
41 | public const OVERRIDE = 1; |
42 | |
43 | /** |
44 | * For addValue(), setValue() and similar functions, if the value does not |
45 | * exist, add it as the first element. In case the new value has no name |
46 | * (numerical index), all indexes will be renumbered. |
47 | * @since 1.21 |
48 | */ |
49 | public const ADD_ON_TOP = 2; |
50 | |
51 | /** |
52 | * For addValue() and similar functions, do not check size while adding a value |
53 | * Don't use this unless you REALLY know what you're doing. |
54 | * Values added while the size checking was disabled will never be counted. |
55 | * Ignored for setValue() and similar functions. |
56 | * @since 1.24 |
57 | */ |
58 | public const NO_SIZE_CHECK = 4; |
59 | |
60 | /** |
61 | * For addValue(), setValue() and similar functions, do not validate data. |
62 | * Also disables size checking. If you think you need to use this, you're |
63 | * probably wrong. |
64 | * @since 1.25 |
65 | */ |
66 | public const NO_VALIDATE = self::NO_SIZE_CHECK | 8; |
67 | |
68 | /** |
69 | * Key for the 'indexed tag name' metadata item. Value is string. |
70 | * @since 1.25 |
71 | */ |
72 | public const META_INDEXED_TAG_NAME = '_element'; |
73 | |
74 | /** |
75 | * Key for the 'subelements' metadata item. Value is string[]. |
76 | * @since 1.25 |
77 | */ |
78 | public const META_SUBELEMENTS = '_subelements'; |
79 | |
80 | /** |
81 | * Key for the 'preserve keys' metadata item. Value is string[]. |
82 | * @since 1.25 |
83 | */ |
84 | public const META_PRESERVE_KEYS = '_preservekeys'; |
85 | |
86 | /** |
87 | * Key for the 'content' metadata item. Value is string. |
88 | * @since 1.25 |
89 | */ |
90 | public const META_CONTENT = '_content'; |
91 | |
92 | /** |
93 | * Key for the 'type' metadata item. Value is one of the following strings: |
94 | * - default: Like 'array' if all (non-metadata) keys are numeric with no |
95 | * gaps, otherwise like 'assoc'. |
96 | * - array: Keys are used for ordering, but are not output. In a format |
97 | * like JSON, outputs as []. |
98 | * - assoc: In a format like JSON, outputs as {}. |
99 | * - kvp: For a format like XML where object keys have a restricted |
100 | * character set, use an alternative output format. For example, |
101 | * <container><item name="key">value</item></container> rather than |
102 | * <container key="value" /> |
103 | * - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode. |
104 | * - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode. |
105 | * - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces |
106 | * the alternative output format for all formats, for example |
107 | * [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set. |
108 | * @since 1.25 |
109 | */ |
110 | public const META_TYPE = '_type'; |
111 | |
112 | /** |
113 | * Key for the metadata item whose value specifies the name used for the |
114 | * kvp key in the alternative output format with META_TYPE 'kvp' or |
115 | * 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>. |
116 | * Value is string. |
117 | * @since 1.25 |
118 | */ |
119 | public const META_KVP_KEY_NAME = '_kvpkeyname'; |
120 | |
121 | /** |
122 | * Key for the metadata item that indicates that the KVP key should be |
123 | * added into an assoc value, i.e. {"key":{"val1":"a","val2":"b"}} |
124 | * transforms to {"name":"key","val1":"a","val2":"b"} rather than |
125 | * {"name":"key","value":{"val1":"a","val2":"b"}}. |
126 | * Value is boolean. |
127 | * @since 1.26 |
128 | */ |
129 | public const META_KVP_MERGE = '_kvpmerge'; |
130 | |
131 | /** |
132 | * Key for the 'BC bools' metadata item. Value is string[]. |
133 | * Note no setter is provided. |
134 | * @since 1.25 |
135 | */ |
136 | public const META_BC_BOOLS = '_BC_bools'; |
137 | |
138 | /** |
139 | * Key for the 'BC subelements' metadata item. Value is string[]. |
140 | * Note no setter is provided. |
141 | * @since 1.25 |
142 | */ |
143 | public const META_BC_SUBELEMENTS = '_BC_subelements'; |
144 | |
145 | private $data; |
146 | private int $size; |
147 | /** @var int|false */ |
148 | private $maxSize; |
149 | private $errorFormatter; |
150 | |
151 | /** |
152 | * @param int|false $maxSize Maximum result "size", or false for no limit |
153 | */ |
154 | public function __construct( $maxSize ) { |
155 | $this->maxSize = $maxSize; |
156 | $this->reset(); |
157 | } |
158 | |
159 | /** |
160 | * @since 1.25 |
161 | * @param ApiErrorFormatter $formatter |
162 | */ |
163 | public function setErrorFormatter( ApiErrorFormatter $formatter ) { |
164 | $this->errorFormatter = $formatter; |
165 | } |
166 | |
167 | /** |
168 | * Allow for adding one ApiResult into another |
169 | * @since 1.25 |
170 | * @return mixed |
171 | */ |
172 | public function serializeForApiResult() { |
173 | return $this->data; |
174 | } |
175 | |
176 | /***************************************************************************/ |
177 | // region Content |
178 | /** @name Content */ |
179 | |
180 | /** |
181 | * Clear the current result data. |
182 | */ |
183 | public function reset() { |
184 | $this->data = [ |
185 | self::META_TYPE => 'assoc', // Usually what's desired |
186 | ]; |
187 | $this->size = 0; |
188 | } |
189 | |
190 | /** |
191 | * Get the result data array |
192 | * |
193 | * The returned value should be considered read-only. |
194 | * |
195 | * Transformations include: |
196 | * |
197 | * Custom: (callable) Applied before other transformations. Signature is |
198 | * function ( &$data, &$metadata ), return value is ignored. Called for |
199 | * each nested array. |
200 | * |
201 | * BC: (array) This transformation does various adjustments to bring the |
202 | * output in line with the pre-1.25 result format. The value array is a |
203 | * list of flags: 'nobool', 'no*', 'nosub'. |
204 | * - Boolean-valued items are changed to '' if true or removed if false, |
205 | * unless listed in META_BC_BOOLS. This may be skipped by including |
206 | * 'nobool' in the value array. |
207 | * - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is |
208 | * set to '*'. This may be skipped by including 'no*' in the value |
209 | * array. |
210 | * - Tags listed in META_BC_SUBELEMENTS will have their values changed to |
211 | * [ '*' => $value ]. This may be skipped by including 'nosub' in |
212 | * the value array. |
213 | * - If META_TYPE is 'BCarray', set it to 'default' |
214 | * - If META_TYPE is 'BCassoc', set it to 'default' |
215 | * - If META_TYPE is 'BCkvp', perform the transformation (even if |
216 | * the Types transformation is not being applied). |
217 | * |
218 | * Types: (assoc) Apply transformations based on META_TYPE. The values |
219 | * array is an associative array with the following possible keys: |
220 | * - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc' |
221 | * as objects. |
222 | * - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp' |
223 | * and 'BCkvp' into arrays of two-element arrays, something like this: |
224 | * $output = []; |
225 | * foreach ( $input as $key => $value ) { |
226 | * $pair = []; |
227 | * $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key; |
228 | * ApiResult::setContentValue( $pair, 'value', $value ); |
229 | * $output[] = $pair; |
230 | * } |
231 | * |
232 | * Strip: (string) Strips metadata keys from the result. |
233 | * - 'all': Strip all metadata, recursively |
234 | * - 'base': Strip metadata at the top-level only. |
235 | * - 'none': Do not strip metadata. |
236 | * - 'bc': Like 'all', but leave certain pre-1.25 keys. |
237 | * |
238 | * @since 1.25 |
239 | * @param array|string|null $path Path to fetch, see ApiResult::addValue |
240 | * @param array $transforms See above |
241 | * @return mixed Result data, or null if not found |
242 | */ |
243 | public function getResultData( $path = [], $transforms = [] ) { |
244 | $path = (array)$path; |
245 | if ( !$path ) { |
246 | return self::applyTransformations( $this->data, $transforms ); |
247 | } |
248 | |
249 | $last = array_pop( $path ); |
250 | $ret = &$this->path( $path, 'dummy' ); |
251 | if ( !isset( $ret[$last] ) ) { |
252 | return null; |
253 | } elseif ( is_array( $ret[$last] ) ) { |
254 | return self::applyTransformations( $ret[$last], $transforms ); |
255 | } else { |
256 | return $ret[$last]; |
257 | } |
258 | } |
259 | |
260 | /** |
261 | * Get the size of the result, i.e. the amount of bytes in it |
262 | * @return int |
263 | */ |
264 | public function getSize() { |
265 | return $this->size; |
266 | } |
267 | |
268 | /** |
269 | * Add an output value to the array by name. |
270 | * |
271 | * Verifies that value with the same name has not been added before. |
272 | * |
273 | * @since 1.25 |
274 | * @param array &$arr To add $value to |
275 | * @param string|int|null $name Index of $arr to add $value at, |
276 | * or null to use the next numeric index. |
277 | * @param mixed $value |
278 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
279 | */ |
280 | public static function setValue( array &$arr, $name, $value, $flags = 0 ) { |
281 | if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) { |
282 | $value = self::validateValue( $value ); |
283 | } |
284 | |
285 | if ( $name === null ) { |
286 | if ( $flags & self::ADD_ON_TOP ) { |
287 | array_unshift( $arr, $value ); |
288 | } else { |
289 | $arr[] = $value; |
290 | } |
291 | return; |
292 | } |
293 | |
294 | $exists = isset( $arr[$name] ); |
295 | if ( !$exists || ( $flags & self::OVERRIDE ) ) { |
296 | if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) { |
297 | $arr = [ $name => $value ] + $arr; |
298 | } else { |
299 | $arr[$name] = $value; |
300 | } |
301 | } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) { |
302 | $conflicts = array_intersect_key( $arr[$name], $value ); |
303 | if ( !$conflicts ) { |
304 | $arr[$name] += $value; |
305 | } else { |
306 | $keys = implode( ', ', array_keys( $conflicts ) ); |
307 | throw new RuntimeException( |
308 | "Conflicting keys ($keys) when attempting to merge element $name" |
309 | ); |
310 | } |
311 | } elseif ( $value !== $arr[$name] ) { |
312 | throw new RuntimeException( |
313 | "Attempting to add element $name=$value, existing value is {$arr[$name]}" |
314 | ); |
315 | } |
316 | } |
317 | |
318 | /** |
319 | * Validate a value for addition to the result |
320 | * @param mixed $value |
321 | * @return array|mixed|string |
322 | */ |
323 | private static function validateValue( $value ) { |
324 | if ( is_object( $value ) ) { |
325 | // Note we use is_callable() here instead of instanceof because |
326 | // ApiSerializable is an informal protocol (see docs there for details). |
327 | if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) { |
328 | $oldValue = $value; |
329 | $value = $value->serializeForApiResult(); |
330 | if ( is_object( $value ) ) { |
331 | throw new UnexpectedValueException( |
332 | get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' . |
333 | get_class( $value ) |
334 | ); |
335 | } |
336 | |
337 | // Recursive call instead of fall-through so we can throw a |
338 | // better exception message. |
339 | try { |
340 | return self::validateValue( $value ); |
341 | } catch ( Exception $ex ) { |
342 | throw new UnexpectedValueException( |
343 | get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' . |
344 | $ex->getMessage(), |
345 | 0, |
346 | $ex |
347 | ); |
348 | } |
349 | } elseif ( is_callable( [ $value, '__toString' ] ) ) { |
350 | $value = (string)$value; |
351 | } else { |
352 | $value = (array)$value + [ self::META_TYPE => 'assoc' ]; |
353 | } |
354 | } |
355 | |
356 | if ( is_string( $value ) ) { |
357 | // Optimization: avoid querying the service locator for each value. |
358 | static $contentLanguage = null; |
359 | if ( !$contentLanguage ) { |
360 | $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage(); |
361 | } |
362 | $value = $contentLanguage->normalize( $value ); |
363 | } elseif ( is_array( $value ) ) { |
364 | foreach ( $value as $k => $v ) { |
365 | $value[$k] = self::validateValue( $v ); |
366 | } |
367 | } elseif ( $value !== null && !is_scalar( $value ) ) { |
368 | $type = gettype( $value ); |
369 | // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource |
370 | if ( is_resource( $value ) ) { |
371 | $type .= '(' . get_resource_type( $value ) . ')'; |
372 | } |
373 | throw new InvalidArgumentException( "Cannot add $type to ApiResult" ); |
374 | } elseif ( is_float( $value ) && !is_finite( $value ) ) { |
375 | throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' ); |
376 | } |
377 | |
378 | return $value; |
379 | } |
380 | |
381 | /** |
382 | * Add value to the output data at the given path. |
383 | * |
384 | * Path can be an indexed array, each element specifying the branch at which to add the new |
385 | * value. Setting $path to [ 'a', 'b', 'c' ] is equivalent to data['a']['b']['c'] = $value. |
386 | * If $path is null, the value will be inserted at the data root. |
387 | * |
388 | * @param array|string|int|null $path |
389 | * @param string|int|null $name See ApiResult::setValue() |
390 | * @param mixed $value |
391 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
392 | * This parameter used to be boolean, and the value of OVERRIDE=1 was specifically |
393 | * chosen so that it would be backwards compatible with the new method signature. |
394 | * @return bool True if $value fits in the result, false if not |
395 | * @since 1.21 int $flags replaced boolean $override |
396 | */ |
397 | public function addValue( $path, $name, $value, $flags = 0 ) { |
398 | $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' ); |
399 | |
400 | if ( !( $flags & self::NO_SIZE_CHECK ) ) { |
401 | // self::size needs the validated value. Then flag |
402 | // to not re-validate later. |
403 | $value = self::validateValue( $value ); |
404 | $flags |= self::NO_VALIDATE; |
405 | |
406 | $newsize = $this->size + self::size( $value ); |
407 | if ( $this->maxSize !== false && $newsize > $this->maxSize ) { |
408 | $this->errorFormatter->addWarning( |
409 | 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ] |
410 | ); |
411 | return false; |
412 | } |
413 | $this->size = $newsize; |
414 | } |
415 | |
416 | self::setValue( $arr, $name, $value, $flags ); |
417 | return true; |
418 | } |
419 | |
420 | /** |
421 | * Remove an output value to the array by name. |
422 | * @param array &$arr To remove $value from |
423 | * @param string|int $name Index of $arr to remove |
424 | * @return mixed Old value, or null |
425 | */ |
426 | public static function unsetValue( array &$arr, $name ) { |
427 | $ret = null; |
428 | if ( isset( $arr[$name] ) ) { |
429 | $ret = $arr[$name]; |
430 | unset( $arr[$name] ); |
431 | } |
432 | return $ret; |
433 | } |
434 | |
435 | /** |
436 | * Remove value from the output data at the given path. |
437 | * |
438 | * @since 1.25 |
439 | * @param array|string|null $path See ApiResult::addValue() |
440 | * @param string|int|null $name Index to remove at $path. |
441 | * If null, $path itself is removed. |
442 | * @param int $flags Flags used when adding the value |
443 | * @return mixed Old value, or null |
444 | */ |
445 | public function removeValue( $path, $name, $flags = 0 ) { |
446 | $path = (array)$path; |
447 | if ( $name === null ) { |
448 | if ( !$path ) { |
449 | throw new InvalidArgumentException( 'Cannot remove the data root' ); |
450 | } |
451 | $name = array_pop( $path ); |
452 | } |
453 | $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name ); |
454 | if ( !( $flags & self::NO_SIZE_CHECK ) ) { |
455 | $newsize = $this->size - self::size( $ret ); |
456 | $this->size = max( $newsize, 0 ); |
457 | } |
458 | return $ret; |
459 | } |
460 | |
461 | /** |
462 | * Add an output value to the array by name and mark as META_CONTENT. |
463 | * |
464 | * @since 1.25 |
465 | * @param array &$arr To add $value to |
466 | * @param string|int $name Index of $arr to add $value at. |
467 | * @param mixed $value |
468 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
469 | */ |
470 | public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) { |
471 | if ( $name === null ) { |
472 | throw new InvalidArgumentException( 'Content value must be named' ); |
473 | } |
474 | self::setContentField( $arr, $name, $flags ); |
475 | self::setValue( $arr, $name, $value, $flags ); |
476 | } |
477 | |
478 | /** |
479 | * Add value to the output data at the given path and mark as META_CONTENT |
480 | * |
481 | * @since 1.25 |
482 | * @param array|string|null $path See ApiResult::addValue() |
483 | * @param string|int $name See ApiResult::setValue() |
484 | * @param mixed $value |
485 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
486 | * @return bool True if $value fits in the result, false if not |
487 | */ |
488 | public function addContentValue( $path, $name, $value, $flags = 0 ) { |
489 | if ( $name === null ) { |
490 | throw new InvalidArgumentException( 'Content value must be named' ); |
491 | } |
492 | $this->addContentField( $path, $name, $flags ); |
493 | return $this->addValue( $path, $name, $value, $flags ); |
494 | } |
495 | |
496 | /** |
497 | * Add the numeric limit for a limit=max to the result. |
498 | * |
499 | * @since 1.25 |
500 | * @param string $moduleName |
501 | * @param int $limit |
502 | */ |
503 | public function addParsedLimit( $moduleName, $limit ) { |
504 | // Add value, allowing overwriting |
505 | $this->addValue( 'limits', $moduleName, $limit, |
506 | self::OVERRIDE | self::NO_SIZE_CHECK ); |
507 | } |
508 | |
509 | // endregion -- end of Content |
510 | |
511 | /***************************************************************************/ |
512 | // region Metadata |
513 | /** @name Metadata */ |
514 | |
515 | /** |
516 | * Set the name of the content field name (META_CONTENT) |
517 | * |
518 | * @since 1.25 |
519 | * @param array &$arr |
520 | * @param string|int $name Name of the field |
521 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
522 | */ |
523 | public static function setContentField( array &$arr, $name, $flags = 0 ) { |
524 | if ( isset( $arr[self::META_CONTENT] ) && |
525 | isset( $arr[$arr[self::META_CONTENT]] ) && |
526 | !( $flags & self::OVERRIDE ) |
527 | ) { |
528 | throw new RuntimeException( |
529 | "Attempting to set content element as $name when " . $arr[self::META_CONTENT] . |
530 | ' is already set as the content element' |
531 | ); |
532 | } |
533 | $arr[self::META_CONTENT] = $name; |
534 | } |
535 | |
536 | /** |
537 | * Set the name of the content field name (META_CONTENT) |
538 | * |
539 | * @since 1.25 |
540 | * @param array|string|null $path See ApiResult::addValue() |
541 | * @param string|int $name Name of the field |
542 | * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. |
543 | */ |
544 | public function addContentField( $path, $name, $flags = 0 ) { |
545 | $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' ); |
546 | self::setContentField( $arr, $name, $flags ); |
547 | } |
548 | |
549 | /** |
550 | * Causes the elements with the specified names to be output as |
551 | * subelements rather than attributes. |
552 | * @since 1.25 is static |
553 | * @param array &$arr |
554 | * @param array|string|int $names The element name(s) to be output as subelements |
555 | */ |
556 | public static function setSubelementsList( array &$arr, $names ) { |
557 | if ( !isset( $arr[self::META_SUBELEMENTS] ) ) { |
558 | $arr[self::META_SUBELEMENTS] = (array)$names; |
559 | } else { |
560 | $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names ); |
561 | } |
562 | } |
563 | |
564 | /** |
565 | * Causes the elements with the specified names to be output as |
566 | * subelements rather than attributes. |
567 | * @since 1.25 |
568 | * @param array|string|null $path See ApiResult::addValue() |
569 | * @param array|string|int $names The element name(s) to be output as subelements |
570 | */ |
571 | public function addSubelementsList( $path, $names ) { |
572 | $arr = &$this->path( $path ); |
573 | self::setSubelementsList( $arr, $names ); |
574 | } |
575 | |
576 | /** |
577 | * Causes the elements with the specified names to be output as |
578 | * attributes (when possible) rather than as subelements. |
579 | * @since 1.25 |
580 | * @param array &$arr |
581 | * @param array|string|int $names The element name(s) to not be output as subelements |
582 | */ |
583 | public static function unsetSubelementsList( array &$arr, $names ) { |
584 | if ( isset( $arr[self::META_SUBELEMENTS] ) ) { |
585 | $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names ); |
586 | } |
587 | } |
588 | |
589 | /** |
590 | * Causes the elements with the specified names to be output as |
591 | * attributes (when possible) rather than as subelements. |
592 | * @since 1.25 |
593 | * @param array|string|null $path See ApiResult::addValue() |
594 | * @param array|string|int $names The element name(s) to not be output as subelements |
595 | */ |
596 | public function removeSubelementsList( $path, $names ) { |
597 | $arr = &$this->path( $path ); |
598 | self::unsetSubelementsList( $arr, $names ); |
599 | } |
600 | |
601 | /** |
602 | * Set the tag name for numeric-keyed values in XML format |
603 | * @since 1.25 is static |
604 | * @param array &$arr |
605 | * @param string $tag Tag name |
606 | */ |
607 | public static function setIndexedTagName( array &$arr, $tag ) { |
608 | if ( !is_string( $tag ) ) { |
609 | throw new InvalidArgumentException( 'Bad tag name' ); |
610 | } |
611 | $arr[self::META_INDEXED_TAG_NAME] = $tag; |
612 | } |
613 | |
614 | /** |
615 | * Set the tag name for numeric-keyed values in XML format |
616 | * @since 1.25 |
617 | * @param array|string|null $path See ApiResult::addValue() |
618 | * @param string $tag Tag name |
619 | */ |
620 | public function addIndexedTagName( $path, $tag ) { |
621 | $arr = &$this->path( $path ); |
622 | self::setIndexedTagName( $arr, $tag ); |
623 | } |
624 | |
625 | /** |
626 | * Set indexed tag name on $arr and all subarrays |
627 | * |
628 | * @since 1.25 |
629 | * @param array &$arr |
630 | * @param string $tag Tag name |
631 | */ |
632 | public static function setIndexedTagNameRecursive( array &$arr, $tag ) { |
633 | if ( !is_string( $tag ) ) { |
634 | throw new InvalidArgumentException( 'Bad tag name' ); |
635 | } |
636 | $arr[self::META_INDEXED_TAG_NAME] = $tag; |
637 | foreach ( $arr as $k => &$v ) { |
638 | if ( is_array( $v ) && !self::isMetadataKey( $k ) ) { |
639 | self::setIndexedTagNameRecursive( $v, $tag ); |
640 | } |
641 | } |
642 | } |
643 | |
644 | /** |
645 | * Set indexed tag name on $path and all subarrays |
646 | * |
647 | * @since 1.25 |
648 | * @param array|string|null $path See ApiResult::addValue() |
649 | * @param string $tag Tag name |
650 | */ |
651 | public function addIndexedTagNameRecursive( $path, $tag ) { |
652 | $arr = &$this->path( $path ); |
653 | self::setIndexedTagNameRecursive( $arr, $tag ); |
654 | } |
655 | |
656 | /** |
657 | * Preserve specified keys. |
658 | * |
659 | * This prevents XML name mangling and preventing keys from being removed |
660 | * by self::stripMetadata(). |
661 | * |
662 | * @since 1.25 |
663 | * @param array &$arr |
664 | * @param array|string $names The element name(s) to preserve |
665 | */ |
666 | public static function setPreserveKeysList( array &$arr, $names ) { |
667 | if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) { |
668 | $arr[self::META_PRESERVE_KEYS] = (array)$names; |
669 | } else { |
670 | $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names ); |
671 | } |
672 | } |
673 | |
674 | /** |
675 | * Preserve specified keys. |
676 | * @since 1.25 |
677 | * @see self::setPreserveKeysList() |
678 | * @param array|string|null $path See ApiResult::addValue() |
679 | * @param array|string $names The element name(s) to preserve |
680 | */ |
681 | public function addPreserveKeysList( $path, $names ) { |
682 | $arr = &$this->path( $path ); |
683 | self::setPreserveKeysList( $arr, $names ); |
684 | } |
685 | |
686 | /** |
687 | * Don't preserve specified keys. |
688 | * @since 1.25 |
689 | * @see self::setPreserveKeysList() |
690 | * @param array &$arr |
691 | * @param array|string $names The element name(s) to not preserve |
692 | */ |
693 | public static function unsetPreserveKeysList( array &$arr, $names ) { |
694 | if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) { |
695 | $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names ); |
696 | } |
697 | } |
698 | |
699 | /** |
700 | * Don't preserve specified keys. |
701 | * @since 1.25 |
702 | * @see self::setPreserveKeysList() |
703 | * @param array|string|null $path See ApiResult::addValue() |
704 | * @param array|string $names The element name(s) to not preserve |
705 | */ |
706 | public function removePreserveKeysList( $path, $names ) { |
707 | $arr = &$this->path( $path ); |
708 | self::unsetPreserveKeysList( $arr, $names ); |
709 | } |
710 | |
711 | /** |
712 | * Set the array data type |
713 | * |
714 | * @since 1.25 |
715 | * @param array &$arr |
716 | * @param string $type See ApiResult::META_TYPE |
717 | * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME |
718 | */ |
719 | public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) { |
720 | if ( !in_array( $type, [ |
721 | 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp' |
722 | ], true ) ) { |
723 | throw new InvalidArgumentException( 'Bad type' ); |
724 | } |
725 | $arr[self::META_TYPE] = $type; |
726 | if ( is_string( $kvpKeyName ) ) { |
727 | $arr[self::META_KVP_KEY_NAME] = $kvpKeyName; |
728 | } |
729 | } |
730 | |
731 | /** |
732 | * Set the array data type for a path |
733 | * @since 1.25 |
734 | * @param array|string|null $path See ApiResult::addValue() |
735 | * @param string $tag See ApiResult::META_TYPE |
736 | * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME |
737 | */ |
738 | public function addArrayType( $path, $tag, $kvpKeyName = null ) { |
739 | $arr = &$this->path( $path ); |
740 | self::setArrayType( $arr, $tag, $kvpKeyName ); |
741 | } |
742 | |
743 | /** |
744 | * Set the array data type recursively |
745 | * @since 1.25 |
746 | * @param array &$arr |
747 | * @param string $type See ApiResult::META_TYPE |
748 | * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME |
749 | */ |
750 | public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) { |
751 | self::setArrayType( $arr, $type, $kvpKeyName ); |
752 | foreach ( $arr as $k => &$v ) { |
753 | if ( is_array( $v ) && !self::isMetadataKey( $k ) ) { |
754 | self::setArrayTypeRecursive( $v, $type, $kvpKeyName ); |
755 | } |
756 | } |
757 | } |
758 | |
759 | /** |
760 | * Set the array data type for a path recursively |
761 | * @since 1.25 |
762 | * @param array|string|null $path See ApiResult::addValue() |
763 | * @param string $tag See ApiResult::META_TYPE |
764 | * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME |
765 | */ |
766 | public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) { |
767 | $arr = &$this->path( $path ); |
768 | self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName ); |
769 | } |
770 | |
771 | // endregion -- end of Metadata |
772 | |
773 | /***************************************************************************/ |
774 | // region Utility |
775 | /** @name Utility */ |
776 | |
777 | /** |
778 | * Test whether a key should be considered metadata |
779 | * |
780 | * @param string|int $key |
781 | * @return bool |
782 | */ |
783 | public static function isMetadataKey( $key ) { |
784 | // Optimization: This is a very hot and highly optimized code path. Note that ord() only |
785 | // considers the first character and also works with empty strings and integers. |
786 | // 95 corresponds to the '_' character. |
787 | return ord( $key ) === 95; |
788 | } |
789 | |
790 | /** |
791 | * Apply transformations to an array, returning the transformed array. |
792 | * |
793 | * @see ApiResult::getResultData() |
794 | * @since 1.25 |
795 | * @param array $dataIn |
796 | * @param array $transforms |
797 | * @return array|stdClass |
798 | */ |
799 | protected static function applyTransformations( array $dataIn, array $transforms ) { |
800 | $strip = $transforms['Strip'] ?? 'none'; |
801 | if ( $strip === 'base' ) { |
802 | $transforms['Strip'] = 'none'; |
803 | } |
804 | $transformTypes = $transforms['Types'] ?? null; |
805 | if ( $transformTypes !== null && !is_array( $transformTypes ) ) { |
806 | throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' ); |
807 | } |
808 | |
809 | $metadata = []; |
810 | $data = self::stripMetadataNonRecursive( $dataIn, $metadata ); |
811 | |
812 | if ( isset( $transforms['Custom'] ) ) { |
813 | if ( !is_callable( $transforms['Custom'] ) ) { |
814 | throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' ); |
815 | } |
816 | call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] ); |
817 | } |
818 | |
819 | if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) && |
820 | isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' && |
821 | !isset( $metadata[self::META_KVP_KEY_NAME] ) |
822 | ) { |
823 | throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' . |
824 | 'ApiResult::META_KVP_KEY_NAME metadata item' ); |
825 | } |
826 | |
827 | // BC transformations |
828 | $boolKeys = null; |
829 | if ( isset( $transforms['BC'] ) ) { |
830 | if ( !is_array( $transforms['BC'] ) ) { |
831 | throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' ); |
832 | } |
833 | if ( !in_array( 'nobool', $transforms['BC'], true ) ) { |
834 | $boolKeys = isset( $metadata[self::META_BC_BOOLS] ) |
835 | ? array_fill_keys( $metadata[self::META_BC_BOOLS], true ) |
836 | : []; |
837 | } |
838 | |
839 | if ( !in_array( 'no*', $transforms['BC'], true ) && |
840 | isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*' |
841 | ) { |
842 | $k = $metadata[self::META_CONTENT]; |
843 | $data['*'] = $data[$k]; |
844 | unset( $data[$k] ); |
845 | $metadata[self::META_CONTENT] = '*'; |
846 | } |
847 | |
848 | if ( !in_array( 'nosub', $transforms['BC'], true ) && |
849 | isset( $metadata[self::META_BC_SUBELEMENTS] ) |
850 | ) { |
851 | foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) { |
852 | if ( isset( $data[$k] ) ) { |
853 | $data[$k] = [ |
854 | '*' => $data[$k], |
855 | self::META_CONTENT => '*', |
856 | self::META_TYPE => 'assoc', |
857 | ]; |
858 | } |
859 | } |
860 | } |
861 | |
862 | if ( isset( $metadata[self::META_TYPE] ) ) { |
863 | switch ( $metadata[self::META_TYPE] ) { |
864 | case 'BCarray': |
865 | case 'BCassoc': |
866 | $metadata[self::META_TYPE] = 'default'; |
867 | break; |
868 | case 'BCkvp': |
869 | $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME]; |
870 | break; |
871 | } |
872 | } |
873 | } |
874 | |
875 | // Figure out type, do recursive calls, and do boolean transform if necessary |
876 | $defaultType = 'array'; |
877 | $maxKey = -1; |
878 | foreach ( $data as $k => &$v ) { |
879 | $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v; |
880 | if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) { |
881 | if ( !$v ) { |
882 | unset( $data[$k] ); |
883 | continue; |
884 | } |
885 | $v = ''; |
886 | } |
887 | if ( is_string( $k ) ) { |
888 | $defaultType = 'assoc'; |
889 | } elseif ( $k > $maxKey ) { |
890 | $maxKey = $k; |
891 | } |
892 | } |
893 | unset( $v ); |
894 | |
895 | // Determine which metadata to keep |
896 | switch ( $strip ) { |
897 | case 'all': |
898 | case 'base': |
899 | $keepMetadata = []; |
900 | break; |
901 | case 'none': |
902 | $keepMetadata = &$metadata; |
903 | break; |
904 | case 'bc': |
905 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args |
906 | $keepMetadata = array_intersect_key( $metadata, [ |
907 | self::META_INDEXED_TAG_NAME => 1, |
908 | self::META_SUBELEMENTS => 1, |
909 | ] ); |
910 | break; |
911 | default: |
912 | throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' ); |
913 | } |
914 | |
915 | // No type transformation |
916 | if ( $transformTypes === null ) { |
917 | return $data + $keepMetadata; |
918 | } |
919 | |
920 | if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) { |
921 | $defaultType = 'assoc'; |
922 | } |
923 | |
924 | // Override type, if provided |
925 | $type = $defaultType; |
926 | if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) { |
927 | $type = $metadata[self::META_TYPE]; |
928 | } |
929 | if ( ( $type === 'kvp' || $type === 'BCkvp' ) && |
930 | empty( $transformTypes['ArmorKVP'] ) |
931 | ) { |
932 | $type = 'assoc'; |
933 | } elseif ( $type === 'BCarray' ) { |
934 | $type = 'array'; |
935 | } elseif ( $type === 'BCassoc' ) { |
936 | $type = 'assoc'; |
937 | } |
938 | |
939 | // Apply transformation |
940 | switch ( $type ) { |
941 | case 'assoc': |
942 | $metadata[self::META_TYPE] = 'assoc'; |
943 | $data += $keepMetadata; |
944 | return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data; |
945 | |
946 | case 'array': |
947 | // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys, |
948 | // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions. |
949 | // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number |
950 | // (which yields zero if the string was non-numeric), and then performs the comparison, |
951 | // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by |
952 | // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string |
953 | // and compares those two strings instead. We therefore use a custom comparison function |
954 | // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items |
955 | // irrespective of the PHP version (T326480). |
956 | uksort( $data, static function ( $a, $b ): int { |
957 | // In a comparison of a number or numeric string with a non-numeric string, |
958 | // coerce both values into a string prior to comparing and compare the resulting strings. |
959 | // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace |
960 | // to be numeric, so trim the inputs prior to the numeric checks to make the behavior |
961 | // consistent across PHP versions. |
962 | if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) { |
963 | return (string)$a <=> (string)$b; |
964 | } |
965 | |
966 | return $a <=> $b; |
967 | } ); |
968 | |
969 | $data = array_values( $data ); |
970 | $metadata[self::META_TYPE] = 'array'; |
971 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args |
972 | return $data + $keepMetadata; |
973 | |
974 | case 'kvp': |
975 | case 'BCkvp': |
976 | $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP']; |
977 | $valKey = isset( $transforms['BC'] ) ? '*' : 'value'; |
978 | $assocAsObject = !empty( $transformTypes['AssocAsObject'] ); |
979 | $merge = !empty( $metadata[self::META_KVP_MERGE] ); |
980 | |
981 | $ret = []; |
982 | foreach ( $data as $k => $v ) { |
983 | if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) { |
984 | $vArr = (array)$v; |
985 | if ( isset( $vArr[self::META_TYPE] ) ) { |
986 | $mergeType = $vArr[self::META_TYPE]; |
987 | } elseif ( is_object( $v ) ) { |
988 | $mergeType = 'assoc'; |
989 | } else { |
990 | $keys = array_keys( $vArr ); |
991 | sort( $keys, SORT_NUMERIC ); |
992 | $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc'; |
993 | } |
994 | } else { |
995 | $mergeType = 'n/a'; |
996 | } |
997 | if ( $mergeType === 'assoc' ) { |
998 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used |
999 | $item = $vArr + [ |
1000 | $key => $k, |
1001 | ]; |
1002 | if ( $strip === 'none' ) { |
1003 | self::setPreserveKeysList( $item, [ $key ] ); |
1004 | } |
1005 | } else { |
1006 | $item = [ |
1007 | $key => $k, |
1008 | $valKey => $v, |
1009 | ]; |
1010 | if ( $strip === 'none' ) { |
1011 | $item += [ |
1012 | self::META_PRESERVE_KEYS => [ $key ], |
1013 | self::META_CONTENT => $valKey, |
1014 | self::META_TYPE => 'assoc', |
1015 | ]; |
1016 | } |
1017 | } |
1018 | $ret[] = $assocAsObject ? (object)$item : $item; |
1019 | } |
1020 | $metadata[self::META_TYPE] = 'array'; |
1021 | |
1022 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args |
1023 | return $ret + $keepMetadata; |
1024 | |
1025 | default: |
1026 | throw new UnexpectedValueException( "Unknown type '$type'" ); |
1027 | } |
1028 | } |
1029 | |
1030 | /** |
1031 | * Recursively remove metadata keys from a data array or object |
1032 | * |
1033 | * Note this removes all potential metadata keys, not just the defined |
1034 | * ones. |
1035 | * |
1036 | * @since 1.25 |
1037 | * @param array|stdClass $data |
1038 | * @return array|stdClass |
1039 | */ |
1040 | public static function stripMetadata( $data ) { |
1041 | if ( is_array( $data ) || is_object( $data ) ) { |
1042 | $isObj = is_object( $data ); |
1043 | if ( $isObj ) { |
1044 | $data = (array)$data; |
1045 | } |
1046 | $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] ) |
1047 | ? (array)$data[self::META_PRESERVE_KEYS] |
1048 | : []; |
1049 | foreach ( $data as $k => $v ) { |
1050 | if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) { |
1051 | unset( $data[$k] ); |
1052 | } elseif ( is_array( $v ) || is_object( $v ) ) { |
1053 | $data[$k] = self::stripMetadata( $v ); |
1054 | } |
1055 | } |
1056 | if ( $isObj ) { |
1057 | $data = (object)$data; |
1058 | } |
1059 | } |
1060 | return $data; |
1061 | } |
1062 | |
1063 | /** |
1064 | * Remove metadata keys from a data array or object, non-recursive |
1065 | * |
1066 | * Note this removes all potential metadata keys, not just the defined |
1067 | * ones. |
1068 | * |
1069 | * @since 1.25 |
1070 | * @param array|stdClass $data |
1071 | * @param array|null &$metadata Store metadata here, if provided |
1072 | * @return array|stdClass |
1073 | */ |
1074 | public static function stripMetadataNonRecursive( $data, &$metadata = null ) { |
1075 | if ( !is_array( $metadata ) ) { |
1076 | $metadata = []; |
1077 | } |
1078 | if ( is_array( $data ) || is_object( $data ) ) { |
1079 | $isObj = is_object( $data ); |
1080 | if ( $isObj ) { |
1081 | $data = (array)$data; |
1082 | } |
1083 | $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] ) |
1084 | ? (array)$data[self::META_PRESERVE_KEYS] |
1085 | : []; |
1086 | foreach ( $data as $k => $v ) { |
1087 | if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) { |
1088 | $metadata[$k] = $v; |
1089 | unset( $data[$k] ); |
1090 | } |
1091 | } |
1092 | if ( $isObj ) { |
1093 | $data = (object)$data; |
1094 | } |
1095 | } |
1096 | return $data; |
1097 | } |
1098 | |
1099 | /** |
1100 | * Get the 'real' size of a result item. This means the strlen() of the item, |
1101 | * or the sum of the strlen()s of the elements if the item is an array. |
1102 | * @param mixed $value Validated value (see self::validateValue()) |
1103 | * @return int |
1104 | */ |
1105 | private static function size( $value ) { |
1106 | $s = 0; |
1107 | if ( is_array( $value ) ) { |
1108 | foreach ( $value as $k => $v ) { |
1109 | if ( !self::isMetadataKey( $k ) ) { |
1110 | $s += self::size( $v ); |
1111 | } |
1112 | } |
1113 | } elseif ( is_scalar( $value ) ) { |
1114 | $s = strlen( $value ); |
1115 | } |
1116 | |
1117 | return $s; |
1118 | } |
1119 | |
1120 | /** |
1121 | * Return a reference to the internal data at $path |
1122 | * |
1123 | * @param array|string|null $path |
1124 | * @param string $create |
1125 | * If 'append', append empty arrays. |
1126 | * If 'prepend', prepend empty arrays. |
1127 | * If 'dummy', return a dummy array. |
1128 | * Else, raise an error. |
1129 | * @return array |
1130 | */ |
1131 | private function &path( $path, $create = 'append' ) { |
1132 | $path = (array)$path; |
1133 | $ret = &$this->data; |
1134 | foreach ( $path as $i => $k ) { |
1135 | if ( !isset( $ret[$k] ) ) { |
1136 | switch ( $create ) { |
1137 | case 'append': |
1138 | $ret[$k] = []; |
1139 | break; |
1140 | case 'prepend': |
1141 | $ret = [ $k => [] ] + $ret; |
1142 | break; |
1143 | case 'dummy': |
1144 | $tmp = []; |
1145 | return $tmp; |
1146 | default: |
1147 | $fail = implode( '.', array_slice( $path, 0, $i + 1 ) ); |
1148 | throw new InvalidArgumentException( "Path $fail does not exist" ); |
1149 | } |
1150 | } |
1151 | if ( !is_array( $ret[$k] ) ) { |
1152 | $fail = implode( '.', array_slice( $path, 0, $i + 1 ) ); |
1153 | throw new InvalidArgumentException( "Path $fail is not an array" ); |
1154 | } |
1155 | $ret = &$ret[$k]; |
1156 | } |
1157 | return $ret; |
1158 | } |
1159 | |
1160 | /** |
1161 | * Add the correct metadata to an array of vars we want to export through |
1162 | * the API. |
1163 | * |
1164 | * @param array $vars |
1165 | * @param bool $forceHash |
1166 | * @return array |
1167 | */ |
1168 | public static function addMetadataToResultVars( $vars, $forceHash = true ) { |
1169 | // Process subarrays and determine if this is a JS [] or {} |
1170 | $hash = $forceHash; |
1171 | $maxKey = -1; |
1172 | $bools = []; |
1173 | foreach ( $vars as $k => $v ) { |
1174 | if ( is_array( $v ) || is_object( $v ) ) { |
1175 | $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) ); |
1176 | } elseif ( is_bool( $v ) ) { |
1177 | // Better here to use real bools even in BC formats |
1178 | $bools[] = $k; |
1179 | } |
1180 | if ( is_string( $k ) ) { |
1181 | $hash = true; |
1182 | } elseif ( $k > $maxKey ) { |
1183 | $maxKey = $k; |
1184 | } |
1185 | } |
1186 | if ( !$hash && $maxKey !== count( $vars ) - 1 ) { |
1187 | $hash = true; |
1188 | } |
1189 | |
1190 | // Set metadata appropriately |
1191 | if ( $hash ) { |
1192 | // Get the list of keys we actually care about. Unfortunately, we can't support |
1193 | // certain keys that conflict with ApiResult metadata. |
1194 | $keys = array_diff( array_keys( $vars ), [ |
1195 | self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME, |
1196 | self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS |
1197 | ] ); |
1198 | |
1199 | return [ |
1200 | self::META_TYPE => 'kvp', |
1201 | self::META_KVP_KEY_NAME => 'key', |
1202 | self::META_PRESERVE_KEYS => $keys, |
1203 | self::META_BC_BOOLS => $bools, |
1204 | self::META_INDEXED_TAG_NAME => 'var', |
1205 | ] + $vars; |
1206 | } else { |
1207 | return [ |
1208 | self::META_TYPE => 'array', |
1209 | self::META_BC_BOOLS => $bools, |
1210 | self::META_INDEXED_TAG_NAME => 'value', |
1211 | ] + $vars; |
1212 | } |
1213 | } |
1214 | |
1215 | /** |
1216 | * Format an expiry timestamp for API output |
1217 | * @since 1.29 |
1218 | * @param string $expiry Expiry timestamp, likely from the database |
1219 | * @param string $infinity Use this string for infinite expiry |
1220 | * (only use this to maintain backward compatibility with existing output) |
1221 | * @return string Formatted expiry |
1222 | */ |
1223 | public static function formatExpiry( $expiry, $infinity = 'infinity' ) { |
1224 | static $dbInfinity; |
1225 | $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider() |
1226 | ->getReplicaDatabase() |
1227 | ->getInfinity(); |
1228 | |
1229 | if ( $expiry === '' || $expiry === null || $expiry === false || |
1230 | wfIsInfinity( $expiry ) || $expiry === $dbInfinity |
1231 | ) { |
1232 | return $infinity; |
1233 | } else { |
1234 | return wfTimestamp( TS_ISO_8601, $expiry ); |
1235 | } |
1236 | } |
1237 | |
1238 | // endregion -- end of Utility |
1239 | |
1240 | } |
1241 | |
1242 | /* |
1243 | * This file uses VisualStudio style region/endregion fold markers which are |
1244 | * recognised by PHPStorm. If modelines are enabled, the following editor |
1245 | * configuration will also enable folding in vim, if it is in the last 5 lines |
1246 | * of the file. We also use "@name" which creates sections in Doxygen. |
1247 | * |
1248 | * vim: foldmarker=//\ region,//\ endregion foldmethod=marker |
1249 | */ |