Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1426014
ApiResult.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
39 KB
Referenced Files
None
Subscribers
None
ApiResult.php
View Options
<?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
*/
namespace
MediaWiki\Api
;
use
Exception
;
use
InvalidArgumentException
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Message\Message
;
use
RuntimeException
;
use
stdClass
;
use
UnexpectedValueException
;
/**
* 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
*/
public
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
*/
public
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
*/
public
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
*/
public
const
NO_VALIDATE
=
self
::
NO_SIZE_CHECK
|
8
;
/**
* Key for the 'indexed tag name' metadata item. Value is string.
* @since 1.25
*/
public
const
META_INDEXED_TAG_NAME
=
'_element'
;
/**
* Key for the 'subelements' metadata item. Value is string[].
* @since 1.25
*/
public
const
META_SUBELEMENTS
=
'_subelements'
;
/**
* Key for the 'preserve keys' metadata item. Value is string[].
* @since 1.25
*/
public
const
META_PRESERVE_KEYS
=
'_preservekeys'
;
/**
* Key for the 'content' metadata item. Value is string.
* @since 1.25
*/
public
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
*/
public
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
*/
public
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
*/
public
const
META_KVP_MERGE
=
'_kvpmerge'
;
/**
* Key for the 'BC bools' metadata item. Value is string[].
* Note no setter is provided.
* @since 1.25
*/
public
const
META_BC_BOOLS
=
'_BC_bools'
;
/**
* Key for the 'BC subelements' metadata item. Value is string[].
* Note no setter is provided.
* @since 1.25
*/
public
const
META_BC_SUBELEMENTS
=
'_BC_subelements'
;
/** @var mixed */
private
$data
;
private
int
$size
;
/** @var int|false */
private
$maxSize
;
private
ApiErrorFormatter
$errorFormatter
;
/**
* @param int|false $maxSize Maximum result "size", or false for no limit
*/
public
function
__construct
(
$maxSize
)
{
$this
->
maxSize
=
$maxSize
;
$this
->
reset
();
}
/**
* @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
;
}
/***************************************************************************/
// region Content
/** @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
{
$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"
);
}
}
elseif
(
$value
!==
$arr
[
$name
]
)
{
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
)
{
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
(
$value
instanceof
\Wikimedia\Message\MessageParam
)
{
// HACK Support code that puts $msg->getParams() directly into API responses
// (e.g. ApiErrorFormatter::formatRawMessage()).
$value
=
$value
->
getType
()
===
'text'
?
$value
->
getValue
()
:
$value
->
jsonSerialize
();
}
elseif
(
is_callable
(
[
$value
,
'__toString'
]
)
)
{
$value
=
(
string
)
$value
;
}
else
{
$value
=
(
array
)
$value
+
[
self
::
META_TYPE
=>
'assoc'
];
}
}
if
(
is_string
(
$value
)
)
{
// Optimization: avoid querying the service locator for each value.
static
$contentLanguage
=
null
;
if
(
!
$contentLanguage
)
{
$contentLanguage
=
MediaWikiServices
::
getInstance
()->
getContentLanguage
();
}
$value
=
$contentLanguage
->
normalize
(
$value
);
}
elseif
(
is_array
(
$value
)
)
{
foreach
(
$value
as
$k
=>
$v
)
{
$value
[
$k
]
=
self
::
validateValue
(
$v
);
}
}
elseif
(
$value
!==
null
&&
!
is_scalar
(
$value
)
)
{
$type
=
get_debug_type
(
$value
);
throw
new
InvalidArgumentException
(
"Cannot add $type to ApiResult"
);
}
elseif
(
is_float
(
$value
)
&&
!
is_finite
(
$value
)
)
{
throw
new
InvalidArgumentException
(
'Cannot add non-finite floats 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
(
!(
$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
(
!(
$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
);
return
$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
);
}
// endregion -- end of Content
/***************************************************************************/
// region Metadata
/** @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
(
is_array
(
$v
)
&&
!
self
::
isMetadataKey
(
$k
)
)
{
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|null $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|null $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|null $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
(
is_array
(
$v
)
&&
!
self
::
isMetadataKey
(
$k
)
)
{
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|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
*/
public
function
addArrayTypeRecursive
(
$path
,
$tag
,
$kvpKeyName
=
null
)
{
$arr
=
&
$this
->
path
(
$path
);
self
::
setArrayTypeRecursive
(
$arr
,
$tag
,
$kvpKeyName
);
}
// endregion -- end of Metadata
/***************************************************************************/
// region Utility
/** @name Utility */
/**
* Test whether a key should be considered metadata
*
* @param string|int $key
* @return bool
*/
public
static
function
isMetadataKey
(
$key
)
{
// Optimization: This is a very hot and highly optimized code path. Note that ord() only
// considers the first character and also works with empty strings and integers.
// 95 corresponds to the '_' character.
return
ord
(
$key
)
===
95
;
}
/**
* Apply transformations to an array, returning the transformed array.
*
* @see ApiResult::getResultData()
* @since 1.25
* @param array $dataIn
* @param array $transforms
* @return array|stdClass
*/
protected
static
function
applyTransformations
(
array
$dataIn
,
array
$transforms
)
{
$strip
=
$transforms
[
'Strip'
]
??
'none'
;
if
(
$strip
===
'base'
)
{
$transforms
[
'Strip'
]
=
'none'
;
}
$transformTypes
=
$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_fill_keys
(
$metadata
[
self
::
META_BC_BOOLS
],
true
)
:
[];
}
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'
:
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
$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"'
);
}
// No type transformation
if
(
$transformTypes
===
null
)
{
return
$data
+
$keepMetadata
;
}
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'
:
// Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
// for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
// Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
// (which yields zero if the string was non-numeric), and then performs the comparison,
// while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
// PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
// and compares those two strings instead. We therefore use a custom comparison function
// implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
// irrespective of the PHP version (T326480).
uksort
(
$data
,
static
function
(
$a
,
$b
):
int
{
// In a comparison of a number or numeric string with a non-numeric string,
// coerce both values into a string prior to comparing and compare the resulting strings.
// Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
// to be numeric, so trim the inputs prior to the numeric checks to make the behavior
// consistent across PHP versions.
if
(
is_numeric
(
trim
(
$a
)
)
xor
is_numeric
(
trim
(
$b
)
)
)
{
return
(
string
)
$a
<=>
(
string
)
$b
;
}
return
$a
<=>
$b
;
}
);
$data
=
array_values
(
$data
);
$metadata
[
self
::
META_TYPE
]
=
'array'
;
// @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
return
$data
+
$keepMetadata
;
case
'kvp'
:
case
'BCkvp'
:
$key
=
$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'
)
{
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
$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'
;
// @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
return
$ret
+
$keepMetadata
;
default
:
throw
new
UnexpectedValueException
(
"Unknown type '$type'"
);
}
}
/**
* 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|stdClass $data
* @return array|stdClass
*/
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|stdClass $data
* @param array|null &$metadata Store metadata here, if provided
* @return array|stdClass
*/
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
;
$dbInfinity
??=
MediaWikiServices
::
getInstance
()->
getConnectionProvider
()
->
getReplicaDatabase
()
->
getInfinity
();
if
(
$expiry
===
''
||
$expiry
===
null
||
$expiry
===
false
||
wfIsInfinity
(
$expiry
)
||
$expiry
===
$dbInfinity
)
{
return
$infinity
;
}
else
{
return
wfTimestamp
(
TS_ISO_8601
,
$expiry
);
}
}
// endregion -- end of Utility
}
/*
* This file uses VisualStudio style region/endregion fold markers which are
* recognised by PHPStorm. If modelines are enabled, the following editor
* configuration will also enable folding in vim, if it is in the last 5 lines
* of the file. We also use "@name" which creates sections in Doxygen.
*
* vim: foldmarker=//\ region,//\ endregion foldmethod=marker
*/
/** @deprecated class alias since 1.43 */
class_alias
(
ApiResult
::
class
,
'ApiResult'
);
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 12:25 (1 d, 3 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
da/17/00648432e040de5e0d90dbc95af9
Default Alt Text
ApiResult.php (39 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment