In change I625a48a6ecd3fad5c2ed76b23343a0fef91e1b83 I am planning to make Wikimedia\Message\MessageValue use it, and we try to pretend that it is a library separate from MediaWiki, so it makes sense to move MessageSpecifier to the same namespace under Wikimedia\. Bug: T353458 Change-Id: I9ff4ff7beb098b60c92f564591937c7d789c6684
600 lines
18 KiB
PHP
600 lines
18 KiB
PHP
<?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\MessageSpecifier;
|
|
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 implements Stringable {
|
|
|
|
/**
|
|
* @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 ) {
|
|
[ 'type' => $newType, 'message' => $newKey, 'params' => $newParams ] = $newError;
|
|
if ( $newKey instanceof MessageSpecifier ) {
|
|
if ( $newParams ) {
|
|
// 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' );
|
|
}
|
|
$newParams = $newKey->getParams();
|
|
$newKey = $newKey->getKey();
|
|
}
|
|
|
|
foreach ( $this->errors as [ 'type' => &$type, 'message' => $key, 'params' => $params ] ) {
|
|
if ( $key instanceof MessageSpecifier ) {
|
|
$params = $key->getParams();
|
|
$key = $key->getKey();
|
|
}
|
|
if ( $newKey === $key && $newParams === $params ) {
|
|
if ( $type === 'warning' && $newType === '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.
|
|
*
|
|
* @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;
|
|
}
|
|
}
|