ApiResult.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. /**
  21. * This class represents the result of the API operations.
  22. * It simply wraps a nested array structure, adding some functions to simplify
  23. * array's modifications. As various modules execute, they add different pieces
  24. * of information to this result, structuring it as it will be given to the client.
  25. *
  26. * Each subarray may either be a dictionary - key-value pairs with unique keys,
  27. * or lists, where the items are added using $data[] = $value notation.
  28. *
  29. * @since 1.25 this is no longer a subclass of ApiBase
  30. * @ingroup API
  31. */
  32. class ApiResult implements ApiSerializable {
  33. /**
  34. * Override existing value in addValue(), setValue(), and similar functions
  35. * @since 1.21
  36. */
  37. const OVERRIDE = 1;
  38. /**
  39. * For addValue(), setValue() and similar functions, if the value does not
  40. * exist, add it as the first element. In case the new value has no name
  41. * (numerical index), all indexes will be renumbered.
  42. * @since 1.21
  43. */
  44. const ADD_ON_TOP = 2;
  45. /**
  46. * For addValue() and similar functions, do not check size while adding a value
  47. * Don't use this unless you REALLY know what you're doing.
  48. * Values added while the size checking was disabled will never be counted.
  49. * Ignored for setValue() and similar functions.
  50. * @since 1.24
  51. */
  52. const NO_SIZE_CHECK = 4;
  53. /**
  54. * For addValue(), setValue() and similar functions, do not validate data.
  55. * Also disables size checking. If you think you need to use this, you're
  56. * probably wrong.
  57. * @since 1.25
  58. */
  59. const NO_VALIDATE = 12;
  60. /**
  61. * Key for the 'indexed tag name' metadata item. Value is string.
  62. * @since 1.25
  63. */
  64. const META_INDEXED_TAG_NAME = '_element';
  65. /**
  66. * Key for the 'subelements' metadata item. Value is string[].
  67. * @since 1.25
  68. */
  69. const META_SUBELEMENTS = '_subelements';
  70. /**
  71. * Key for the 'preserve keys' metadata item. Value is string[].
  72. * @since 1.25
  73. */
  74. const META_PRESERVE_KEYS = '_preservekeys';
  75. /**
  76. * Key for the 'content' metadata item. Value is string.
  77. * @since 1.25
  78. */
  79. const META_CONTENT = '_content';
  80. /**
  81. * Key for the 'type' metadata item. Value is one of the following strings:
  82. * - default: Like 'array' if all (non-metadata) keys are numeric with no
  83. * gaps, otherwise like 'assoc'.
  84. * - array: Keys are used for ordering, but are not output. In a format
  85. * like JSON, outputs as [].
  86. * - assoc: In a format like JSON, outputs as {}.
  87. * - kvp: For a format like XML where object keys have a restricted
  88. * character set, use an alternative output format. For example,
  89. * <container><item name="key">value</item></container> rather than
  90. * <container key="value" />
  91. * - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode.
  92. * - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode.
  93. * - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces
  94. * the alternative output format for all formats, for example
  95. * [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set.
  96. * @since 1.25
  97. */
  98. const META_TYPE = '_type';
  99. /**
  100. * Key for the metadata item whose value specifies the name used for the
  101. * kvp key in the alternative output format with META_TYPE 'kvp' or
  102. * 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>.
  103. * Value is string.
  104. * @since 1.25
  105. */
  106. const META_KVP_KEY_NAME = '_kvpkeyname';
  107. /**
  108. * Key for the metadata item that indicates that the KVP key should be
  109. * added into an assoc value, i.e. {"key":{"val1":"a","val2":"b"}}
  110. * transforms to {"name":"key","val1":"a","val2":"b"} rather than
  111. * {"name":"key","value":{"val1":"a","val2":"b"}}.
  112. * Value is boolean.
  113. * @since 1.26
  114. */
  115. const META_KVP_MERGE = '_kvpmerge';
  116. /**
  117. * Key for the 'BC bools' metadata item. Value is string[].
  118. * Note no setter is provided.
  119. * @since 1.25
  120. */
  121. const META_BC_BOOLS = '_BC_bools';
  122. /**
  123. * Key for the 'BC subelements' metadata item. Value is string[].
  124. * Note no setter is provided.
  125. * @since 1.25
  126. */
  127. const META_BC_SUBELEMENTS = '_BC_subelements';
  128. private $data, $size, $maxSize;
  129. private $errorFormatter;
  130. // Deprecated fields
  131. private $checkingSize, $mainForContinuation;
  132. /**
  133. * @param int|bool $maxSize Maximum result "size", or false for no limit
  134. * @since 1.25 Takes an integer|bool rather than an ApiMain
  135. */
  136. public function __construct( $maxSize ) {
  137. if ( $maxSize instanceof ApiMain ) {
  138. wfDeprecated( 'ApiMain to ' . __METHOD__, '1.25' );
  139. $this->errorFormatter = $maxSize->getErrorFormatter();
  140. $this->mainForContinuation = $maxSize;
  141. $maxSize = $maxSize->getConfig()->get( 'APIMaxResultSize' );
  142. }
  143. $this->maxSize = $maxSize;
  144. $this->checkingSize = true;
  145. $this->reset();
  146. }
  147. /**
  148. * Set the error formatter
  149. * @since 1.25
  150. * @param ApiErrorFormatter $formatter
  151. */
  152. public function setErrorFormatter( ApiErrorFormatter $formatter ) {
  153. $this->errorFormatter = $formatter;
  154. }
  155. /**
  156. * Allow for adding one ApiResult into another
  157. * @since 1.25
  158. * @return mixed
  159. */
  160. public function serializeForApiResult() {
  161. return $this->data;
  162. }
  163. /************************************************************************//**
  164. * @name Content
  165. * @{
  166. */
  167. /**
  168. * Clear the current result data.
  169. */
  170. public function reset() {
  171. $this->data = [
  172. self::META_TYPE => 'assoc', // Usually what's desired
  173. ];
  174. $this->size = 0;
  175. }
  176. /**
  177. * Get the result data array
  178. *
  179. * The returned value should be considered read-only.
  180. *
  181. * Transformations include:
  182. *
  183. * Custom: (callable) Applied before other transformations. Signature is
  184. * function ( &$data, &$metadata ), return value is ignored. Called for
  185. * each nested array.
  186. *
  187. * BC: (array) This transformation does various adjustments to bring the
  188. * output in line with the pre-1.25 result format. The value array is a
  189. * list of flags: 'nobool', 'no*', 'nosub'.
  190. * - Boolean-valued items are changed to '' if true or removed if false,
  191. * unless listed in META_BC_BOOLS. This may be skipped by including
  192. * 'nobool' in the value array.
  193. * - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is
  194. * set to '*'. This may be skipped by including 'no*' in the value
  195. * array.
  196. * - Tags listed in META_BC_SUBELEMENTS will have their values changed to
  197. * [ '*' => $value ]. This may be skipped by including 'nosub' in
  198. * the value array.
  199. * - If META_TYPE is 'BCarray', set it to 'default'
  200. * - If META_TYPE is 'BCassoc', set it to 'default'
  201. * - If META_TYPE is 'BCkvp', perform the transformation (even if
  202. * the Types transformation is not being applied).
  203. *
  204. * Types: (assoc) Apply transformations based on META_TYPE. The values
  205. * array is an associative array with the following possible keys:
  206. * - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc'
  207. * as objects.
  208. * - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
  209. * and 'BCkvp' into arrays of two-element arrays, something like this:
  210. * $output = [];
  211. * foreach ( $input as $key => $value ) {
  212. * $pair = [];
  213. * $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
  214. * ApiResult::setContentValue( $pair, 'value', $value );
  215. * $output[] = $pair;
  216. * }
  217. *
  218. * Strip: (string) Strips metadata keys from the result.
  219. * - 'all': Strip all metadata, recursively
  220. * - 'base': Strip metadata at the top-level only.
  221. * - 'none': Do not strip metadata.
  222. * - 'bc': Like 'all', but leave certain pre-1.25 keys.
  223. *
  224. * @since 1.25
  225. * @param array|string|null $path Path to fetch, see ApiResult::addValue
  226. * @param array $transforms See above
  227. * @return mixed Result data, or null if not found
  228. */
  229. public function getResultData( $path = [], $transforms = [] ) {
  230. $path = (array)$path;
  231. if ( !$path ) {
  232. return self::applyTransformations( $this->data, $transforms );
  233. }
  234. $last = array_pop( $path );
  235. $ret = &$this->path( $path, 'dummy' );
  236. if ( !isset( $ret[$last] ) ) {
  237. return null;
  238. } elseif ( is_array( $ret[$last] ) ) {
  239. return self::applyTransformations( $ret[$last], $transforms );
  240. } else {
  241. return $ret[$last];
  242. }
  243. }
  244. /**
  245. * Get the size of the result, i.e. the amount of bytes in it
  246. * @return int
  247. */
  248. public function getSize() {
  249. return $this->size;
  250. }
  251. /**
  252. * Add an output value to the array by name.
  253. *
  254. * Verifies that value with the same name has not been added before.
  255. *
  256. * @since 1.25
  257. * @param array &$arr To add $value to
  258. * @param string|int|null $name Index of $arr to add $value at,
  259. * or null to use the next numeric index.
  260. * @param mixed $value
  261. * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
  262. */
  263. public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
  264. if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
  265. $value = self::validateValue( $value );
  266. }
  267. if ( $name === null ) {
  268. if ( $flags & self::ADD_ON_TOP ) {
  269. array_unshift( $arr, $value );
  270. } else {
  271. array_push( $arr, $value );
  272. }
  273. return;
  274. }
  275. $exists = isset( $arr[$name] );
  276. if ( !$exists || ( $flags & self::OVERRIDE ) ) {
  277. if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
  278. $arr = [ $name => $value ] + $arr;
  279. } else {
  280. $arr[$name] = $value;
  281. }
  282. } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
  283. $conflicts = array_intersect_key( $arr[$name], $value );
  284. if ( !$conflicts ) {
  285. $arr[$name] += $value;
  286. } else {
  287. $keys = implode( ', ', array_keys( $conflicts ) );
  288. throw new RuntimeException(
  289. "Conflicting keys ($keys) when attempting to merge element $name"
  290. );
  291. }
  292. } else {
  293. throw new RuntimeException(
  294. "Attempting to add element $name=$value, existing value is {$arr[$name]}"
  295. );
  296. }
  297. }
  298. /**
  299. * Validate a value for addition to the result
  300. * @param mixed $value
  301. * @return array|mixed|string
  302. */
  303. private static function validateValue( $value ) {
  304. global $wgContLang;
  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 = $wgContLang->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. $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 $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 $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 $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 $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 = isset( $transforms['Strip'] ) ? $transforms['Strip'] : 'none';
  743. if ( $strip === 'base' ) {
  744. $transforms['Strip'] = 'none';
  745. }
  746. $transformTypes = isset( $transforms['Types'] ) ? $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 = isset( $metadata[self::META_KVP_KEY_NAME] )
  879. ? $metadata[self::META_KVP_KEY_NAME]
  880. : $transformTypes['ArmorKVP'];
  881. $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
  882. $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
  883. $merge = !empty( $metadata[self::META_KVP_MERGE] );
  884. $ret = [];
  885. foreach ( $data as $k => $v ) {
  886. if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
  887. $vArr = (array)$v;
  888. if ( isset( $vArr[self::META_TYPE] ) ) {
  889. $mergeType = $vArr[self::META_TYPE];
  890. } elseif ( is_object( $v ) ) {
  891. $mergeType = 'assoc';
  892. } else {
  893. $keys = array_keys( $vArr );
  894. sort( $keys, SORT_NUMERIC );
  895. $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
  896. }
  897. } else {
  898. $mergeType = 'n/a';
  899. }
  900. if ( $mergeType === 'assoc' ) {
  901. $item = $vArr + [
  902. $key => $k,
  903. ];
  904. if ( $strip === 'none' ) {
  905. self::setPreserveKeysList( $item, [ $key ] );
  906. }
  907. } else {
  908. $item = [
  909. $key => $k,
  910. $valKey => $v,
  911. ];
  912. if ( $strip === 'none' ) {
  913. $item += [
  914. self::META_PRESERVE_KEYS => [ $key ],
  915. self::META_CONTENT => $valKey,
  916. self::META_TYPE => 'assoc',
  917. ];
  918. }
  919. }
  920. $ret[] = $assocAsObject ? (object)$item : $item;
  921. }
  922. $metadata[self::META_TYPE] = 'array';
  923. return $ret + $keepMetadata;
  924. default:
  925. throw new UnexpectedValueException( "Unknown type '$type'" );
  926. }
  927. } else {
  928. return $data + $keepMetadata;
  929. }
  930. }
  931. /**
  932. * Recursively remove metadata keys from a data array or object
  933. *
  934. * Note this removes all potential metadata keys, not just the defined
  935. * ones.
  936. *
  937. * @since 1.25
  938. * @param array|object $data
  939. * @return array|object
  940. */
  941. public static function stripMetadata( $data ) {
  942. if ( is_array( $data ) || is_object( $data ) ) {
  943. $isObj = is_object( $data );
  944. if ( $isObj ) {
  945. $data = (array)$data;
  946. }
  947. $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
  948. ? (array)$data[self::META_PRESERVE_KEYS]
  949. : [];
  950. foreach ( $data as $k => $v ) {
  951. if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
  952. unset( $data[$k] );
  953. } elseif ( is_array( $v ) || is_object( $v ) ) {
  954. $data[$k] = self::stripMetadata( $v );
  955. }
  956. }
  957. if ( $isObj ) {
  958. $data = (object)$data;
  959. }
  960. }
  961. return $data;
  962. }
  963. /**
  964. * Remove metadata keys from a data array or object, non-recursive
  965. *
  966. * Note this removes all potential metadata keys, not just the defined
  967. * ones.
  968. *
  969. * @since 1.25
  970. * @param array|object $data
  971. * @param array &$metadata Store metadata here, if provided
  972. * @return array|object
  973. */
  974. public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
  975. if ( !is_array( $metadata ) ) {
  976. $metadata = [];
  977. }
  978. if ( is_array( $data ) || is_object( $data ) ) {
  979. $isObj = is_object( $data );
  980. if ( $isObj ) {
  981. $data = (array)$data;
  982. }
  983. $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
  984. ? (array)$data[self::META_PRESERVE_KEYS]
  985. : [];
  986. foreach ( $data as $k => $v ) {
  987. if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
  988. $metadata[$k] = $v;
  989. unset( $data[$k] );
  990. }
  991. }
  992. if ( $isObj ) {
  993. $data = (object)$data;
  994. }
  995. }
  996. return $data;
  997. }
  998. /**
  999. * Get the 'real' size of a result item. This means the strlen() of the item,
  1000. * or the sum of the strlen()s of the elements if the item is an array.
  1001. * @param mixed $value Validated value (see self::validateValue())
  1002. * @return int
  1003. */
  1004. private static function size( $value ) {
  1005. $s = 0;
  1006. if ( is_array( $value ) ) {
  1007. foreach ( $value as $k => $v ) {
  1008. if ( !self::isMetadataKey( $k ) ) {
  1009. $s += self::size( $v );
  1010. }
  1011. }
  1012. } elseif ( is_scalar( $value ) ) {
  1013. $s = strlen( $value );
  1014. }
  1015. return $s;
  1016. }
  1017. /**
  1018. * Return a reference to the internal data at $path
  1019. *
  1020. * @param array|string|null $path
  1021. * @param string $create
  1022. * If 'append', append empty arrays.
  1023. * If 'prepend', prepend empty arrays.
  1024. * If 'dummy', return a dummy array.
  1025. * Else, raise an error.
  1026. * @return array
  1027. */
  1028. private function &path( $path, $create = 'append' ) {
  1029. $path = (array)$path;
  1030. $ret = &$this->data;
  1031. foreach ( $path as $i => $k ) {
  1032. if ( !isset( $ret[$k] ) ) {
  1033. switch ( $create ) {
  1034. case 'append':
  1035. $ret[$k] = [];
  1036. break;
  1037. case 'prepend':
  1038. $ret = [ $k => [] ] + $ret;
  1039. break;
  1040. case 'dummy':
  1041. $tmp = [];
  1042. return $tmp;
  1043. default:
  1044. $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
  1045. throw new InvalidArgumentException( "Path $fail does not exist" );
  1046. }
  1047. }
  1048. if ( !is_array( $ret[$k] ) ) {
  1049. $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
  1050. throw new InvalidArgumentException( "Path $fail is not an array" );
  1051. }
  1052. $ret = &$ret[$k];
  1053. }
  1054. return $ret;
  1055. }
  1056. /**
  1057. * Add the correct metadata to an array of vars we want to export through
  1058. * the API.
  1059. *
  1060. * @param array $vars
  1061. * @param bool $forceHash
  1062. * @return array
  1063. */
  1064. public static function addMetadataToResultVars( $vars, $forceHash = true ) {
  1065. // Process subarrays and determine if this is a JS [] or {}
  1066. $hash = $forceHash;
  1067. $maxKey = -1;
  1068. $bools = [];
  1069. foreach ( $vars as $k => $v ) {
  1070. if ( is_array( $v ) || is_object( $v ) ) {
  1071. $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
  1072. } elseif ( is_bool( $v ) ) {
  1073. // Better here to use real bools even in BC formats
  1074. $bools[] = $k;
  1075. }
  1076. if ( is_string( $k ) ) {
  1077. $hash = true;
  1078. } elseif ( $k > $maxKey ) {
  1079. $maxKey = $k;
  1080. }
  1081. }
  1082. if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
  1083. $hash = true;
  1084. }
  1085. // Set metadata appropriately
  1086. if ( $hash ) {
  1087. // Get the list of keys we actually care about. Unfortunately, we can't support
  1088. // certain keys that conflict with ApiResult metadata.
  1089. $keys = array_diff( array_keys( $vars ), [
  1090. self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
  1091. self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
  1092. ] );
  1093. return [
  1094. self::META_TYPE => 'kvp',
  1095. self::META_KVP_KEY_NAME => 'key',
  1096. self::META_PRESERVE_KEYS => $keys,
  1097. self::META_BC_BOOLS => $bools,
  1098. self::META_INDEXED_TAG_NAME => 'var',
  1099. ] + $vars;
  1100. } else {
  1101. return [
  1102. self::META_TYPE => 'array',
  1103. self::META_BC_BOOLS => $bools,
  1104. self::META_INDEXED_TAG_NAME => 'value',
  1105. ] + $vars;
  1106. }
  1107. }
  1108. /**
  1109. * Format an expiry timestamp for API output
  1110. * @since 1.29
  1111. * @param string $expiry Expiry timestamp, likely from the database
  1112. * @param string $infinity Use this string for infinite expiry
  1113. * (only use this to maintain backward compatibility with existing output)
  1114. * @return string Formatted expiry
  1115. */
  1116. public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
  1117. static $dbInfinity;
  1118. if ( $dbInfinity === null ) {
  1119. $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
  1120. }
  1121. if ( $expiry === '' || $expiry === null || $expiry === false ||
  1122. wfIsInfinity( $expiry ) || $expiry === $dbInfinity
  1123. ) {
  1124. return $infinity;
  1125. } else {
  1126. return wfTimestamp( TS_ISO_8601, $expiry );
  1127. }
  1128. }
  1129. /**@}*/
  1130. }
  1131. /**
  1132. * For really cool vim folding this needs to be at the end:
  1133. * vim: foldmarker=@{,@} foldmethod=marker
  1134. */