value rather than * * - 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 value. * 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 = array( 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 * array( '*' => $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 = array(); * foreach ( $input as $key => $value ) { * $pair = array(); * $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 = array(), $transforms = array() ) { $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 & ApiResult::NO_VALIDATE ) !== ApiResult::NO_VALIDATE ) { $value = self::validateValue( $value ); } if ( $name === null ) { if ( $flags & ApiResult::ADD_ON_TOP ) { array_unshift( $arr, $value ); } else { array_push( $arr, $value ); } return; } $exists = isset( $arr[$name] ); if ( !$exists || ( $flags & ApiResult::OVERRIDE ) ) { if ( !$exists && ( $flags & ApiResult::ADD_ON_TOP ) ) { $arr = array( $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 = join( ', ', 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 */ 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( array( $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( array( $value, '__toString' ) ) ) { $value = (string)$value; } else { $value = (array)$value + array( self::META_TYPE => 'assoc' ); } } if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { $value[$k] = self::validateValue( $v ); } } 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 array('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 & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' ); if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) { // self::valueSize needs the validated value. Then flag // to not re-validate later. $value = self::validateValue( $value ); $flags |= ApiResult::NO_VALIDATE; $newsize = $this->size + self::valueSize( $value ); if ( $this->maxSize !== false && $newsize > $this->maxSize ) { /// @todo Add i18n message when replacing calls to ->setWarning() $msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' . ' be larger than the limit of $1 bytes', 'truncatedresult' ); $msg->numParams( $this->maxSize ); $this->errorFormatter->addWarning( 'result', $msg ); 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 & ApiResult::NO_SIZE_CHECK ) ) { $newsize = $this->size - self::valueSize( $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, ApiResult::OVERRIDE | ApiResult::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 & ApiResult::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, array( '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 $type 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 $type 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 $data * @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 = array(); $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'], array( &$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; $forceKVP = false; 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] ) : array(); } 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] = array( '*' => $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 = array(); break; case 'none': $keepMetadata = &$metadata; break; case 'bc': $keepMetadata = array_intersect_key( $metadata, array( 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 = array(); 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 + array( $key => $k, ); if ( $strip === 'none' ) { self::setPreserveKeysList( $item, array( $key ) ); } } else { $item = array( $key => $k, $valKey => $v, ); if ( $strip === 'none' ) { $item += array( self::META_PRESERVE_KEYS => array( $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] : array(); 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 = array(); } 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] : array(); 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. * @note Once the deprecated public self::size is removed, we can rename * this back to a less awkward name. * @param mixed $value Validated value (see self::validateValue()) * @return int */ private static function valueSize( $value ) { $s = 0; if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { if ( !self::isMetadataKey( $s ) ) { $s += self::valueSize( $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] = array(); break; case 'prepend': $ret = array( $k => array() ) + $ret; break; case 'dummy': $tmp = array(); return $tmp; default: $fail = join( '.', array_slice( $path, 0, $i + 1 ) ); throw new InvalidArgumentException( "Path $fail does not exist" ); } } if ( !is_array( $ret[$k] ) ) { $fail = join( '.', 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 boolean $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 = array(); foreach ( $vars as $k => $v ) { if ( is_array( $v ) || is_object( $v ) ) { $vars[$k] = ApiResult::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 ), array( ApiResult::META_TYPE, ApiResult::META_PRESERVE_KEYS, ApiResult::META_KVP_KEY_NAME, ApiResult::META_INDEXED_TAG_NAME, ApiResult::META_BC_BOOLS ) ); return array( ApiResult::META_TYPE => 'kvp', ApiResult::META_KVP_KEY_NAME => 'key', ApiResult::META_PRESERVE_KEYS => $keys, ApiResult::META_BC_BOOLS => $bools, ApiResult::META_INDEXED_TAG_NAME => 'var', ) + $vars; } else { return array( ApiResult::META_TYPE => 'array', ApiResult::META_BC_BOOLS => $bools, ApiResult::META_INDEXED_TAG_NAME => 'value', ) + $vars; } } /**@}*/ /************************************************************************//** * @name Deprecated * @{ */ /** * Formerly used to enable/disable "raw mode". * @deprecated since 1.25, you shouldn't have been using it in the first place * @since 1.23 $flag parameter added * @param bool $flag Set the raw mode flag to this state */ public function setRawMode( $flag = true ) { wfDeprecated( __METHOD__, '1.25' ); } /** * Returns true, the equivalent of "raw mode" is always enabled now * @deprecated since 1.25, you shouldn't have been using it in the first place * @return bool */ public function getIsRawMode() { wfDeprecated( __METHOD__, '1.25' ); return true; } /** * Get the result's internal data array (read-only) * @deprecated since 1.25, use $this->getResultData() instead * @return array */ public function getData() { wfDeprecated( __METHOD__, '1.25' ); return $this->getResultData( null, array( 'BC' => array(), 'Types' => array(), 'Strip' => 'all', ) ); } /** * Disable size checking in addValue(). Don't use this unless you * REALLY know what you're doing. Values added while size checking * was disabled will not be counted (ever) * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK */ public function disableSizeCheck() { wfDeprecated( __METHOD__, '1.24' ); $this->checkingSize = false; } /** * Re-enable size checking in addValue() * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK */ public function enableSizeCheck() { wfDeprecated( __METHOD__, '1.24' ); $this->checkingSize = true; } /** * Alias for self::setValue() * * @since 1.21 int $flags replaced boolean $override * @deprecated since 1.25, use self::setValue() instead * @param array $arr To add $value to * @param string $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. * 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. */ public static function setElement( &$arr, $name, $value, $flags = 0 ) { wfDeprecated( __METHOD__, '1.25' ); self::setValue( $arr, $name, $value, $flags ); } /** * Adds a content element to an array. * Use this function instead of hardcoding the '*' element. * @deprecated since 1.25, use self::setContentValue() instead * @param array $arr To add the content element to * @param mixed $value * @param string $subElemName When present, content element is created * as a sub item of $arr. Use this parameter to create elements in * format "text" without attributes. */ public static function setContent( &$arr, $value, $subElemName = null ) { wfDeprecated( __METHOD__, '1.25' ); if ( is_array( $value ) ) { throw new InvalidArgumentException( __METHOD__ . ': Bad parameter' ); } if ( is_null( $subElemName ) ) { self::setContentValue( $arr, 'content', $value ); } else { if ( !isset( $arr[$subElemName] ) ) { $arr[$subElemName] = array(); } self::setContentValue( $arr[$subElemName], 'content', $value ); } } /** * Set indexed tag name on all subarrays of $arr * * Does not set the tag name for $arr itself. * * @deprecated since 1.25, use self::setIndexedTagNameRecursive() instead * @param array $arr * @param string $tag Tag name */ public function setIndexedTagName_recursive( &$arr, $tag ) { wfDeprecated( __METHOD__, '1.25' ); if ( !is_array( $arr ) ) { return; } if ( !is_string( $tag ) ) { throw new InvalidArgumentException( 'Bad tag name' ); } foreach ( $arr as $k => &$v ) { if ( !self::isMetadataKey( $k ) && is_array( $v ) ) { $v[self::META_INDEXED_TAG_NAME] = $tag; $this->setIndexedTagName_recursive( $v, $tag ); } } } /** * Alias for self::addIndexedTagName() * @deprecated since 1.25, use $this->addIndexedTagName() instead * @param array $path Path to the array, like addValue()'s $path * @param string $tag */ public function setIndexedTagName_internal( $path, $tag ) { wfDeprecated( __METHOD__, '1.25' ); $this->addIndexedTagName( $path, $tag ); } /** * Alias for self::addParsedLimit() * @deprecated since 1.25, use $this->addParsedLimit() instead * @param string $moduleName * @param int $limit */ public function setParsedLimit( $moduleName, $limit ) { wfDeprecated( __METHOD__, '1.25' ); $this->addParsedLimit( $moduleName, $limit ); } /** * Set the ApiMain for use by $this->beginContinuation() * @since 1.25 * @deprecated for backwards compatibility only, do not use * @param ApiMain $main */ public function setMainForContinuation( ApiMain $main ) { $this->mainForContinuation = $main; } /** * Parse a 'continue' parameter and return status information. * * This must be balanced by a call to endContinuation(). * * @since 1.24 * @deprecated since 1.25, use ApiContinuationManager instead * @param string|null $continue * @param ApiBase[] $allModules * @param array $generatedModules * @return array */ public function beginContinuation( $continue, array $allModules = array(), array $generatedModules = array() ) { wfDeprecated( __METHOD__, '1.25' ); if ( $this->mainForContinuation->getContinuationManager() ) { throw new UnexpectedValueException( __METHOD__ . ': Continuation already in progress from ' . $this->mainForContinuation->getContinuationManager()->getSource() ); } // Ugh. If $continue doesn't match that in the request, temporarily // replace the request when creating the ApiContinuationManager. if ( $continue === null ) { $continue = ''; } if ( $this->mainForContinuation->getVal( 'continue', '' ) !== $continue ) { $oldCtx = $this->mainForContinuation->getContext(); $newCtx = new DerivativeContext( $oldCtx ); $newCtx->setRequest( new DerivativeRequest( $oldCtx->getRequest(), array( 'continue' => $continue ) + $oldCtx->getRequest()->getValues(), $oldCtx->getRequest()->wasPosted() ) ); $this->mainForContinuation->setContext( $newCtx ); $reset = new ScopedCallback( array( $this->mainForContinuation, 'setContext' ), array( $oldCtx ) ); } $manager = new ApiContinuationManager( $this->mainForContinuation, $allModules, $generatedModules ); $reset = null; $this->mainForContinuation->setContinuationManager( $manager ); return array( $manager->isGeneratorDone(), $manager->getRunModules(), ); } /** * @since 1.24 * @deprecated since 1.25, use ApiContinuationManager instead * @param ApiBase $module * @param string $paramName * @param string|array $paramValue */ public function setContinueParam( ApiBase $module, $paramName, $paramValue ) { wfDeprecated( __METHOD__, '1.25' ); if ( $this->mainForContinuation->getContinuationManager() ) { $this->mainForContinuation->getContinuationManager()->addContinueParam( $module, $paramName, $paramValue ); } } /** * @since 1.24 * @deprecated since 1.25, use ApiContinuationManager instead * @param ApiBase $module * @param string $paramName * @param string|array $paramValue */ public function setGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) { wfDeprecated( __METHOD__, '1.25' ); if ( $this->mainForContinuation->getContinuationManager() ) { $this->mainForContinuation->getContinuationManager()->addGeneratorContinueParam( $module, $paramName, $paramValue ); } } /** * Close continuation, writing the data into the result * @since 1.24 * @deprecated since 1.25, use ApiContinuationManager instead * @param string $style 'standard' for the new style since 1.21, 'raw' for * the style used in 1.20 and earlier. */ public function endContinuation( $style = 'standard' ) { wfDeprecated( __METHOD__, '1.25' ); if ( !$this->mainForContinuation->getContinuationManager() ) { return; } if ( $style === 'raw' ) { $data = $this->mainForContinuation->getContinuationManager()->getRawContinuation(); if ( $data ) { $this->addValue( null, 'query-continue', $data, self::ADD_ON_TOP | self::NO_SIZE_CHECK ); } } else { $this->mainForContinuation->getContinuationManager()->setContinuationIntoResult( $this ); } } /** * No-op, this is now checked on insert. * @deprecated since 1.25 */ public function cleanUpUTF8() { wfDeprecated( __METHOD__, '1.25' ); } /** * 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. * @deprecated since 1.25, no external users known and there doesn't seem * to be any case for such use over just checking the return value from the * add/set methods. * @param mixed $value * @return int */ public static function size( $value ) { wfDeprecated( __METHOD__, '1.25' ); return self::valueSize( self::validateValue( $value ) ); } /** * Converts a Status object to an array suitable for addValue * @deprecated since 1.25, use ApiErrorFormatter::arrayFromStatus() * @param Status $status * @param string $errorType * @return array */ public function convertStatusToArray( $status, $errorType = 'error' ) { wfDeprecated( __METHOD__, '1.25' ); return $this->errorFormatter->arrayFromStatus( $status, $errorType ); } /**@}*/ } /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker */