blob: 53001560c7e29688e079cc1245dcbc03c2e75cda [file] [log] [blame]
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\Message\Converter;
use MediaWiki\Message\Message;
use Wikimedia\Assert\Assert;
use Wikimedia\Message\MessageValue;
/**
* Generic operation result class
* Has warning/error list, boolean status and arbitrary value
*
* "Good" means the operation was completed with no warnings or errors.
*
* "OK" means the operation was partially or wholly completed.
*
* An operation which is not OK should have errors so that the user can be
* informed as to what went wrong. Calling the fatal() function sets an error
* message and simultaneously switches off the OK flag.
*
* The recommended pattern for Status objects is to return a StatusValue
* unconditionally, i.e. both on success and on failure -- so that the
* developer of the calling code is reminded that the function can fail, and
* so that a lack of error-handling will be explicit.
*
* The use of Message objects should be avoided when serializability is needed.
*
* @newable
* @stable to extend
* @since 1.25
*/
class StatusValue {
/**
* @var bool
* @internal Only for use by Status. Use {@link self::isOK()} or {@link self::setOK()}.
*/
protected $ok = true;
/**
* @var array[]
* @internal Only for use by Status. Use {@link self::getErrors()} (get full list),
* {@link self::splitByErrorType()} (get errors/warnings), or
* {@link self::fatal()}, {@link self::error()} or {@link self::warning()} (add error/warning).
*/
protected $errors = [];
/** @var mixed */
public $value;
/** @var bool[] Map of (key => bool) to indicate success of each part of batch operations */
public $success = [];
/** @var int Counter for batch operations */
public $successCount = 0;
/** @var int Counter for batch operations */
public $failCount = 0;
/** @var mixed arbitrary extra data about the operation */
public $statusData;
/**
* Factory function for fatal errors
*
* @param string|MessageSpecifier $message Message key or object
* @param mixed ...$parameters
* @return static
*/
public static function newFatal( $message, ...$parameters ) {
$result = new static();
$result->fatal( $message, ...$parameters );
return $result;
}
/**
* Factory function for good results
*
* @param mixed|null $value
* @return static
*/
public static function newGood( $value = null ) {
$result = new static();
$result->value = $value;
return $result;
}
/**
* Splits this StatusValue object into two new StatusValue objects, one which contains only
* the error messages, and one that contains the warnings, only. The returned array is
* defined as:
* [
* 0 => object(StatusValue) # the StatusValue with error messages, only
* 1 => object(StatusValue) # The StatusValue with warning messages, only
* ]
*
* @return static[]
*/
public function splitByErrorType() {
$errorsOnlyStatusValue = static::newGood();
$warningsOnlyStatusValue = static::newGood();
$warningsOnlyStatusValue->setResult( true, $this->getValue() );
$errorsOnlyStatusValue->setResult( $this->isOK(), $this->getValue() );
foreach ( $this->errors as $item ) {
if ( $item['type'] === 'warning' ) {
$warningsOnlyStatusValue->errors[] = $item;
} else {
$errorsOnlyStatusValue->errors[] = $item;
}
}
return [ $errorsOnlyStatusValue, $warningsOnlyStatusValue ];
}
/**
* Returns whether the operation completed and didn't have any error or
* warnings
*
* @return bool
*/
public function isGood() {
return $this->ok && !$this->errors;
}
/**
* Returns whether the operation completed
*
* @return bool
*/
public function isOK() {
return $this->ok;
}
/**
* @return mixed
*/
public function getValue() {
return $this->value;
}
/**
* Get the list of errors
*
* Each error is a (message:string or MessageSpecifier,params:array) map
*
* @deprecated since 1.43 Use `->getMessages()` instead
* @return array[]
* @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[]
*/
public function getErrors() {
return $this->errors;
}
/**
* Change operation status
*
* @param bool $ok
* @return $this
*/
public function setOK( $ok ) {
$this->ok = $ok;
return $this;
}
/**
* Change operation result
*
* @param bool $ok Whether the operation completed
* @param mixed|null $value
* @return $this
*/
public function setResult( $ok, $value = null ) {
$this->ok = (bool)$ok;
$this->value = $value;
return $this;
}
/**
* Add a new error to the error array ($this->errors) if that error is not already in the
* error array. Each error is passed as an array with the following fields:
*
* - type: 'error' or 'warning'
* - message: a string (message key) or MessageSpecifier
* - params: an array of string parameters
*
* If the new error is of type 'error' and it matches an existing error of type 'warning',
* the existing error is upgraded to type 'error'. An error provided as a MessageSpecifier
* will successfully match an error provided as the same string message key and array of
* parameters as separate array elements.
*
* @param array $newError
* @phan-param array{type:'warning'|'error', message:string|MessageSpecifier, params:array} $newError
* @return $this
*/
private function addError( array $newError ) {
if ( $newError[ 'message' ] instanceof MessageSpecifier ) {
if ( $newError['params'] ) {
// Deprecate code like `Status::newFatal( wfMessage( 'foo' ), 'param' )`
// - the parameters have always been ignored, so this is usually a mistake.
wfDeprecatedMsg( 'Combining MessageSpecifier and parameters array' .
' was deprecated in MediaWiki 1.43', '1.43' );
}
$isEqual = static function ( $key, $params ) use ( $newError ) {
if ( $key instanceof MessageSpecifier ) {
// compare attributes of both MessageSpecifiers
return $newError['message'] == $key;
} else {
return $newError['message']->getKey() === $key &&
$newError['message']->getParams() === $params;
}
};
} else {
$isEqual = static function ( $key, $params ) use ( $newError ) {
if ( $key instanceof MessageSpecifier ) {
$params = $key->getParams();
$key = $key->getKey();
}
return $newError['message'] === $key && $newError['params'] === $params;
};
}
foreach ( $this->errors as [ 'type' => &$type, 'message' => $key, 'params' => $params ] ) {
if ( $isEqual( $key, $params ) ) {
if ( $type === 'warning' && $newError['type'] === 'error' ) {
$type = 'error';
}
return $this;
}
}
$this->errors[] = $newError;
return $this;
}
/**
* Add a new warning
*
* @param string|MessageSpecifier|MessageValue $message Message key or object
* @param mixed ...$parameters
* @return $this
*/
public function warning( $message, ...$parameters ) {
$message = $this->normalizeMessage( $message );
return $this->addError( [
'type' => 'warning',
'message' => $message,
'params' => $parameters
] );
}
/**
* Add an error, do not set fatal flag
* This can be used for non-fatal errors
*
* @param string|MessageSpecifier|MessageValue $message Message key or object
* @param mixed ...$parameters
* @return $this
*/
public function error( $message, ...$parameters ) {
$message = $this->normalizeMessage( $message );
return $this->addError( [
'type' => 'error',
'message' => $message,
'params' => $parameters
] );
}
/**
* Add an error and set OK to false, indicating that the operation
* as a whole was fatal
*
* @param string|MessageSpecifier|MessageValue $message Message key or object
* @param mixed ...$parameters
* @return $this
*/
public function fatal( $message, ...$parameters ) {
$this->ok = false;
return $this->error( $message, ...$parameters );
}
/**
* Merge another status object into this one
*
* @param StatusValue $other
* @param bool $overwriteValue Whether to override the "value" member
* @return $this
*/
public function merge( $other, $overwriteValue = false ) {
if ( $this->statusData !== null && $other->statusData !== null ) {
throw new RuntimeException( "Status cannot be merged, because they both have \$statusData" );
} else {
$this->statusData ??= $other->statusData;
}
foreach ( $other->errors as $error ) {
$this->addError( $error );
}
$this->ok = $this->ok && $other->ok;
if ( $overwriteValue ) {
$this->value = $other->value;
}
$this->successCount += $other->successCount;
$this->failCount += $other->failCount;
return $this;
}
/**
* Returns a list of status messages of the given type
*
* Each entry is a map of:
* - message: string message key or MessageSpecifier
* - params: array list of parameters
*
* @deprecated since 1.43 Use `->getMessages( $type )` instead
* @param string $type
* @return array[]
* @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[]
*/
public function getErrorsByType( $type ) {
$result = [];
foreach ( $this->errors as $error ) {
if ( $error['type'] === $type ) {
$result[] = $error;
}
}
return $result;
}
/**
* Returns a list of error messages, optionally only those of the given type
*
* @since 1.43
* @param ?string $type If provided, only return messages of the type 'warning' or 'error'
* @phan-param null|'warning'|'error' $type
* @return MessageSpecifier[]
*/
public function getMessages( ?string $type = null ): array {
Assert::parameter( $type === null || $type === 'warning' || $type === 'error',
'$type', "must be null, 'warning', or 'error'" );
$result = [];
foreach ( $this->errors as $error ) {
if ( $type === null || $error['type'] === $type ) {
[ 'message' => $key, 'params' => $params ] = $error;
if ( $key instanceof MessageSpecifier ) {
$result[] = $key;
} else {
// TODO: Make MessageValue implement MessageSpecifier, and use a MessageValue here
$result[] = new Message( $key, $params );
}
}
}
return $result;
}
/**
* Returns true if the specified message is present as a warning or error.
* Any message using the same key will be found (ignoring the message parameters).
*
* @param string $message Message key to search for
* (this parameter used to allow MessageSpecifier|MessageValue too, deprecated since 1.43)
* @return bool
*/
public function hasMessage( $message ) {
if ( $message instanceof MessageSpecifier ) {
wfDeprecatedMsg( 'Passing MessageSpecifier to hasMessage()' .
' was deprecated in MediaWiki 1.43', '1.43' );
$message = $message->getKey();
} elseif ( $message instanceof MessageValue ) {
wfDeprecatedMsg( 'Passing MessageValue to hasMessage()' .
' was deprecated in MediaWiki 1.43', '1.43' );
$message = $message->getKey();
}
foreach ( $this->errors as [ 'message' => $key ] ) {
if ( ( $key instanceof MessageSpecifier && $key->getKey() === $message ) ||
$key === $message
) {
return true;
}
}
return false;
}
/**
* Returns true if any other message than the specified ones is present as a warning or error.
* Any messages using the same keys will be found (ignoring the message parameters).
*
* @param string ...$messages Message keys to search for
* (this parameter used to allow MessageSpecifier|MessageValue too, deprecated since 1.43)
* @return bool
*/
public function hasMessagesExcept( ...$messages ) {
$exceptedKeys = [];
foreach ( $messages as $message ) {
if ( $message instanceof MessageSpecifier ) {
wfDeprecatedMsg( 'Passing MessageSpecifier to hasMessagesExcept()' .
' was deprecated in MediaWiki 1.43', '1.43' );
$message = $message->getKey();
} elseif ( $message instanceof MessageValue ) {
wfDeprecatedMsg( 'Passing MessageValue to hasMessagesExcept()' .
' was deprecated in MediaWiki 1.43', '1.43' );
$message = $message->getKey();
}
$exceptedKeys[] = $message;
}
foreach ( $this->errors as [ 'message' => $key ] ) {
if ( $key instanceof MessageSpecifier ) {
$key = $key->getKey();
}
if ( !in_array( $key, $exceptedKeys, true ) ) {
return true;
}
}
return false;
}
/**
* If the specified source message exists, replace it with the specified
* destination message, but keep the same parameters as in the original error.
*
* When using a string as the `$source` parameter, any message using the same key will be replaced
* (regardless of whether it was stored as string or as MessageSpecifier, and ignoring the
* message parameters).
*
* When using a MessageValue as the `$source` parameter, this function does not work. This is a
* bug, but it's impractical to fix. Therefore, passing a MessageValue is deprecated (since 1.43).
*
* When using a MessageSpecifier as the `$source` parameter, the message will only be replaced
* when the same MessageSpecifier object was stored in the StatusValue (compared with `===`).
* Since the only reliable way to obtain one is to use getErrors(), which is deprecated,
* passing a MessageSpecifier is deprecated as well (since 1.43).
*
* @param string $source Message key to search for
* (this parameter used to allow MessageSpecifier|MessageValue too, deprecated since 1.43)
* @param MessageSpecifier|MessageValue|string $dest Replacement message key or object
* @return bool Return true if the replacement was done, false otherwise.
*/
public function replaceMessage( $source, $dest ) {
$replaced = false;
if ( $source instanceof MessageSpecifier ) {
wfDeprecatedMsg( 'Passing MessageSpecifier as $source to replaceMessage()' .
' was deprecated in MediaWiki 1.43', '1.43' );
} elseif ( $source instanceof MessageValue ) {
wfDeprecatedMsg( 'Passing MessageValue as $source to replaceMessage()' .
' was deprecated in MediaWiki 1.43', '1.43' );
$source = $this->normalizeMessage( $source );
}
$dest = $this->normalizeMessage( $dest );
foreach ( $this->errors as [ 'message' => &$message, 'params' => &$params ] ) {
if ( $message === $source ||
( $message instanceof MessageSpecifier && $message->getKey() === $source )
) {
$message = $dest;
if ( $dest instanceof MessageSpecifier ) {
// 'params' will be ignored now, so remove them from the internal array
$params = [];
}
$replaced = true;
}
}
return $replaced;
}
/**
* Returns a string representation of the status for debugging.
* This is fairly verbose and may change without notice.
*
* @return string
*/
public function __toString() {
$status = $this->isOK() ? "OK" : "Error";
if ( count( $this->errors ) ) {
$errorcount = "collected " . ( count( $this->errors ) ) . " message(s) on the way";
} else {
$errorcount = "no errors detected";
}
if ( isset( $this->value ) ) {
$valstr = gettype( $this->value ) . " value set";
if ( is_object( $this->value ) ) {
$valstr .= "\"" . get_class( $this->value ) . "\" instance";
}
} else {
$valstr = "no value set";
}
$out = sprintf( "<%s, %s, %s>",
$status,
$errorcount,
$valstr
);
if ( count( $this->errors ) > 0 ) {
$hdr = sprintf( "+-%'-8s-+-%'-25s-+-%'-36s-+\n", "", "", "" );
$out .= "\n" . $hdr;
foreach ( $this->errors as [ 'type' => $type, 'message' => $key, 'params' => $params ] ) {
if ( $key instanceof MessageSpecifier ) {
$params = $key->getParams();
$key = $key->getKey();
}
$keyChunks = mb_str_split( $key, 25 );
$paramsChunks = mb_str_split( $this->flattenParams( $params, " | " ), 36 );
// array_map(null,...) is like Python's zip()
foreach ( array_map( null, [ $type ], $keyChunks, $paramsChunks )
as [ $typeChunk, $keyChunk, $paramsChunk ]
) {
$out .= sprintf( "| %-8s | %-25s | %-36s |\n",
$typeChunk,
$keyChunk,
$paramsChunk
);
}
}
$out .= $hdr;
}
return $out;
}
/**
* @param array $params Message parameters
* @param string $joiner
*
* @return string String representation
*/
private function flattenParams( array $params, string $joiner = ', ' ): string {
$ret = [];
foreach ( $params as $p ) {
if ( is_array( $p ) ) {
$r = '[ ' . self::flattenParams( $p ) . ' ]';
} elseif ( $p instanceof MessageSpecifier ) {
$r = '{ ' . $p->getKey() . ': ' . self::flattenParams( $p->getParams() ) . ' }';
} else {
$r = (string)$p;
}
$ret[] = mb_strlen( $r ) > 100 ? mb_substr( $r, 0, 99 ) . "..." : $r;
}
return implode( $joiner, $ret );
}
/**
* Returns a list of status messages of the given type (or all if false)
*
* @internal Only for use by Status.
* @note this handles RawMessage poorly
*
* @param string|bool $type
* @return array[]
*/
protected function getStatusArray( $type = false ) {
$result = [];
foreach ( $this->getErrors() as $error ) {
if ( !$type || $error['type'] === $type ) {
if ( $error['message'] instanceof MessageSpecifier ) {
$result[] = [ $error['message']->getKey(), ...$error['message']->getParams() ];
} else {
$result[] = [ $error['message'], ...$error['params'] ];
}
}
}
return $result;
}
/**
* @param MessageSpecifier|MessageValue|string $message
*
* @return MessageSpecifier|string
*/
private function normalizeMessage( $message ) {
if ( $message instanceof MessageValue ) {
$converter = new Converter();
return $converter->convertMessageValue( $message );
}
return $message;
}
}