Merge "Improve docs for Title::getInternalURL/getCanonicalURL"
[lhc/web/wiklou.git] / includes / api / ApiResult.php
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 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 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 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 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 const META_INDEXED_TAG_NAME = '_element';
73
74 /**
75 * Key for the 'subelements' metadata item. Value is string[].
76 * @since 1.25
77 */
78 const META_SUBELEMENTS = '_subelements';
79
80 /**
81 * Key for the 'preserve keys' metadata item. Value is string[].
82 * @since 1.25
83 */
84 const META_PRESERVE_KEYS = '_preservekeys';
85
86 /**
87 * Key for the 'content' metadata item. Value is string.
88 * @since 1.25
89 */
90 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 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 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 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 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 const META_BC_SUBELEMENTS = '_BC_subelements';
144
145 private $data, $size, $maxSize;
146 private $errorFormatter;
147
148 // Deprecated fields
149 private $checkingSize, $mainForContinuation;
150
151 /**
152 * @param int|bool $maxSize Maximum result "size", or false for no limit
153 * @since 1.25 Takes an integer|bool rather than an ApiMain
154 */
155 public function __construct( $maxSize ) {
156 if ( $maxSize instanceof ApiMain ) {
157 wfDeprecated( 'ApiMain to ' . __METHOD__, '1.25' );
158 $this->errorFormatter = $maxSize->getErrorFormatter();
159 $this->mainForContinuation = $maxSize;
160 $maxSize = $maxSize->getConfig()->get( 'APIMaxResultSize' );
161 }
162
163 $this->maxSize = $maxSize;
164 $this->checkingSize = true;
165 $this->reset();
166 }
167
168 /**
169 * Set the error formatter
170 * @since 1.25
171 * @param ApiErrorFormatter $formatter
172 */
173 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
174 $this->errorFormatter = $formatter;
175 }
176
177 /**
178 * Allow for adding one ApiResult into another
179 * @since 1.25
180 * @return mixed
181 */
182 public function serializeForApiResult() {
183 return $this->data;
184 }
185
186 /************************************************************************//**
187 * @name Content
188 * @{
189 */
190
191 /**
192 * Clear the current result data.
193 */
194 public function reset() {
195 $this->data = [
196 self::META_TYPE => 'assoc', // Usually what's desired
197 ];
198 $this->size = 0;
199 }
200
201 /**
202 * Get the result data array
203 *
204 * The returned value should be considered read-only.
205 *
206 * Transformations include:
207 *
208 * Custom: (callable) Applied before other transformations. Signature is
209 * function ( &$data, &$metadata ), return value is ignored. Called for
210 * each nested array.
211 *
212 * BC: (array) This transformation does various adjustments to bring the
213 * output in line with the pre-1.25 result format. The value array is a
214 * list of flags: 'nobool', 'no*', 'nosub'.
215 * - Boolean-valued items are changed to '' if true or removed if false,
216 * unless listed in META_BC_BOOLS. This may be skipped by including
217 * 'nobool' in the value array.
218 * - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is
219 * set to '*'. This may be skipped by including 'no*' in the value
220 * array.
221 * - Tags listed in META_BC_SUBELEMENTS will have their values changed to
222 * [ '*' => $value ]. This may be skipped by including 'nosub' in
223 * the value array.
224 * - If META_TYPE is 'BCarray', set it to 'default'
225 * - If META_TYPE is 'BCassoc', set it to 'default'
226 * - If META_TYPE is 'BCkvp', perform the transformation (even if
227 * the Types transformation is not being applied).
228 *
229 * Types: (assoc) Apply transformations based on META_TYPE. The values
230 * array is an associative array with the following possible keys:
231 * - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc'
232 * as objects.
233 * - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
234 * and 'BCkvp' into arrays of two-element arrays, something like this:
235 * $output = [];
236 * foreach ( $input as $key => $value ) {
237 * $pair = [];
238 * $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
239 * ApiResult::setContentValue( $pair, 'value', $value );
240 * $output[] = $pair;
241 * }
242 *
243 * Strip: (string) Strips metadata keys from the result.
244 * - 'all': Strip all metadata, recursively
245 * - 'base': Strip metadata at the top-level only.
246 * - 'none': Do not strip metadata.
247 * - 'bc': Like 'all', but leave certain pre-1.25 keys.
248 *
249 * @since 1.25
250 * @param array|string|null $path Path to fetch, see ApiResult::addValue
251 * @param array $transforms See above
252 * @return mixed Result data, or null if not found
253 */
254 public function getResultData( $path = [], $transforms = [] ) {
255 $path = (array)$path;
256 if ( !$path ) {
257 return self::applyTransformations( $this->data, $transforms );
258 }
259
260 $last = array_pop( $path );
261 $ret = &$this->path( $path, 'dummy' );
262 if ( !isset( $ret[$last] ) ) {
263 return null;
264 } elseif ( is_array( $ret[$last] ) ) {
265 return self::applyTransformations( $ret[$last], $transforms );
266 } else {
267 return $ret[$last];
268 }
269 }
270
271 /**
272 * Get the size of the result, i.e. the amount of bytes in it
273 * @return int
274 */
275 public function getSize() {
276 return $this->size;
277 }
278
279 /**
280 * Add an output value to the array by name.
281 *
282 * Verifies that value with the same name has not been added before.
283 *
284 * @since 1.25
285 * @param array &$arr To add $value to
286 * @param string|int|null $name Index of $arr to add $value at,
287 * or null to use the next numeric index.
288 * @param mixed $value
289 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
290 */
291 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
292 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
293 $value = self::validateValue( $value );
294 }
295
296 if ( $name === null ) {
297 if ( $flags & self::ADD_ON_TOP ) {
298 array_unshift( $arr, $value );
299 } else {
300 array_push( $arr, $value );
301 }
302 return;
303 }
304
305 $exists = isset( $arr[$name] );
306 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
307 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
308 $arr = [ $name => $value ] + $arr;
309 } else {
310 $arr[$name] = $value;
311 }
312 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
313 $conflicts = array_intersect_key( $arr[$name], $value );
314 if ( !$conflicts ) {
315 $arr[$name] += $value;
316 } else {
317 $keys = implode( ', ', array_keys( $conflicts ) );
318 throw new RuntimeException(
319 "Conflicting keys ($keys) when attempting to merge element $name"
320 );
321 }
322 } else {
323 throw new RuntimeException(
324 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
325 );
326 }
327 }
328
329 /**
330 * Validate a value for addition to the result
331 * @param mixed $value
332 * @return array|mixed|string
333 */
334 private static function validateValue( $value ) {
335 if ( is_object( $value ) ) {
336 // Note we use is_callable() here instead of instanceof because
337 // ApiSerializable is an informal protocol (see docs there for details).
338 if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
339 $oldValue = $value;
340 $value = $value->serializeForApiResult();
341 if ( is_object( $value ) ) {
342 throw new UnexpectedValueException(
343 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
344 get_class( $value )
345 );
346 }
347
348 // Recursive call instead of fall-through so we can throw a
349 // better exception message.
350 try {
351 return self::validateValue( $value );
352 } catch ( Exception $ex ) {
353 throw new UnexpectedValueException(
354 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
355 $ex->getMessage(),
356 0,
357 $ex
358 );
359 }
360 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
361 $value = (string)$value;
362 } else {
363 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
364 }
365 }
366 if ( is_array( $value ) ) {
367 // Work around https://bugs.php.net/bug.php?id=45959 by copying to a temporary
368 // (in this case, foreach gets $k === "1" but $tmp[$k] assigns as if $k === 1)
369 $tmp = [];
370 foreach ( $value as $k => $v ) {
371 $tmp[$k] = self::validateValue( $v );
372 }
373 $value = $tmp;
374 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
375 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
376 } elseif ( is_string( $value ) ) {
377 $value = MediaWikiServices::getInstance()->getContentLanguage()->normalize( $value );
378 } elseif ( $value !== null && !is_scalar( $value ) ) {
379 $type = gettype( $value );
380 if ( is_resource( $value ) ) {
381 $type .= '(' . get_resource_type( $value ) . ')';
382 }
383 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
384 }
385
386 return $value;
387 }
388
389 /**
390 * Add value to the output data at the given path.
391 *
392 * Path can be an indexed array, each element specifying the branch at which to add the new
393 * value. Setting $path to [ 'a', 'b', 'c' ] is equivalent to data['a']['b']['c'] = $value.
394 * If $path is null, the value will be inserted at the data root.
395 *
396 * @param array|string|int|null $path
397 * @param string|int|null $name See ApiResult::setValue()
398 * @param mixed $value
399 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
400 * This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
401 * chosen so that it would be backwards compatible with the new method signature.
402 * @return bool True if $value fits in the result, false if not
403 * @since 1.21 int $flags replaced boolean $override
404 */
405 public function addValue( $path, $name, $value, $flags = 0 ) {
406 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
407
408 if ( $this->checkingSize && !( $flags & self::NO_SIZE_CHECK ) ) {
409 // self::size needs the validated value. Then flag
410 // to not re-validate later.
411 $value = self::validateValue( $value );
412 $flags |= self::NO_VALIDATE;
413
414 $newsize = $this->size + self::size( $value );
415 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
416 $this->errorFormatter->addWarning(
417 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
418 );
419 return false;
420 }
421 $this->size = $newsize;
422 }
423
424 self::setValue( $arr, $name, $value, $flags );
425 return true;
426 }
427
428 /**
429 * Remove an output value to the array by name.
430 * @param array &$arr To remove $value from
431 * @param string|int $name Index of $arr to remove
432 * @return mixed Old value, or null
433 */
434 public static function unsetValue( array &$arr, $name ) {
435 $ret = null;
436 if ( isset( $arr[$name] ) ) {
437 $ret = $arr[$name];
438 unset( $arr[$name] );
439 }
440 return $ret;
441 }
442
443 /**
444 * Remove value from the output data at the given path.
445 *
446 * @since 1.25
447 * @param array|string|null $path See ApiResult::addValue()
448 * @param string|int|null $name Index to remove at $path.
449 * If null, $path itself is removed.
450 * @param int $flags Flags used when adding the value
451 * @return mixed Old value, or null
452 */
453 public function removeValue( $path, $name, $flags = 0 ) {
454 $path = (array)$path;
455 if ( $name === null ) {
456 if ( !$path ) {
457 throw new InvalidArgumentException( 'Cannot remove the data root' );
458 }
459 $name = array_pop( $path );
460 }
461 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
462 if ( $this->checkingSize && !( $flags & self::NO_SIZE_CHECK ) ) {
463 $newsize = $this->size - self::size( $ret );
464 $this->size = max( $newsize, 0 );
465 }
466 return $ret;
467 }
468
469 /**
470 * Add an output value to the array by name and mark as META_CONTENT.
471 *
472 * @since 1.25
473 * @param array &$arr To add $value to
474 * @param string|int $name Index of $arr to add $value at.
475 * @param mixed $value
476 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
477 */
478 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
479 if ( $name === null ) {
480 throw new InvalidArgumentException( 'Content value must be named' );
481 }
482 self::setContentField( $arr, $name, $flags );
483 self::setValue( $arr, $name, $value, $flags );
484 }
485
486 /**
487 * Add value to the output data at the given path and mark as META_CONTENT
488 *
489 * @since 1.25
490 * @param array|string|null $path See ApiResult::addValue()
491 * @param string|int $name See ApiResult::setValue()
492 * @param mixed $value
493 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
494 * @return bool True if $value fits in the result, false if not
495 */
496 public function addContentValue( $path, $name, $value, $flags = 0 ) {
497 if ( $name === null ) {
498 throw new InvalidArgumentException( 'Content value must be named' );
499 }
500 $this->addContentField( $path, $name, $flags );
501 return $this->addValue( $path, $name, $value, $flags );
502 }
503
504 /**
505 * Add the numeric limit for a limit=max to the result.
506 *
507 * @since 1.25
508 * @param string $moduleName
509 * @param int $limit
510 */
511 public function addParsedLimit( $moduleName, $limit ) {
512 // Add value, allowing overwriting
513 $this->addValue( 'limits', $moduleName, $limit,
514 self::OVERRIDE | self::NO_SIZE_CHECK );
515 }
516
517 /**@}*/
518
519 /************************************************************************//**
520 * @name Metadata
521 * @{
522 */
523
524 /**
525 * Set the name of the content field name (META_CONTENT)
526 *
527 * @since 1.25
528 * @param array &$arr
529 * @param string|int $name Name of the field
530 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
531 */
532 public static function setContentField( array &$arr, $name, $flags = 0 ) {
533 if ( isset( $arr[self::META_CONTENT] ) &&
534 isset( $arr[$arr[self::META_CONTENT]] ) &&
535 !( $flags & self::OVERRIDE )
536 ) {
537 throw new RuntimeException(
538 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
539 ' is already set as the content element'
540 );
541 }
542 $arr[self::META_CONTENT] = $name;
543 }
544
545 /**
546 * Set the name of the content field name (META_CONTENT)
547 *
548 * @since 1.25
549 * @param array|string|null $path See ApiResult::addValue()
550 * @param string|int $name Name of the field
551 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
552 */
553 public function addContentField( $path, $name, $flags = 0 ) {
554 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
555 self::setContentField( $arr, $name, $flags );
556 }
557
558 /**
559 * Causes the elements with the specified names to be output as
560 * subelements rather than attributes.
561 * @since 1.25 is static
562 * @param array &$arr
563 * @param array|string|int $names The element name(s) to be output as subelements
564 */
565 public static function setSubelementsList( array &$arr, $names ) {
566 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
567 $arr[self::META_SUBELEMENTS] = (array)$names;
568 } else {
569 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
570 }
571 }
572
573 /**
574 * Causes the elements with the specified names to be output as
575 * subelements rather than attributes.
576 * @since 1.25
577 * @param array|string|null $path See ApiResult::addValue()
578 * @param array|string|int $names The element name(s) to be output as subelements
579 */
580 public function addSubelementsList( $path, $names ) {
581 $arr = &$this->path( $path );
582 self::setSubelementsList( $arr, $names );
583 }
584
585 /**
586 * Causes the elements with the specified names to be output as
587 * attributes (when possible) rather than as subelements.
588 * @since 1.25
589 * @param array &$arr
590 * @param array|string|int $names The element name(s) to not be output as subelements
591 */
592 public static function unsetSubelementsList( array &$arr, $names ) {
593 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
594 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
595 }
596 }
597
598 /**
599 * Causes the elements with the specified names to be output as
600 * attributes (when possible) rather than as subelements.
601 * @since 1.25
602 * @param array|string|null $path See ApiResult::addValue()
603 * @param array|string|int $names The element name(s) to not be output as subelements
604 */
605 public function removeSubelementsList( $path, $names ) {
606 $arr = &$this->path( $path );
607 self::unsetSubelementsList( $arr, $names );
608 }
609
610 /**
611 * Set the tag name for numeric-keyed values in XML format
612 * @since 1.25 is static
613 * @param array &$arr
614 * @param string $tag Tag name
615 */
616 public static function setIndexedTagName( array &$arr, $tag ) {
617 if ( !is_string( $tag ) ) {
618 throw new InvalidArgumentException( 'Bad tag name' );
619 }
620 $arr[self::META_INDEXED_TAG_NAME] = $tag;
621 }
622
623 /**
624 * Set the tag name for numeric-keyed values in XML format
625 * @since 1.25
626 * @param array|string|null $path See ApiResult::addValue()
627 * @param string $tag Tag name
628 */
629 public function addIndexedTagName( $path, $tag ) {
630 $arr = &$this->path( $path );
631 self::setIndexedTagName( $arr, $tag );
632 }
633
634 /**
635 * Set indexed tag name on $arr and all subarrays
636 *
637 * @since 1.25
638 * @param array &$arr
639 * @param string $tag Tag name
640 */
641 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
642 if ( !is_string( $tag ) ) {
643 throw new InvalidArgumentException( 'Bad tag name' );
644 }
645 $arr[self::META_INDEXED_TAG_NAME] = $tag;
646 foreach ( $arr as $k => &$v ) {
647 if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
648 self::setIndexedTagNameRecursive( $v, $tag );
649 }
650 }
651 }
652
653 /**
654 * Set indexed tag name on $path and all subarrays
655 *
656 * @since 1.25
657 * @param array|string|null $path See ApiResult::addValue()
658 * @param string $tag Tag name
659 */
660 public function addIndexedTagNameRecursive( $path, $tag ) {
661 $arr = &$this->path( $path );
662 self::setIndexedTagNameRecursive( $arr, $tag );
663 }
664
665 /**
666 * Preserve specified keys.
667 *
668 * This prevents XML name mangling and preventing keys from being removed
669 * by self::stripMetadata().
670 *
671 * @since 1.25
672 * @param array &$arr
673 * @param array|string $names The element name(s) to preserve
674 */
675 public static function setPreserveKeysList( array &$arr, $names ) {
676 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
677 $arr[self::META_PRESERVE_KEYS] = (array)$names;
678 } else {
679 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
680 }
681 }
682
683 /**
684 * Preserve specified keys.
685 * @since 1.25
686 * @see self::setPreserveKeysList()
687 * @param array|string|null $path See ApiResult::addValue()
688 * @param array|string $names The element name(s) to preserve
689 */
690 public function addPreserveKeysList( $path, $names ) {
691 $arr = &$this->path( $path );
692 self::setPreserveKeysList( $arr, $names );
693 }
694
695 /**
696 * Don't preserve specified keys.
697 * @since 1.25
698 * @see self::setPreserveKeysList()
699 * @param array &$arr
700 * @param array|string $names The element name(s) to not preserve
701 */
702 public static function unsetPreserveKeysList( array &$arr, $names ) {
703 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
704 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
705 }
706 }
707
708 /**
709 * Don't preserve specified keys.
710 * @since 1.25
711 * @see self::setPreserveKeysList()
712 * @param array|string|null $path See ApiResult::addValue()
713 * @param array|string $names The element name(s) to not preserve
714 */
715 public function removePreserveKeysList( $path, $names ) {
716 $arr = &$this->path( $path );
717 self::unsetPreserveKeysList( $arr, $names );
718 }
719
720 /**
721 * Set the array data type
722 *
723 * @since 1.25
724 * @param array &$arr
725 * @param string $type See ApiResult::META_TYPE
726 * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
727 */
728 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
729 if ( !in_array( $type, [
730 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
731 ], true ) ) {
732 throw new InvalidArgumentException( 'Bad type' );
733 }
734 $arr[self::META_TYPE] = $type;
735 if ( is_string( $kvpKeyName ) ) {
736 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
737 }
738 }
739
740 /**
741 * Set the array data type for a path
742 * @since 1.25
743 * @param array|string|null $path See ApiResult::addValue()
744 * @param string $tag See ApiResult::META_TYPE
745 * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
746 */
747 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
748 $arr = &$this->path( $path );
749 self::setArrayType( $arr, $tag, $kvpKeyName );
750 }
751
752 /**
753 * Set the array data type recursively
754 * @since 1.25
755 * @param array &$arr
756 * @param string $type See ApiResult::META_TYPE
757 * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
758 */
759 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
760 self::setArrayType( $arr, $type, $kvpKeyName );
761 foreach ( $arr as $k => &$v ) {
762 if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
763 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
764 }
765 }
766 }
767
768 /**
769 * Set the array data type for a path recursively
770 * @since 1.25
771 * @param array|string|null $path See ApiResult::addValue()
772 * @param string $tag See ApiResult::META_TYPE
773 * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
774 */
775 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
776 $arr = &$this->path( $path );
777 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
778 }
779
780 /**@}*/
781
782 /************************************************************************//**
783 * @name Utility
784 * @{
785 */
786
787 /**
788 * Test whether a key should be considered metadata
789 *
790 * @param string $key
791 * @return bool
792 */
793 public static function isMetadataKey( $key ) {
794 return substr( $key, 0, 1 ) === '_';
795 }
796
797 /**
798 * Apply transformations to an array, returning the transformed array.
799 *
800 * @see ApiResult::getResultData()
801 * @since 1.25
802 * @param array $dataIn
803 * @param array $transforms
804 * @return array|object
805 */
806 protected static function applyTransformations( array $dataIn, array $transforms ) {
807 $strip = $transforms['Strip'] ?? 'none';
808 if ( $strip === 'base' ) {
809 $transforms['Strip'] = 'none';
810 }
811 $transformTypes = $transforms['Types'] ?? null;
812 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
813 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
814 }
815
816 $metadata = [];
817 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
818
819 if ( isset( $transforms['Custom'] ) ) {
820 if ( !is_callable( $transforms['Custom'] ) ) {
821 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
822 }
823 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
824 }
825
826 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
827 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
828 !isset( $metadata[self::META_KVP_KEY_NAME] )
829 ) {
830 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
831 'ApiResult::META_KVP_KEY_NAME metadata item' );
832 }
833
834 // BC transformations
835 $boolKeys = null;
836 if ( isset( $transforms['BC'] ) ) {
837 if ( !is_array( $transforms['BC'] ) ) {
838 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
839 }
840 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
841 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
842 ? array_flip( $metadata[self::META_BC_BOOLS] )
843 : [];
844 }
845
846 if ( !in_array( 'no*', $transforms['BC'], true ) &&
847 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
848 ) {
849 $k = $metadata[self::META_CONTENT];
850 $data['*'] = $data[$k];
851 unset( $data[$k] );
852 $metadata[self::META_CONTENT] = '*';
853 }
854
855 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
856 isset( $metadata[self::META_BC_SUBELEMENTS] )
857 ) {
858 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
859 if ( isset( $data[$k] ) ) {
860 $data[$k] = [
861 '*' => $data[$k],
862 self::META_CONTENT => '*',
863 self::META_TYPE => 'assoc',
864 ];
865 }
866 }
867 }
868
869 if ( isset( $metadata[self::META_TYPE] ) ) {
870 switch ( $metadata[self::META_TYPE] ) {
871 case 'BCarray':
872 case 'BCassoc':
873 $metadata[self::META_TYPE] = 'default';
874 break;
875 case 'BCkvp':
876 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
877 break;
878 }
879 }
880 }
881
882 // Figure out type, do recursive calls, and do boolean transform if necessary
883 $defaultType = 'array';
884 $maxKey = -1;
885 foreach ( $data as $k => &$v ) {
886 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
887 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
888 if ( !$v ) {
889 unset( $data[$k] );
890 continue;
891 }
892 $v = '';
893 }
894 if ( is_string( $k ) ) {
895 $defaultType = 'assoc';
896 } elseif ( $k > $maxKey ) {
897 $maxKey = $k;
898 }
899 }
900 unset( $v );
901
902 // Determine which metadata to keep
903 switch ( $strip ) {
904 case 'all':
905 case 'base':
906 $keepMetadata = [];
907 break;
908 case 'none':
909 $keepMetadata = &$metadata;
910 break;
911 case 'bc':
912 $keepMetadata = array_intersect_key( $metadata, [
913 self::META_INDEXED_TAG_NAME => 1,
914 self::META_SUBELEMENTS => 1,
915 ] );
916 break;
917 default:
918 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
919 }
920
921 // Type transformation
922 if ( $transformTypes !== null ) {
923 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
924 $defaultType = 'assoc';
925 }
926
927 // Override type, if provided
928 $type = $defaultType;
929 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
930 $type = $metadata[self::META_TYPE];
931 }
932 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
933 empty( $transformTypes['ArmorKVP'] )
934 ) {
935 $type = 'assoc';
936 } elseif ( $type === 'BCarray' ) {
937 $type = 'array';
938 } elseif ( $type === 'BCassoc' ) {
939 $type = 'assoc';
940 }
941
942 // Apply transformation
943 switch ( $type ) {
944 case 'assoc':
945 $metadata[self::META_TYPE] = 'assoc';
946 $data += $keepMetadata;
947 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
948
949 case 'array':
950 ksort( $data );
951 $data = array_values( $data );
952 $metadata[self::META_TYPE] = 'array';
953 return $data + $keepMetadata;
954
955 case 'kvp':
956 case 'BCkvp':
957 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
958 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
959 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
960 $merge = !empty( $metadata[self::META_KVP_MERGE] );
961
962 $ret = [];
963 foreach ( $data as $k => $v ) {
964 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
965 $vArr = (array)$v;
966 if ( isset( $vArr[self::META_TYPE] ) ) {
967 $mergeType = $vArr[self::META_TYPE];
968 } elseif ( is_object( $v ) ) {
969 $mergeType = 'assoc';
970 } else {
971 $keys = array_keys( $vArr );
972 sort( $keys, SORT_NUMERIC );
973 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
974 }
975 } else {
976 $mergeType = 'n/a';
977 }
978 if ( $mergeType === 'assoc' ) {
979 $item = $vArr + [
980 $key => $k,
981 ];
982 if ( $strip === 'none' ) {
983 self::setPreserveKeysList( $item, [ $key ] );
984 }
985 } else {
986 $item = [
987 $key => $k,
988 $valKey => $v,
989 ];
990 if ( $strip === 'none' ) {
991 $item += [
992 self::META_PRESERVE_KEYS => [ $key ],
993 self::META_CONTENT => $valKey,
994 self::META_TYPE => 'assoc',
995 ];
996 }
997 }
998 $ret[] = $assocAsObject ? (object)$item : $item;
999 }
1000 $metadata[self::META_TYPE] = 'array';
1001
1002 return $ret + $keepMetadata;
1003
1004 default:
1005 throw new UnexpectedValueException( "Unknown type '$type'" );
1006 }
1007 } else {
1008 return $data + $keepMetadata;
1009 }
1010 }
1011
1012 /**
1013 * Recursively remove metadata keys from a data array or object
1014 *
1015 * Note this removes all potential metadata keys, not just the defined
1016 * ones.
1017 *
1018 * @since 1.25
1019 * @param array|object $data
1020 * @return array|object
1021 */
1022 public static function stripMetadata( $data ) {
1023 if ( is_array( $data ) || is_object( $data ) ) {
1024 $isObj = is_object( $data );
1025 if ( $isObj ) {
1026 $data = (array)$data;
1027 }
1028 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1029 ? (array)$data[self::META_PRESERVE_KEYS]
1030 : [];
1031 foreach ( $data as $k => $v ) {
1032 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1033 unset( $data[$k] );
1034 } elseif ( is_array( $v ) || is_object( $v ) ) {
1035 $data[$k] = self::stripMetadata( $v );
1036 }
1037 }
1038 if ( $isObj ) {
1039 $data = (object)$data;
1040 }
1041 }
1042 return $data;
1043 }
1044
1045 /**
1046 * Remove metadata keys from a data array or object, non-recursive
1047 *
1048 * Note this removes all potential metadata keys, not just the defined
1049 * ones.
1050 *
1051 * @since 1.25
1052 * @param array|object $data
1053 * @param array|null &$metadata Store metadata here, if provided
1054 * @return array|object
1055 */
1056 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1057 if ( !is_array( $metadata ) ) {
1058 $metadata = [];
1059 }
1060 if ( is_array( $data ) || is_object( $data ) ) {
1061 $isObj = is_object( $data );
1062 if ( $isObj ) {
1063 $data = (array)$data;
1064 }
1065 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1066 ? (array)$data[self::META_PRESERVE_KEYS]
1067 : [];
1068 foreach ( $data as $k => $v ) {
1069 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1070 $metadata[$k] = $v;
1071 unset( $data[$k] );
1072 }
1073 }
1074 if ( $isObj ) {
1075 $data = (object)$data;
1076 }
1077 }
1078 return $data;
1079 }
1080
1081 /**
1082 * Get the 'real' size of a result item. This means the strlen() of the item,
1083 * or the sum of the strlen()s of the elements if the item is an array.
1084 * @param mixed $value Validated value (see self::validateValue())
1085 * @return int
1086 */
1087 private static function size( $value ) {
1088 $s = 0;
1089 if ( is_array( $value ) ) {
1090 foreach ( $value as $k => $v ) {
1091 if ( !self::isMetadataKey( $k ) ) {
1092 $s += self::size( $v );
1093 }
1094 }
1095 } elseif ( is_scalar( $value ) ) {
1096 $s = strlen( $value );
1097 }
1098
1099 return $s;
1100 }
1101
1102 /**
1103 * Return a reference to the internal data at $path
1104 *
1105 * @param array|string|null $path
1106 * @param string $create
1107 * If 'append', append empty arrays.
1108 * If 'prepend', prepend empty arrays.
1109 * If 'dummy', return a dummy array.
1110 * Else, raise an error.
1111 * @return array
1112 */
1113 private function &path( $path, $create = 'append' ) {
1114 $path = (array)$path;
1115 $ret = &$this->data;
1116 foreach ( $path as $i => $k ) {
1117 if ( !isset( $ret[$k] ) ) {
1118 switch ( $create ) {
1119 case 'append':
1120 $ret[$k] = [];
1121 break;
1122 case 'prepend':
1123 $ret = [ $k => [] ] + $ret;
1124 break;
1125 case 'dummy':
1126 $tmp = [];
1127 return $tmp;
1128 default:
1129 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1130 throw new InvalidArgumentException( "Path $fail does not exist" );
1131 }
1132 }
1133 if ( !is_array( $ret[$k] ) ) {
1134 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1135 throw new InvalidArgumentException( "Path $fail is not an array" );
1136 }
1137 $ret = &$ret[$k];
1138 }
1139 return $ret;
1140 }
1141
1142 /**
1143 * Add the correct metadata to an array of vars we want to export through
1144 * the API.
1145 *
1146 * @param array $vars
1147 * @param bool $forceHash
1148 * @return array
1149 */
1150 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1151 // Process subarrays and determine if this is a JS [] or {}
1152 $hash = $forceHash;
1153 $maxKey = -1;
1154 $bools = [];
1155 foreach ( $vars as $k => $v ) {
1156 if ( is_array( $v ) || is_object( $v ) ) {
1157 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1158 } elseif ( is_bool( $v ) ) {
1159 // Better here to use real bools even in BC formats
1160 $bools[] = $k;
1161 }
1162 if ( is_string( $k ) ) {
1163 $hash = true;
1164 } elseif ( $k > $maxKey ) {
1165 $maxKey = $k;
1166 }
1167 }
1168 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1169 $hash = true;
1170 }
1171
1172 // Set metadata appropriately
1173 if ( $hash ) {
1174 // Get the list of keys we actually care about. Unfortunately, we can't support
1175 // certain keys that conflict with ApiResult metadata.
1176 $keys = array_diff( array_keys( $vars ), [
1177 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1178 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1179 ] );
1180
1181 return [
1182 self::META_TYPE => 'kvp',
1183 self::META_KVP_KEY_NAME => 'key',
1184 self::META_PRESERVE_KEYS => $keys,
1185 self::META_BC_BOOLS => $bools,
1186 self::META_INDEXED_TAG_NAME => 'var',
1187 ] + $vars;
1188 } else {
1189 return [
1190 self::META_TYPE => 'array',
1191 self::META_BC_BOOLS => $bools,
1192 self::META_INDEXED_TAG_NAME => 'value',
1193 ] + $vars;
1194 }
1195 }
1196
1197 /**
1198 * Format an expiry timestamp for API output
1199 * @since 1.29
1200 * @param string $expiry Expiry timestamp, likely from the database
1201 * @param string $infinity Use this string for infinite expiry
1202 * (only use this to maintain backward compatibility with existing output)
1203 * @return string Formatted expiry
1204 */
1205 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1206 static $dbInfinity;
1207 if ( $dbInfinity === null ) {
1208 $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
1209 }
1210
1211 if ( $expiry === '' || $expiry === null || $expiry === false ||
1212 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1213 ) {
1214 return $infinity;
1215 } else {
1216 return wfTimestamp( TS_ISO_8601, $expiry );
1217 }
1218 }
1219
1220 /**@}*/
1221
1222 }
1223
1224 /**
1225 * For really cool vim folding this needs to be at the end:
1226 * vim: foldmarker=@{,@} foldmethod=marker
1227 */