Status.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <?php
  2. /**
  3. * Generic operation result.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. /**
  23. * Generic operation result class
  24. * Has warning/error list, boolean status and arbitrary value
  25. *
  26. * "Good" means the operation was completed with no warnings or errors.
  27. *
  28. * "OK" means the operation was partially or wholly completed.
  29. *
  30. * An operation which is not OK should have errors so that the user can be
  31. * informed as to what went wrong. Calling the fatal() function sets an error
  32. * message and simultaneously switches off the OK flag.
  33. *
  34. * The recommended pattern for Status objects is to return a Status object
  35. * unconditionally, i.e. both on success and on failure -- so that the
  36. * developer of the calling code is reminded that the function can fail, and
  37. * so that a lack of error-handling will be explicit.
  38. */
  39. class Status extends StatusValue {
  40. /** @var callable|false */
  41. public $cleanCallback = false;
  42. /**
  43. * Succinct helper method to wrap a StatusValue
  44. *
  45. * This is is useful when formatting StatusValue objects:
  46. * @code
  47. * $this->getOutput()->addHtml( Status::wrap( $sv )->getHTML() );
  48. * @endcode
  49. *
  50. * @param StatusValue|Status $sv
  51. * @return Status
  52. */
  53. public static function wrap( $sv ) {
  54. if ( $sv instanceof static ) {
  55. return $sv;
  56. }
  57. $result = new static();
  58. $result->ok =& $sv->ok;
  59. $result->errors =& $sv->errors;
  60. $result->value =& $sv->value;
  61. $result->successCount =& $sv->successCount;
  62. $result->failCount =& $sv->failCount;
  63. $result->success =& $sv->success;
  64. return $result;
  65. }
  66. /**
  67. * Backwards compatibility logic
  68. *
  69. * @param string $name
  70. * @return mixed
  71. * @throws RuntimeException
  72. */
  73. function __get( $name ) {
  74. if ( $name === 'ok' ) {
  75. return $this->isOK();
  76. }
  77. if ( $name === 'errors' ) {
  78. return $this->getErrors();
  79. }
  80. throw new RuntimeException( "Cannot get '$name' property." );
  81. }
  82. /**
  83. * Change operation result
  84. * Backwards compatibility logic
  85. *
  86. * @param string $name
  87. * @param mixed $value
  88. * @throws RuntimeException
  89. */
  90. function __set( $name, $value ) {
  91. if ( $name === 'ok' ) {
  92. $this->setOK( $value );
  93. } elseif ( !property_exists( $this, $name ) ) {
  94. // Caller is using undeclared ad-hoc properties
  95. $this->$name = $value;
  96. } else {
  97. throw new RuntimeException( "Cannot set '$name' property." );
  98. }
  99. }
  100. /**
  101. * Splits this Status object into two new Status objects, one which contains only
  102. * the error messages, and one that contains the warnings, only. The returned array is
  103. * defined as:
  104. * [
  105. * 0 => object(Status) # The Status with error messages, only
  106. * 1 => object(Status) # The Status with warning messages, only
  107. * ]
  108. *
  109. * @return Status[]
  110. * @suppress PhanUndeclaredProperty Status vs StatusValue
  111. */
  112. public function splitByErrorType() {
  113. list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType();
  114. $errorsOnlyStatus->cleanCallback =
  115. $warningsOnlyStatus->cleanCallback = $this->cleanCallback;
  116. return [ $errorsOnlyStatus, $warningsOnlyStatus ];
  117. }
  118. /**
  119. * Returns the wrapped StatusValue object
  120. * @return StatusValue
  121. * @since 1.27
  122. */
  123. public function getStatusValue() {
  124. return $this;
  125. }
  126. /**
  127. * @param array $params
  128. * @return array
  129. */
  130. protected function cleanParams( array $params ) {
  131. if ( !$this->cleanCallback ) {
  132. return $params;
  133. }
  134. $cleanParams = [];
  135. foreach ( $params as $i => $param ) {
  136. $cleanParams[$i] = call_user_func( $this->cleanCallback, $param );
  137. }
  138. return $cleanParams;
  139. }
  140. /**
  141. * @param string|Language|null|StubUserLang $lang Language to use for processing
  142. * messages, or null to default to the user language.
  143. * @return Language|StubUserLang
  144. */
  145. protected function languageFromParam( $lang ) {
  146. if ( $lang === null ) {
  147. return RequestContext::getMain()->getLanguage();
  148. }
  149. if ( $lang instanceof Language || $lang instanceof StubUserLang ) {
  150. return $lang;
  151. }
  152. return Language::factory( $lang );
  153. }
  154. /**
  155. * Get the error list as a wikitext formatted list
  156. *
  157. * @param string|bool $shortContext A short enclosing context message name, to
  158. * be used when there is a single error
  159. * @param string|bool $longContext A long enclosing context message name, for a list
  160. * @param string|Language|null|StubUserLang $lang Language to use for processing messages
  161. * @return string
  162. */
  163. public function getWikiText( $shortContext = false, $longContext = false, $lang = null ) {
  164. $lang = $this->languageFromParam( $lang );
  165. $rawErrors = $this->getErrors();
  166. if ( count( $rawErrors ) === 0 ) {
  167. if ( $this->isOK() ) {
  168. $this->fatal( 'internalerror_info',
  169. __METHOD__ . " called for a good result, this is incorrect\n" );
  170. } else {
  171. $this->fatal( 'internalerror_info',
  172. __METHOD__ . ": Invalid result object: no error text but not OK\n" );
  173. }
  174. $rawErrors = $this->getErrors(); // just added a fatal
  175. }
  176. if ( count( $rawErrors ) === 1 ) {
  177. $s = $this->getErrorMessage( $rawErrors[0], $lang )->plain();
  178. if ( $shortContext ) {
  179. $s = wfMessage( $shortContext, $s )->inLanguage( $lang )->plain();
  180. } elseif ( $longContext ) {
  181. $s = wfMessage( $longContext, "* $s\n" )->inLanguage( $lang )->plain();
  182. }
  183. } else {
  184. $errors = $this->getErrorMessageArray( $rawErrors, $lang );
  185. foreach ( $errors as &$error ) {
  186. $error = $error->plain();
  187. }
  188. $s = '* ' . implode( "\n* ", $errors ) . "\n";
  189. if ( $longContext ) {
  190. $s = wfMessage( $longContext, $s )->inLanguage( $lang )->plain();
  191. } elseif ( $shortContext ) {
  192. $s = wfMessage( $shortContext, "\n$s\n" )->inLanguage( $lang )->plain();
  193. }
  194. }
  195. return $s;
  196. }
  197. /**
  198. * Get a bullet list of the errors as a Message object.
  199. *
  200. * $shortContext and $longContext can be used to wrap the error list in some text.
  201. * $shortContext will be preferred when there is a single error; $longContext will be
  202. * preferred when there are multiple ones. In either case, $1 will be replaced with
  203. * the list of errors.
  204. *
  205. * $shortContext is assumed to use $1 as an inline parameter: if there is a single item,
  206. * it will not be made into a list; if there are multiple items, newlines will be inserted
  207. * around the list.
  208. * $longContext is assumed to use $1 as a standalone parameter; it will always receive a list.
  209. *
  210. * If both parameters are missing, and there is only one error, no bullet will be added.
  211. *
  212. * @param string|string[]|bool $shortContext A message name or an array of message names.
  213. * @param string|string[]|bool $longContext A message name or an array of message names.
  214. * @param string|Language|null $lang Language to use for processing messages
  215. * @return Message
  216. */
  217. public function getMessage( $shortContext = false, $longContext = false, $lang = null ) {
  218. $lang = $this->languageFromParam( $lang );
  219. $rawErrors = $this->getErrors();
  220. if ( count( $rawErrors ) === 0 ) {
  221. if ( $this->isOK() ) {
  222. $this->fatal( 'internalerror_info',
  223. __METHOD__ . " called for a good result, this is incorrect\n" );
  224. } else {
  225. $this->fatal( 'internalerror_info',
  226. __METHOD__ . ": Invalid result object: no error text but not OK\n" );
  227. }
  228. $rawErrors = $this->getErrors(); // just added a fatal
  229. }
  230. if ( count( $rawErrors ) === 1 ) {
  231. $s = $this->getErrorMessage( $rawErrors[0], $lang );
  232. if ( $shortContext ) {
  233. $s = wfMessage( $shortContext, $s )->inLanguage( $lang );
  234. } elseif ( $longContext ) {
  235. $wrapper = new RawMessage( "* \$1\n" );
  236. $wrapper->params( $s )->parse();
  237. $s = wfMessage( $longContext, $wrapper )->inLanguage( $lang );
  238. }
  239. } else {
  240. $msgs = $this->getErrorMessageArray( $rawErrors, $lang );
  241. $msgCount = count( $msgs );
  242. $s = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) );
  243. $s->params( $msgs )->parse();
  244. if ( $longContext ) {
  245. $s = wfMessage( $longContext, $s )->inLanguage( $lang );
  246. } elseif ( $shortContext ) {
  247. $wrapper = new RawMessage( "\n\$1\n", [ $s ] );
  248. $wrapper->parse();
  249. $s = wfMessage( $shortContext, $wrapper )->inLanguage( $lang );
  250. }
  251. }
  252. return $s;
  253. }
  254. /**
  255. * Return the message for a single error
  256. *
  257. * The code string can be used a message key with per-language versions.
  258. * If $error is an array, the "params" field is a list of parameters for the message.
  259. *
  260. * @param array|string $error Code string or (key: code string, params: string[]) map
  261. * @param string|Language|null $lang Language to use for processing messages
  262. * @return Message
  263. */
  264. protected function getErrorMessage( $error, $lang = null ) {
  265. if ( is_array( $error ) ) {
  266. if ( isset( $error['message'] ) && $error['message'] instanceof Message ) {
  267. $msg = $error['message'];
  268. } elseif ( isset( $error['message'] ) && isset( $error['params'] ) ) {
  269. $msg = wfMessage( $error['message'],
  270. array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) );
  271. } else {
  272. $msgName = array_shift( $error );
  273. $msg = wfMessage( $msgName,
  274. array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) );
  275. }
  276. } elseif ( is_string( $error ) ) {
  277. $msg = wfMessage( $error );
  278. } else {
  279. throw new UnexpectedValueException( 'Got ' . get_class( $error ) . ' for key.' );
  280. }
  281. $msg->inLanguage( $this->languageFromParam( $lang ) );
  282. return $msg;
  283. }
  284. /**
  285. * Get the error message as HTML. This is done by parsing the wikitext error message
  286. * @param string|bool $shortContext A short enclosing context message name, to
  287. * be used when there is a single error
  288. * @param string|bool $longContext A long enclosing context message name, for a list
  289. * @param string|Language|null $lang Language to use for processing messages
  290. * @return string
  291. */
  292. public function getHTML( $shortContext = false, $longContext = false, $lang = null ) {
  293. $lang = $this->languageFromParam( $lang );
  294. $text = $this->getWikiText( $shortContext, $longContext, $lang );
  295. $out = MessageCache::singleton()->parse( $text, null, true, true, $lang );
  296. return $out instanceof ParserOutput
  297. ? $out->getText( [ 'enableSectionEditLinks' => false ] )
  298. : $out;
  299. }
  300. /**
  301. * Return an array with a Message object for each error.
  302. * @param array $errors
  303. * @param string|Language|null $lang Language to use for processing messages
  304. * @return Message[]
  305. */
  306. protected function getErrorMessageArray( $errors, $lang = null ) {
  307. $lang = $this->languageFromParam( $lang );
  308. return array_map( function ( $e ) use ( $lang ) {
  309. return $this->getErrorMessage( $e, $lang );
  310. }, $errors );
  311. }
  312. /**
  313. * Get the list of errors (but not warnings)
  314. *
  315. * @return array A list in which each entry is an array with a message key as its first element.
  316. * The remaining array elements are the message parameters.
  317. * @deprecated since 1.25
  318. */
  319. public function getErrorsArray() {
  320. return $this->getStatusArray( 'error' );
  321. }
  322. /**
  323. * Get the list of warnings (but not errors)
  324. *
  325. * @return array A list in which each entry is an array with a message key as its first element.
  326. * The remaining array elements are the message parameters.
  327. * @deprecated since 1.25
  328. */
  329. public function getWarningsArray() {
  330. return $this->getStatusArray( 'warning' );
  331. }
  332. /**
  333. * Returns a list of status messages of the given type (or all if false)
  334. *
  335. * @note this handles RawMessage poorly
  336. *
  337. * @param string|bool $type
  338. * @return array
  339. */
  340. protected function getStatusArray( $type = false ) {
  341. $result = [];
  342. foreach ( $this->getErrors() as $error ) {
  343. if ( $type === false || $error['type'] === $type ) {
  344. if ( $error['message'] instanceof MessageSpecifier ) {
  345. $result[] = array_merge(
  346. [ $error['message']->getKey() ],
  347. $error['message']->getParams()
  348. );
  349. } elseif ( $error['params'] ) {
  350. $result[] = array_merge( [ $error['message'] ], $error['params'] );
  351. } else {
  352. $result[] = [ $error['message'] ];
  353. }
  354. }
  355. }
  356. return $result;
  357. }
  358. /**
  359. * Don't save the callback when serializing, because Closures can't be
  360. * serialized and we're going to clear it in __wakeup anyway.
  361. * @return array
  362. */
  363. function __sleep() {
  364. $keys = array_keys( get_object_vars( $this ) );
  365. return array_diff( $keys, [ 'cleanCallback' ] );
  366. }
  367. /**
  368. * Sanitize the callback parameter on wakeup, to avoid arbitrary execution.
  369. */
  370. function __wakeup() {
  371. $this->cleanCallback = false;
  372. }
  373. }