ApiResult.php 37 KB

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