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 23use MediaWiki\MediaWikiServices; 24 25/** 26 * Generic operation result class 27 * Has warning/error list, boolean status and arbitrary value 28 * 29 * "Good" means the operation was completed with no warnings or errors. 30 * 31 * "OK" means the operation was partially or wholly completed. 32 * 33 * An operation which is not OK should have errors so that the user can be 34 * informed as to what went wrong. Calling the fatal() function sets an error 35 * message and simultaneously switches off the OK flag. 36 * 37 * The recommended pattern for Status objects is to return a Status object 38 * unconditionally, i.e. both on success and on failure -- so that the 39 * developer of the calling code is reminded that the function can fail, and 40 * so that a lack of error-handling will be explicit. 41 * 42 * @newable 43 */ 44class Status extends StatusValue { 45 /** @var callable|false */ 46 public $cleanCallback = false; 47 48 /** @var MessageLocalizer|null */ 49 protected $messageLocalizer; 50 51 /** 52 * Succinct helper method to wrap a StatusValue 53 * 54 * This is is useful when formatting StatusValue objects: 55 * @code 56 * $this->getOutput()->addHtml( Status::wrap( $sv )->getHTML() ); 57 * @endcode 58 * 59 * @param StatusValue|Status $sv 60 * @return Status 61 */ 62 public static function wrap( $sv ) { 63 if ( $sv instanceof static ) { 64 return $sv; 65 } 66 67 $result = new static(); 68 $result->ok =& $sv->ok; 69 $result->errors =& $sv->errors; 70 $result->value =& $sv->value; 71 $result->successCount =& $sv->successCount; 72 $result->failCount =& $sv->failCount; 73 $result->success =& $sv->success; 74 75 return $result; 76 } 77 78 /** 79 * Backwards compatibility logic 80 * 81 * @param string $name 82 * @return mixed 83 * @throws RuntimeException 84 */ 85 public function __get( $name ) { 86 if ( $name === 'ok' ) { 87 return $this->isOK(); 88 } 89 if ( $name === 'errors' ) { 90 return $this->getErrors(); 91 } 92 93 throw new RuntimeException( "Cannot get '$name' property." ); 94 } 95 96 /** 97 * Change operation result 98 * Backwards compatibility logic 99 * 100 * @param string $name 101 * @param mixed $value 102 * @throws RuntimeException 103 */ 104 public function __set( $name, $value ) { 105 if ( $name === 'ok' ) { 106 $this->setOK( $value ); 107 } elseif ( !property_exists( $this, $name ) ) { 108 // Caller is using undeclared ad-hoc properties 109 $this->$name = $value; 110 } else { 111 throw new RuntimeException( "Cannot set '$name' property." ); 112 } 113 } 114 115 /** 116 * Makes this Status object use the given localizer instead of the global one. 117 * If it is an IContextSource or a ResourceLoaderContext, it will also be used to 118 * determine the interface language. 119 * @note This setting does not survive serialization. That's usually for the best 120 * (there's no guarantee we'll still have the same localization settings after 121 * unserialization); it is the caller's responsibility to set the localizer again 122 * if needed. 123 * @param MessageLocalizer $messageLocalizer 124 */ 125 public function setMessageLocalizer( MessageLocalizer $messageLocalizer ) { 126 $this->messageLocalizer = $messageLocalizer; 127 } 128 129 /** 130 * Splits this Status object into two new Status objects, one which contains only 131 * the error messages, and one that contains the warnings, only. The returned array is 132 * defined as: 133 * [ 134 * 0 => object(Status) # The Status with error messages, only 135 * 1 => object(Status) # The Status with warning messages, only 136 * ] 137 * 138 * @return Status[] 139 */ 140 public function splitByErrorType() { 141 list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType(); 142 // phan/phan#2133? 143 '@phan-var Status $errorsOnlyStatus'; 144 '@phan-var Status $warningsOnlyStatus'; 145 146 if ( $this->messageLocalizer ) { 147 $errorsOnlyStatus->setMessageLocalizer( $this->messageLocalizer ); 148 $warningsOnlyStatus->setMessageLocalizer( $this->messageLocalizer ); 149 } 150 $errorsOnlyStatus->cleanCallback = 151 $warningsOnlyStatus->cleanCallback = $this->cleanCallback; 152 153 return [ $errorsOnlyStatus, $warningsOnlyStatus ]; 154 } 155 156 /** 157 * Returns the wrapped StatusValue object 158 * @return StatusValue 159 * @since 1.27 160 */ 161 public function getStatusValue() { 162 return $this; 163 } 164 165 /** 166 * @param array $params 167 * @return array 168 */ 169 protected function cleanParams( array $params ) { 170 if ( !$this->cleanCallback ) { 171 return $params; 172 } 173 $cleanParams = []; 174 foreach ( $params as $i => $param ) { 175 $cleanParams[$i] = call_user_func( $this->cleanCallback, $param ); 176 } 177 return $cleanParams; 178 } 179 180 /** 181 * Get the error list as a wikitext formatted list 182 * 183 * @param string|bool $shortContext A short enclosing context message name, to 184 * be used when there is a single error 185 * @param string|bool $longContext A long enclosing context message name, for a list 186 * @param string|Language|StubUserLang|null $lang Language to use for processing messages 187 * @return string 188 */ 189 public function getWikiText( $shortContext = false, $longContext = false, $lang = null ) { 190 $rawErrors = $this->getErrors(); 191 if ( count( $rawErrors ) === 0 ) { 192 if ( $this->isOK() ) { 193 $this->fatal( 'internalerror_info', 194 __METHOD__ . " called for a good result, this is incorrect\n" ); 195 } else { 196 $this->fatal( 'internalerror_info', 197 __METHOD__ . ": Invalid result object: no error text but not OK\n" ); 198 } 199 $rawErrors = $this->getErrors(); // just added a fatal 200 } 201 if ( count( $rawErrors ) === 1 ) { 202 $s = $this->getErrorMessage( $rawErrors[0], $lang )->plain(); 203 if ( $shortContext ) { 204 $s = $this->msgInLang( $shortContext, $lang, $s )->plain(); 205 } elseif ( $longContext ) { 206 $s = $this->msgInLang( $longContext, $lang, "* $s\n" )->plain(); 207 } 208 } else { 209 $errors = $this->getErrorMessageArray( $rawErrors, $lang ); 210 foreach ( $errors as &$error ) { 211 $error = $error->plain(); 212 } 213 $s = '* ' . implode( "\n* ", $errors ) . "\n"; 214 if ( $longContext ) { 215 $s = $this->msgInLang( $longContext, $lang, $s )->plain(); 216 } elseif ( $shortContext ) { 217 $s = $this->msgInLang( $shortContext, $lang, "\n$s\n" )->plain(); 218 } 219 } 220 return $s; 221 } 222 223 /** 224 * Get a bullet list of the errors as a Message object. 225 * 226 * $shortContext and $longContext can be used to wrap the error list in some text. 227 * $shortContext will be preferred when there is a single error; $longContext will be 228 * preferred when there are multiple ones. In either case, $1 will be replaced with 229 * the list of errors. 230 * 231 * $shortContext is assumed to use $1 as an inline parameter: if there is a single item, 232 * it will not be made into a list; if there are multiple items, newlines will be inserted 233 * around the list. 234 * $longContext is assumed to use $1 as a standalone parameter; it will always receive a list. 235 * 236 * If both parameters are missing, and there is only one error, no bullet will be added. 237 * 238 * @param string|string[]|bool $shortContext A message name or an array of message names. 239 * @param string|string[]|bool $longContext A message name or an array of message names. 240 * @param string|Language|StubUserLang|null $lang Language to use for processing messages 241 * @return Message 242 */ 243 public function getMessage( $shortContext = false, $longContext = false, $lang = null ) { 244 $rawErrors = $this->getErrors(); 245 if ( count( $rawErrors ) === 0 ) { 246 if ( $this->isOK() ) { 247 $this->fatal( 'internalerror_info', 248 __METHOD__ . " called for a good result, this is incorrect\n" ); 249 } else { 250 $this->fatal( 'internalerror_info', 251 __METHOD__ . ": Invalid result object: no error text but not OK\n" ); 252 } 253 $rawErrors = $this->getErrors(); // just added a fatal 254 } 255 if ( count( $rawErrors ) === 1 ) { 256 $s = $this->getErrorMessage( $rawErrors[0], $lang ); 257 if ( $shortContext ) { 258 $s = $this->msgInLang( $shortContext, $lang, $s ); 259 } elseif ( $longContext ) { 260 $wrapper = new RawMessage( "* \$1\n" ); 261 $wrapper->params( $s )->parse(); 262 $s = $this->msgInLang( $longContext, $lang, $wrapper ); 263 } 264 } else { 265 $msgs = $this->getErrorMessageArray( $rawErrors, $lang ); 266 $msgCount = count( $msgs ); 267 268 $s = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) ); 269 $s->params( $msgs )->parse(); 270 271 if ( $longContext ) { 272 $s = $this->msgInLang( $longContext, $lang, $s ); 273 } elseif ( $shortContext ) { 274 $wrapper = new RawMessage( "\n\$1\n", [ $s ] ); 275 $wrapper->parse(); 276 $s = $this->msgInLang( $shortContext, $lang, $wrapper ); 277 } 278 } 279 280 return $s; 281 } 282 283 /** 284 * Return the message for a single error 285 * 286 * The code string can be used a message key with per-language versions. 287 * If $error is an array, the "params" field is a list of parameters for the message. 288 * 289 * @param array|string $error Code string or (key: code string, params: string[]) map 290 * @param string|Language|null $lang Language to use for processing messages 291 * @return Message 292 */ 293 protected function getErrorMessage( $error, $lang = null ) { 294 if ( is_array( $error ) ) { 295 if ( isset( $error['message'] ) && $error['message'] instanceof Message ) { 296 $msg = $error['message']; 297 } elseif ( isset( $error['message'] ) && isset( $error['params'] ) ) { 298 $msg = $this->msg( $error['message'], array_map( function ( $param ) { 299 return is_string( $param ) ? wfEscapeWikiText( $param ) : $param; 300 }, $this->cleanParams( $error['params'] ) ) ); 301 } else { 302 $msgName = array_shift( $error ); 303 $msg = $this->msg( $msgName, array_map( function ( $param ) { 304 return is_string( $param ) ? wfEscapeWikiText( $param ) : $param; 305 }, $this->cleanParams( $error ) ) ); 306 } 307 } elseif ( is_string( $error ) ) { 308 $msg = $this->msg( $error ); 309 } else { 310 throw new UnexpectedValueException( 'Got ' . get_class( $error ) . ' for key.' ); 311 } 312 313 if ( $lang ) { 314 $msg->inLanguage( $lang ); 315 } 316 return $msg; 317 } 318 319 /** 320 * Get the error message as HTML. This is done by parsing the wikitext error message 321 * @param string|bool $shortContext A short enclosing context message name, to 322 * be used when there is a single error 323 * @param string|bool $longContext A long enclosing context message name, for a list 324 * @param string|Language|null $lang Language to use for processing messages 325 * @return string 326 */ 327 public function getHTML( $shortContext = false, $longContext = false, $lang = null ) { 328 $text = $this->getWikiText( $shortContext, $longContext, $lang ); 329 $out = MediaWikiServices::getInstance()->getMessageCache() 330 ->parse( $text, null, true, true, $lang ); 331 return $out instanceof ParserOutput 332 ? $out->getText( [ 'enableSectionEditLinks' => false ] ) 333 : $out; 334 } 335 336 /** 337 * Return an array with a Message object for each error. 338 * @param array $errors 339 * @param string|Language|null $lang Language to use for processing messages 340 * @return Message[] 341 */ 342 protected function getErrorMessageArray( $errors, $lang = null ) { 343 return array_map( function ( $e ) use ( $lang ) { 344 return $this->getErrorMessage( $e, $lang ); 345 }, $errors ); 346 } 347 348 /** 349 * Get the list of errors (but not warnings) 350 * 351 * @return array[] A list in which each entry is an array with a message key as its first element. 352 * The remaining array elements are the message parameters. 353 * @deprecated since 1.25 354 */ 355 public function getErrorsArray() { 356 return $this->getStatusArray( 'error' ); 357 } 358 359 /** 360 * Get the list of warnings (but not errors) 361 * 362 * @return array[] A list in which each entry is an array with a message key as its first element. 363 * The remaining array elements are the message parameters. 364 * @deprecated since 1.25 365 */ 366 public function getWarningsArray() { 367 return $this->getStatusArray( 'warning' ); 368 } 369 370 /** 371 * Returns a list of status messages of the given type (or all if false) 372 * 373 * @note this handles RawMessage poorly 374 * 375 * @param string|bool $type 376 * @return array[] 377 */ 378 protected function getStatusArray( $type = false ) { 379 $result = []; 380 381 foreach ( $this->getErrors() as $error ) { 382 if ( $type === false || $error['type'] === $type ) { 383 if ( $error['message'] instanceof MessageSpecifier ) { 384 $result[] = array_merge( 385 [ $error['message']->getKey() ], 386 $error['message']->getParams() 387 ); 388 } elseif ( $error['params'] ) { 389 $result[] = array_merge( [ $error['message'] ], $error['params'] ); 390 } else { 391 $result[] = [ $error['message'] ]; 392 } 393 } 394 } 395 396 return $result; 397 } 398 399 /** 400 * Don't save the callback when serializing, because Closures can't be 401 * serialized and we're going to clear it in __wakeup anyway. 402 * Don't save the localizer, because it can be pretty much anything. Restoring it is 403 * the caller's responsibility (otherwise it will just fall back to the global request context). 404 * @return array 405 */ 406 public function __sleep() { 407 $keys = array_keys( get_object_vars( $this ) ); 408 return array_diff( $keys, [ 'cleanCallback', 'messageLocalizer' ] ); 409 } 410 411 /** 412 * Sanitize the callback parameter on wakeup, to avoid arbitrary execution. 413 */ 414 public function __wakeup() { 415 $this->cleanCallback = false; 416 $this->messageLocalizer = null; 417 } 418 419 /** 420 * @param string|MessageSpecifier $key 421 * @param string|string[] ...$params 422 * @return Message 423 */ 424 private function msg( $key, ...$params ) : Message { 425 if ( $this->messageLocalizer ) { 426 return $this->messageLocalizer->msg( $key, ...$params ); 427 } else { 428 return wfMessage( $key, ...$params ); 429 } 430 } 431 432 /** 433 * @param string|MessageSpecifier $key 434 * @param string|Language|StubUserLang|null $lang 435 * @param mixed ...$params 436 * @return Message 437 */ 438 private function msgInLang( $key, $lang, ...$params ) : Message { 439 $msg = $this->msg( $key, ...$params ); 440 if ( $lang ) { 441 $msg->inLanguage( $lang ); 442 } 443 return $msg; 444 } 445} 446