diff options
Diffstat (limited to 'www/wiki/includes/api/ApiResult.php')
-rw-r--r-- | www/wiki/includes/api/ApiResult.php | 1229 |
1 files changed, 1229 insertions, 0 deletions
diff --git a/www/wiki/includes/api/ApiResult.php b/www/wiki/includes/api/ApiResult.php new file mode 100644 index 00000000..468d8783 --- /dev/null +++ b/www/wiki/includes/api/ApiResult.php @@ -0,0 +1,1229 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * This class represents the result of the API operations. + * It simply wraps a nested array structure, adding some functions to simplify + * array's modifications. As various modules execute, they add different pieces + * of information to this result, structuring it as it will be given to the client. + * + * Each subarray may either be a dictionary - key-value pairs with unique keys, + * or lists, where the items are added using $data[] = $value notation. + * + * @since 1.25 this is no longer a subclass of ApiBase + * @ingroup API + */ +class ApiResult implements ApiSerializable { + + /** + * Override existing value in addValue(), setValue(), and similar functions + * @since 1.21 + */ + const OVERRIDE = 1; + + /** + * For addValue(), setValue() and similar functions, if the value does not + * exist, add it as the first element. In case the new value has no name + * (numerical index), all indexes will be renumbered. + * @since 1.21 + */ + const ADD_ON_TOP = 2; + + /** + * For addValue() and similar functions, do not check size while adding a value + * Don't use this unless you REALLY know what you're doing. + * Values added while the size checking was disabled will never be counted. + * Ignored for setValue() and similar functions. + * @since 1.24 + */ + const NO_SIZE_CHECK = 4; + + /** + * For addValue(), setValue() and similar functions, do not validate data. + * Also disables size checking. If you think you need to use this, you're + * probably wrong. + * @since 1.25 + */ + const NO_VALIDATE = 12; + + /** + * Key for the 'indexed tag name' metadata item. Value is string. + * @since 1.25 + */ + const META_INDEXED_TAG_NAME = '_element'; + + /** + * Key for the 'subelements' metadata item. Value is string[]. + * @since 1.25 + */ + const META_SUBELEMENTS = '_subelements'; + + /** + * Key for the 'preserve keys' metadata item. Value is string[]. + * @since 1.25 + */ + const META_PRESERVE_KEYS = '_preservekeys'; + + /** + * Key for the 'content' metadata item. Value is string. + * @since 1.25 + */ + const META_CONTENT = '_content'; + + /** + * Key for the 'type' metadata item. Value is one of the following strings: + * - default: Like 'array' if all (non-metadata) keys are numeric with no + * gaps, otherwise like 'assoc'. + * - array: Keys are used for ordering, but are not output. In a format + * like JSON, outputs as []. + * - assoc: In a format like JSON, outputs as {}. + * - kvp: For a format like XML where object keys have a restricted + * character set, use an alternative output format. For example, + * <container><item name="key">value</item></container> rather than + * <container key="value" /> + * - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode. + * - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode. + * - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces + * the alternative output format for all formats, for example + * [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set. + * @since 1.25 + */ + const META_TYPE = '_type'; + + /** + * Key for the metadata item whose value specifies the name used for the + * kvp key in the alternative output format with META_TYPE 'kvp' or + * 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>. + * Value is string. + * @since 1.25 + */ + const META_KVP_KEY_NAME = '_kvpkeyname'; + + /** + * Key for the metadata item that indicates that the KVP key should be + * added into an assoc value, i.e. {"key":{"val1":"a","val2":"b"}} + * transforms to {"name":"key","val1":"a","val2":"b"} rather than + * {"name":"key","value":{"val1":"a","val2":"b"}}. + * Value is boolean. + * @since 1.26 + */ + const META_KVP_MERGE = '_kvpmerge'; + + /** + * Key for the 'BC bools' metadata item. Value is string[]. + * Note no setter is provided. + * @since 1.25 + */ + const META_BC_BOOLS = '_BC_bools'; + + /** + * Key for the 'BC subelements' metadata item. Value is string[]. + * Note no setter is provided. + * @since 1.25 + */ + const META_BC_SUBELEMENTS = '_BC_subelements'; + + private $data, $size, $maxSize; + private $errorFormatter; + + // Deprecated fields + private $checkingSize, $mainForContinuation; + + /** + * @param int|bool $maxSize Maximum result "size", or false for no limit + * @since 1.25 Takes an integer|bool rather than an ApiMain + */ + public function __construct( $maxSize ) { + if ( $maxSize instanceof ApiMain ) { + wfDeprecated( 'ApiMain to ' . __METHOD__, '1.25' ); + $this->errorFormatter = $maxSize->getErrorFormatter(); + $this->mainForContinuation = $maxSize; + $maxSize = $maxSize->getConfig()->get( 'APIMaxResultSize' ); + } + + $this->maxSize = $maxSize; + $this->checkingSize = true; + $this->reset(); + } + + /** + * Set the error formatter + * @since 1.25 + * @param ApiErrorFormatter $formatter + */ + public function setErrorFormatter( ApiErrorFormatter $formatter ) { + $this->errorFormatter = $formatter; + } + + /** + * Allow for adding one ApiResult into another + * @since 1.25 + * @return mixed + */ + public function serializeForApiResult() { + return $this->data; + } + + /************************************************************************//** + * @name Content + * @{ + */ + + /** + * Clear the current result data. + */ + public function reset() { + $this->data = [ + self::META_TYPE => 'assoc', // Usually what's desired + ]; + $this->size = 0; + } + + /** + * Get the result data array + * + * The returned value should be considered read-only. + * + * Transformations include: + * + * Custom: (callable) Applied before other transformations. Signature is + * function ( &$data, &$metadata ), return value is ignored. Called for + * each nested array. + * + * BC: (array) This transformation does various adjustments to bring the + * output in line with the pre-1.25 result format. The value array is a + * list of flags: 'nobool', 'no*', 'nosub'. + * - Boolean-valued items are changed to '' if true or removed if false, + * unless listed in META_BC_BOOLS. This may be skipped by including + * 'nobool' in the value array. + * - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is + * set to '*'. This may be skipped by including 'no*' in the value + * array. + * - Tags listed in META_BC_SUBELEMENTS will have their values changed to + * [ '*' => $value ]. This may be skipped by including 'nosub' in + * the value array. + * - If META_TYPE is 'BCarray', set it to 'default' + * - If META_TYPE is 'BCassoc', set it to 'default' + * - If META_TYPE is 'BCkvp', perform the transformation (even if + * the Types transformation is not being applied). + * + * Types: (assoc) Apply transformations based on META_TYPE. The values + * array is an associative array with the following possible keys: + * - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc' + * as objects. + * - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp' + * and 'BCkvp' into arrays of two-element arrays, something like this: + * $output = []; + * foreach ( $input as $key => $value ) { + * $pair = []; + * $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key; + * ApiResult::setContentValue( $pair, 'value', $value ); + * $output[] = $pair; + * } + * + * Strip: (string) Strips metadata keys from the result. + * - 'all': Strip all metadata, recursively + * - 'base': Strip metadata at the top-level only. + * - 'none': Do not strip metadata. + * - 'bc': Like 'all', but leave certain pre-1.25 keys. + * + * @since 1.25 + * @param array|string|null $path Path to fetch, see ApiResult::addValue + * @param array $transforms See above + * @return mixed Result data, or null if not found + */ + public function getResultData( $path = [], $transforms = [] ) { + $path = (array)$path; + if ( !$path ) { + return self::applyTransformations( $this->data, $transforms ); + } + + $last = array_pop( $path ); + $ret = &$this->path( $path, 'dummy' ); + if ( !isset( $ret[$last] ) ) { + return null; + } elseif ( is_array( $ret[$last] ) ) { + return self::applyTransformations( $ret[$last], $transforms ); + } else { + return $ret[$last]; + } + } + + /** + * Get the size of the result, i.e. the amount of bytes in it + * @return int + */ + public function getSize() { + return $this->size; + } + + /** + * Add an output value to the array by name. + * + * Verifies that value with the same name has not been added before. + * + * @since 1.25 + * @param array &$arr To add $value to + * @param string|int|null $name Index of $arr to add $value at, + * or null to use the next numeric index. + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + */ + public static function setValue( array &$arr, $name, $value, $flags = 0 ) { + if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) { + $value = self::validateValue( $value ); + } + + if ( $name === null ) { + if ( $flags & self::ADD_ON_TOP ) { + array_unshift( $arr, $value ); + } else { + array_push( $arr, $value ); + } + return; + } + + $exists = isset( $arr[$name] ); + if ( !$exists || ( $flags & self::OVERRIDE ) ) { + if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) { + $arr = [ $name => $value ] + $arr; + } else { + $arr[$name] = $value; + } + } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) { + $conflicts = array_intersect_key( $arr[$name], $value ); + if ( !$conflicts ) { + $arr[$name] += $value; + } else { + $keys = implode( ', ', array_keys( $conflicts ) ); + throw new RuntimeException( + "Conflicting keys ($keys) when attempting to merge element $name" + ); + } + } else { + throw new RuntimeException( + "Attempting to add element $name=$value, existing value is {$arr[$name]}" + ); + } + } + + /** + * Validate a value for addition to the result + * @param mixed $value + * @return array|mixed|string + */ + private static function validateValue( $value ) { + global $wgContLang; + + if ( is_object( $value ) ) { + // Note we use is_callable() here instead of instanceof because + // ApiSerializable is an informal protocol (see docs there for details). + if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) { + $oldValue = $value; + $value = $value->serializeForApiResult(); + if ( is_object( $value ) ) { + throw new UnexpectedValueException( + get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' . + get_class( $value ) + ); + } + + // Recursive call instead of fall-through so we can throw a + // better exception message. + try { + return self::validateValue( $value ); + } catch ( Exception $ex ) { + throw new UnexpectedValueException( + get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' . + $ex->getMessage(), + 0, + $ex + ); + } + } elseif ( is_callable( [ $value, '__toString' ] ) ) { + $value = (string)$value; + } else { + $value = (array)$value + [ self::META_TYPE => 'assoc' ]; + } + } + if ( is_array( $value ) ) { + // Work around https://bugs.php.net/bug.php?id=45959 by copying to a temporary + // (in this case, foreach gets $k === "1" but $tmp[$k] assigns as if $k === 1) + $tmp = []; + foreach ( $value as $k => $v ) { + $tmp[$k] = self::validateValue( $v ); + } + $value = $tmp; + } elseif ( is_float( $value ) && !is_finite( $value ) ) { + throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' ); + } elseif ( is_string( $value ) ) { + $value = $wgContLang->normalize( $value ); + } elseif ( $value !== null && !is_scalar( $value ) ) { + $type = gettype( $value ); + if ( is_resource( $value ) ) { + $type .= '(' . get_resource_type( $value ) . ')'; + } + throw new InvalidArgumentException( "Cannot add $type to ApiResult" ); + } + + return $value; + } + + /** + * Add value to the output data at the given path. + * + * Path can be an indexed array, each element specifying the branch at which to add the new + * value. Setting $path to [ 'a', 'b', 'c' ] is equivalent to data['a']['b']['c'] = $value. + * If $path is null, the value will be inserted at the data root. + * + * @param array|string|int|null $path + * @param string|int|null $name See ApiResult::setValue() + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + * This parameter used to be boolean, and the value of OVERRIDE=1 was specifically + * chosen so that it would be backwards compatible with the new method signature. + * @return bool True if $value fits in the result, false if not + * @since 1.21 int $flags replaced boolean $override + */ + public function addValue( $path, $name, $value, $flags = 0 ) { + $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' ); + + if ( $this->checkingSize && !( $flags & self::NO_SIZE_CHECK ) ) { + // self::size needs the validated value. Then flag + // to not re-validate later. + $value = self::validateValue( $value ); + $flags |= self::NO_VALIDATE; + + $newsize = $this->size + self::size( $value ); + if ( $this->maxSize !== false && $newsize > $this->maxSize ) { + $this->errorFormatter->addWarning( + 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ] + ); + return false; + } + $this->size = $newsize; + } + + self::setValue( $arr, $name, $value, $flags ); + return true; + } + + /** + * Remove an output value to the array by name. + * @param array &$arr To remove $value from + * @param string|int $name Index of $arr to remove + * @return mixed Old value, or null + */ + public static function unsetValue( array &$arr, $name ) { + $ret = null; + if ( isset( $arr[$name] ) ) { + $ret = $arr[$name]; + unset( $arr[$name] ); + } + return $ret; + } + + /** + * Remove value from the output data at the given path. + * + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string|int|null $name Index to remove at $path. + * If null, $path itself is removed. + * @param int $flags Flags used when adding the value + * @return mixed Old value, or null + */ + public function removeValue( $path, $name, $flags = 0 ) { + $path = (array)$path; + if ( $name === null ) { + if ( !$path ) { + throw new InvalidArgumentException( 'Cannot remove the data root' ); + } + $name = array_pop( $path ); + } + $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name ); + if ( $this->checkingSize && !( $flags & self::NO_SIZE_CHECK ) ) { + $newsize = $this->size - self::size( $ret ); + $this->size = max( $newsize, 0 ); + } + return $ret; + } + + /** + * Add an output value to the array by name and mark as META_CONTENT. + * + * @since 1.25 + * @param array &$arr To add $value to + * @param string|int $name Index of $arr to add $value at. + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + */ + public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) { + if ( $name === null ) { + throw new InvalidArgumentException( 'Content value must be named' ); + } + self::setContentField( $arr, $name, $flags ); + self::setValue( $arr, $name, $value, $flags ); + } + + /** + * Add value to the output data at the given path and mark as META_CONTENT + * + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string|int $name See ApiResult::setValue() + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + * @return bool True if $value fits in the result, false if not + */ + public function addContentValue( $path, $name, $value, $flags = 0 ) { + if ( $name === null ) { + throw new InvalidArgumentException( 'Content value must be named' ); + } + $this->addContentField( $path, $name, $flags ); + $this->addValue( $path, $name, $value, $flags ); + } + + /** + * Add the numeric limit for a limit=max to the result. + * + * @since 1.25 + * @param string $moduleName + * @param int $limit + */ + public function addParsedLimit( $moduleName, $limit ) { + // Add value, allowing overwriting + $this->addValue( 'limits', $moduleName, $limit, + self::OVERRIDE | self::NO_SIZE_CHECK ); + } + + /**@}*/ + + /************************************************************************//** + * @name Metadata + * @{ + */ + + /** + * Set the name of the content field name (META_CONTENT) + * + * @since 1.25 + * @param array &$arr + * @param string|int $name Name of the field + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + */ + public static function setContentField( array &$arr, $name, $flags = 0 ) { + if ( isset( $arr[self::META_CONTENT] ) && + isset( $arr[$arr[self::META_CONTENT]] ) && + !( $flags & self::OVERRIDE ) + ) { + throw new RuntimeException( + "Attempting to set content element as $name when " . $arr[self::META_CONTENT] . + ' is already set as the content element' + ); + } + $arr[self::META_CONTENT] = $name; + } + + /** + * Set the name of the content field name (META_CONTENT) + * + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string|int $name Name of the field + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + */ + public function addContentField( $path, $name, $flags = 0 ) { + $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' ); + self::setContentField( $arr, $name, $flags ); + } + + /** + * Causes the elements with the specified names to be output as + * subelements rather than attributes. + * @since 1.25 is static + * @param array &$arr + * @param array|string|int $names The element name(s) to be output as subelements + */ + public static function setSubelementsList( array &$arr, $names ) { + if ( !isset( $arr[self::META_SUBELEMENTS] ) ) { + $arr[self::META_SUBELEMENTS] = (array)$names; + } else { + $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names ); + } + } + + /** + * Causes the elements with the specified names to be output as + * subelements rather than attributes. + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param array|string|int $names The element name(s) to be output as subelements + */ + public function addSubelementsList( $path, $names ) { + $arr = &$this->path( $path ); + self::setSubelementsList( $arr, $names ); + } + + /** + * Causes the elements with the specified names to be output as + * attributes (when possible) rather than as subelements. + * @since 1.25 + * @param array &$arr + * @param array|string|int $names The element name(s) to not be output as subelements + */ + public static function unsetSubelementsList( array &$arr, $names ) { + if ( isset( $arr[self::META_SUBELEMENTS] ) ) { + $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names ); + } + } + + /** + * Causes the elements with the specified names to be output as + * attributes (when possible) rather than as subelements. + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param array|string|int $names The element name(s) to not be output as subelements + */ + public function removeSubelementsList( $path, $names ) { + $arr = &$this->path( $path ); + self::unsetSubelementsList( $arr, $names ); + } + + /** + * Set the tag name for numeric-keyed values in XML format + * @since 1.25 is static + * @param array &$arr + * @param string $tag Tag name + */ + public static function setIndexedTagName( array &$arr, $tag ) { + if ( !is_string( $tag ) ) { + throw new InvalidArgumentException( 'Bad tag name' ); + } + $arr[self::META_INDEXED_TAG_NAME] = $tag; + } + + /** + * Set the tag name for numeric-keyed values in XML format + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string $tag Tag name + */ + public function addIndexedTagName( $path, $tag ) { + $arr = &$this->path( $path ); + self::setIndexedTagName( $arr, $tag ); + } + + /** + * Set indexed tag name on $arr and all subarrays + * + * @since 1.25 + * @param array &$arr + * @param string $tag Tag name + */ + public static function setIndexedTagNameRecursive( array &$arr, $tag ) { + if ( !is_string( $tag ) ) { + throw new InvalidArgumentException( 'Bad tag name' ); + } + $arr[self::META_INDEXED_TAG_NAME] = $tag; + foreach ( $arr as $k => &$v ) { + if ( !self::isMetadataKey( $k ) && is_array( $v ) ) { + self::setIndexedTagNameRecursive( $v, $tag ); + } + } + } + + /** + * Set indexed tag name on $path and all subarrays + * + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string $tag Tag name + */ + public function addIndexedTagNameRecursive( $path, $tag ) { + $arr = &$this->path( $path ); + self::setIndexedTagNameRecursive( $arr, $tag ); + } + + /** + * Preserve specified keys. + * + * This prevents XML name mangling and preventing keys from being removed + * by self::stripMetadata(). + * + * @since 1.25 + * @param array &$arr + * @param array|string $names The element name(s) to preserve + */ + public static function setPreserveKeysList( array &$arr, $names ) { + if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) { + $arr[self::META_PRESERVE_KEYS] = (array)$names; + } else { + $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names ); + } + } + + /** + * Preserve specified keys. + * @since 1.25 + * @see self::setPreserveKeysList() + * @param array|string|null $path See ApiResult::addValue() + * @param array|string $names The element name(s) to preserve + */ + public function addPreserveKeysList( $path, $names ) { + $arr = &$this->path( $path ); + self::setPreserveKeysList( $arr, $names ); + } + + /** + * Don't preserve specified keys. + * @since 1.25 + * @see self::setPreserveKeysList() + * @param array &$arr + * @param array|string $names The element name(s) to not preserve + */ + public static function unsetPreserveKeysList( array &$arr, $names ) { + if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) { + $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names ); + } + } + + /** + * Don't preserve specified keys. + * @since 1.25 + * @see self::setPreserveKeysList() + * @param array|string|null $path See ApiResult::addValue() + * @param array|string $names The element name(s) to not preserve + */ + public function removePreserveKeysList( $path, $names ) { + $arr = &$this->path( $path ); + self::unsetPreserveKeysList( $arr, $names ); + } + + /** + * Set the array data type + * + * @since 1.25 + * @param array &$arr + * @param string $type See ApiResult::META_TYPE + * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME + */ + public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) { + if ( !in_array( $type, [ + 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp' + ], true ) ) { + throw new InvalidArgumentException( 'Bad type' ); + } + $arr[self::META_TYPE] = $type; + if ( is_string( $kvpKeyName ) ) { + $arr[self::META_KVP_KEY_NAME] = $kvpKeyName; + } + } + + /** + * Set the array data type for a path + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string $tag See ApiResult::META_TYPE + * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME + */ + public function addArrayType( $path, $tag, $kvpKeyName = null ) { + $arr = &$this->path( $path ); + self::setArrayType( $arr, $tag, $kvpKeyName ); + } + + /** + * Set the array data type recursively + * @since 1.25 + * @param array &$arr + * @param string $type See ApiResult::META_TYPE + * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME + */ + public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) { + self::setArrayType( $arr, $type, $kvpKeyName ); + foreach ( $arr as $k => &$v ) { + if ( !self::isMetadataKey( $k ) && is_array( $v ) ) { + self::setArrayTypeRecursive( $v, $type, $kvpKeyName ); + } + } + } + + /** + * Set the array data type for a path recursively + * @since 1.25 + * @param array|string|null $path See ApiResult::addValue() + * @param string $tag See ApiResult::META_TYPE + * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME + */ + public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) { + $arr = &$this->path( $path ); + self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName ); + } + + /**@}*/ + + /************************************************************************//** + * @name Utility + * @{ + */ + + /** + * Test whether a key should be considered metadata + * + * @param string $key + * @return bool + */ + public static function isMetadataKey( $key ) { + return substr( $key, 0, 1 ) === '_'; + } + + /** + * Apply transformations to an array, returning the transformed array. + * + * @see ApiResult::getResultData() + * @since 1.25 + * @param array $dataIn + * @param array $transforms + * @return array|object + */ + protected static function applyTransformations( array $dataIn, array $transforms ) { + $strip = isset( $transforms['Strip'] ) ? $transforms['Strip'] : 'none'; + if ( $strip === 'base' ) { + $transforms['Strip'] = 'none'; + } + $transformTypes = isset( $transforms['Types'] ) ? $transforms['Types'] : null; + if ( $transformTypes !== null && !is_array( $transformTypes ) ) { + throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' ); + } + + $metadata = []; + $data = self::stripMetadataNonRecursive( $dataIn, $metadata ); + + if ( isset( $transforms['Custom'] ) ) { + if ( !is_callable( $transforms['Custom'] ) ) { + throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' ); + } + call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] ); + } + + if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) && + isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' && + !isset( $metadata[self::META_KVP_KEY_NAME] ) + ) { + throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' . + 'ApiResult::META_KVP_KEY_NAME metadata item' ); + } + + // BC transformations + $boolKeys = null; + if ( isset( $transforms['BC'] ) ) { + if ( !is_array( $transforms['BC'] ) ) { + throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' ); + } + if ( !in_array( 'nobool', $transforms['BC'], true ) ) { + $boolKeys = isset( $metadata[self::META_BC_BOOLS] ) + ? array_flip( $metadata[self::META_BC_BOOLS] ) + : []; + } + + if ( !in_array( 'no*', $transforms['BC'], true ) && + isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*' + ) { + $k = $metadata[self::META_CONTENT]; + $data['*'] = $data[$k]; + unset( $data[$k] ); + $metadata[self::META_CONTENT] = '*'; + } + + if ( !in_array( 'nosub', $transforms['BC'], true ) && + isset( $metadata[self::META_BC_SUBELEMENTS] ) + ) { + foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) { + if ( isset( $data[$k] ) ) { + $data[$k] = [ + '*' => $data[$k], + self::META_CONTENT => '*', + self::META_TYPE => 'assoc', + ]; + } + } + } + + if ( isset( $metadata[self::META_TYPE] ) ) { + switch ( $metadata[self::META_TYPE] ) { + case 'BCarray': + case 'BCassoc': + $metadata[self::META_TYPE] = 'default'; + break; + case 'BCkvp': + $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME]; + break; + } + } + } + + // Figure out type, do recursive calls, and do boolean transform if necessary + $defaultType = 'array'; + $maxKey = -1; + foreach ( $data as $k => &$v ) { + $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v; + if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) { + if ( !$v ) { + unset( $data[$k] ); + continue; + } + $v = ''; + } + if ( is_string( $k ) ) { + $defaultType = 'assoc'; + } elseif ( $k > $maxKey ) { + $maxKey = $k; + } + } + unset( $v ); + + // Determine which metadata to keep + switch ( $strip ) { + case 'all': + case 'base': + $keepMetadata = []; + break; + case 'none': + $keepMetadata = &$metadata; + break; + case 'bc': + $keepMetadata = array_intersect_key( $metadata, [ + self::META_INDEXED_TAG_NAME => 1, + self::META_SUBELEMENTS => 1, + ] ); + break; + default: + throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' ); + } + + // Type transformation + if ( $transformTypes !== null ) { + if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) { + $defaultType = 'assoc'; + } + + // Override type, if provided + $type = $defaultType; + if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) { + $type = $metadata[self::META_TYPE]; + } + if ( ( $type === 'kvp' || $type === 'BCkvp' ) && + empty( $transformTypes['ArmorKVP'] ) + ) { + $type = 'assoc'; + } elseif ( $type === 'BCarray' ) { + $type = 'array'; + } elseif ( $type === 'BCassoc' ) { + $type = 'assoc'; + } + + // Apply transformation + switch ( $type ) { + case 'assoc': + $metadata[self::META_TYPE] = 'assoc'; + $data += $keepMetadata; + return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data; + + case 'array': + ksort( $data ); + $data = array_values( $data ); + $metadata[self::META_TYPE] = 'array'; + return $data + $keepMetadata; + + case 'kvp': + case 'BCkvp': + $key = isset( $metadata[self::META_KVP_KEY_NAME] ) + ? $metadata[self::META_KVP_KEY_NAME] + : $transformTypes['ArmorKVP']; + $valKey = isset( $transforms['BC'] ) ? '*' : 'value'; + $assocAsObject = !empty( $transformTypes['AssocAsObject'] ); + $merge = !empty( $metadata[self::META_KVP_MERGE] ); + + $ret = []; + foreach ( $data as $k => $v ) { + if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) { + $vArr = (array)$v; + if ( isset( $vArr[self::META_TYPE] ) ) { + $mergeType = $vArr[self::META_TYPE]; + } elseif ( is_object( $v ) ) { + $mergeType = 'assoc'; + } else { + $keys = array_keys( $vArr ); + sort( $keys, SORT_NUMERIC ); + $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc'; + } + } else { + $mergeType = 'n/a'; + } + if ( $mergeType === 'assoc' ) { + $item = $vArr + [ + $key => $k, + ]; + if ( $strip === 'none' ) { + self::setPreserveKeysList( $item, [ $key ] ); + } + } else { + $item = [ + $key => $k, + $valKey => $v, + ]; + if ( $strip === 'none' ) { + $item += [ + self::META_PRESERVE_KEYS => [ $key ], + self::META_CONTENT => $valKey, + self::META_TYPE => 'assoc', + ]; + } + } + $ret[] = $assocAsObject ? (object)$item : $item; + } + $metadata[self::META_TYPE] = 'array'; + + return $ret + $keepMetadata; + + default: + throw new UnexpectedValueException( "Unknown type '$type'" ); + } + } else { + return $data + $keepMetadata; + } + } + + /** + * Recursively remove metadata keys from a data array or object + * + * Note this removes all potential metadata keys, not just the defined + * ones. + * + * @since 1.25 + * @param array|object $data + * @return array|object + */ + public static function stripMetadata( $data ) { + if ( is_array( $data ) || is_object( $data ) ) { + $isObj = is_object( $data ); + if ( $isObj ) { + $data = (array)$data; + } + $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] ) + ? (array)$data[self::META_PRESERVE_KEYS] + : []; + foreach ( $data as $k => $v ) { + if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) { + unset( $data[$k] ); + } elseif ( is_array( $v ) || is_object( $v ) ) { + $data[$k] = self::stripMetadata( $v ); + } + } + if ( $isObj ) { + $data = (object)$data; + } + } + return $data; + } + + /** + * Remove metadata keys from a data array or object, non-recursive + * + * Note this removes all potential metadata keys, not just the defined + * ones. + * + * @since 1.25 + * @param array|object $data + * @param array &$metadata Store metadata here, if provided + * @return array|object + */ + public static function stripMetadataNonRecursive( $data, &$metadata = null ) { + if ( !is_array( $metadata ) ) { + $metadata = []; + } + if ( is_array( $data ) || is_object( $data ) ) { + $isObj = is_object( $data ); + if ( $isObj ) { + $data = (array)$data; + } + $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] ) + ? (array)$data[self::META_PRESERVE_KEYS] + : []; + foreach ( $data as $k => $v ) { + if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) { + $metadata[$k] = $v; + unset( $data[$k] ); + } + } + if ( $isObj ) { + $data = (object)$data; + } + } + return $data; + } + + /** + * Get the 'real' size of a result item. This means the strlen() of the item, + * or the sum of the strlen()s of the elements if the item is an array. + * @param mixed $value Validated value (see self::validateValue()) + * @return int + */ + private static function size( $value ) { + $s = 0; + if ( is_array( $value ) ) { + foreach ( $value as $k => $v ) { + if ( !self::isMetadataKey( $k ) ) { + $s += self::size( $v ); + } + } + } elseif ( is_scalar( $value ) ) { + $s = strlen( $value ); + } + + return $s; + } + + /** + * Return a reference to the internal data at $path + * + * @param array|string|null $path + * @param string $create + * If 'append', append empty arrays. + * If 'prepend', prepend empty arrays. + * If 'dummy', return a dummy array. + * Else, raise an error. + * @return array + */ + private function &path( $path, $create = 'append' ) { + $path = (array)$path; + $ret = &$this->data; + foreach ( $path as $i => $k ) { + if ( !isset( $ret[$k] ) ) { + switch ( $create ) { + case 'append': + $ret[$k] = []; + break; + case 'prepend': + $ret = [ $k => [] ] + $ret; + break; + case 'dummy': + $tmp = []; + return $tmp; + default: + $fail = implode( '.', array_slice( $path, 0, $i + 1 ) ); + throw new InvalidArgumentException( "Path $fail does not exist" ); + } + } + if ( !is_array( $ret[$k] ) ) { + $fail = implode( '.', array_slice( $path, 0, $i + 1 ) ); + throw new InvalidArgumentException( "Path $fail is not an array" ); + } + $ret = &$ret[$k]; + } + return $ret; + } + + /** + * Add the correct metadata to an array of vars we want to export through + * the API. + * + * @param array $vars + * @param bool $forceHash + * @return array + */ + public static function addMetadataToResultVars( $vars, $forceHash = true ) { + // Process subarrays and determine if this is a JS [] or {} + $hash = $forceHash; + $maxKey = -1; + $bools = []; + foreach ( $vars as $k => $v ) { + if ( is_array( $v ) || is_object( $v ) ) { + $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) ); + } elseif ( is_bool( $v ) ) { + // Better here to use real bools even in BC formats + $bools[] = $k; + } + if ( is_string( $k ) ) { + $hash = true; + } elseif ( $k > $maxKey ) { + $maxKey = $k; + } + } + if ( !$hash && $maxKey !== count( $vars ) - 1 ) { + $hash = true; + } + + // Set metadata appropriately + if ( $hash ) { + // Get the list of keys we actually care about. Unfortunately, we can't support + // certain keys that conflict with ApiResult metadata. + $keys = array_diff( array_keys( $vars ), [ + self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME, + self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS + ] ); + + return [ + self::META_TYPE => 'kvp', + self::META_KVP_KEY_NAME => 'key', + self::META_PRESERVE_KEYS => $keys, + self::META_BC_BOOLS => $bools, + self::META_INDEXED_TAG_NAME => 'var', + ] + $vars; + } else { + return [ + self::META_TYPE => 'array', + self::META_BC_BOOLS => $bools, + self::META_INDEXED_TAG_NAME => 'value', + ] + $vars; + } + } + + /** + * Format an expiry timestamp for API output + * @since 1.29 + * @param string $expiry Expiry timestamp, likely from the database + * @param string $infinity Use this string for infinite expiry + * (only use this to maintain backward compatibility with existing output) + * @return string Formatted expiry + */ + public static function formatExpiry( $expiry, $infinity = 'infinity' ) { + static $dbInfinity; + if ( $dbInfinity === null ) { + $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity(); + } + + if ( $expiry === '' || $expiry === null || $expiry === false || + wfIsInfinity( $expiry ) || $expiry === $dbInfinity + ) { + return $infinity; + } else { + return wfTimestamp( TS_ISO_8601, $expiry ); + } + } + + /**@}*/ + +} + +/** + * For really cool vim folding this needs to be at the end: + * vim: foldmarker=@{,@} foldmethod=marker + */ |