Viewing File: /home/omtekel/www/wp-content/upgrade/backup/libs.tar
Message/ParamType.php 0000666 00000005247 15133502362 0010557 0 ustar 00 <?php
namespace Wikimedia\Message;
/**
* The constants used to specify parameter types. The values of the constants
* are an unstable implementation detail.
*
* Unless otherwise noted, these should be used with an instance of ScalarParam.
*/
class ParamType {
/** A simple text string or another MessageValue, not otherwise formatted. */
public const TEXT = 'text';
/** A number, to be formatted using local digits and separators */
public const NUM = 'num';
/**
* A number of seconds, to be formatted as natural language text.
* The value will be output exactly.
*/
public const DURATION_LONG = 'duration';
/**
* A number of seconds, to be formatted as natural language text in an abbreviated way.
* The output will be rounded to an appropriate magnitude.
*/
public const DURATION_SHORT = 'period';
/**
* An expiry time.
*
* The input is either a timestamp in one of the formats accepted by the
* Wikimedia\Timestamp library, or "infinity" if the thing doesn't expire.
*
* The output is a date and time in local format, or a string representing
* an "infinite" expiry.
*/
public const EXPIRY = 'expiry';
/**
* A date time in one of the formats accepted by the Wikimedia\Timestamp library.
*
* The output is a date and time in local format.
*/
public const DATETIME = 'datetime';
/**
* A date in one of the formats accepted by the Wikimedia\Timestamp library.
*
* The output is a date in local format.
*/
public const DATE = 'date';
/**
* A time in one of the formats accepted by the Wikimedia\Timestamp library.
*
* The output is a time in local format.
*/
public const TIME = 'time';
/**
* User Group
* @since 1.38
*/
public const GROUP = 'group';
/**
* For arbitrary stringable objects
* @since 1.38
*/
public const OBJECT = 'object';
/** A number of bytes. The output will be rounded to an appropriate magnitude. */
public const SIZE = 'size';
/** A number of bits per second. The output will be rounded to an appropriate magnitude. */
public const BITRATE = 'bitrate';
/** A list of values. Must be used with ListParam. */
public const LIST = 'list';
/**
* A text parameter which is substituted after formatter processing.
*
* The creator of the parameter and message is responsible for ensuring
* that the value will be safe for the intended output format, and
* documenting what that intended output format is.
*/
public const RAW = 'raw';
/**
* A text parameter which is substituted after formatter processing.
* The output will be escaped as appropriate for the output format so
* as to represent plain text rather than any sort of markup.
*/
public const PLAINTEXT = 'plaintext';
}
Message/ListType.php 0000666 00000000710 15133502362 0010420 0 ustar 00 <?php
namespace Wikimedia\Message;
/**
* The constants used to specify list types. The values of the constants are an
* unstable implementation detail.
*/
class ListType {
/** A comma-separated list */
public const COMMA = 'comma';
/** A semicolon-separated list */
public const SEMICOLON = 'semicolon';
/** A pipe-separated list */
public const PIPE = 'pipe';
/** A natural-language list separated by "and" */
public const AND = 'text';
}
rdbms/dbal/TimestampType.php 0000666 00000001635 15133502362 0012104 0 ustar 00 <?php
namespace Wikimedia\Rdbms;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
/**
* Handling timestamp edge cases in mediawiki.
* https://www.mediawiki.org/wiki/Manual:Timestamp
*/
class TimestampType extends Type {
public const TIMESTAMP = 'mwtimestamp';
public function getSQLDeclaration( array $fieldDeclaration, AbstractPlatform $platform ) {
if ( $platform->getName() == 'mysql' ) {
// "infinite" (in expiry values has to be VARBINARY)
if ( isset( $fieldDeclaration['allowInfinite'] ) && $fieldDeclaration['allowInfinite'] ) {
return 'VARBINARY(14)';
}
return 'BINARY(14)';
}
if ( $platform->getName() == 'sqlite' ) {
return 'BLOB';
}
if ( $platform->getName() == 'postgresql' ) {
return 'TIMESTAMPTZ';
}
return $platform->getDateTimeTzTypeDeclarationSQL( $fieldDeclaration );
}
public function getName() {
return self::TIMESTAMP;
}
}
rdbms/dbal/EnumType.php 0000666 00000004524 15133502362 0011045 0 ustar 00 <?php
namespace Wikimedia\Rdbms;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
/**
* Custom handling for ENUM datatype
*
* NOTE: Use of this type is discouraged unless necessary. Please use
* alternative types where possible. See T119173 for the RFC discussion
* about this type and potential alternatives.
*
* @see https://phabricator.wikimedia.org/T119173
*/
class EnumType extends Type {
public const ENUM = 'mwenum';
/**
* Gets the SQL declaration snippet for an ENUM column
*
* @param mixed[] $column Column definition
* @param AbstractPlatform $platform
*
* @return string
*/
public function getSQLDeclaration( array $column, AbstractPlatform $platform ) {
// SQLite does not support ENUM type
if ( $platform->getName() == 'sqlite' ) {
return 'TEXT';
}
// PostgreSQL does support but needs custom handling.
// This just returns a string name that references the
// actual ENUM which will be created by CREATE TYPE command
// If 'fixed' option is not passed, this field will use TEXT
if ( $platform->getName() == 'postgresql' ) {
if ( !$column['fixed'] ) {
return 'TEXT';
}
return strtoupper( $column['name'] . '_enum' );
}
if ( $platform->getName() == 'mysql' ) {
$enumValues = $this->formatValues( $column['enum_values'] );
return "ENUM( $enumValues )";
}
}
/**
* Gets the sql portion to create ENUM for Postgres table column
*
* @param mixed[] $column
* @param AbstractPlatform $platform
*
* @see MWPostgreSqlPlatform::_getCreateTableSQL()
* @throws \InvalidArgumentException
* @return string
*/
public function makeEnumTypeSql( $column, $platform ) {
if ( $platform->getName() !== 'postgresql' ) {
throw new \InvalidArgumentException(
__METHOD__ . ' can only be called on Postgres platform'
);
}
$enumName = strtoupper( $column['name'] . '_enum' );
$enumValues = $this->formatValues( $column['enum_values'] );
$typeSql = "\n\nCREATE TYPE $enumName AS ENUM( $enumValues )";
return $typeSql;
}
/**
* Get the imploded values suitable for pushing directly into ENUM();
*
* @param string[] $values
* @return string
*/
public function formatValues( $values ) {
$values = implode( "','", $values );
$enumValues = "'" . $values . "'";
return $enumValues;
}
public function getName() {
return self::ENUM;
}
}
rdbms/dbal/TinyIntType.php 0000666 00000002244 15133502362 0011534 0 ustar 00 <?php
namespace Wikimedia\Rdbms;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\PhpIntegerMappingType;
use Doctrine\DBAL\Types\Type;
/**
* Handling smallest integer data type
*/
class TinyIntType extends Type implements PhpIntegerMappingType {
public const TINYINT = 'mwtinyint';
public function getSQLDeclaration( array $fieldDeclaration, AbstractPlatform $platform ) {
if ( $platform->getName() == 'mysql' ) {
if ( !empty( $fieldDeclaration['length'] ) && is_numeric( $fieldDeclaration['length'] ) ) {
$length = $fieldDeclaration['length'];
return "TINYINT($length)" . $this->getCommonIntegerTypeDeclarationForMySQL( $fieldDeclaration );
}
return 'TINYINT' . $this->getCommonIntegerTypeDeclarationForMySQL( $fieldDeclaration );
}
return $platform->getSmallIntTypeDeclarationSQL( $fieldDeclaration );
}
protected function getCommonIntegerTypeDeclarationForMySQL( array $columnDef ) {
$autoinc = '';
if ( !empty( $columnDef['autoincrement'] ) ) {
$autoinc = ' AUTO_INCREMENT';
}
return !empty( $columnDef['unsigned'] ) ? ' UNSIGNED' . $autoinc : $autoinc;
}
public function getName() {
return self::TINYINT;
}
}
ParamValidator/TypeDef/EnumDef.php 0000666 00000016715 15133502362 0013064 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\ListParam;
use Wikimedia\Message\ListType;
use Wikimedia\Message\MessageParam;
use Wikimedia\Message\MessageValue;
use Wikimedia\Message\ParamType;
use Wikimedia\Message\ScalarParam;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for enumeration types.
*
* This class expects that PARAM_TYPE is an array of allowed values. Subclasses
* may override getEnumValues() to determine the allowed values differently.
*
* The result from validate() is one of the defined values.
*
* Failure codes:
* - 'badvalue': The value is not a recognized value. No data.
*
* Additional codes may be generated when using certain PARAM constants. See
* the constants' documentation for details.
*
* @since 1.34
* @unstable
*/
class EnumDef extends TypeDef {
/**
* (array) Associative array of deprecated values.
*
* Keys are the deprecated parameter values. Value is one of the following:
* - null: Parameter isn't actually deprecated.
* - true: Parameter is deprecated.
* - MessageValue: Parameter is deprecated, and this message (converted to a DataMessageValue)
* is used in place of the default for passing to $this->failure().
*
* Note that this does not add any values to the enumeration, it only
* documents existing values as being deprecated.
*
* Failure codes: (non-fatal)
* - 'deprecated-value': A deprecated value was encountered. No data.
*/
public const PARAM_DEPRECATED_VALUES = 'param-deprecated-values';
public function validate( $name, $value, array $settings, array $options ) {
$values = $this->getEnumValues( $name, $settings, $options );
if ( in_array( $value, $values, true ) ) {
// Set a warning if a deprecated parameter value has been passed
if ( empty( $options['is-default'] ) &&
isset( $settings[self::PARAM_DEPRECATED_VALUES][$value] )
) {
$msg = $settings[self::PARAM_DEPRECATED_VALUES][$value];
if ( $msg instanceof MessageValue ) {
$message = DataMessageValue::new(
$msg->getKey(),
$msg->getParams(),
'deprecated-value',
$msg instanceof DataMessageValue ? $msg->getData() : null
);
} else {
$message = $this->failureMessage( 'deprecated-value' );
}
$this->failure( $message, $name, $value, $settings, $options, false );
}
return $value;
}
$isMulti = isset( $options['values-list'] );
$this->failure(
$this->failureMessage( 'badvalue', [], $isMulti ? 'enummulti' : 'enumnotmulti' )
->textListParams( array_map( static function ( $v ) {
return new ScalarParam( ParamType::PLAINTEXT, $v );
}, $values ) )
->numParams( count( $values ) ),
$name, $value, $settings, $options
);
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'][] = self::PARAM_DEPRECATED_VALUES;
$dv = $settings[self::PARAM_DEPRECATED_VALUES] ?? [];
if ( !is_array( $dv ) ) {
$ret['issues'][self::PARAM_DEPRECATED_VALUES] = 'PARAM_DEPRECATED_VALUES must be an array, got '
. gettype( $dv );
} else {
$values = array_map( function ( $v ) use ( $name, $settings, $options ) {
return $this->stringifyValue( $name, $v, $settings, $options );
}, $this->getEnumValues( $name, $settings, $options ) );
foreach ( $dv as $k => $v ) {
$k = $this->stringifyValue( $name, $k, $settings, $options );
if ( !in_array( $k, $values, true ) ) {
$ret['issues'][] = "PARAM_DEPRECATED_VALUES contains \"$k\", which is not "
. 'one of the enumerated values';
} elseif ( $v instanceof MessageValue ) {
$ret['messages'][] = $v;
} elseif ( $v !== null && $v !== true ) {
$type = $v === false ? 'false' : ( is_object( $v ) ? get_class( $v ) : gettype( $v ) );
$ret['issues'][] = 'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, '
. "but value for \"$k\" is $type";
}
}
}
return $ret;
}
public function getEnumValues( $name, array $settings, array $options ) {
return array_values( $settings[ParamValidator::PARAM_TYPE] );
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
if ( !is_array( $value ) ) {
return parent::stringifyValue( $name, $value, $settings, $options );
}
return ParamValidator::implodeMultiValue( $value );
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['type'] = $this->sortEnumValues(
$name,
$this->getEnumValues( $name, $settings, $options ),
$settings,
$options
);
if ( !empty( $settings[self::PARAM_DEPRECATED_VALUES] ) ) {
$deprecatedValues = array_intersect(
array_keys( $settings[self::PARAM_DEPRECATED_VALUES] ),
$this->getEnumValues( $name, $settings, $options )
);
if ( $deprecatedValues ) {
$deprecatedValues = $this->sortEnumValues( $name, $deprecatedValues, $settings, $options );
$info['deprecatedvalues'] = array_values( $deprecatedValues );
}
}
return $info;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );
$values = $this->getEnumValuesForHelp( $name, $settings, $options );
$count = count( $values );
$i = array_search( '', $values, true );
if ( $i === false ) {
$valuesParam = new ListParam( ListType::COMMA, $values );
} else {
unset( $values[$i] );
$valuesParam = MessageValue::new( 'paramvalidator-help-type-enum-can-be-empty' )
->commaListParams( $values )
->numParams( count( $values ) );
}
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-enum' )
->params( $isMulti ? 2 : 1 )
->params( $valuesParam )
->numParams( $count );
// Suppress standard ISMULTI message, it should be incorporated into our type message.
$info[ParamValidator::PARAM_ISMULTI] = null;
return $info;
}
/**
* Sort enum values for help/param info output
*
* @param string $name Parameter name being described.
* @param string[] $values Values being sorted
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return string[]
*/
protected function sortEnumValues(
string $name, array $values, array $settings, array $options
): array {
// sort values by deprecation status and name
$flags = [];
foreach ( $values as $k => $value ) {
$flag = 0;
if ( isset( $settings[self::PARAM_DEPRECATED_VALUES][$value] ) ) {
$flag |= 1;
}
$flags[$k] = $flag;
}
array_multisort( $flags, $values, SORT_NATURAL );
return $values;
}
/**
* Return enum values formatted for the help message
*
* @param string $name Parameter name being described.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return (MessageParam|string)[]
*/
protected function getEnumValuesForHelp( $name, array $settings, array $options ) {
$values = $this->getEnumValues( $name, $settings, $options );
$values = $this->sortEnumValues( $name, $values, $settings, $options );
// @todo Indicate deprecated values in some manner. Probably that needs
// MessageValue and/or MessageParam to have a generic ability to wrap
// values in HTML without that HTML coming out in the text format too.
return $values;
}
}
ParamValidator/TypeDef/PresenceBooleanDef.php 0000666 00000004611 15133502362 0015214 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for checkbox-like boolean types
*
* This boolean is considered true if the parameter is present in the request,
* regardless of value. The only way for it to be false is for the parameter to
* be omitted entirely.
*
* The result from validate() is a PHP boolean.
*
* @since 1.34
* @unstable
*/
class PresenceBooleanDef extends TypeDef {
public function getValue( $name, array $settings, array $options ) {
return $this->callbacks->hasParam( $name, $options ) ? true : null;
}
public function validate( $name, $value, array $settings, array $options ) {
return (bool)$value;
}
public function normalizeSettings( array $settings ) {
// Cannot be multi-valued
$settings[ParamValidator::PARAM_ISMULTI] = false;
// Default the default to false so ParamValidator::getValue() returns false (T244440)
$settings += [ ParamValidator::PARAM_DEFAULT => false ];
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for presence-boolean-type parameters';
}
if ( ( $settings[ParamValidator::PARAM_DEFAULT] ?? false ) !== false &&
!isset( $ret['issues'][ParamValidator::PARAM_DEFAULT] )
) {
$ret['issues'][ParamValidator::PARAM_DEFAULT] =
'Default for presence-boolean-type parameters must be false or null';
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
// No need to report the default of "false"
$info['default'] = null;
return $info;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new(
'paramvalidator-help-type-presenceboolean'
)->params( 1 );
// No need to report the default of "false"
$info[ParamValidator::PARAM_DEFAULT] = null;
return $info;
}
}
ParamValidator/TypeDef/BooleanDef.php 0000666 00000003671 15133502362 0013534 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\Message\ParamType;
use Wikimedia\Message\ScalarParam;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for boolean types
*
* This type accepts certain defined strings to mean 'true' or 'false'.
* The result from validate() is a PHP boolean.
*
* Failure codes:
* - 'badbool': The value is not a recognized boolean. No data.
*
* @since 1.34
* @unstable
*/
class BooleanDef extends TypeDef {
public static $TRUEVALS = [ 'true', 't', 'yes', 'y', 'on', '1' ];
public static $FALSEVALS = [ 'false', 'f', 'no', 'n', 'off', '0' ];
public function validate( $name, $value, array $settings, array $options ) {
$value = strtolower( $value );
if ( in_array( $value, self::$TRUEVALS, true ) ) {
return true;
}
if ( $value === '' || in_array( $value, self::$FALSEVALS, true ) ) {
return false;
}
$this->failure(
$this->failureMessage( 'badbool' )
->textListParams( array_map( [ $this, 'quoteVal' ], self::$TRUEVALS ) )
->numParams( count( self::$TRUEVALS ) )
->textListParams( array_merge(
array_map( [ $this, 'quoteVal' ], self::$FALSEVALS ),
[ MessageValue::new( 'paramvalidator-emptystring' ) ]
) )
->numParams( count( self::$FALSEVALS ) + 1 ),
$name, $value, $settings, $options
);
}
private function quoteVal( $v ) {
return new ScalarParam( ParamType::TEXT, "\"$v\"" );
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
return $value ? self::$TRUEVALS[0] : self::$FALSEVALS[0];
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-boolean' )
->params( empty( $settings[ParamValidator::PARAM_ISMULTI] ) ? 1 : 2 );
return $info;
}
}
ParamValidator/TypeDef/LimitDef.php 0000666 00000004202 15133502362 0013222 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Type definition for "limit" types
*
* A limit type is an integer type that also accepts the magic value "max".
* IntegerDef::PARAM_MIN defaults to 0 for this type.
*
* @see IntegerDef
* @since 1.34
* @unstable
*/
class LimitDef extends IntegerDef {
/**
* @inheritDoc
*
* Additional `$options` accepted:
* - 'parse-limit': (bool) Default true, set false to return 'max' rather
* than determining the effective value.
*/
public function validate( $name, $value, array $settings, array $options ) {
if ( $value === 'max' ) {
if ( $options['parse-limit'] ?? true ) {
$value = $this->callbacks->useHighLimits( $options )
? $settings[self::PARAM_MAX2] ?? $settings[self::PARAM_MAX] ?? PHP_INT_MAX
: $settings[self::PARAM_MAX] ?? PHP_INT_MAX;
}
return $value;
}
return parent::validate( $name, $value, $settings, $options );
}
public function normalizeSettings( array $settings ) {
$settings += [
self::PARAM_MIN => 0,
];
// Cannot be multi-valued
$settings[ParamValidator::PARAM_ISMULTI] = false;
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for limit-type parameters';
}
if ( ( $settings[self::PARAM_MIN] ?? 0 ) < 0 ) {
$ret['issues'][] = 'PARAM_MIN must be greater than or equal to 0';
}
if ( !isset( $settings[self::PARAM_MAX] ) ) {
$ret['issues'][] = 'PARAM_MAX must be set';
}
return $ret;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-limit' )
->params( 1 );
return $info;
}
}
ParamValidator/TypeDef/FloatDef.php 0000666 00000003564 15133502362 0013223 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Type definition for a floating-point type
*
* A valid representation consists of:
* - an optional sign (`+` or `-`)
* - a decimal number, using `.` as the decimal separator and no grouping
* - an optional E-notation suffix: the letter 'e' or 'E', an optional
* sign, and an integer
*
* Thus, for example, "12", "-.4", "6.022e23", or "+1.7e-10".
*
* The result from validate() is a PHP float.
*
* Failure codes:
* - 'badfloat': The value was invalid. No data.
* - 'badfloat-notfinite': The value was in a valid format, but conversion resulted in
* infinity or NAN.
*
* @since 1.34
* @unstable
*/
class FloatDef extends NumericDef {
protected $valueType = 'double';
public function validate( $name, $value, array $settings, array $options ) {
// Use a regex so as to avoid any potential oddness PHP's default conversion might allow.
if ( !preg_match( '/^[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?$/D', $value ) ) {
$this->failure( 'badfloat', $name, $value, $settings, $options );
}
$ret = (float)$value;
if ( !is_finite( $ret ) ) {
$this->failure( 'badfloat-notfinite', $name, $value, $settings, $options );
}
return $this->checkRange( $ret, $name, $value, $settings, $options );
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
// Ensure sufficient precision for round-tripping
$digits = PHP_FLOAT_DIG;
return sprintf( "%.{$digits}g", $value );
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-float' )
->params( empty( $settings[ParamValidator::PARAM_ISMULTI] ) ? 1 : 2 );
return $info;
}
}
ParamValidator/TypeDef/NumericDef.php 0000666 00000015205 15133502362 0013553 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\Message\ParamType;
use Wikimedia\Message\ScalarParam;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ValidationException;
/**
* Type definition base class for numeric types
*
* * Failure codes:
* - 'outofrange': The value was outside of the allowed range. Data:
* - 'min': Minimum allowed, or null if there is no limit.
* - 'curmax': Current maximum allowed, or null if there is no limit.
* - 'max': Normal maximum allowed, or null if there is no limit.
* - 'highmax': High limits maximum allowed, or null if there is no limit.
*
* @stable to extend
* @since 1.35
* @unstable
*/
abstract class NumericDef extends TypeDef {
/**
* (bool) Whether to enforce the specified range.
*
* If set and truthy, the 'outofrange' failure is non-fatal.
*/
public const PARAM_IGNORE_RANGE = 'param-ignore-range';
/**
* (int|float) Minimum allowed value.
*/
public const PARAM_MIN = 'param-min';
/**
* (int|float) Maximum allowed value (normal limits)
*/
public const PARAM_MAX = 'param-max';
/**
* (int|float) Maximum allowed value (high limits)
*/
public const PARAM_MAX2 = 'param-max2';
/** @var string PHP type (as from `gettype()`) of values this NumericDef handles */
protected $valueType = 'integer';
/**
* Check the range of a value
* @param int|float $value Value to check.
* @param string $name Parameter name being validated.
* @param mixed $origValue Original value being validated.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return int|float Validated value, may differ from $value if
* PARAM_IGNORE_RANGE was set.
* @throws ValidationException if the value out of range, and PARAM_IGNORE_RANGE wasn't set.
*/
protected function checkRange( $value, $name, $origValue, array $settings, array $options ) {
$min = $settings[self::PARAM_MIN] ?? null;
$max1 = $settings[self::PARAM_MAX] ?? null;
$max2 = $settings[self::PARAM_MAX2] ?? $max1;
$err = false;
if ( $min !== null && $value < $min ) {
$err = true;
$value = $min;
} elseif ( $max1 !== null && $value > $max1 ) {
if ( $max2 > $max1 && $this->callbacks->useHighLimits( $options ) ) {
if ( $value > $max2 ) {
$err = true;
$value = $max2;
}
} else {
$err = true;
$value = $max1;
}
}
if ( $err ) {
$what = '';
if ( $min !== null ) {
$what .= 'min';
}
if ( $max1 !== null ) {
$what .= 'max';
}
$max = $max2 !== null && $max2 > $max1 && $this->callbacks->useHighLimits( $options )
? $max2 : $max1;
$this->failure(
$this->failureMessage( 'outofrange', [
'min' => $min,
'curmax' => $max,
'max' => $max1,
'highmax' => $max2,
], $what )->numParams( $min ?? '', $max ?? '' ),
$name, $origValue, $settings, $options,
empty( $settings[self::PARAM_IGNORE_RANGE] )
);
}
return $value;
}
/**
* @inheritDoc
* @stable to override
*/
public function normalizeSettings( array $settings ) {
if ( !isset( $settings[self::PARAM_MAX] ) ) {
unset( $settings[self::PARAM_MAX2] );
}
if ( isset( $settings[self::PARAM_MAX2] ) && isset( $settings[self::PARAM_MAX] ) &&
$settings[self::PARAM_MAX2] < $settings[self::PARAM_MAX]
) {
$settings[self::PARAM_MAX2] = $settings[self::PARAM_MAX];
}
return parent::normalizeSettings( $settings );
}
/**
* @inheritDoc
* @stable to override
*/
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_IGNORE_RANGE, self::PARAM_MIN, self::PARAM_MAX, self::PARAM_MAX2,
] );
if ( !is_bool( $settings[self::PARAM_IGNORE_RANGE] ?? false ) ) {
$ret['issues'][self::PARAM_IGNORE_RANGE] = 'PARAM_IGNORE_RANGE must be boolean, got '
. gettype( $settings[self::PARAM_IGNORE_RANGE] );
}
$min = $settings[self::PARAM_MIN] ?? null;
$max = $settings[self::PARAM_MAX] ?? null;
$max2 = $settings[self::PARAM_MAX2] ?? null;
if ( $min !== null && gettype( $min ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MIN] = "PARAM_MIN must be $this->valueType, got " . gettype( $min );
}
if ( $max !== null && gettype( $max ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MAX] = "PARAM_MAX must be $this->valueType, got " . gettype( $max );
}
if ( $max2 !== null && gettype( $max2 ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MAX2] = "PARAM_MAX2 must be $this->valueType, got "
. gettype( $max2 );
}
if ( $min !== null && $max !== null && $min > $max ) {
$ret['issues'][] = "PARAM_MIN must be less than or equal to PARAM_MAX, but $min > $max";
}
if ( $max2 !== null ) {
if ( $max === null ) {
$ret['issues'][] = 'PARAM_MAX2 cannot be used without PARAM_MAX';
} elseif ( $max2 < $max ) {
$ret['issues'][] = "PARAM_MAX2 must be greater than or equal to PARAM_MAX, but $max2 < $max";
}
}
return $ret;
}
/**
* @inheritDoc
* @stable to override
*/
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['min'] = $settings[self::PARAM_MIN] ?? null;
$info['max'] = $settings[self::PARAM_MAX] ?? null;
$info['highmax'] = $settings[self::PARAM_MAX2] ?? $info['max'];
if ( $info['max'] === null || $info['highmax'] <= $info['max'] ) {
unset( $info['highmax'] );
}
return $info;
}
/**
* @inheritDoc
* @stable to override
*/
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$min = '−∞';
$max = '∞';
$msg = '';
if ( isset( $settings[self::PARAM_MIN] ) ) {
$msg .= 'min';
$min = new ScalarParam( ParamType::NUM, $settings[self::PARAM_MIN] );
}
if ( isset( $settings[self::PARAM_MAX] ) ) {
$msg .= 'max';
$max = $settings[self::PARAM_MAX];
if ( isset( $settings[self::PARAM_MAX2] ) && $settings[self::PARAM_MAX2] > $max &&
$this->callbacks->useHighLimits( $options )
) {
$max = $settings[self::PARAM_MAX2];
}
$max = new ScalarParam( ParamType::NUM, $max );
}
if ( $msg !== '' ) {
$isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );
// Messages: paramvalidator-help-type-number-min, paramvalidator-help-type-number-max,
// paramvalidator-help-type-number-minmax
$info[self::PARAM_MIN] = MessageValue::new( "paramvalidator-help-type-number-$msg" )
->params( $isMulti ? 2 : 1, $min, $max );
}
return $info;
}
}
ParamValidator/TypeDef/UploadDef.php 0000666 00000011677 15133502362 0013406 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use InvalidArgumentException;
use Psr\Http\Message\UploadedFileInterface;
use UnexpectedValueException;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\Util\UploadedFile;
/**
* Type definition for upload types
*
* The result from validate() is an object implementing UploadedFileInterface.
*
* Failure codes:
* - 'badupload': The upload is not valid. Data:
* - 'code': A value indicating why the upload was not valid:
* - 'inisize': The upload exceeded the maximum in php.ini.
* - 'formsize': The upload exceeded the maximum in the form post.
* - 'partial': The file was only partially uploaded.
* - 'nofile': There was no file.
* - 'notmpdir': PHP has no temporary directory to store the upload.
* - 'cantwrite': PHP could not store the upload.
* - 'phpext': A PHP extension rejected the upload.
* - 'notupload': The field was present in the submission but was not encoded as an upload.
* - 'size': The configured size (in bytes), if 'code' is 'inisize'.
*
* @since 1.34
* @unstable
*/
class UploadDef extends TypeDef {
public function getValue( $name, array $settings, array $options ) {
$ret = $this->callbacks->getUploadedFile( $name, $options );
if ( $ret && $ret->getError() === UPLOAD_ERR_NO_FILE &&
!$this->callbacks->hasParam( $name, $options )
) {
// This seems to be that the client explicitly specified "no file" for the field
// instead of just omitting the field completely. DWTM.
$ret = null;
} elseif ( !$ret && $this->callbacks->hasParam( $name, $options ) ) {
// The client didn't format their upload properly so it came in as an ordinary
// field. Convert it to an error.
$ret = new UploadedFile( [
'name' => '',
'type' => '',
'tmp_name' => '',
'error' => -42, // PHP's UPLOAD_ERR_* are all positive numbers.
'size' => 0,
] );
}
return $ret;
}
/**
* Fetch the value of PHP's upload_max_filesize ini setting
*
* This method exists so it can be mocked by unit tests that can't
* affect ini_get() directly.
*
* @codeCoverageIgnore
* @return string|false
*/
protected function getIniSize() {
return ini_get( 'upload_max_filesize' );
}
public function validate( $name, $value, array $settings, array $options ) {
static $codemap = [
-42 => 'notupload', // Local from getValue()
UPLOAD_ERR_FORM_SIZE => 'formsize',
UPLOAD_ERR_PARTIAL => 'partial',
UPLOAD_ERR_NO_FILE => 'nofile',
UPLOAD_ERR_NO_TMP_DIR => 'notmpdir',
UPLOAD_ERR_CANT_WRITE => 'cantwrite',
UPLOAD_ERR_EXTENSION => 'phpext',
];
if ( !$value instanceof UploadedFileInterface ) {
// Err?
$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
throw new InvalidArgumentException( "\$value must be UploadedFileInterface, got $type" );
}
$err = $value->getError();
if ( $err === UPLOAD_ERR_OK ) {
return $value;
} elseif ( $err === UPLOAD_ERR_INI_SIZE ) {
static $prefixes = [
'g' => 1024 ** 3,
'm' => 1024 ** 2,
'k' => 1024 ** 1,
];
$size = $this->getIniSize();
$last = strtolower( substr( $size, -1 ) );
$size = intval( $size, 10 ) * ( $prefixes[$last] ?? 1 );
$this->failure(
$this->failureMessage( 'badupload', [
'code' => 'inisize',
'size' => $size,
], 'inisize' )->sizeParams( $size ),
$name, '', $settings, $options
);
} elseif ( isset( $codemap[$err] ) ) {
$this->failure(
$this->failureMessage( 'badupload', [ 'code' => $codemap[$err] ], $codemap[$err] ),
$name, '', $settings, $options
);
} else {
$constant = '';
foreach ( get_defined_constants() as $c => $v ) {
// @phan-suppress-next-line PhanTypeComparisonFromArray
if ( $v === $err && str_starts_with( $c, 'UPLOAD_ERR_' ) ) {
$constant = " ($c?)";
}
}
throw new UnexpectedValueException( "Unrecognized PHP upload error value $err$constant" );
}
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( isset( $settings[ParamValidator::PARAM_DEFAULT] ) ) {
$ret['issues'][ParamValidator::PARAM_DEFAULT] =
'Cannot specify a default for upload-type parameters';
}
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for upload-type parameters';
}
return $ret;
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
// Not going to happen.
return null;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-upload' );
return $info;
}
}
ParamValidator/TypeDef/TimestampDef.php 0000666 00000011311 15133502362 0014106 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use InvalidArgumentException;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia\Timestamp\TimestampException;
/**
* Type definition for timestamp types
*
* This uses the wikimedia/timestamp library for parsing and formatting the
* timestamps.
*
* The result from validate() is a ConvertibleTimestamp by default, but this
* may be changed by both a constructor option and a PARAM constant.
*
* Failure codes:
* - 'badtimestamp': The timestamp is not valid. No data, but the
* TimestampException is available via Exception::getPrevious().
* - 'unclearnowtimestamp': Non-fatal. The value is the empty string or "0".
* Use 'now' instead if you really want the current timestamp. No data.
*
* @since 1.34
* @unstable
*/
class TimestampDef extends TypeDef {
/**
* (string|int) Timestamp format to return from validate()
*
* Values include:
* - 'ConvertibleTimestamp': A ConvertibleTimestamp object.
* - 'DateTime': A PHP DateTime object
* - One of ConvertibleTimestamp's TS_* constants.
*
* This does not affect the format returned by stringifyValue().
*/
public const PARAM_TIMESTAMP_FORMAT = 'param-timestamp-format';
/** @var string|int */
protected $defaultFormat;
/** @var int */
protected $stringifyFormat;
/**
* @param Callbacks $callbacks
* @param array $options Options:
* - defaultFormat: (string|int) Default for PARAM_TIMESTAMP_FORMAT.
* Default if not specified is 'ConvertibleTimestamp'.
* - stringifyFormat: (int) Format to use for stringifyValue().
* Default is TS_ISO_8601.
*/
public function __construct( Callbacks $callbacks, array $options = [] ) {
parent::__construct( $callbacks );
$this->defaultFormat = $options['defaultFormat'] ?? 'ConvertibleTimestamp';
$this->stringifyFormat = $options['stringifyFormat'] ?? TS_ISO_8601;
// Check values by trying to convert 0
if ( $this->defaultFormat !== 'ConvertibleTimestamp' && $this->defaultFormat !== 'DateTime' &&
ConvertibleTimestamp::convert( $this->defaultFormat, 0 ) === false
) {
throw new InvalidArgumentException( 'Invalid value for $options[\'defaultFormat\']' );
}
if ( ConvertibleTimestamp::convert( $this->stringifyFormat, 0 ) === false ) {
throw new InvalidArgumentException( 'Invalid value for $options[\'stringifyFormat\']' );
}
}
public function validate( $name, $value, array $settings, array $options ) {
// Confusing synonyms for the current time accepted by ConvertibleTimestamp
if ( !$value ) {
$this->failure( 'unclearnowtimestamp', $name, $value, $settings, $options, false );
$value = 'now';
}
$format = $settings[self::PARAM_TIMESTAMP_FORMAT] ?? $this->defaultFormat;
try {
$timestampObj = new ConvertibleTimestamp( $value === 'now' ? false : $value );
$timestamp = ( $format !== 'ConvertibleTimestamp' && $format !== 'DateTime' )
? $timestampObj->getTimestamp( $format )
: null;
} catch ( TimestampException $ex ) {
// $this->failure() doesn't handle passing a previous exception
throw new ValidationException(
$this->failureMessage( 'badtimestamp' )->plaintextParams( $name, $value ),
$name, $value, $settings, $ex
);
}
switch ( $format ) {
case 'ConvertibleTimestamp':
return $timestampObj;
case 'DateTime':
// Eew, no getter.
return $timestampObj->timestamp;
default:
return $timestamp;
}
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_TIMESTAMP_FORMAT,
] );
$f = $settings[self::PARAM_TIMESTAMP_FORMAT] ?? $this->defaultFormat;
if ( $f !== 'ConvertibleTimestamp' && $f !== 'DateTime' &&
ConvertibleTimestamp::convert( $f, 0 ) === false
) {
$ret['issues'][self::PARAM_TIMESTAMP_FORMAT] = 'Value for PARAM_TIMESTAMP_FORMAT is not valid';
}
return $ret;
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
if ( !$value instanceof ConvertibleTimestamp ) {
$value = new ConvertibleTimestamp( $value );
}
return $value->getTimestamp( $this->stringifyFormat );
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-timestamp' )
->params( empty( $settings[ParamValidator::PARAM_ISMULTI] ) ? 1 : 2 );
return $info;
}
}
ParamValidator/TypeDef/StringDef.php 0000666 00000012170 15133502362 0013415 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for string types
*
* The result from validate() is a PHP string.
*
* Failure codes:
* - 'missingparam': The parameter is the empty string (and that's not allowed). No data.
*
* Additional codes may be generated when using certain PARAM constants. See
* the constants' documentation for details.
*
* @since 1.34
* @unstable
*/
class StringDef extends TypeDef {
/**
* (integer) Maximum length of a string in bytes.
*
* Failure codes:
* - 'maxbytes': The string is too long. Data:
* - 'maxbytes': The maximum number of bytes allowed, or null if no limit
* - 'maxchars': The maximum number of characters allowed, or null if no limit
*/
public const PARAM_MAX_BYTES = 'param-max-bytes';
/**
* (integer) Maximum length of a string in characters (Unicode codepoints).
*
* The string is assumed to be encoded as UTF-8.
*
* Failure codes:
* - 'maxchars': The string is too long. Data:
* - 'maxbytes': The maximum number of bytes allowed, or null if no limit
* - 'maxchars': The maximum number of characters allowed, or null if no limit
*/
public const PARAM_MAX_CHARS = 'param-max-chars';
protected $allowEmptyWhenRequired = false;
/**
* @param Callbacks $callbacks
* @param array $options Options:
* - allowEmptyWhenRequired: (bool) Whether to reject the empty string when PARAM_REQUIRED.
* Defaults to false.
*/
public function __construct( Callbacks $callbacks, array $options = [] ) {
parent::__construct( $callbacks );
$this->allowEmptyWhenRequired = !empty( $options['allowEmptyWhenRequired'] );
}
public function validate( $name, $value, array $settings, array $options ) {
if ( !$this->allowEmptyWhenRequired && $value === '' &&
!empty( $settings[ParamValidator::PARAM_REQUIRED] )
) {
$this->failure( 'missingparam', $name, $value, $settings, $options );
}
$len = strlen( $value );
if ( isset( $settings[self::PARAM_MAX_BYTES] ) && $len > $settings[self::PARAM_MAX_BYTES] ) {
$this->failure(
$this->failureMessage( 'maxbytes', [
'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? null,
'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? null,
] )->numParams( $settings[self::PARAM_MAX_BYTES], $len ),
$name, $value, $settings, $options
);
}
$len = mb_strlen( $value, 'UTF-8' );
if ( isset( $settings[self::PARAM_MAX_CHARS] ) && $len > $settings[self::PARAM_MAX_CHARS] ) {
$this->failure(
$this->failureMessage( 'maxchars', [
'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? null,
'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? null,
] )->numParams( $settings[self::PARAM_MAX_CHARS], $len ),
$name, $value, $settings, $options
);
}
return $value;
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_MAX_BYTES, self::PARAM_MAX_CHARS,
] );
$maxb = $settings[self::PARAM_MAX_BYTES] ?? PHP_INT_MAX;
if ( !is_int( $maxb ) ) {
$ret['issues'][self::PARAM_MAX_BYTES] = 'PARAM_MAX_BYTES must be an integer, got '
. gettype( $maxb );
} elseif ( $maxb < 0 ) {
$ret['issues'][self::PARAM_MAX_BYTES] = 'PARAM_MAX_BYTES must be greater than or equal to 0';
}
$maxc = $settings[self::PARAM_MAX_CHARS] ?? PHP_INT_MAX;
if ( !is_int( $maxc ) ) {
$ret['issues'][self::PARAM_MAX_CHARS] = 'PARAM_MAX_CHARS must be an integer, got '
. gettype( $maxc );
} elseif ( $maxc < 0 ) {
$ret['issues'][self::PARAM_MAX_CHARS] = 'PARAM_MAX_CHARS must be greater than or equal to 0';
}
if ( !$this->allowEmptyWhenRequired && !empty( $settings[ParamValidator::PARAM_REQUIRED] ) ) {
if ( $maxb === 0 ) {
$ret['issues'][] = 'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and '
. 'PARAM_MAX_BYTES is 0. That\'s impossible to satisfy.';
}
if ( $maxc === 0 ) {
$ret['issues'][] = 'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and '
. 'PARAM_MAX_CHARS is 0. That\'s impossible to satisfy.';
}
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['maxbytes'] = $settings[self::PARAM_MAX_BYTES] ?? null;
$info['maxchars'] = $settings[self::PARAM_MAX_CHARS] ?? null;
return $info;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
$info[self::PARAM_MAX_BYTES] = MessageValue::new( 'paramvalidator-help-type-string-maxbytes' )
->numParams( $settings[self::PARAM_MAX_BYTES] );
}
if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
$info[self::PARAM_MAX_CHARS] = MessageValue::new( 'paramvalidator-help-type-string-maxchars' )
->numParams( $settings[self::PARAM_MAX_CHARS] );
}
return $info;
}
}
ParamValidator/TypeDef/ExpiryDef.php 0000666 00000012731 15133502362 0013432 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use InvalidArgumentException;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* Type definition for expiry timestamps.
*
* @since 1.35
*/
class ExpiryDef extends TypeDef {
/** @var array Possible values that mean "doesn't expire". */
public const INFINITY_VALS = [ 'infinite', 'indefinite', 'infinity', 'never' ];
/**
* (bool) If truthy, the value given for the PARAM_MAX setting is used if the provided expiry
* exceeds it, and the 'badexpiry-duration' message is shown as a warning.
*
* If false, 'badexpiry-duration' is shown and is fatal.
*/
public const PARAM_USE_MAX = 'param-use-max';
/**
* (int|float) Maximum non-infinity duration.
*/
public const PARAM_MAX = 'param-max';
public function validate( $name, $value, array $settings, array $options ) {
try {
$expiry = self::normalizeExpiry( $value, TS_ISO_8601 );
} catch ( InvalidArgumentException $e ) {
$this->failure( 'badexpiry', $name, $value, $settings, $options );
}
if ( $expiry !== 'infinity' && $expiry < ConvertibleTimestamp::now( TS_ISO_8601 ) ) {
$this->failure( 'badexpiry-past', $name, $value, $settings, $options );
}
$max = $settings[self::PARAM_MAX] ?? null;
if ( self::expiryExceedsMax( $expiry, $max ) ) {
$dontUseMax = empty( $settings[self::PARAM_USE_MAX] );
// Show warning that expiry exceeds the max, and that the max is being used instead.
$msg = DataMessageValue::new(
$dontUseMax
? 'paramvalidator-badexpiry-duration'
: 'paramvalidator-badexpiry-duration-max',
[ $max ]
);
$this->failure( $msg, $name, $value, $settings, $options, $dontUseMax );
return self::normalizeExpiry( $max, TS_ISO_8601 );
}
return $expiry;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-expiry' )
->params( empty( $settings[ParamValidator::PARAM_ISMULTI] ) ? 1 : 2 )
->textListParams(
// Should be quoted or monospace for presentation purposes,
// but textListParams() doesn't do this.
array_map( static function ( $val ) {
return "\"$val\"";
}, self::INFINITY_VALS )
);
return $info;
}
/**
* Normalize a user-inputted expiry in ConvertibleTimestamp.
* @param string|null $expiry
* @param int|null $style null or in a format acceptable to ConvertibleTimestamp (TS_* constants)
*
* @return ConvertibleTimestamp|string|null Timestamp as ConvertibleTimestamp if $style is null, a string
* timestamp in $style is not null, 'infinity' if $expiry is one of the self::INFINITY_VALS,
* or null if $expiry is null.
*
* @throws InvalidArgumentException if $expiry is invalid
*/
public static function normalizeExpiry( ?string $expiry = null, ?int $style = null ) {
if ( $expiry === null ) {
return null;
}
if ( in_array( $expiry, self::INFINITY_VALS, true ) ) {
return 'infinity';
}
// ConvertibleTimestamp::time() used so we can fake the current time in ExpiryDefTest.
$unix = strtotime( $expiry, ConvertibleTimestamp::time() );
if ( $unix === false ) {
// Invalid expiry.
throw new InvalidArgumentException( "Invalid expiry value: {$expiry}" );
}
// Don't pass 0, since ConvertibleTimestamp interprets that to mean the current timestamp.
// '00' does the right thing. Without this check, calling normalizeExpiry()
// with 1970-01-01T00:00:00Z incorrectly returns the current time.
$expiryConvertibleTimestamp = new ConvertibleTimestamp( $unix === 0 ? '00' : $unix );
if ( $style !== null ) {
return $expiryConvertibleTimestamp->getTimestamp( $style );
}
return $expiryConvertibleTimestamp;
}
/**
* Returns a normalized expiry or the max expiry if the given expiry exceeds it.
* @param string|null $expiry
* @param string|null $maxExpiryDuration
* @param int|null $style null or in a format acceptable to ConvertibleTimestamp (TS_* constants)
* @return ConvertibleTimestamp|string|null Timestamp as ConvertibleTimestamp if $style is null, a string
* timestamp in $style is not null, 'infinity' if $expiry is one of the self::INFINITY_VALS,
* or null if $expiry is null.
*/
public static function normalizeUsingMaxExpiry( ?string $expiry, ?string $maxExpiryDuration, ?int $style ) {
if ( self::expiryExceedsMax( $expiry, $maxExpiryDuration ) ) {
return self::normalizeExpiry( $maxExpiryDuration, $style );
}
return self::normalizeExpiry( $expiry, $style );
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'][] = self::PARAM_USE_MAX;
$ret['allowedKeys'][] = self::PARAM_MAX;
return $ret;
}
/**
* Given an expiry, test if the normalized value exceeds the given maximum.
*
* @param string|null $expiry
* @param string|null $max Relative maximum duration acceptable by strtotime() (i.e. '6 months')
* @return bool
*/
private static function expiryExceedsMax( ?string $expiry, ?string $max = null ): bool {
$expiry = self::normalizeExpiry( $expiry );
$max = self::normalizeExpiry( $max );
if ( !$max || !$expiry || $expiry === 'infinity' ) {
// Either there is no max or given expiry was invalid.
return false;
}
return $expiry > $max;
}
}
ParamValidator/TypeDef/PasswordDef.php 0000666 00000001624 15133502362 0013753 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Type definition for "password" types
*
* This is a string type that forces PARAM_SENSITIVE = true.
*
* @see StringDef
* @since 1.34
* @unstable
*/
class PasswordDef extends StringDef {
public function normalizeSettings( array $settings ) {
$settings[ParamValidator::PARAM_SENSITIVE] = true;
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( ( $settings[ParamValidator::PARAM_SENSITIVE] ?? true ) !== true &&
!isset( $ret['issues'][ParamValidator::PARAM_SENSITIVE] )
) {
$ret['issues'][ParamValidator::PARAM_SENSITIVE] =
'Cannot set PARAM_SENSITIVE to false for password-type parameters';
}
return $ret;
}
}
ParamValidator/TypeDef/IntegerDef.php 0000666 00000003311 15133502362 0013541 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Type definition for integer types
*
* A valid representation consists of an optional sign (`+` or `-`) followed by
* one or more decimal digits.
*
* The result from validate() is a PHP integer.
*
* * Failure codes:
* - 'badinteger': The value was invalid or could not be represented as a PHP
* integer. No data.
*
* @since 1.34
* @unstable
*/
class IntegerDef extends NumericDef {
public function validate( $name, $value, array $settings, array $options ) {
if ( is_array( $value ) || !preg_match( '/^[+-]?\d+$/D', $value ) ) {
$this->failure( 'badinteger', $name, $value, $settings, $options );
}
$ret = intval( $value, 10 );
// intval() returns min/max on overflow, so check that
if ( $ret === PHP_INT_MAX || $ret === PHP_INT_MIN ) {
$tmp = ( $ret < 0 ? '-' : '' ) . ltrim( $value, '-0' );
if ( $tmp !== (string)$ret ) {
$this->failure( 'badinteger', $name, $value, $settings, $options );
}
}
return $this->checkRange( $ret, $name, $value, $settings, $options );
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-integer' )
->params( empty( $settings[ParamValidator::PARAM_ISMULTI] ) ? 1 : 2 );
return $info;
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
if ( !is_array( $value ) ) {
return parent::stringifyValue( $name, $value, $settings, $options );
}
return ParamValidator::implodeMultiValue( $value );
}
}
ParamValidator/TypeDef.php 0000666 00000021471 15133502362 0011534 0 ustar 00 <?php
namespace Wikimedia\ParamValidator;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
/**
* Base definition for ParamValidator types.
*
* Most methods in this class accept an "options array". This is just the `$options`
* passed to ParamValidator::getValue(), ParamValidator::validateValue(), and the like
* and is intended for communication of non-global state to the Callbacks.
*
* @stable to extend
* @since 1.34
* @unstable
*/
abstract class TypeDef {
/** @var Callbacks */
protected $callbacks;
/**
* @stable to call
*
* @param Callbacks $callbacks
*/
public function __construct( Callbacks $callbacks ) {
$this->callbacks = $callbacks;
}
/**
* Record a failure message
*
* Depending on `$fatal`, this will either throw a ValidationException or
* call $this->callbacks->recordCondition().
*
* Note that parameters for `$name` and `$value` are always added as `$1`
* and `$2`.
*
* @param DataMessageValue|string $failure Failure code or message.
* @param string $name Parameter name being validated.
* @param mixed $value Value being validated.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @param bool $fatal Whether the failure is fatal
*/
protected function failure(
$failure, $name, $value, array $settings, array $options, $fatal = true
) {
if ( !is_string( $value ) ) {
$value = (string)$this->stringifyValue( $name, $value, $settings, $options );
}
if ( is_string( $failure ) ) {
$mv = $this->failureMessage( $failure )
->plaintextParams( $name, $value );
} else {
$mv = DataMessageValue::new( $failure->getKey(), [], $failure->getCode(), $failure->getData() )
->plaintextParams( $name, $value )
->params( ...$failure->getParams() );
}
if ( $fatal ) {
throw new ValidationException( $mv, $name, $value, $settings );
}
$this->callbacks->recordCondition( $mv, $name, $value, $settings, $options );
}
/**
* Create a DataMessageValue representing a failure
*
* The message key will be "paramvalidator-$code" or "paramvalidator-$code-$suffix".
*
* Use DataMessageValue's param mutators to add additional MessageParams.
* Note that `failure()` will prepend parameters for `$name` and `$value`.
*
* @param string $code Failure code.
* @param array|null $data Failure data.
* @param string|null $suffix Suffix to append when producing the message key
* @return DataMessageValue
*/
protected function failureMessage( $code, array $data = null, $suffix = null ): DataMessageValue {
return DataMessageValue::new(
"paramvalidator-$code" . ( $suffix !== null ? "-$suffix" : '' ),
[], $code, $data
);
}
/**
* Get the value from the request
* @stable to override
*
* @note Only override this if you need to use something other than
* $this->callbacks->getValue() to fetch the value. Reformatting from a
* string should typically be done by self::validate().
* @note Handling of ParamValidator::PARAM_DEFAULT should be left to ParamValidator,
* as should PARAM_REQUIRED and the like.
*
* @param string $name Parameter name being fetched.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return null|mixed Return null if the value wasn't present, otherwise a
* value to be passed to self::validate().
*/
public function getValue( $name, array $settings, array $options ) {
return $this->callbacks->getValue( $name, null, $options );
}
/**
* Validate the value
*
* When ParamValidator is processing a multi-valued parameter, this will be
* called once for each of the supplied values. Which may mean zero calls.
*
* When getValue() returned null, this will not be called.
*
* @param string $name Parameter name being validated.
* @param mixed $value Value to validate, from getValue().
* @param array $settings Parameter settings array.
* @param array $options Options array. Note the following values that may be set
* by ParamValidator:
* - is-default: (bool) If present and true, the value was taken from PARAM_DEFAULT rather
* that being supplied by the client.
* - values-list: (string[]) If defined, values of a multi-valued parameter are being processed
* (and this array holds the full set of values).
* @return mixed Validated value
* @throws ValidationException if the value is invalid
*/
abstract public function validate( $name, $value, array $settings, array $options );
/**
* Normalize a settings array
* @stable to override
* @param array $settings
* @return array
*/
public function normalizeSettings( array $settings ) {
return $settings;
}
/**
* Validate a parameter settings array
*
* This is intended for validation of parameter settings during unit or
* integration testing, and should implement strict checks.
*
* The rest of the code should generally be more permissive.
*
* @see ParamValidator::checkSettings()
* @stable to override
*
* @param string $name Parameter name
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array, passed through to the TypeDef and Callbacks.
* @param array $ret
* - 'issues': (string[]) Errors detected in $settings, as English text. If the settings
* are valid, this will be the empty array. Keys on input are ParamValidator constants,
* allowing the typedef to easily override core validation; this need not be preserved
* when returned.
* - 'allowedKeys': (string[]) ParamValidator keys that are allowed in `$settings`.
* - 'messages': (MessageValue[]) Messages to be checked for existence.
* @return array $ret, with any relevant changes.
*/
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
return $ret;
}
/**
* Get the values for enum-like parameters
*
* This is primarily intended for documentation and implementation of
* PARAM_ALL; it is the responsibility of the TypeDef to ensure that validate()
* accepts the values returned here.
* @stable to override
*
* @param string $name Parameter name being validated.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return array|null All possible enumerated values, or null if this is
* not an enumeration.
*/
public function getEnumValues( $name, array $settings, array $options ) {
return null;
}
/**
* Convert a value to a string representation.
*
* This is intended as the inverse of getValue() and validate(): this
* should accept anything returned by those methods or expected to be used
* as PARAM_DEFAULT, and if the string from this method is passed in as client
* input or PARAM_DEFAULT it should give equivalent output from validate().
*
* @param string $name Parameter name being converted.
* @param mixed $value Parameter value being converted. Do not pass null.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return string|null Return null if there is no representation of $value
* reasonably satisfying the description given.
*/
public function stringifyValue( $name, $value, array $settings, array $options ) {
return (string)$value;
}
/**
* Describe parameter settings in a machine-readable format.
*
* Keys should be short strings using lowercase ASCII letters. Values
* should generally be values that could be encoded in JSON or the like.
*
* This is intended to handle PARAM constants specific to this class. It
* generally shouldn't handle constants defined on ParamValidator itself.
* @stable to override
*
* @param string $name Parameter name.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return array
*/
public function getParamInfo( $name, array $settings, array $options ) {
return [];
}
/**
* Describe parameter settings in human-readable format
*
* Keys in the returned array should generally correspond to PARAM
* constants.
*
* If relevant, a MessageValue describing the type itself should be
* returned with key ParamValidator::PARAM_TYPE.
*
* The default messages for other ParamValidator-defined PARAM constants
* may be suppressed by returning null as the value for those constants, or
* replaced by returning a replacement MessageValue. Normally, however,
* the default messages should not be changed.
*
* MessageValues describing any other constraints applied via PARAM
* constants specific to this class should also be returned.
* @stable to override
*
* @param string $name Parameter name being described.
* @param array $settings Parameter settings array.
* @param array $options Options array.
* @return (MessageValue|null)[]
*/
public function getHelpInfo( $name, array $settings, array $options ) {
return [];
}
}
mime/XmlTypeCheck.php 0000666 00000035742 15133502362 0010563 0 ustar 00 <?php
/**
* XML syntax and type checker.
*
* Since 1.24.2, it uses XMLReader instead of xml_parse, which gives us
* more control over the expansion of XML entities. When passed to the
* callback, entities will be fully expanded, but may report the XML is
* invalid if expanding the entities are likely to cause a DoS.
*
* 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
*/
class XmlTypeCheck {
/**
* @var bool|null Will be set to true or false to indicate whether the file is
* well-formed XML. Note that this doesn't check schema validity.
*/
public $wellFormed = null;
/**
* @var bool Will be set to true if the optional element filter returned
* a match at some point.
*/
public $filterMatch = false;
/**
* Will contain the type of filter hit if the optional element filter returned
* a match at some point.
* @var mixed
*/
public $filterMatchType = false;
/**
* @var string Name of the document's root element, including any namespace
* as an expanded URL.
*/
public $rootElement = '';
/**
* @var string[] A stack of strings containing the data of each xml element as it's processed.
* Append data to the top string of the stack, then pop off the string and process it when the
* element is closed.
*/
protected $elementData = [];
/**
* @var array A stack of element names and attributes, as we process them.
*/
protected $elementDataContext = [];
/**
* @var int Current depth of the data stack.
*/
protected $stackDepth = 0;
/** @var callable|null */
protected $filterCallback;
/**
* @var array Additional parsing options
*/
private $parserOptions = [
'processing_instruction_handler' => null,
'external_dtd_handler' => '',
'dtd_handler' => '',
'require_safe_dtd' => true
];
/**
* Allow filtering an XML file.
*
* Filters should return either true or a string to indicate something
* is wrong with the file. $this->filterMatch will store if the
* file failed validation (true = failed validation).
* $this->filterMatchType will contain the validation error.
* $this->wellFormed will contain whether the xml file is well-formed.
*
* @note If multiple filters are hit, only one of them will have the
* result stored in $this->filterMatchType.
*
* @param string $input a filename or string containing the XML element
* @param callable|null $filterCallback (optional)
* Function to call to do additional custom validity checks from the
* SAX element handler event. This gives you access to the element
* namespace, name, attributes, and text contents.
* Filter should return a truthy value describing the error.
* @param bool $isFile (optional) indicates if the first parameter is a
* filename (default, true) or if it is a string (false)
* @param array $options list of additional parsing options:
* processing_instruction_handler: Callback for xml_set_processing_instruction_handler
* external_dtd_handler: Callback for the url of external dtd subset
* dtd_handler: Callback given the full text of the <!DOCTYPE declaration.
* require_safe_dtd: Only allow non-recursive entities in internal dtd (default true)
*/
public function __construct( $input, $filterCallback = null, $isFile = true, $options = [] ) {
$this->filterCallback = $filterCallback;
$this->parserOptions = array_merge( $this->parserOptions, $options );
$this->validateFromInput( $input, $isFile );
}
/**
* Alternative constructor: from filename
*
* @param string $fname the filename of an XML document
* @param callable|null $filterCallback (optional)
* Function to call to do additional custom validity checks from the
* SAX element handler event. This gives you access to the element
* namespace, name, and attributes, but not to text contents.
* Filter should return 'true' to toggle on $this->filterMatch
* @return XmlTypeCheck
*/
public static function newFromFilename( $fname, $filterCallback = null ) {
return new self( $fname, $filterCallback, true );
}
/**
* Alternative constructor: from string
*
* @param string $string a string containing an XML element
* @param callable|null $filterCallback (optional)
* Function to call to do additional custom validity checks from the
* SAX element handler event. This gives you access to the element
* namespace, name, and attributes, but not to text contents.
* Filter should return 'true' to toggle on $this->filterMatch
* @return XmlTypeCheck
*/
public static function newFromString( $string, $filterCallback = null ) {
return new self( $string, $filterCallback, false );
}
/**
* Get the root element. Simple accessor to $rootElement
*
* @return string
*/
public function getRootElement() {
return $this->rootElement;
}
/**
* @param string $xml
* @param bool $isFile
*/
private function validateFromInput( $xml, $isFile ) {
$reader = new XMLReader();
if ( $isFile ) {
$s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
} else {
$s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
}
if ( $s !== true ) {
// Couldn't open the XML
$this->wellFormed = false;
} else {
// phpcs:ignore Generic.PHP.NoSilencedErrors -- suppress deprecation per T268847
$oldDisable = @libxml_disable_entity_loader( true );
$reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
try {
$this->validate( $reader );
} catch ( Exception $e ) {
// Calling this malformed, because we didn't parse the whole
// thing. Maybe just an external entity refernce.
$this->wellFormed = false;
$reader->close();
// phpcs:ignore Generic.PHP.NoSilencedErrors
@libxml_disable_entity_loader( $oldDisable );
throw $e;
}
$reader->close();
// phpcs:ignore Generic.PHP.NoSilencedErrors
@libxml_disable_entity_loader( $oldDisable );
}
}
private function readNext( XMLReader $reader ) {
set_error_handler( function ( $line, $file ) {
$this->wellFormed = false;
return true;
} );
$ret = $reader->read();
restore_error_handler();
return $ret;
}
private function validate( $reader ) {
// First, move through anything that isn't an element, and
// handle any processing instructions with the callback
do {
if ( !$this->readNext( $reader ) ) {
// Hit the end of the document before any elements
$this->wellFormed = false;
return;
}
if ( $reader->nodeType === XMLReader::PI ) {
$this->processingInstructionHandler( $reader->name, $reader->value );
}
if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
$this->dtdHandler( $reader );
}
} while ( $reader->nodeType != XMLReader::ELEMENT );
// Process the rest of the document
do {
switch ( $reader->nodeType ) {
case XMLReader::ELEMENT:
$name = $this->expandNS(
$reader->name,
$reader->namespaceURI
);
if ( $this->rootElement === '' ) {
$this->rootElement = $name;
}
$empty = $reader->isEmptyElement;
$attrs = $this->getAttributesArray( $reader );
$this->elementOpen( $name, $attrs );
if ( $empty ) {
$this->elementClose();
}
break;
case XMLReader::END_ELEMENT:
$this->elementClose();
break;
case XMLReader::WHITESPACE:
case XMLReader::SIGNIFICANT_WHITESPACE:
case XMLReader::CDATA:
case XMLReader::TEXT:
$this->elementData( $reader->value );
break;
case XMLReader::ENTITY_REF:
// Unexpanded entity (maybe external?),
// don't send to the filter (xml_parse didn't)
break;
case XMLReader::COMMENT:
// Don't send to the filter (xml_parse didn't)
break;
case XMLReader::PI:
// Processing instructions can happen after the header too
$this->processingInstructionHandler(
$reader->name,
$reader->value
);
break;
case XMLReader::DOC_TYPE:
// We should never see a doctype after first
// element.
$this->wellFormed = false;
break;
default:
// One of DOC, ENTITY, END_ENTITY,
// NOTATION, or XML_DECLARATION
// xml_parse didn't send these to the filter, so we won't.
}
} while ( $this->readNext( $reader ) );
if ( $this->stackDepth !== 0 ) {
$this->wellFormed = false;
} elseif ( $this->wellFormed === null ) {
$this->wellFormed = true;
}
}
/**
* Get all of the attributes for an XMLReader's current node
* @param XMLReader $r
* @return array of attributes
*/
private function getAttributesArray( XMLReader $r ) {
$attrs = [];
while ( $r->moveToNextAttribute() ) {
if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) {
// XMLReader treats xmlns attributes as normal
// attributes, while xml_parse doesn't
continue;
}
$name = $this->expandNS( $r->name, $r->namespaceURI );
$attrs[$name] = $r->value;
}
return $attrs;
}
/**
* @param string $name element or attribute name, maybe with a full or short prefix
* @param string $namespaceURI
* @return string the name prefixed with namespaceURI
*/
private function expandNS( $name, $namespaceURI ) {
if ( $namespaceURI ) {
$parts = explode( ':', $name );
$localname = array_pop( $parts );
return "$namespaceURI:$localname";
}
return $name;
}
/**
* @param string $name
* @param array $attribs
*/
private function elementOpen( $name, $attribs ) {
$this->elementDataContext[] = [ $name, $attribs ];
$this->elementData[] = '';
$this->stackDepth++;
}
private function elementClose() {
[ $name, $attribs ] = array_pop( $this->elementDataContext );
$data = array_pop( $this->elementData );
$this->stackDepth--;
$callbackReturn = false;
if ( is_callable( $this->filterCallback ) ) {
$callbackReturn = ( $this->filterCallback )( $name, $attribs, $data );
}
if ( $callbackReturn ) {
// Filter hit!
$this->filterMatch = true;
$this->filterMatchType = $callbackReturn;
}
}
/**
* @param string $data
*/
private function elementData( $data ) {
// Collect any data here, and we'll run the callback in elementClose
$this->elementData[ $this->stackDepth - 1 ] .= trim( $data );
}
/**
* @param string $target
* @param string $data
*/
private function processingInstructionHandler( $target, $data ) {
$callbackReturn = false;
if ( $this->parserOptions['processing_instruction_handler'] ) {
// @phan-suppress-next-line PhanTypeInvalidCallable false positive
$callbackReturn = $this->parserOptions['processing_instruction_handler'](
$target,
$data
);
}
if ( $callbackReturn ) {
// Filter hit!
$this->filterMatch = true;
$this->filterMatchType = $callbackReturn;
}
}
/**
* Handle coming across a <!DOCTYPE declaration.
*
* @param XMLReader $reader Reader currently pointing at DOCTYPE node.
*/
private function dtdHandler( XMLReader $reader ) {
$externalCallback = $this->parserOptions['external_dtd_handler'];
$generalCallback = $this->parserOptions['dtd_handler'];
$checkIfSafe = $this->parserOptions['require_safe_dtd'];
if ( !$externalCallback && !$generalCallback && !$checkIfSafe ) {
return;
}
$dtd = $reader->readOuterXml();
$callbackReturn = false;
if ( $generalCallback ) {
$callbackReturn = $generalCallback( $dtd );
}
if ( $callbackReturn ) {
// Filter hit!
$this->filterMatch = true;
$this->filterMatchType = $callbackReturn;
$callbackReturn = false;
}
$parsedDTD = $this->parseDTD( $dtd );
if ( $externalCallback && isset( $parsedDTD['type'] ) ) {
$callbackReturn = $externalCallback(
$parsedDTD['type'],
$parsedDTD['publicid'] ?? null,
$parsedDTD['systemid'] ?? null
);
}
if ( $callbackReturn ) {
// Filter hit!
$this->filterMatch = true;
$this->filterMatchType = $callbackReturn;
}
if ( $checkIfSafe && isset( $parsedDTD['internal'] ) &&
!$this->checkDTDIsSafe( $parsedDTD['internal'] )
) {
$this->wellFormed = false;
}
}
/**
* Check if the internal subset of the DTD is safe.
*
* We whitelist an extremely restricted subset of DTD features.
*
* Safe is defined as:
* * Only contains entity definitions (e.g. No <!ATLIST )
* * Entity definitions are not "system" entities
* * Entity definitions are not "parameter" (i.e. %) entities
* * Entity definitions do not reference other entities except &
* and quotes. Entity aliases (where the entity contains only
* another entity are allowed)
* * Entity references aren't overly long (>255 bytes).
* * <!ATTLIST svg xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink">
* allowed if matched exactly for compatibility with graphviz
* * Comments.
*
* @param string $internalSubset The internal subset of the DTD
* @return bool true if safe.
*/
private function checkDTDIsSafe( $internalSubset ) {
$res = preg_match(
'/^(?:\s*<!ENTITY\s+\S+\s+' .
'(?:"(?:&[^"%&;]{1,64};|(?:[^"%&]|&|"){0,255})"' .
'|\'(?:&[^"%&;]{1,64};|(?:[^\'%&]|&|'){0,255})\')\s*>' .
'|\s*<!--(?:[^-]|-[^-])*-->' .
'|\s*<!ATTLIST svg xmlns:xlink CDATA #FIXED ' .
'"http:\/\/www.w3.org\/1999\/xlink">)*\s*$/',
$internalSubset
);
return (bool)$res;
}
/**
* Parse DTD into parts.
*
* If there is an error parsing the dtd, sets wellFormed to false.
*
* @param string $dtd
* @return array Possibly containing keys publicid, systemid, type and internal.
*/
private function parseDTD( $dtd ) {
$m = [];
$res = preg_match(
'/^<!DOCTYPE\s*\S+\s*' .
'(?:(?P<typepublic>PUBLIC)\s*' .
'(?:"(?P<pubquote>[^"]*)"|\'(?P<pubapos>[^\']*)\')' . // public identifer
'\s*"(?P<pubsysquote>[^"]*)"|\'(?P<pubsysapos>[^\']*)\'' . // system identifier
'|(?P<typesystem>SYSTEM)\s*' .
'(?:"(?P<sysquote>[^"]*)"|\'(?P<sysapos>[^\']*)\')' .
')?\s*' .
'(?:\[\s*(?P<internal>.*)\])?\s*>$/s',
$dtd,
$m
);
if ( !$res ) {
$this->wellFormed = false;
return [];
}
$parsed = [];
foreach ( $m as $field => $value ) {
if ( $value === '' || is_numeric( $field ) ) {
continue;
}
switch ( $field ) {
case 'typepublic':
case 'typesystem':
$parsed['type'] = $value;
break;
case 'pubquote':
case 'pubapos':
$parsed['publicid'] = $value;
break;
case 'pubsysquote':
case 'pubsysapos':
case 'sysquote':
case 'sysapos':
$parsed['systemid'] = $value;
break;
case 'internal':
$parsed['internal'] = $value;
break;
}
}
return $parsed;
}
}
ParamValidator/TypeDefTest.php 0000666 00000011755 15133512024 0012374 0 ustar 00 <?php
namespace Wikimedia\ParamValidator;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\TestingAccessWrapper;
/**
* @covers Wikimedia\ParamValidator\TypeDef
*/
class TypeDefTest extends \PHPUnit\Framework\TestCase {
public function testMisc() {
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
->getMockForAbstractClass();
$this->assertSame( [ 'foobar' ], $typeDef->normalizeSettings( [ 'foobar' ] ) );
$ret = [ 'issues' => [], 'allowedKeys' => [], 'messages' => [] ];
$this->assertSame( $ret, $typeDef->checkSettings( 'foobar', [], [], $ret ) );
$this->assertNull( $typeDef->getEnumValues( 'foobar', [], [] ) );
$this->assertSame( '123', $typeDef->stringifyValue( 'foobar', 123, [], [] ) );
}
public function testGetValue() {
$options = [ (object)[] ];
$callbacks = $this->getMockBuilder( Callbacks::class )->getMockForAbstractClass();
$callbacks->expects( $this->once() )->method( 'getValue' )
->with(
$this->identicalTo( 'foobar' ),
$this->identicalTo( null ),
$this->identicalTo( $options )
)
->willReturn( 'zyx' );
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->getMockForAbstractClass();
$this->assertSame(
'zyx',
$typeDef->getValue( 'foobar', [ ParamValidator::PARAM_DEFAULT => 'foo' ], $options )
);
}
public function testGetParamInfo() {
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
->getMockForAbstractClass();
$this->assertSame( [], $typeDef->getParamInfo( 'foobar', [], [] ) );
}
public function testGetHelpInfo() {
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
->getMockForAbstractClass();
$this->assertSame( [], $typeDef->getHelpInfo( 'foobar', [], [] ) );
}
/** @dataProvider provideFailureMessage */
public function testFailureMessage( $expect, $code, array $data = null, $suffix = null ) {
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
->getMockForAbstractClass();
$ret = TestingAccessWrapper::newFromObject( $typeDef )->failureMessage( $code, $data, $suffix );
$this->assertInstanceOf( DataMessageValue::class, $ret );
$this->assertSame( $expect, $ret->dump() );
}
public static function provideFailureMessage() {
return [
'Basic' => [
'<datamessage key="paramvalidator-foobar" code="foobar"></datamessage>',
'foobar',
],
'With data' => [
'<datamessage key="paramvalidator-foobar" code="foobar"><data>{"x":123}</data></datamessage>',
'foobar', [ 'x' => 123 ]
],
'With suffix' => [
'<datamessage key="paramvalidator-foobar-baz" code="foobar"><data>[]</data></datamessage>',
'foobar', [], 'baz'
],
];
}
/** @dataProvider provideFailure */
public function testFailure_fatal(
$expect, $failure, $name, $value, array $settings, array $options
) {
$callbacks = new SimpleCallbacks( [] );
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->getMockForAbstractClass();
try {
TestingAccessWrapper::newFromObject( $typeDef )
->failure( $failure, $name, $value, $settings, $options );
$this->fail( 'Expected exception not thrown' );
} catch ( ValidationException $ex ) {
$this->assertSame( $expect, $ex->getFailureMessage()->dump() );
$this->assertSame( $name, $ex->getParamName() );
$this->assertSame( (string)$value, $ex->getParamValue() );
$this->assertSame( $settings, $ex->getSettings() );
}
$this->assertSame( [], $callbacks->getRecordedConditions() );
}
/** @dataProvider provideFailure */
public function testFailure_nonfatal(
$expect, $failure, $name, $value, array $settings, array $options
) {
$callbacks = new SimpleCallbacks( [] );
$typeDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->getMockForAbstractClass();
TestingAccessWrapper::newFromObject( $typeDef )
->failure( $failure, $name, $value, $settings, $options, false );
$conds = $callbacks->getRecordedConditions();
$this->assertCount( 1, $conds );
$conds[0]['message'] = $conds[0]['message']->dump();
$this->assertSame( [
'message' => $expect,
'name' => $name,
'value' => (string)$value,
'settings' => $settings,
], $conds[0] );
}
public static function provideFailure() {
return [
'Basic' => [
'<datamessage key="paramvalidator-foobar" code="foobar"><params><plaintext>test</plaintext><plaintext>1234</plaintext></params></datamessage>',
'foobar', 'test', 1234, [], []
],
'DataMessageValue' => [
'<datamessage key="XXX-msg" code="foobar"><params><plaintext>test</plaintext><plaintext>XXX</plaintext><text>a</text><text>b</text><plaintext>pt</plaintext></params><data>{"data":"!!!"}</data></datamessage>',
DataMessageValue::new( 'XXX-msg', [ 'a', 'b' ], 'foobar', [ 'data' => '!!!' ] )
->plaintextParams( 'pt' ),
'test', 'XXX', [], []
],
];
}
}
ParamValidator/TypeDef/EnumDefTest.php 0000666 00000015231 15133512024 0013710 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers Wikimedia\ParamValidator\TypeDef\EnumDef
*/
class EnumDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new EnumDef( $callbacks, $options );
}
public function provideValidate() {
$settings = [
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e' ],
EnumDef::PARAM_DEPRECATED_VALUES => [
'b' => MessageValue::new( 'not-to-be', [ '??' ] ),
'c' => true,
'e' => DataMessageValue::new( 'xyz', [ '??' ], 'bogus', [ 'x' => 'y' ] ),
],
];
return [
'Basic' => [ 'a', 'a', $settings ],
'Deprecated' => [ 'c', 'c', $settings, [], [
[ 'code' => 'deprecated-value', 'data' => null ],
] ],
'Deprecated with message' => [
'b', 'b', $settings, [], [
[ 'code' => 'deprecated-value', 'data' => null ]
] ],
'Deprecated with data message' => [
'e', 'e', $settings, [], [
[ 'code' => 'deprecated-value', 'data' => [ 'x' => 'y' ] ]
] ],
'Deprecated, from default' => [
'c', 'c', $settings, [ 'is-default' => true ], []
],
'Bad value, non-multi' => [
'x',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue-enumnotmulti', [], 'badvalue', [] ),
'test', 'x', $settings
),
$settings,
],
'Bad value, non-multi but looks like it' => [
'x|y',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue-enumnotmulti', [], 'badvalue', [] ),
'test', 'x|y', $settings
),
$settings,
],
'Bad value, multi' => [
'x|y',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue-enummulti', [], 'badvalue', [] ),
'test', 'x|y', $settings + [ ParamValidator::PARAM_ISMULTI => true ]
),
$settings + [ ParamValidator::PARAM_ISMULTI => true ],
[ 'values-list' => [ 'x|y' ] ],
],
];
}
public function provideCheckSettings() {
return [
'Basic test' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e' ],
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [],
],
],
'Bad type for PARAM_DEPRECATED_VALUES' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e' ],
EnumDef::PARAM_DEPRECATED_VALUES => false,
],
self::STDRET,
[
'issues' => [
'X',
EnumDef::PARAM_DEPRECATED_VALUES => 'PARAM_DEPRECATED_VALUES must be an array, got boolean',
],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [],
],
],
'PARAM_DEPRECATED_VALUES value errors' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 0, '1' ],
EnumDef::PARAM_DEPRECATED_VALUES => [
'b' => null,
'c' => false,
'd' => true,
'e' => MessageValue::new( 'e' ),
'f' => 'f',
'g' => $this,
0 => true,
1 => true,
'x' => null,
],
],
self::STDRET,
[
'issues' => [
'X',
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "c" is false',
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "f" is string',
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "g" is ' . static::class,
// phpcs:enable
'PARAM_DEPRECATED_VALUES contains "x", which is not one of the enumerated values',
],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [
MessageValue::new( 'e' ),
],
],
],
];
}
public function provideGetEnumValues() {
return [
'Basic test' => [
[ ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ] ],
[ 'a', 'b', 'c', 'd' ],
],
];
}
public function provideStringifyValue() {
return [
'Basic test' => [ 123, '123' ],
'Array' => [ [ 1, 2, 3 ], '1|2|3' ],
'Array with pipes' => [ [ 1, 2, '3|4', 5 ], "\x1f1\x1f2\x1f3|4\x1f5" ],
];
}
public function provideGetInfo() {
return [
'Non-multi' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
],
[
'type' => [ 'a', 'b', 'c', 'd' ],
],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>a</text><text>b</text><text>c</text><text>d</text></list><num>4</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
],
'Multi' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
ParamValidator::PARAM_ISMULTI => true,
],
[
'type' => [ 'a', 'b', 'c', 'd' ],
],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>2</text><list listType="comma"><text>a</text><text>b</text><text>c</text><text>d</text></list><num>4</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
],
'Deprecated values' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
EnumDef::PARAM_DEPRECATED_VALUES => [ 'b' => 'B', 'c' => false, 'x' => true ],
],
[
'type' => [ 'a', 'd', 'b', 'c' ],
'deprecatedvalues' => [ 'b', 'c' ],
],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>a</text><text>d</text><text>b</text><text>c</text></list><num>4</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
],
'Deprecated values are all not allowed values' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
EnumDef::PARAM_DEPRECATED_VALUES => [ 'x' => true ],
],
[
'type' => [ 'a', 'b', 'c', 'd' ],
],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>a</text><text>b</text><text>c</text><text>d</text></list><num>4</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
],
'Empty-string is a value' => [
[
ParamValidator::PARAM_TYPE => [ '', 'a', 'b', 'c', 'd' ],
],
[
'type' => [ '', 'a', 'b', 'c', 'd' ],
],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><text><message key="paramvalidator-help-type-enum-can-be-empty"><list listType="comma"><text>a</text><text>b</text><text>c</text><text>d</text></list><num>4</num></message></text><num>5</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
],
];
}
}
ParamValidator/TypeDef/UploadDefTest.php 0000666 00000016302 15133512024 0014230 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\Util\UploadedFile;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers Wikimedia\ParamValidator\TypeDef\UploadDef
*/
class UploadDefTest extends TypeDefTestCase {
protected function getCallbacks( $value, array $options ) {
if ( $value instanceof UploadedFile ) {
return new SimpleCallbacks( [], [ 'test' => $value ] );
} else {
return new SimpleCallbacks( [ 'test' => $value ] );
}
}
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
$ret = $this->getMockBuilder( UploadDef::class )
->setConstructorArgs( [ $callbacks ] )
->onlyMethods( [ 'getIniSize' ] )
->getMock();
$ret->method( 'getIniSize' )->willReturn( $options['inisize'] ?? 2 * 1024 * 1024 );
return $ret;
}
private function makeUpload( $err = UPLOAD_ERR_OK ) {
return new UploadedFile( [
'name' => 'example.txt',
'type' => 'text/plain',
'size' => 0,
'tmp_name' => '...',
'error' => $err,
] );
}
public function testGetNoFile() {
$typeDef = $this->getInstance(
$this->getCallbacks( $this->makeUpload( UPLOAD_ERR_NO_FILE ), [] ),
[]
);
$this->assertNull( $typeDef->getValue( 'test', [], [] ) );
$this->assertNull( $typeDef->getValue( 'nothing', [], [] ) );
}
public function provideValidate() {
$okFile = $this->makeUpload();
$iniFile = $this->makeUpload( UPLOAD_ERR_INI_SIZE );
$exIni = new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-inisize', [], 'badupload', [
'code' => 'inisize',
'size' => 2 * 1024 * 1024 * 1024,
] ),
'test', '', []
);
return [
'Valid upload' => [ $okFile, $okFile ],
'Not an upload' => [
'bar',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-notupload', [], 'badupload', [
'code' => 'notupload'
] ),
'test', 'bar', []
),
],
'Too big (bytes)' => [ $iniFile, $exIni, [], [ 'inisize' => 2 * 1024 * 1024 * 1024 ] ],
'Too big (k)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 * 1024 ) . 'k' ] ],
'Too big (K)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 * 1024 ) . 'K' ] ],
'Too big (m)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 ) . 'm' ] ],
'Too big (M)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 ) . 'M' ] ],
'Too big (g)' => [ $iniFile, $exIni, [], [ 'inisize' => '2g' ] ],
'Too big (G)' => [ $iniFile, $exIni, [], [ 'inisize' => '2G' ] ],
'Form size' => [
$this->makeUpload( UPLOAD_ERR_FORM_SIZE ),
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-formsize', [], 'badupload', [
'code' => 'formsize',
] ),
'test', '', []
),
],
'Partial' => [
$this->makeUpload( UPLOAD_ERR_PARTIAL ),
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-partial', [], 'badupload', [
'code' => 'partial',
] ),
'test', '', []
),
],
'No tmp' => [
$this->makeUpload( UPLOAD_ERR_NO_TMP_DIR ),
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-notmpdir', [], 'badupload', [
'code' => 'notmpdir',
] ),
'test', '', []
),
],
'Can\'t write' => [
$this->makeUpload( UPLOAD_ERR_CANT_WRITE ),
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-cantwrite', [], 'badupload', [
'code' => 'cantwrite',
] ),
'test', '', []
),
],
'Ext abort' => [
$this->makeUpload( UPLOAD_ERR_EXTENSION ),
new ValidationException(
DataMessageValue::new( 'paramvalidator-badupload-phpext', [], 'badupload', [
'code' => 'phpext',
] ),
'test', '', []
),
],
];
}
public function testValidate_badType() {
$callbacks = $this->getCallbacks( 'foo', [] );
$typeDef = $this->getInstance( $callbacks, [] );
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( '$value must be UploadedFileInterface, got string' );
$typeDef->validate( 'test', 'foo', [], [] );
}
public function testValidate_badType2() {
$callbacks = $this->getCallbacks( 'foo', [] );
$typeDef = $this->getInstance( $callbacks, [] );
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( '$value must be UploadedFileInterface, got NULL' );
$typeDef->validate( 'test', null, [], [] );
}
public function testValidate_unknownError() {
// -43 should be safe from ever being a valid UPLOAD_ERR_ constant
$callbacks = $this->getCallbacks( $this->makeUpload( -43 ), [] );
$typeDef = $this->getInstance( $callbacks, [] );
$value = $typeDef->getValue( 'test', [], [] );
$this->expectException( \UnexpectedValueException::class );
$this->expectExceptionMessage( 'Unrecognized PHP upload error value -43' );
$typeDef->validate( 'test', $value, [], [] );
}
public function testValidate_unknownError2() {
define( 'UPLOAD_ERR_UPLOADDEFTEST', -44 );
$callbacks = $this->getCallbacks( $this->makeUpload( UPLOAD_ERR_UPLOADDEFTEST ), [] );
$typeDef = $this->getInstance( $callbacks, [] );
$value = $typeDef->getValue( 'test', [], [] );
$this->expectException( \UnexpectedValueException::class );
$this->expectExceptionMessage(
'Unrecognized PHP upload error value -44 (UPLOAD_ERR_UPLOADDEFTEST?)'
);
$typeDef->validate( 'test', $value, [], [] );
}
public function provideCheckSettings() {
return [
'Basic test' => [
[],
self::STDRET,
self::STDRET,
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI
=> 'PARAM_ISMULTI cannot be used for upload-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT can be null' => [
[ ParamValidator::PARAM_DEFAULT => null ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT is otherwise not allowed' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT => 'Cannot specify a default for upload-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
return [
'Yeah, right' => [ $this->makeUpload(), null ],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-upload"></message>',
],
],
];
}
}
ParamValidator/TypeDef/TypeDefTestCase.php 0000666 00000000713 15133512024 0014520 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
/**
* Test case infrastructure for TypeDef subclasses
*
* Generally you'll only need to implement self::getInstance() and
* data providers methods.
*/
abstract class TypeDefTestCase extends \PHPUnit\Framework\TestCase {
use TypeDefTestCaseTrait;
/** Standard "$ret" array for provideCheckSettings */
protected const STDRET =
[ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
}
ParamValidator/TypeDef/TimestampDefTest.php 0000666 00000015210 15133512024 0014744 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* @covers Wikimedia\ParamValidator\TypeDef\TimestampDef
*/
class TimestampDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new TimestampDef( $callbacks, $options );
}
/** @dataProvider provideConstructorOptions */
public function testConstructorOptions( array $options, $ok ): void {
if ( $ok ) {
$this->assertTrue( true ); // dummy
} else {
$this->expectException( \InvalidArgumentException::class );
}
$this->getInstance( new SimpleCallbacks( [] ), $options );
}
public static function provideConstructorOptions(): array {
return [
'Basic test' => [ [], true ],
'Default format ConvertibleTimestamp' => [ [ 'defaultFormat' => 'ConvertibleTimestamp' ], true ],
'Default format DateTime' => [ [ 'defaultFormat' => 'DateTime' ], true ],
'Default format TS_ISO_8601' => [ [ 'defaultFormat' => TS_ISO_8601 ], true ],
'Default format invalid (string)' => [ [ 'defaultFormat' => 'foobar' ], false ],
'Default format invalid (int)' => [ [ 'defaultFormat' => 1000 ], false ],
'Stringify format ConvertibleTimestamp' => [
[ 'stringifyFormat' => 'ConvertibleTimestamp' ], false
],
'Stringify format DateTime' => [ [ 'stringifyFormat' => 'DateTime' ], false ],
'Stringify format TS_ISO_8601' => [ [ 'stringifyFormat' => TS_ISO_8601 ], true ],
'Stringify format invalid (string)' => [ [ 'stringifyFormat' => 'foobar' ], false ],
'Stringify format invalid (int)' => [ [ 'stringifyFormat' => 1000 ], false ],
];
}
/** @dataProvider provideValidate */
public function testValidate(
$value, $expect, array $settings = [], array $options = [], array $expectConds = []
) {
ConvertibleTimestamp::setFakeTime( 1559764242 );
parent::testValidate( $value, $expect, $settings, $options, $expectConds );
}
public function provideValidate() {
$specific = new ConvertibleTimestamp( 1517630706 );
$specificMs = new ConvertibleTimestamp( 1517630706.999 );
$now = new ConvertibleTimestamp( 1559764242 );
$formatDT = [ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'DateTime' ];
$formatMW = [ TimestampDef::PARAM_TIMESTAMP_FORMAT => TS_MW ];
return [
// We don't try to validate all formats supported by ConvertibleTimestamp, just
// some of the interesting ones.
'ISO format' => [ '2018-02-03T04:05:06Z', $specific ],
'ISO format with TZ' => [ '2018-02-03T00:05:06-04:00', $specific ],
'ISO format without punctuation' => [ '20180203T040506', $specific ],
'ISO format with ms' => [ '2018-02-03T04:05:06.999000Z', $specificMs ],
'ISO format with ms without punctuation' => [ '20180203T040506.999', $specificMs ],
'MW format' => [ '20180203040506', $specific ],
'Generic format' => [ '2018-02-03 04:05:06', $specific ],
'Generic format + GMT' => [ '2018-02-03 04:05:06 GMT', $specific ],
'Generic format + TZ +0100' => [ '2018-02-03 05:05:06+0100', $specific ],
'Generic format + TZ -01' => [ '2018-02-03 03:05:06-01', $specific ],
'Seconds-since-epoch format' => [ '1517630706', $specific ],
'Seconds-since-epoch format with ms' => [ '1517630706.9990', $specificMs ],
'Now' => [ 'now', $now ],
// Warnings
'Empty' => [ '', $now, [], [], [ [ 'code' => 'unclearnowtimestamp', 'data' => null ] ] ],
'Zero' => [ '0', $now, [], [], [ [ 'code' => 'unclearnowtimestamp', 'data' => null ] ] ],
// Error handling
'Bad value' => [
'bogus',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badtimestamp', [], 'badtimestamp' ),
'test', 'bogus', []
),
],
// T272637
'Incomplete MW format' => [
'2014815210101',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badtimestamp', [], 'badtimestamp' ),
'test', '2014815210101', []
),
$formatMW
],
// Formatting
'=> DateTime' => [ 'now', $now->timestamp, $formatDT ],
'=> TS_MW' => [ 'now', '20190605195042', $formatMW ],
'=> TS_MW as default' => [ 'now', '20190605195042', [], [ 'defaultFormat' => TS_MW ] ],
'=> TS_MW overriding default'
=> [ 'now', '20190605195042', $formatMW, [ 'defaultFormat' => TS_ISO_8601 ] ],
];
}
public function provideCheckSettings() {
$keys = [ 'Y', TimestampDef::PARAM_TIMESTAMP_FORMAT ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format ConvertibleTimestamp' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'ConvertibleTimestamp' ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format DateTime' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'DateTime' ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format TS_ISO_8601' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => TS_ISO_8601 ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with invalid format (string)' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'foobar' ],
self::STDRET,
[
'issues' => [
'X',
TimestampDef::PARAM_TIMESTAMP_FORMAT => 'Value for PARAM_TIMESTAMP_FORMAT is not valid',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with invalid format (int)' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 1000 ],
self::STDRET,
[
'issues' => [
'X',
TimestampDef::PARAM_TIMESTAMP_FORMAT => 'Value for PARAM_TIMESTAMP_FORMAT is not valid',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
$specific = new ConvertibleTimestamp( '20180203040506' );
return [
[ '20180203040506', '2018-02-03T04:05:06Z' ],
[ $specific, '2018-02-03T04:05:06Z' ],
[ $specific->timestamp, '2018-02-03T04:05:06Z' ],
[ $specific, '20180203040506', [], [ 'stringifyFormat' => TS_MW ] ],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-timestamp"><text>1</text></message>',
],
],
'Multi-valued' => [
[ ParamValidator::PARAM_ISMULTI => true ],
[],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-timestamp"><text>2</text></message>',
],
],
];
}
}
ParamValidator/TypeDef/FloatDefTest.php 0000666 00000011436 15133512024 0014054 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers Wikimedia\ParamValidator\TypeDef\FloatDef
*/
class FloatDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new FloatDef( $callbacks, $options );
}
public function provideValidate() {
return [
[ '123', 123.0 ],
[ '123.4', 123.4 ],
[ '0.4', 0.4 ],
[ '.4', 0.4 ],
[ '+123', 123.0 ],
[ '+123.4', 123.4 ],
[ '+0.4', 0.4 ],
[ '+.4', 0.4 ],
[ '-123', -123.0 ],
[ '-123.4', -123.4 ],
[ '-.4', -0.4 ],
[ '-.4', -0.4 ],
[ '123e5', 12300000.0 ],
[ '123E5', 12300000.0 ],
[ '123.4e+5', 12340000.0 ],
[ '123E5', 12300000.0 ],
[ '-123.4e-5', -0.001234 ],
[ '.4E-5', 0.000004 ],
[ '0', 0 ],
[ '000000', 0 ],
[ '0000.0000', 0 ],
[ '000001.0002000000', 1.0002 ],
[ '1e0', 1 ],
[ '1e-0000', 1 ],
[ '1e+00010', 1e10 ],
'Weird, but ok' => [ '-0', 0 ],
'Underflow is ok' => [ '1e-9999', 0 ],
'Empty decimal part' => [ '1.', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat', [], 'badfloat' ),
'test', '1.', []
) ],
'Bad sign' => [ ' 1', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat', [], 'badfloat' ),
'test', ' 1', []
) ],
'Comma as decimal separator or thousands grouping?' => [ '1,234', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat', [], 'badfloat' ),
'test', '1,234', []
) ],
'U+2212 minus' => [ '−1', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat', [], 'badfloat' ),
'test', '−1', []
) ],
'Overflow' => [ '1e9999', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat-notfinite', [], 'badfloat-notfinite' ),
'test', '1e9999', []
) ],
'Overflow, -INF' => [ '-1e9999', new ValidationException(
DataMessageValue::new( 'paramvalidator-badfloat-notfinite', [], 'badfloat-notfinite' ),
'test', '-1e9999', []
) ],
'Bogus value' => [ 'foo', new ValidationException(
DataMessageValue::new( 'paramvalidator-notfinite', [], 'badfloat' ),
'test', 'foo', []
) ],
'Bogus value 2' => [ '123f4', new ValidationException(
DataMessageValue::new( 'paramvalidator-notfinite', [], 'badfloat' ),
'test', '123f4', []
) ],
'Newline' => [ "123\n", new ValidationException(
DataMessageValue::new( 'paramvalidator-notfinite', [], 'badfloat' ),
'test', "123\n", []
) ],
];
}
public function provideCheckSettings() {
$keys = [
'Y', FloatDef::PARAM_IGNORE_RANGE,
FloatDef::PARAM_MIN, FloatDef::PARAM_MAX, FloatDef::PARAM_MAX2
];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
FloatDef::PARAM_IGNORE_RANGE => true,
FloatDef::PARAM_MIN => -100.0,
FloatDef::PARAM_MAX => -90.0,
FloatDef::PARAM_MAX2 => -80.0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
FloatDef::PARAM_IGNORE_RANGE => 1,
FloatDef::PARAM_MIN => 1,
FloatDef::PARAM_MAX => '2',
FloatDef::PARAM_MAX2 => '3',
],
self::STDRET,
[
'issues' => [
'X',
FloatDef::PARAM_IGNORE_RANGE => 'PARAM_IGNORE_RANGE must be boolean, got integer',
FloatDef::PARAM_MIN => 'PARAM_MIN must be double, got integer',
FloatDef::PARAM_MAX => 'PARAM_MAX must be double, got string',
FloatDef::PARAM_MAX2 => 'PARAM_MAX2 must be double, got string',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
$digits = defined( 'PHP_FLOAT_DIG' ) ? PHP_FLOAT_DIG : 15;
return [
[ 1.2, '1.2' ],
[ 10 / 3, '3.' . str_repeat( '3', $digits - 1 ) ],
[ 1e100, '1.0e+100' ],
[ 6.022e-23, '6.022e-23' ],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[ 'min' => null, 'max' => null ],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-float"><text>1</text></message>',
],
],
'Various settings' => [
[
FloatDef::PARAM_MIN => 1,
FloatDef::PARAM_MAX => 100,
ParamValidator::PARAM_ISMULTI => true
],
[ 'min' => 1, 'max' => 100 ],
[
FloatDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-minmax"><text>2</text><num>1</num><num>100</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-float"><text>2</text></message>',
],
],
];
}
}
ParamValidator/TypeDef/LimitDefTest.php 0000666 00000010300 15133512024 0014052 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
/**
* @covers Wikimedia\ParamValidator\TypeDef\LimitDef
*/
class LimitDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new LimitDef( $callbacks, $options );
}
public function provideValidate() {
$useHigh = [ 'useHighLimits' => true ];
$max = [ LimitDef::PARAM_MAX => 2 ];
$max2 = [ LimitDef::PARAM_MAX => 2, LimitDef::PARAM_MAX2 => 4 ];
yield 'Max' => [ 'max', 2, $max ];
yield 'Max, use high' => [ 'max', 2, $max, $useHigh ];
yield 'Max2' => [ 'max', 2, $max2 ];
yield 'Max2, use high' => [ 'max', 4, $max2, $useHigh ];
// Test an arbitrary number for coverage. Actual number handling is tested via
// the base class IntegerDef's tests.
yield 'A number' => [ '123', 123 ];
}
public function provideNormalizeSettings() {
$def = [ LimitDef::PARAM_MIN => 0, ParamValidator::PARAM_ISMULTI => false ];
return [
[
[],
$def,
],
[
[ ParamValidator::PARAM_ISMULTI => true ],
[ ParamValidator::PARAM_ISMULTI => false ] + $def,
],
[
[ LimitDef::PARAM_MAX => 2 ],
[ LimitDef::PARAM_MAX => 2 ] + $def,
],
[
[ LimitDef::PARAM_MIN => 1, LimitDef::PARAM_MAX => 2, LimitDef::PARAM_MAX2 => 4 ],
[ LimitDef::PARAM_MIN => 1, LimitDef::PARAM_MAX => 2, LimitDef::PARAM_MAX2 => 4 ] + $def,
],
[
[ LimitDef::PARAM_MIN => 1, LimitDef::PARAM_MAX => 4, LimitDef::PARAM_MAX2 => 2 ],
[ LimitDef::PARAM_MIN => 1, LimitDef::PARAM_MAX => 4, LimitDef::PARAM_MAX2 => 4 ] + $def,
],
[
[ LimitDef::PARAM_MAX2 => 2 ],
$def,
],
];
}
public function provideCheckSettings() {
$keys = [
'Y', IntegerDef::PARAM_IGNORE_RANGE,
IntegerDef::PARAM_MIN, IntegerDef::PARAM_MAX, IntegerDef::PARAM_MAX2
];
return [
'Basic test' => [
[
LimitDef::PARAM_MAX => 10,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
LimitDef::PARAM_IGNORE_RANGE => true,
LimitDef::PARAM_MIN => 0,
LimitDef::PARAM_MAX => 10,
LimitDef::PARAM_MAX2 => 100,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
LimitDef::PARAM_MAX => 10,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI => 'PARAM_ISMULTI cannot be used for limit-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
LimitDef::PARAM_MAX => 10,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MIN == 0' => [
[
LimitDef::PARAM_MIN => 0,
LimitDef::PARAM_MAX => 2,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MIN < 0' => [
[
LimitDef::PARAM_MIN => -1,
LimitDef::PARAM_MAX => 2,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MIN must be greater than or equal to 0',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MAX is required' => [
[],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX must be set',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic' => [
[],
[ 'min' => 0, 'max' => null ],
[
LimitDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-min"><text>1</text><num>0</num><text>∞</text></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-limit"><text>1</text></message>',
],
],
];
}
}
ParamValidator/TypeDef/IntegerDefTest.php 0000666 00000023447 15133512024 0014411 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers Wikimedia\ParamValidator\TypeDef\IntegerDef
* @covers Wikimedia\ParamValidator\TypeDef\NumericDef
*/
class IntegerDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new IntegerDef( $callbacks, $options );
}
/**
* @param string $v Representing a positive integer
* @return string Representing $v + 1
*/
private static function plusOne( $v ) {
for ( $i = strlen( $v ) - 1; $i >= 0; $i-- ) {
if ( $v[$i] === '9' ) {
$v[$i] = '0';
} else {
$v[$i] = $v[$i] + 1;
return $v;
}
}
return '1' . $v;
}
public function provideValidate() {
$badinteger = new ValidationException(
DataMessageValue::new( 'XXX', [], 'badinteger' ),
'test', '...', []
);
$outofrange = new ValidationException(
DataMessageValue::new( 'XXX', [], 'outofrange', [
'min' => 0, 'curmax' => 2, 'max' => 2, 'highmax' => 2
] ),
'test', '...', []
);
$outofrange2 = new ValidationException(
DataMessageValue::new( 'XXX', [], 'outofrange', [
'min' => 0, 'curmax' => 2, 'max' => 2, 'highmax' => 4
] ),
'test', '...', []
);
$outofrange2h = new ValidationException(
DataMessageValue::new( 'XXX', [], 'outofrange', [
'min' => 0, 'curmax' => 4, 'max' => 2, 'highmax' => 4
] ),
'test', '...', []
);
$asWarn = static function ( ValidationException $ex ) {
return [
'code' => $ex->getFailureMessage()->getCode(),
'data' => $ex->getFailureMessage()->getData(),
];
};
$minmax = [
IntegerDef::PARAM_MIN => 0,
IntegerDef::PARAM_MAX => 2,
];
$minmax2 = [
IntegerDef::PARAM_MIN => 0,
IntegerDef::PARAM_MAX => 2,
IntegerDef::PARAM_MAX2 => 4,
];
$ignore = [
IntegerDef::PARAM_IGNORE_RANGE => true,
];
$usehigh = [ 'useHighLimits' => true ];
return [
[ '123', 123 ],
[ '-123', -123 ],
[ '000123', 123 ],
[ '000', 0 ],
[ '-0', 0 ],
[ (string)PHP_INT_MAX, PHP_INT_MAX ],
[ '0000' . PHP_INT_MAX, PHP_INT_MAX ],
[ (string)PHP_INT_MIN, PHP_INT_MIN ],
[ '-0000' . substr( PHP_INT_MIN, 1 ), PHP_INT_MIN ],
'Overflow' => [ self::plusOne( (string)PHP_INT_MAX ), $badinteger ],
'Negative overflow' => [ '-' . self::plusOne( substr( PHP_INT_MIN, 1 ) ), $badinteger ],
'Float' => [ '1.5', $badinteger ],
'Float (e notation)' => [ '1e1', $badinteger ],
'Bad sign (space)' => [ ' 1', $badinteger ],
'Bad sign (newline)' => [ "\n1", $badinteger ],
'Bogus value' => [ 'max', $badinteger ],
'Bogus value (2)' => [ '1foo', $badinteger ],
'Hex value' => [ '0x123', $badinteger ],
'Newline' => [ "1\n", $badinteger ],
'Array' => [ [ '1.5' ], $badinteger ],
'Ok with range' => [ '1', 1, $minmax ],
'Below minimum' => [ '-1', $outofrange, $minmax ],
'Below minimum, ignored' => [ '-1', 0, $minmax + $ignore, [], [ $asWarn( $outofrange ) ] ],
'Above maximum' => [ '3', $outofrange, $minmax ],
'Above maximum, ignored' => [ '3', 2, $minmax + $ignore, [], [ $asWarn( $outofrange ) ] ],
'Not above max2 but can\'t use it' => [ '3', $outofrange2, $minmax2, [] ],
'Not above max2 but can\'t use it, ignored'
=> [ '3', 2, $minmax2 + $ignore, [], [ $asWarn( $outofrange2 ) ] ],
'Not above max2' => [ '3', 3, $minmax2, $usehigh ],
'Above max2' => [ '5', $outofrange2h, $minmax2, $usehigh ],
'Above max2, ignored'
=> [ '5', 4, $minmax2 + $ignore, $usehigh, [ $asWarn( $outofrange2h ) ] ],
];
}
public function provideNormalizeSettings() {
return [
[ [], [] ],
[
[ IntegerDef::PARAM_MAX => 2 ],
[ IntegerDef::PARAM_MAX => 2 ],
],
[
[ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
[ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
],
[
[ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 2 ],
[ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 4 ],
],
[
[ IntegerDef::PARAM_MAX2 => 2 ],
[],
],
];
}
public function provideCheckSettings() {
$keys = [
'Y', IntegerDef::PARAM_IGNORE_RANGE,
IntegerDef::PARAM_MIN, IntegerDef::PARAM_MAX, IntegerDef::PARAM_MAX2
];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
IntegerDef::PARAM_IGNORE_RANGE => true,
IntegerDef::PARAM_MIN => -100,
IntegerDef::PARAM_MAX => -90,
IntegerDef::PARAM_MAX2 => -80,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
IntegerDef::PARAM_IGNORE_RANGE => 1,
IntegerDef::PARAM_MIN => 1.0,
IntegerDef::PARAM_MAX => '2',
IntegerDef::PARAM_MAX2 => '3',
],
self::STDRET,
[
'issues' => [
'X',
IntegerDef::PARAM_IGNORE_RANGE => 'PARAM_IGNORE_RANGE must be boolean, got integer',
IntegerDef::PARAM_MIN => 'PARAM_MIN must be integer, got double',
IntegerDef::PARAM_MAX => 'PARAM_MAX must be integer, got string',
IntegerDef::PARAM_MAX2 => 'PARAM_MAX2 must be integer, got string',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Min == max' => [
[
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => 1,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Min > max' => [
[
IntegerDef::PARAM_MIN => 2,
IntegerDef::PARAM_MAX => 1,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MIN must be less than or equal to PARAM_MAX, but 2 > 1',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 without max' => [
[
IntegerDef::PARAM_MAX2 => 1,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX2 cannot be used without PARAM_MAX',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 == max' => [
[
IntegerDef::PARAM_MAX => 1,
IntegerDef::PARAM_MAX2 => 1,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 < max' => [
[
IntegerDef::PARAM_MAX => -10,
IntegerDef::PARAM_MAX2 => -11,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX2 must be greater than or equal to PARAM_MAX, but -11 < -10',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic' => [
[],
[ 'min' => null, 'max' => null ],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
],
'Min' => [
[ IntegerDef::PARAM_MIN => 0, ParamValidator::PARAM_ISMULTI => true ],
[ 'min' => 0, 'max' => null ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-min"><text>2</text><num>0</num><text>∞</text></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>2</text></message>',
],
],
'Max' => [
[ IntegerDef::PARAM_MAX => 2 ],
[ 'min' => null, 'max' => 2 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-max"><text>1</text><text>−∞</text><num>2</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
],
'Max2' => [
[ IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
[ 'min' => null, 'max' => 2, 'highmax' => 4 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-max"><text>1</text><text>−∞</text><num>2</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
],
'Max2, highlimits' => [
[ IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
[ 'min' => null, 'max' => 2, 'highmax' => 4 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-max"><text>1</text><text>−∞</text><num>4</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
[ 'useHighLimits' => true ],
],
'Minmax' => [
[ IntegerDef::PARAM_MIN => 0, IntegerDef::PARAM_MAX => 2 ],
[ 'min' => 0, 'max' => 2 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-minmax"><text>1</text><num>0</num><num>2</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
],
'Minmax2' => [
[ IntegerDef::PARAM_MIN => 0, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
[ 'min' => 0, 'max' => 2, 'highmax' => 4 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-minmax"><text>1</text><num>0</num><num>2</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>1</text></message>',
],
],
'Minmax2, highlimits' => [
[
IntegerDef::PARAM_MIN => 0, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4,
ParamValidator::PARAM_ISMULTI => true
],
[ 'min' => 0, 'max' => 2, 'highmax' => 4 ],
[
IntegerDef::PARAM_MIN => '<message key="paramvalidator-help-type-number-minmax"><text>2</text><num>0</num><num>4</num></message>',
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-integer"><text>2</text></message>',
],
[ 'useHighLimits' => true ],
],
];
}
}
ParamValidator/TypeDef/ExpiryDefTest.php 0000666 00000012067 15133512024 0014270 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use InvalidArgumentException;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* @covers \Wikimedia\ParamValidator\TypeDef\ExpiryDef
*/
class ExpiryDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new ExpiryDef( $callbacks, $options );
}
/**
* Get an entry for the provideValidate() provider, where a given value
* is asserted to cause a ValidationException with the given message.
* @param string $value
* @param string $msg
* @param array $settings
* @return array
*/
private function getValidationAssertion( string $value, string $msg, array $settings = [] ) {
return [
$value,
new ValidationException(
DataMessageValue::new( $msg ),
'expiry',
$value,
[]
),
$settings
];
}
/**
* @dataProvider provideValidate
*/
public function testValidate(
$value, $expect, array $settings = [], array $options = [], array $expectConds = []
) {
ConvertibleTimestamp::setFakeTime( 1559764242 );
parent::testValidate( $value, $expect, $settings, $options, $expectConds );
}
public function provideValidate() {
$settings = [
ExpiryDef::PARAM_MAX => '6 months',
ExpiryDef::PARAM_USE_MAX => true,
];
return [
'Valid infinity' => [ 'indefinite', 'infinity' ],
'Invalid expiry' => $this->getValidationAssertion( 'foobar', 'badexpiry' ),
'Expiry in past' => $this->getValidationAssertion( '20150123T12:34:56Z', 'badexpiry-past' ),
'Expiry in past with unix 0' => $this->getValidationAssertion(
'1970-01-01T00:00:00Z',
'badexpiry-past'
),
'Expiry in past with negative unix time' => $this->getValidationAssertion(
'1969-12-31T23:59:59Z',
'badexpiry-past',
$settings
),
'Valid expiry' => [
'99990123123456',
'9999-01-23T12:34:56Z'
],
'Valid relative expiry' => [
'1 month',
'2019-07-05T19:50:42Z'
],
'Expiry less than max' => [ '20190701123456', '2019-07-01T12:34:56Z', $settings ],
'Relative expiry less than max' => [ '1 day', '2019-06-06T19:50:42Z', $settings ],
'Infinity less than max' => [ 'indefinite', 'infinity', $settings ],
'Expiry exceeds max' => [
'9999-01-23T12:34:56Z',
'2019-12-05T19:50:42Z',
$settings,
[],
[
[
'code' => 'paramvalidator-badexpiry-duration-max',
'data' => null,
]
],
],
'Relative expiry exceeds max' => [
'10 years',
'2019-12-05T19:50:42Z',
$settings,
[],
[
[
'code' => 'paramvalidator-badexpiry-duration-max',
'data' => null,
]
],
],
'Expiry exceeds max, fatal' => $this->getValidationAssertion(
'9999-01-23T12:34:56Z',
'paramvalidator-badexpiry-duration',
[
ExpiryDef::PARAM_MAX => '6 months',
]
),
];
}
public function testNormalizeExpiry() {
$this->assertNull( ExpiryDef::normalizeExpiry( null ) );
$this->assertSame(
'infinity',
ExpiryDef::normalizeExpiry( 'indefinite' )
);
$this->assertSame(
'2050-01-01T00:00:00Z',
ExpiryDef::normalizeExpiry( '205001010000', TS_ISO_8601 )
);
$this->assertSame(
'1970-01-01T00:00:00Z',
ExpiryDef::normalizeExpiry( '1970-01-01T00:00:00Z', TS_ISO_8601 )
);
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'Invalid expiry value: 0' );
ExpiryDef::normalizeExpiry( 0, TS_ISO_8601 );
}
public function provideGetInfo() {
return [
'Basic' => [
[],
[],
[
'param-type' => '<message key="paramvalidator-help-type-expiry"><text>1</text><list listType="text"><text>"infinite"</text><text>"indefinite"</text><text>"infinity"</text><text>"never"</text></list></message>'
]
]
];
}
public function provideCheckSettings() {
$keys = [ 'Y', ExpiryDef::PARAM_USE_MAX, ExpiryDef::PARAM_MAX ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
]
];
}
/**
* @covers \Wikimedia\ParamValidator\TypeDef\ExpiryDef::normalizeUsingMaxExpiry
*/
public function testNormalizeUsingMaxExpiry() {
// Fake current time to be 2020-05-27T00:00:00Z
$fakeTime = ConvertibleTimestamp::setFakeTime( '20200527000000' );
$this->assertSame(
'2020-11-27T00:00:00Z',
ExpiryDef::normalizeUsingMaxExpiry( '10 months', '6 months', TS_ISO_8601 )
);
$this->assertSame(
'2020-10-27T00:00:00Z',
ExpiryDef::normalizeUsingMaxExpiry( '2020-10-27T00:00:00Z', '6 months', TS_ISO_8601 )
);
$this->assertSame(
'infinity',
ExpiryDef::normalizeUsingMaxExpiry( 'infinity', '6 months', TS_ISO_8601 )
);
$this->assertNull( ExpiryDef::normalizeUsingMaxExpiry( null, '6 months', TS_ISO_8601 ) );
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'Invalid expiry value: invalid expiry' );
ExpiryDef::normalizeUsingMaxExpiry( 'invalid expiry', '6 months', TS_ISO_8601 );
}
}
ParamValidator/TypeDef/BooleanDefTest.php 0000666 00000003412 15133512024 0014361 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers \Wikimedia\ParamValidator\TypeDef\BooleanDef
*/
class BooleanDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new BooleanDef( $callbacks, $options );
}
public function provideValidate() {
foreach ( [
[ BooleanDef::$TRUEVALS, true ],
[ BooleanDef::$FALSEVALS, false ],
[ [ '' ], false ],
] as [ $vals, $expect ] ) {
foreach ( $vals as $v ) {
yield "Value '$v'" => [ $v, $expect ];
$v2 = ucfirst( $v );
if ( $v2 !== $v ) {
yield "Value '$v2'" => [ $v2, $expect ];
}
$v3 = strtoupper( $v );
if ( $v3 !== $v2 ) {
yield "Value '$v3'" => [ $v3, $expect ];
}
}
}
yield "Value '2'" => [ 2, new ValidationException(
DataMessageValue::new( 'paramvalidator-badbool', [], 'badbool' ),
'test', '2', []
) ];
yield "Value 'foobar'" => [ 'foobar', new ValidationException(
DataMessageValue::new( 'paramvalidator-badbool', [], 'badbool' ),
'test', 'foobar', []
) ];
}
public function provideStringifyValue() {
return [
[ true, 'true' ],
[ false, 'false' ],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-boolean"><text>1</text></message>',
],
],
'Multi-valued' => [
[ ParamValidator::PARAM_ISMULTI => true ],
[],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-boolean"><text>2</text></message>',
],
],
];
}
}
ParamValidator/TypeDef/StringDefTest.php 0000666 00000012531 15133512024 0014252 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers Wikimedia\ParamValidator\TypeDef\StringDef
*/
class StringDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new StringDef( $callbacks, $options );
}
public function provideValidate() {
$req = [
ParamValidator::PARAM_REQUIRED => true,
];
$maxBytes = [
StringDef::PARAM_MAX_BYTES => 4,
];
$maxChars = [
StringDef::PARAM_MAX_CHARS => 2,
];
return [
'Basic' => [ '123', '123' ],
'Empty' => [ '', '' ],
'Empty, required' => [
'',
new ValidationException(
DataMessageValue::new( 'paramvalidator-missingparam', [], 'missingparam' ),
'test', '', []
),
$req,
],
'Empty, required, allowed' => [ '', '', $req, [ 'allowEmptyWhenRequired' => true ] ],
'Max bytes, ok' => [ 'abcd', 'abcd', $maxBytes ],
'Max bytes, exceeded' => [
'abcde',
new ValidationException(
DataMessageValue::new( 'paramvalidator-maxbytes', [], 'maxbytes', [
'maxbytes' => 4, 'maxchars' => null,
] ),
'test', '', []
),
$maxBytes,
],
'Max bytes, ok (2)' => [ '😄', '😄', $maxBytes ],
'Max bytes, exceeded (2)' => [
'😭?',
new ValidationException(
DataMessageValue::new( 'paramvalidator-maxbytes', [], 'maxbytes', [
'maxbytes' => 4, 'maxchars' => null,
] ),
'test', '', []
),
$maxBytes,
],
'Max chars, ok' => [ 'ab', 'ab', $maxChars ],
'Max chars, exceeded' => [
'abc',
new ValidationException(
DataMessageValue::new( 'paramvalidator-maxchars', [], 'maxchars', [
'maxbytes' => null, 'maxchars' => 2,
] ),
'test', '', []
),
$maxChars,
],
'Max chars, ok (2)' => [ '😄😄', '😄😄', $maxChars ],
'Max chars, exceeded (2)' => [
'😭??',
new ValidationException(
DataMessageValue::new( 'paramvalidator-maxchars', [], 'maxchars', [
'maxbytes' => null, 'maxchars' => 2,
] ),
'test', '', []
),
$maxChars,
],
];
}
public function provideCheckSettings() {
$keys = [ 'Y', StringDef::PARAM_MAX_BYTES, StringDef::PARAM_MAX_CHARS ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
StringDef::PARAM_MAX_BYTES => 255,
StringDef::PARAM_MAX_CHARS => 100,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
StringDef::PARAM_MAX_BYTES => '255',
StringDef::PARAM_MAX_CHARS => 100.0,
],
self::STDRET,
[
'issues' => [
'X',
StringDef::PARAM_MAX_BYTES => 'PARAM_MAX_BYTES must be an integer, got string',
StringDef::PARAM_MAX_CHARS => 'PARAM_MAX_CHARS must be an integer, got double',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Out of range' => [
[
StringDef::PARAM_MAX_BYTES => -1,
StringDef::PARAM_MAX_CHARS => -1,
],
self::STDRET,
[
'issues' => [
'X',
StringDef::PARAM_MAX_BYTES => 'PARAM_MAX_BYTES must be greater than or equal to 0',
StringDef::PARAM_MAX_CHARS => 'PARAM_MAX_CHARS must be greater than or equal to 0',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Zero not allowed when required and !allowEmptyWhenRequired' => [
[
ParamValidator::PARAM_REQUIRED => true,
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and PARAM_MAX_BYTES is 0. That\'s impossible to satisfy.',
'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and PARAM_MAX_CHARS is 0. That\'s impossible to satisfy.',
],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => false ],
],
'Zero allowed when not required' => [
[
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => false ],
],
'Zero allowed when allowEmptyWhenRequired' => [
[
ParamValidator::PARAM_REQUIRED => true,
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => true ],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[ 'maxbytes' => null, 'maxchars' => null ],
[],
],
'With settings' => [
[
StringDef::PARAM_MAX_BYTES => 4,
StringDef::PARAM_MAX_CHARS => 2,
ParamValidator::PARAM_ISMULTI => true,
],
[ 'maxbytes' => 4, 'maxchars' => 2 ],
[
StringDef::PARAM_MAX_BYTES => '<message key="paramvalidator-help-type-string-maxbytes"><num>4</num></message>',
StringDef::PARAM_MAX_CHARS => '<message key="paramvalidator-help-type-string-maxchars"><num>2</num></message>',
],
],
];
}
}
ParamValidator/TypeDef/PresenceBooleanDefTest.php 0000666 00000006253 15133512024 0016054 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
/**
* @covers Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef
*/
class PresenceBooleanDefTest extends TypeDefTestCase {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new PresenceBooleanDef( $callbacks, $options );
}
public function provideValidate() {
return [
[ null, false ],
[ '', true ],
[ '0', true ],
[ '1', true ],
[ 'anything really', true ],
];
}
public function provideNormalizeSettings() {
return [
[
[],
[ ParamValidator::PARAM_ISMULTI => false, ParamValidator::PARAM_DEFAULT => false ],
],
[
[ ParamValidator::PARAM_ISMULTI => true ],
[ ParamValidator::PARAM_ISMULTI => false, ParamValidator::PARAM_DEFAULT => false ],
],
[
[ ParamValidator::PARAM_DEFAULT => null ],
[ ParamValidator::PARAM_DEFAULT => null, ParamValidator::PARAM_ISMULTI => false ],
],
];
}
public function provideCheckSettings() {
return [
'Basic test' => [
[],
self::STDRET,
self::STDRET,
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI
=> 'PARAM_ISMULTI cannot be used for presence-boolean-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT can be false' => [
[ ParamValidator::PARAM_DEFAULT => false ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT can be null' => [
[ ParamValidator::PARAM_DEFAULT => null ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT cannot be true' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT
=> 'Default for presence-boolean-type parameters must be false or null',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT invalid, but another DEFAULT issue was already logged' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
[
'issues' => [
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[ 'default' => null ],
[
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-presenceboolean"><text>1</text></message>',
ParamValidator::PARAM_DEFAULT => null,
],
],
];
}
}
ParamValidator/TypeDef/PasswordDefTest.php 0000666 00000003070 15133512024 0014604 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
require_once __DIR__ . '/StringDefTest.php';
/**
* @covers Wikimedia\ParamValidator\TypeDef\PasswordDef
*/
class PasswordDefTest extends StringDefTest {
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new PasswordDef( $callbacks, $options );
}
public function provideNormalizeSettings() {
return [
[ [], [ ParamValidator::PARAM_SENSITIVE => true ] ],
[ [ ParamValidator::PARAM_SENSITIVE => false ], [ ParamValidator::PARAM_SENSITIVE => true ] ],
];
}
public function provideCheckSettings() {
$keys = [ 'Y', StringDef::PARAM_MAX_BYTES, StringDef::PARAM_MAX_CHARS ];
yield from parent::provideCheckSettings();
yield 'PARAM_SENSITIVE cannot be false' => [
[
ParamValidator::PARAM_SENSITIVE => false,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_SENSITIVE
=> 'Cannot set PARAM_SENSITIVE to false for password-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
];
yield 'PARAM_SENSITIVE cannot be false, but another PARAM_SENSITIVE issue was already logged' => [
[
ParamValidator::PARAM_SENSITIVE => false,
],
[
'issues' => [
ParamValidator::PARAM_SENSITIVE => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_SENSITIVE => 'XXX',
],
'allowedKeys' => $keys,
'messages' => [],
],
];
}
}
ParamValidator/TypeDef/TypeDefTestCaseTrait.php 0000666 00000016142 15133512024 0015527 0 ustar 00 <?php
namespace Wikimedia\ParamValidator\TypeDef;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\TypeDef;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* Test case infrastructure for TypeDef subclasses
*
* Generally you'll only need to implement self::getInstance() and
* data providers methods.
*/
trait TypeDefTestCaseTrait {
/**
* Reset any fake timestamps so that they don't mess with any other tests.
*
* @after
*/
protected function fakeTimestampTearDown() {
ConvertibleTimestamp::setFakeTime( null );
}
/**
* Create a SimpleCallbacks for testing
*
* The object created here should result in a call to the TypeDef's
* `getValue( 'test' )` returning an appropriate result for testing.
*
* @param mixed $value Value to return for 'test'
* @param array $options Options array.
* @return SimpleCallbacks
*/
protected function getCallbacks( $value, array $options ) {
return new SimpleCallbacks( $value === null ? [] : [ 'test' => $value ] );
}
/**
* Create an instance of the TypeDef subclass being tested
*
* @param SimpleCallbacks $callbacks From $this->getCallbacks()
* @param array $options Options array.
* @return TypeDef
*/
abstract protected function getInstance( SimpleCallbacks $callbacks, array $options );
/**
* @dataProvider provideValidate
* @param mixed $value Value for getCallbacks()
* @param mixed|ValidationException $expect Expected result from TypeDef::validate().
* If a ValidationException, it is expected that a ValidationException
* with matching failure code and data will be thrown. Otherwise, the return value must be equal.
* @param array $settings Settings array.
* @param array $options Options array
* @param array[] $expectConds Expected condition codes and data reported.
*/
public function testValidate(
$value, $expect, array $settings = [], array $options = [], array $expectConds = []
) {
$callbacks = $this->getCallbacks( $value, $options );
$typeDef = $this->getInstance( $callbacks, $options );
$settings = $typeDef->normalizeSettings( $settings );
if ( $expect instanceof ValidationException ) {
try {
$v = $typeDef->getValue( 'test', $settings, $options );
$typeDef->validate( 'test', $v, $settings, $options );
$this->fail( 'Expected exception not thrown' );
} catch ( ValidationException $ex ) {
$this->assertSame(
$expect->getFailureMessage()->getCode(),
$ex->getFailureMessage()->getCode()
);
$this->assertSame(
$expect->getFailureMessage()->getData(),
$ex->getFailureMessage()->getData()
);
}
} else {
$v = $typeDef->getValue( 'test', $settings, $options );
$this->assertEquals( $expect, $typeDef->validate( 'test', $v, $settings, $options ) );
}
$conditions = [];
foreach ( $callbacks->getRecordedConditions() as $c ) {
$conditions[] = [ 'code' => $c['message']->getCode(), 'data' => $c['message']->getData() ];
}
$this->assertSame( $expectConds, $conditions );
}
/**
* @return array|Iterable
*/
abstract public function provideValidate();
/**
* @param array $settings
* @param array $expect
* @param array $options
* @dataProvider provideNormalizeSettings
*/
public function testNormalizeSettings( array $settings, array $expect, array $options = [] ) {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$this->assertSame( $expect, $typeDef->normalizeSettings( $settings ) );
}
/**
* @return array|Iterable
*/
public function provideNormalizeSettings() {
return [
'Basic test' => [ [ 'param-foo' => 'bar' ], [ 'param-foo' => 'bar' ] ],
];
}
/**
* @dataProvider provideCheckSettings
* @param array $settings
* @param array $ret Input $ret array
* @param array $expect
* @param array $options Options array
*/
public function testCheckSettings(
array $settings,
array $ret,
array $expect,
array $options = []
): void {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$this->assertEquals( $expect, $typeDef->checkSettings( 'test', $settings, $options, $ret ) );
}
/**
* @return array|Iterable
*/
public function provideCheckSettings() {
return [
'Basic test' => [ [], self::STDRET, self::STDRET ],
];
}
/**
* @param array $settings
* @param array|null $expect
* @param array $options Options array
* @dataProvider provideGetEnumValues
*/
public function testGetEnumValues( array $settings, $expect, array $options = [] ) {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$settings = $typeDef->normalizeSettings( $settings );
$this->assertSame( $expect, $typeDef->getEnumValues( 'test', $settings, $options ) );
}
/**
* @return array|Iterable
*/
public function provideGetEnumValues() {
return [
'Basic test' => [ [], null ],
];
}
/**
* @param mixed $value
* @param string $expect
* @param array $settings
* @param array $options
* @dataProvider provideStringifyValue
*
*/
public function testStringifyValue( $value, $expect, array $settings = [], array $options = [] ) {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$settings = $typeDef->normalizeSettings( $settings );
$this->assertSame( $expect, $typeDef->stringifyValue( 'test', $value, $settings, $options ) );
}
/**
* @return array|Iterable
*/
public function provideStringifyValue() {
return [
'Basic test' => [ 123, '123' ],
];
}
/**
* @param array $settings
* @param array $expectParamInfo
* @param array $expectHelpInfo
* @param array $options
* @dataProvider provideGetInfo
*/
public function testGetInfo(
array $settings, array $expectParamInfo, array $expectHelpInfo, array $options = []
) {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$settings = $typeDef->normalizeSettings( $settings );
$this->assertSame(
$expectParamInfo,
$typeDef->getParamInfo( 'test', $settings, $options )
);
$actual = [];
$constraint = \PHPUnit\Framework\Assert::logicalOr(
$this->isNull(),
$this->isInstanceOf( MessageValue::class )
);
foreach ( $typeDef->getHelpInfo( 'test', $settings, $options ) as $k => $v ) {
$this->assertThat( $v, $constraint );
$actual[$k] = $v ? $v->dump() : null;
}
$this->assertSame( $expectHelpInfo, $actual );
}
/**
* @return array|Iterable
*/
public function provideGetInfo() {
return [
'Basic test' => [ [], [], [] ],
];
}
/**
* Create a ValidationException that's identical to what the typedef object would throw.
* @param string $code Error code, same as what was passed to TypeDef::failure().
* @param mixed $value Parameter value (ie. second argument to TypeDef::validate()).
* @param array $settings Settings object (ie. third argument to TypeDef::validate()).
* @return ValidationException
*/
protected function getValidationException(
string $code, $value, array $settings = []
): ValidationException {
return new ValidationException(
DataMessageValue::new( "paramvalidator-$code", [], $code ),
'test', $value, $settings );
}
}
XmlTypeCheckTest.php 0000666 00000004207 15133512024 0010460 0 ustar 00 <?php
/**
* @author physikerwelt
* @group Xml
* @covers XmlTypeCheck
*/
class XmlTypeCheckTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
private const WELL_FORMED_XML = "<root><child /></root>";
private const MAL_FORMED_XML = "<root><child /></error>";
private const XML_WITH_PIH = '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="/w/index.php"?><svg><child /></svg>';
public function testWellFormedXML() {
$testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML );
$this->assertTrue( $testXML->wellFormed );
$this->assertEquals( 'root', $testXML->getRootElement() );
}
public function testMalFormedXML() {
$testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML );
$this->assertFalse( $testXML->wellFormed );
}
/**
* Verify we check for recursive entity DOS
*
* (If the DOS isn't properly handled, the test runner will probably go OOM...)
*/
public function testRecursiveEntity() {
$xml = <<<'XML'
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
<!ENTITY test "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
<!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
<!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
<!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
<!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
<!ENTITY g "-00000000000000000000000000000000000000000000000000000000000000000000000-">
]>
<foo>
<bar>&test;</bar>
</foo>
XML;
$check = XmlTypeCheck::newFromString( $xml );
$this->assertFalse( $check->wellFormed );
}
public function testProcessingInstructionHandler() {
$called = false;
$testXML = new XmlTypeCheck(
self::XML_WITH_PIH,
null,
false,
[
'processing_instruction_handler' => static function () use ( &$called ) {
$called = true;
}
]
);
$this->assertTrue( $called );
}
}
rdbms/database/DatabaseTest.php 0000666 00000071002 15133512024 0012474 0 ustar 00 <?php
use MediaWiki\Tests\Unit\Libs\Rdbms\AddQuoterMock;
use MediaWiki\Tests\Unit\Libs\Rdbms\SQLPlatformTestHelper;
use Psr\Log\NullLogger;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\Database\DatabaseFlags;
use Wikimedia\Rdbms\DatabaseDomain;
use Wikimedia\Rdbms\DBLanguageError;
use Wikimedia\Rdbms\DBReadOnlyRoleError;
use Wikimedia\Rdbms\DBTransactionStateError;
use Wikimedia\Rdbms\DBUnexpectedError;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\LBFactorySingle;
use Wikimedia\Rdbms\Platform\SQLPlatform;
use Wikimedia\Rdbms\QueryStatus;
use Wikimedia\Rdbms\Replication\ReplicationReporter;
use Wikimedia\Rdbms\TransactionManager;
use Wikimedia\RequestTimeout\CriticalSectionScope;
use Wikimedia\TestingAccessWrapper;
/**
* @dataProvider provideAddQuotes
* @covers Wikimedia\Rdbms\Database
* @covers Wikimedia\Rdbms\Database\DatabaseFlags
* @covers Wikimedia\Rdbms\Platform\SQLPlatform
*/
class DatabaseTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
/** @var DatabaseTestHelper */
private $db;
protected function setUp(): void {
$this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
}
public static function provideAddQuotes() {
return [
[ null, 'NULL' ],
[ 1234, "1234" ],
[ 1234.5678, "'1234.5678'" ],
[ 'string', "'string'" ],
[ 'string\'s cause trouble', "'string\'s cause trouble'" ],
];
}
/**
* @dataProvider provideAddQuotes
*/
public function testAddQuotes( $input, $expected ) {
$this->assertEquals( $expected, $this->db->addQuotes( $input ) );
}
public static function provideTableName() {
// Formatting is mostly ignored since addIdentifierQuotes is abstract.
// For testing of addIdentifierQuotes, see actual Database subclas tests.
return [
'local' => [
'tablename',
'tablename',
'quoted',
],
'local-raw' => [
'tablename',
'tablename',
'raw',
],
'shared' => [
'sharedb.tablename',
'tablename',
'quoted',
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
],
'shared-raw' => [
'sharedb.tablename',
'tablename',
'raw',
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
],
'shared-prefix' => [
'sharedb.sh_tablename',
'tablename',
'quoted',
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
],
'shared-prefix-raw' => [
'sharedb.sh_tablename',
'tablename',
'raw',
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
],
'foreign' => [
'databasename.tablename',
'databasename.tablename',
'quoted',
],
'foreign-raw' => [
'databasename.tablename',
'databasename.tablename',
'raw',
],
];
}
/**
* @dataProvider provideTableName
*/
public function testTableName( $expected, $table, $format, array $alias = null ) {
if ( $alias ) {
$this->db->setTableAliases( [ $table => $alias ] );
}
$this->assertEquals(
$expected,
$this->db->tableName( $table, $format ?: 'quoted' )
);
}
public static function provideTableNamesWithIndexClauseOrJOIN() {
return [
'one-element array' => [
[ 'table' ], [], 'table '
],
'comma join' => [
[ 'table1', 'table2' ], [], 'table1,table2 '
],
'real join' => [
[ 'table1', 'table2' ],
[ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
],
'real join with multiple conditionals' => [
[ 'table1', 'table2' ],
[ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
],
'join with parenthesized group' => [
[ 'table1', 'n' => [ 'table2', 'table3' ] ],
[
'table3' => [ 'JOIN', 't2_id = t3_id' ],
'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
],
'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
],
'join with degenerate parenthesized group' => [
[ 'table1', 'n' => [ 't2' => 'table2' ] ],
[
'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
],
'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
],
];
}
/**
* @dataProvider provideTableNamesWithIndexClauseOrJOIN
*/
public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
$clause = TestingAccessWrapper::newFromObject( ( new SQLPlatformTestHelper( new AddQuoterMock() ) ) )
->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
$this->assertSame( $expect, $clause );
}
public function testTransactionIdle() {
$db = $this->db;
$db->clearFlag( DBO_TRX );
$called = false;
$flagSet = null;
$callback = static function ( $trigger, IDatabase $db ) use ( &$flagSet, &$called ) {
$called = true;
$flagSet = $db->getFlag( DBO_TRX );
};
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
$this->assertTrue( $called, 'Callback reached' );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
$flagSet = null;
$called = false;
$db->startAtomic( __METHOD__ );
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
$this->assertFalse( $called, 'Callback not reached during TRX' );
$db->endAtomic( __METHOD__ );
$this->assertTrue( $called, 'Callback reached after COMMIT' );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$db->clearFlag( DBO_TRX );
$db->onTransactionCommitOrIdle(
static function ( $trigger, IDatabase $db ) {
$db->setFlag( DBO_TRX );
},
__METHOD__
);
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
}
public function testTransactionIdle_TRX() {
$db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
$db->method( 'isOpen' )->willReturn( true );
$db->method( 'ping' )->willReturn( true );
$db->method( 'getDBname' )->willReturn( '' );
$db->setFlag( DBO_TRX );
$lbFactory = LBFactorySingle::newFromConnection( $db );
// Ask for the connection so that LB sets internal state
// about this connection being the primary connection
$lb = $lbFactory->getMainLB();
$conn = $lb->getConnectionInternal( $lb->getWriterIndex() );
$this->assertSame( $db, $conn, 'Same DB instance' );
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
$called = false;
$flagSet = null;
$callback = static function () use ( $db, &$flagSet, &$called ) {
$called = true;
$flagSet = $db->getFlag( DBO_TRX );
};
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
$this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
$called = false;
$lbFactory->beginPrimaryChanges( __METHOD__ );
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
$lbFactory->commitPrimaryChanges( __METHOD__ );
$this->assertTrue( $called, 'Called when lb-transaction is committed' );
$called = false;
$lbFactory->beginPrimaryChanges( __METHOD__ );
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
$lbFactory->commitPrimaryChanges( __METHOD__ );
$this->assertFalse( $called, 'Not called in next round commit' );
$db->setFlag( DBO_TRX );
try {
$db->onTransactionCommitOrIdle( static function () {
throw new RuntimeException( 'test' );
} );
$this->fail( "Exception not thrown" );
} catch ( RuntimeException $e ) {
$this->assertTrue( $db->getFlag( DBO_TRX ) );
}
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
$lbFactory->flushPrimarySessions( __METHOD__ );
}
public function testTransactionPreCommitOrIdle() {
$db = $this->getMockDB( [ 'isOpen' ] );
$db->method( 'isOpen' )->willReturn( true );
$db->clearFlag( DBO_TRX );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
$called = false;
$db->onTransactionPreCommitOrIdle(
static function ( IDatabase $db ) use ( &$called ) {
$called = true;
},
__METHOD__
);
$this->assertTrue( $called, 'Called when idle' );
$db->begin( __METHOD__ );
$called = false;
$db->onTransactionPreCommitOrIdle(
static function ( IDatabase $db ) use ( &$called ) {
$called = true;
},
__METHOD__
);
$this->assertFalse( $called, 'Not called when transaction is active' );
$db->commit( __METHOD__ );
$this->assertTrue( $called, 'Called when transaction is committed' );
}
public function testTransactionPreCommitOrIdle_TRX() {
$db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
$db->method( 'isOpen' )->willReturn( true );
$db->method( 'ping' )->willReturn( true );
$db->method( 'getDBname' )->willReturn( 'unittest' );
$db->setFlag( DBO_TRX );
$lbFactory = LBFactorySingle::newFromConnection( $db );
// Ask for the connection so that LB sets internal state
// about this connection being the primary connection
$lb = $lbFactory->getMainLB();
$conn = $lb->getConnectionInternal( $lb->getWriterIndex() );
$this->assertSame( $db, $conn, 'Same DB instance' );
$this->assertFalse( $lb->hasPrimaryChanges() );
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
$called = false;
$callback = static function ( IDatabase $db ) use ( &$called ) {
$called = true;
};
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
$this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
$called = false;
$lbFactory->commitPrimaryChanges();
$this->assertFalse( $called );
$called = false;
$lbFactory->beginPrimaryChanges( __METHOD__ );
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
$lbFactory->commitPrimaryChanges( __METHOD__ );
$this->assertTrue( $called, 'Called when lb-transaction is committed' );
$called = false;
$lbFactory->beginPrimaryChanges( __METHOD__ );
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
$this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
$lbFactory->commitPrimaryChanges( __METHOD__ );
$this->assertFalse( $called, 'Not called in next round commit' );
$lbFactory->flushPrimarySessions( __METHOD__ );
}
public function testTransactionResolution() {
$db = $this->db;
$db->clearFlag( DBO_TRX );
$db->begin( __METHOD__ );
$called = false;
$db->onTransactionResolution( static function ( $trigger, IDatabase $db ) use ( &$called ) {
$called = true;
$db->setFlag( DBO_TRX );
} );
$db->commit( __METHOD__ );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$this->assertTrue( $called, 'Callback reached' );
$db->clearFlag( DBO_TRX );
$db->begin( __METHOD__ );
$called = false;
$db->onTransactionResolution( static function ( $trigger, IDatabase $db ) use ( &$called ) {
$called = true;
$db->setFlag( DBO_TRX );
} );
$db->rollback( __METHOD__ );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$this->assertTrue( $called, 'Callback reached' );
}
public function testTransactionListener() {
$db = $this->db;
$db->setTransactionListener( 'ping', static function () use ( &$called ) {
$called = true;
} );
$called = false;
$db->begin( __METHOD__ );
$db->commit( __METHOD__ );
$this->assertTrue( $called, 'Callback reached' );
$called = false;
$db->begin( __METHOD__ );
$db->commit( __METHOD__ );
$this->assertTrue( $called, 'Callback still reached' );
$called = false;
$db->begin( __METHOD__ );
$db->rollback( __METHOD__ );
$this->assertTrue( $called, 'Callback reached' );
$db->setTransactionListener( 'ping', null );
$called = false;
$db->begin( __METHOD__ );
$db->commit( __METHOD__ );
$this->assertFalse( $called, 'Callback not reached' );
}
/**
* Use this mock instead of DatabaseTestHelper for cases where
* DatabaseTestHelper is too inflexibile due to mocking too much
* or being too restrictive about fname matching (e.g. for tests
* that assert behaviour when the name is a mismatch, we need to
* catch the error here instead of there).
*
* @param string[] $methods
* @return Database
*/
private function getMockDB( $methods = [] ) {
static $abstractMethods = [
'lastInsertId',
'closeConnection',
'doSingleStatementQuery',
'fieldInfo',
'getSoftwareLink',
'getServerVersion',
'getType',
'indexInfo',
'insertId',
'lastError',
'lastErrno',
'open',
'strencode',
'tableExists',
'getServer'
];
$db = $this->getMockBuilder( Database::class )
->disableOriginalConstructor()
->onlyMethods( array_values( array_unique( array_merge(
$abstractMethods,
$methods
) ) ) )
->getMock();
$wdb = TestingAccessWrapper::newFromObject( $db );
$wdb->logger = new NullLogger();
$wdb->errorLogger = static function ( Throwable $e ) {
};
$wdb->deprecationLogger = static function ( $msg ) {
};
$wdb->currentDomain = DatabaseDomain::newUnspecified();
$wdb->platform = new SQLPlatform( new AddQuoterMock() );
$wdb->flagsHolder = new DatabaseFlags( 0 );
// Info used for logging/errors
$wdb->connectionParams = [
'host' => 'localhost',
'user' => 'testuser'
];
$wdb->replicationReporter = new ReplicationReporter( IDatabase::ROLE_STREAMING_MASTER, new NullLogger(), new HashBagOStuff() );
$db->method( 'getServer' )->willReturn( '*dummy*' );
$db->setTransactionManager( new TransactionManager() );
$qs = new QueryStatus( false, 0, '', 0 );
$qs->res = true;
$db->method( 'doSingleStatementQuery' )->willReturn( $qs );
return $db;
}
public function testFlushSnapshot() {
$db = $this->getMockDB( [ 'isOpen' ] );
$db->method( 'isOpen' )->willReturn( true );
$db->flushSnapshot( __METHOD__ ); // ok
$db->flushSnapshot( __METHOD__ ); // ok
$db->setFlag( DBO_TRX, IDatabase::REMEMBER_PRIOR );
$db->query( 'SELECT 1', __METHOD__ );
$this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
$db->flushSnapshot( __METHOD__ ); // ok
$db->restoreFlags( $db::RESTORE_PRIOR );
$this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
}
public function testGetScopedLock() {
$db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
$db->method( 'isOpen' )->willReturn( true );
$db->method( 'getDBname' )->willReturn( 'unittest' );
$this->assertSame( 0, $db->trxLevel() );
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
$this->assertTrue( $db->lock( 'x', __METHOD__ ) );
$this->assertFalse( $db->lockIsFree( 'x', __METHOD__ ) );
$this->assertTrue( $db->unlock( 'x', __METHOD__ ) );
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
$this->assertSame( 0, $db->trxLevel() );
$db->setFlag( DBO_TRX );
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
$this->assertTrue( $db->lock( 'x', __METHOD__ ) );
$this->assertFalse( $db->lockIsFree( 'x', __METHOD__ ) );
$this->assertTrue( $db->unlock( 'x', __METHOD__ ) );
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
$db->clearFlag( DBO_TRX );
// Pending writes with DBO_TRX
$this->assertSame( 0, $db->trxLevel() );
$this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
$db->setFlag( DBO_TRX );
$db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
try {
$lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
$this->fail( "Exception not reached" );
} catch ( DBUnexpectedError $e ) {
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
$this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ), 'Lock not acquired' );
}
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
// Pending writes without DBO_TRX
$db->clearFlag( DBO_TRX );
$this->assertSame( 0, $db->trxLevel() );
$this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ) );
$db->begin( __METHOD__ );
$db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
try {
$lock = $db->getScopedLockAndFlush( 'meow2', __METHOD__, 1 );
$this->fail( "Exception not reached" );
} catch ( DBUnexpectedError $e ) {
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
$this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ), 'Lock not acquired' );
}
$db->rollback( __METHOD__ );
// No pending writes, with DBO_TRX
$db->setFlag( DBO_TRX );
$this->assertSame( 0, $db->trxLevel() );
$this->assertTrue( $db->lockIsFree( 'wuff', __METHOD__ ) );
$db->query( "SELECT 1", __METHOD__ );
$this->assertSame( 1, $db->trxLevel() );
$lock = $db->getScopedLockAndFlush( 'wuff', __METHOD__, 1 );
$this->assertSame( 0, $db->trxLevel() );
$this->assertFalse( $db->lockIsFree( 'wuff', __METHOD__ ), 'Lock already acquired' );
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
// No pending writes, without DBO_TRX
$db->clearFlag( DBO_TRX );
$this->assertSame( 0, $db->trxLevel() );
$this->assertTrue( $db->lockIsFree( 'wuff2', __METHOD__ ) );
$db->begin( __METHOD__ );
try {
$lock = $db->getScopedLockAndFlush( 'wuff2', __METHOD__, 1 );
$this->fail( "Exception not reached" );
} catch ( DBUnexpectedError $e ) {
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
$this->assertFalse( $db->lockIsFree( 'wuff2', __METHOD__ ), 'Lock not acquired' );
}
$db->rollback( __METHOD__ );
}
public function testFlagSetting() {
$db = $this->db;
$origTrx = $db->getFlag( DBO_TRX );
$origNoBuffer = $db->getFlag( DBO_NOBUFFER );
$origTrx
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
$origNoBuffer
? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
$this->assertEquals( !$origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$db->restoreFlags( $db::RESTORE_INITIAL );
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$origTrx
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
$origNoBuffer
? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
$db->restoreFlags();
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
$db->restoreFlags();
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
}
public static function provideImmutableDBOFlags() {
return [
[ Database::DBO_IGNORE ],
[ Database::DBO_DEFAULT ],
[ Database::DBO_PERSISTENT ]
];
}
/**
* @dataProvider provideImmutableDBOFlags
* @param int $flag
*/
public function testDBOCannotSet( $flag ) {
$flagsHolder = new DatabaseFlags( 0 );
$this->expectException( DBLanguageError::class );
$flagsHolder->setFlag( $flag );
}
/**
* @dataProvider provideImmutableDBOFlags
* @param int $flag
*/
public function testDBOCannotClear( $flag ) {
$flagsHolder = new DatabaseFlags( 0 );
$this->expectException( DBLanguageError::class );
$flagsHolder->clearFlag( $flag );
}
public function testSchemaAndPrefixMutators() {
$ud = DatabaseDomain::newUnspecified();
$this->assertEquals( $ud->getId(), $this->db->getDomainID() );
$oldDomain = $this->db->getDomainID();
$oldSchema = $this->db->dbSchema();
$oldPrefix = $this->db->tablePrefix();
$this->assertIsString( $oldDomain, 'DB domain is string' );
$this->assertIsString( $oldSchema, 'DB schema is string' );
$this->assertIsString( $oldPrefix, 'Prefix is string' );
$this->assertSame( $oldSchema, $this->db->dbSchema(), "Schema unchanged" );
$this->assertSame( $oldPrefix, $this->db->tablePrefix(), "Prefix unchanged" );
$this->assertSame( $oldPrefix, $this->db->tablePrefix( 'xxx_' ), "Prior prefix upon set" );
$this->assertSame( 'xxx_', $this->db->tablePrefix(), "Prefix set" );
$this->assertSame( $oldSchema, $this->db->dbSchema(), "Schema unchanged" );
$this->db->tablePrefix( $oldPrefix );
$this->assertNotEquals( 'xxx_', $this->db->tablePrefix(), "Prior prefix upon set" );
$this->assertSame( $oldPrefix, $this->db->tablePrefix(), "Prefix restored" );
$this->assertSame( $oldSchema, $this->db->dbSchema(), "Schema unchanged" );
$this->assertSame( $oldDomain, $this->db->getDomainID(), "DB domain restored" );
$newDbDomain = new DatabaseDomain(
'y',
( $oldSchema !== '' ) ? $oldSchema : null,
$oldPrefix
);
$this->db->selectDomain( $newDbDomain );
$this->assertSame( 'y', $this->db->getDBname(), "DB name set" );
$this->assertSame( $oldSchema, $this->db->dbSchema(), "Schema unchanged" );
$this->assertSame( $oldPrefix, $this->db->tablePrefix(), "Prefix unchanged" );
$this->assertSame( $oldSchema, $this->db->dbSchema( 'xxx' ), "Prior schema upon set" );
$this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
$this->assertSame( 'y', $this->db->getDBname(), "DB name unchanged" );
$this->assertSame( $oldPrefix, $this->db->tablePrefix(), "Prefix unchanged" );
$this->assertSame( 'xxx', $this->db->dbSchema( $oldSchema ), "Prior schema upon set" );
$this->assertEquals( $oldSchema, $this->db->dbSchema(), 'Schema restored' );
}
public function testSchemaWithNoDB() {
$ud = DatabaseDomain::newUnspecified();
$this->assertEquals( $ud->getId(), $this->db->getDomainID() );
$this->assertSame( '', $this->db->dbSchema() );
$this->expectException( DBUnexpectedError::class );
$this->db->dbSchema( 'xxx' );
}
public function testSelectDomain() {
$oldDomain = $this->db->getDomainID();
$oldDatabase = $this->db->getDBname();
$oldSchema = $this->db->dbSchema();
$oldPrefix = $this->db->tablePrefix();
$this->db->selectDomain( 'testselectdb-xxx_' );
$this->assertSame( 'testselectdb', $this->db->getDBname() );
$this->assertSame( '', $this->db->dbSchema() );
$this->assertSame( 'xxx_', $this->db->tablePrefix() );
$this->db->selectDomain( $oldDomain );
$this->assertSame( $oldDatabase, $this->db->getDBname() );
$this->assertSame( $oldSchema, $this->db->dbSchema() );
$this->assertSame( $oldPrefix, $this->db->tablePrefix() );
$this->assertSame( $oldDomain, $this->db->getDomainID() );
$this->db->selectDomain( 'testselectdb-schema-xxx_' );
$this->assertSame( 'testselectdb', $this->db->getDBname() );
$this->assertSame( 'schema', $this->db->dbSchema() );
$this->assertSame( 'xxx_', $this->db->tablePrefix() );
$this->db->selectDomain( $oldDomain );
$this->assertSame( $oldDatabase, $this->db->getDBname() );
$this->assertSame( $oldSchema, $this->db->dbSchema() );
$this->assertSame( $oldPrefix, $this->db->tablePrefix() );
$this->assertSame( $oldDomain, $this->db->getDomainID() );
}
public function testGetSetLBInfo() {
$db = $this->getMockDB();
$this->assertEquals( [], $db->getLBInfo() );
$this->assertNull( $db->getLBInfo( 'pringles' ) );
$db->setLBInfo( 'soda', 'water' );
$this->assertEquals( [ 'soda' => 'water' ], $db->getLBInfo() );
$this->assertNull( $db->getLBInfo( 'pringles' ) );
$this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
$db->setLBInfo( 'basketball', 'Lebron' );
$this->assertEquals( [ 'soda' => 'water', 'basketball' => 'Lebron' ], $db->getLBInfo() );
$this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
$this->assertEquals( 'Lebron', $db->getLBInfo( 'basketball' ) );
$db->setLBInfo( 'soda', null );
$this->assertEquals( [ 'basketball' => 'Lebron' ], $db->getLBInfo() );
$db->setLBInfo( [ 'King' => 'James' ] );
$this->assertNull( $db->getLBInfo( 'basketball' ) );
$this->assertEquals( [ 'King' => 'James' ], $db->getLBInfo() );
}
public function testShouldRejectPersistentWriteQueryOnReplicaDatabaseConnection() {
$this->expectException( DBReadOnlyRoleError::class );
$this->expectExceptionMessage( 'Server is configured as a read-only replica database.' );
$dbr = new DatabaseTestHelper(
__CLASS__ . '::' . $this->getName(),
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$dbr->query( "INSERT INTO test_table (a_column) VALUES ('foo');", __METHOD__ );
}
public function testShouldAcceptTemporaryTableOperationsOnReplicaDatabaseConnection() {
$dbr = new DatabaseTestHelper(
__CLASS__ . '::' . $this->getName(),
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$resCreate = $dbr->query(
"CREATE TEMPORARY TABLE temp_test_table (temp_column int);",
__METHOD__
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$resModify = $dbr->query(
"INSERT INTO temp_test_table (temp_column) VALUES (42);",
__METHOD__
);
$this->assertInstanceOf( IResultWrapper::class, $resCreate );
$this->assertInstanceOf( IResultWrapper::class, $resModify );
}
public function testShouldRejectPseudoPermanentTemporaryTableOperationsOnReplicaDatabaseConnection() {
$this->expectException( DBReadOnlyRoleError::class );
$this->expectExceptionMessage( 'Server is configured as a read-only replica database.' );
$dbr = new DatabaseTestHelper(
__CLASS__ . '::' . $this->getName(),
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$dbr->query(
"CREATE TEMPORARY TABLE temp_test_table (temp_column int);",
__METHOD__,
Database::QUERY_PSEUDO_PERMANENT
);
}
public function testShouldAcceptWriteQueryOnPrimaryDatabaseConnection() {
$dbr = new DatabaseTestHelper(
__CLASS__ . '::' . $this->getName(),
[ 'topologyRole' => Database::ROLE_STREAMING_MASTER ]
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$res = $dbr->query( "INSERT INTO test_table (a_column) VALUES ('foo');", __METHOD__ );
$this->assertInstanceOf( IResultWrapper::class, $res );
}
public function testShouldRejectWriteQueryOnPrimaryDatabaseConnectionWhenReplicaQueryRoleFlagIsSet() {
$this->expectException( DBReadOnlyRoleError::class );
$this->expectExceptionMessage( 'Cannot write; target role is DB_REPLICA' );
$dbr = new DatabaseTestHelper(
__CLASS__ . '::' . $this->getName(),
[ 'topologyRole' => Database::ROLE_STREAMING_MASTER ]
);
// phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
$dbr->query(
"INSERT INTO test_table (a_column) VALUES ('foo');",
__METHOD__,
Database::QUERY_REPLICA_ROLE
);
}
public function testCriticalSectionErrorSelect() {
$this->expectException( DBTransactionStateError::class );
$db = TestingAccessWrapper::newFromObject( $this->db );
try {
$this->corruptDbState( $db );
} catch ( RuntimeException $e ) {
$this->assertEquals( "Unexpected error", $e->getMessage() );
}
$db->query( "SELECT 1", __METHOD__ );
}
public function testCriticalSectionErrorRollback() {
$db = TestingAccessWrapper::newFromObject( $this->db );
try {
$this->corruptDbState( $db );
} catch ( RuntimeException $e ) {
$this->assertEquals( "Unexpected error", $e->getMessage() );
}
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertTrue( true, "No exception on ROLLBACK" );
$db->flushSession( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertTrue( true, "No exception on session flush" );
$db->query( "SELECT 1", __METHOD__ );
$this->assertTrue( true, "No exception on next query" );
}
public function testCriticalSectionErrorWithTrxRollback() {
$hits = 0;
$db = TestingAccessWrapper::newFromObject( $this->db );
$db->begin( __METHOD__, IDatabase::TRANSACTION_INTERNAL );
$db->onTransactionResolution( static function () use ( &$hits ) {
++$hits;
} );
try {
$this->corruptDbState( $db );
} catch ( RuntimeException $e ) {
$this->assertEquals( "Unexpected error", $e->getMessage() );
}
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertTrue( true, "No exception on ROLLBACK" );
$db->flushSession( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertTrue( true, "No exception on session flush" );
$db->query( "SELECT 1", __METHOD__ );
$this->assertTrue( true, "No exception on next query" );
}
private function corruptDbState( $db ) {
$cs = $db->commenceCriticalSection( __METHOD__ );
$this->assertInstanceOf( CriticalSectionScope::class, $cs );
throw new RuntimeException( "Unexpected error" );
}
}
rdbms/database/DatabaseDomainTest.php 0000666 00000015271 15133512024 0013632 0 ustar 00 <?php
use Wikimedia\Rdbms\DatabaseDomain;
/**
* @covers Wikimedia\Rdbms\DatabaseDomain
*/
class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
public static function provideConstruct() {
return [
'All strings' =>
[ 'foo', 'bar', 'baz_', 'foo-bar-baz_' ],
'Nothing' =>
[ null, null, '', '' ],
'Invalid $database' =>
[ 0, 'bar', '', '', true ],
'Invalid $schema' =>
[ 'foo', 0, '', '', true ],
'Invalid $prefix' =>
[ 'foo', 'bar', 0, '', true ],
'Dash' =>
[ 'foo-bar', 'baz', 'baa_', 'foo?hbar-baz-baa_' ],
'Question mark' =>
[ 'foo?bar', 'baz', 'baa_', 'foo??bar-baz-baa_' ],
];
}
/**
* @dataProvider provideConstruct
*/
public function testConstruct( $db, $schema, $prefix, $id, $exception = false ) {
if ( $exception ) {
$this->expectException( InvalidArgumentException::class );
new DatabaseDomain( $db, $schema, $prefix );
return;
}
$domain = new DatabaseDomain( $db, $schema, $prefix );
$this->assertInstanceOf( DatabaseDomain::class, $domain );
$this->assertEquals( $db, $domain->getDatabase() );
$this->assertEquals( $schema, $domain->getSchema() );
$this->assertEquals( $prefix, $domain->getTablePrefix() );
$this->assertEquals( $id, $domain->getId() );
$this->assertEquals( $id, strval( $domain ), 'toString' );
}
public static function provideNewFromId() {
return [
'Basic' =>
[ 'foo', 'foo', null, '' ],
'db+prefix' =>
[ 'foo-bar_', 'foo', null, 'bar_' ],
'db+schema+prefix' =>
[ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
'?h -> -' =>
[ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
'?? -> ?' =>
[ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
'? is left alone' =>
[ 'foo?bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
'too many parts' =>
[ 'foo-bar-baz-baa_', '', '', '', true ],
'from instance' =>
[ DatabaseDomain::newUnspecified(), null, null, '' ],
];
}
/**
* @dataProvider provideNewFromId
*/
public function testNewFromId( $id, $db, $schema, $prefix, $exception = false ) {
if ( $exception ) {
$this->expectException( InvalidArgumentException::class );
DatabaseDomain::newFromId( $id );
return;
}
$domain = DatabaseDomain::newFromId( $id );
$this->assertInstanceOf( DatabaseDomain::class, $domain );
$this->assertEquals( $db, $domain->getDatabase() );
$this->assertEquals( $schema, $domain->getSchema() );
$this->assertEquals( $prefix, $domain->getTablePrefix() );
}
public static function provideEquals() {
return [
'Basic' =>
[ 'foo', 'foo', null, '' ],
'db+prefix' =>
[ 'foo-bar_', 'foo', null, 'bar_' ],
'db+schema+prefix' =>
[ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
'?h -> -' =>
[ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
'?? -> ?' =>
[ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
'Nothing' =>
[ '', null, null, '' ],
];
}
/**
* @dataProvider provideEquals
*/
public function testEquals( $id, $db, $schema, $prefix ) {
$fromId = DatabaseDomain::newFromId( $id );
$this->assertInstanceOf( DatabaseDomain::class, $fromId );
$constructed = new DatabaseDomain( $db, $schema, $prefix );
$this->assertTrue( $constructed->equals( $id ), 'constructed equals string' );
$this->assertTrue( $fromId->equals( $id ), 'fromId equals string' );
$this->assertTrue( $constructed->equals( $fromId ), 'compare constructed to newId' );
$this->assertTrue( $fromId->equals( $constructed ), 'compare newId to constructed' );
}
public function testNewUnspecified() {
$domain = DatabaseDomain::newUnspecified();
$this->assertInstanceOf( DatabaseDomain::class, $domain );
$this->assertTrue( $domain->equals( '' ) );
$this->assertSame( null, $domain->getDatabase() );
$this->assertSame( null, $domain->getSchema() );
$this->assertSame( '', $domain->getTablePrefix() );
}
public static function provideIsCompatible() {
return [
'Basic' =>
[ 'foo', 'foo', null, '', true ],
'db+prefix' =>
[ 'foo-bar_', 'foo', null, 'bar_', true ],
'db+schema+prefix' =>
[ 'foo-bar-baz_', 'foo', 'bar', 'baz_', true ],
'db+dontcare_schema+prefix' =>
[ 'foo-bar-baz_', 'foo', null, 'baz_', false ],
'?h -> -' =>
[ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_', true ],
'?? -> ?' =>
[ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_', true ],
'Nothing' =>
[ '', null, null, '', true ],
'dontcaredb+dontcaredbschema+prefix' =>
[ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ],
'db+dontcareschema+prefix' =>
[ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ],
'postgres-db-jobqueue' =>
[ 'postgres-mediawiki-', 'postgres', null, '', false ]
];
}
/**
* @dataProvider provideIsCompatible
*/
public function testIsCompatible( $id, $db, $schema, $prefix, $transitive ) {
$compareIdObj = DatabaseDomain::newFromId( $id );
$this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
$fromId = new DatabaseDomain( $db, $schema, $prefix );
$this->assertTrue( $fromId->isCompatible( $id ), 'constructed equals string' );
$this->assertTrue( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
$this->assertEquals( $transitive, $compareIdObj->isCompatible( $fromId ),
'test transitivity of nulls components' );
}
public static function provideIsCompatible2() {
return [
'db+schema+prefix' =>
[ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ],
'dontcaredb+dontcaredbschema+prefix' =>
[ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ],
'db+dontcareschema+prefix' =>
[ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ],
];
}
/**
* @dataProvider provideIsCompatible2
*/
public function testIsCompatible2( $id, $db, $schema, $prefix ) {
$compareIdObj = DatabaseDomain::newFromId( $id );
$this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
$fromId = new DatabaseDomain( $db, $schema, $prefix );
$this->assertFalse( $fromId->isCompatible( $id ), 'constructed equals string' );
$this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
}
public function testSchemaWithNoDB1() {
$this->expectException( InvalidArgumentException::class );
new DatabaseDomain( null, 'schema', '' );
}
public function testSchemaWithNoDB2() {
$this->expectException( InvalidArgumentException::class );
DatabaseDomain::newFromId( '-schema-prefix' );
}
public function testIsUnspecified() {
$domain = new DatabaseDomain( null, null, '' );
$this->assertTrue( $domain->isUnspecified() );
$domain = new DatabaseDomain( 'mywiki', null, '' );
$this->assertFalse( $domain->isUnspecified() );
$domain = new DatabaseDomain( 'mywiki', null, '' );
$this->assertFalse( $domain->isUnspecified() );
}
}
rdbms/database/DatabaseMysqlBaseTest.php 0000666 00000043250 15133512024 0014321 0 ustar 00 <?php
/**
* Holds tests for DatabaseMysqlBase class.
*
* 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
* @author Antoine Musso
* @copyright © 2013 Antoine Musso
* @copyright © 2013 Wikimedia Foundation and contributors
*/
use MediaWiki\Tests\Unit\Libs\Rdbms\AddQuoterMock;
use Wikimedia\Rdbms\DatabaseDomain;
use Wikimedia\Rdbms\DatabaseMySQL;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\MySQLPrimaryPos;
use Wikimedia\Rdbms\Platform\MySQLPlatform;
use Wikimedia\Rdbms\Replication\MysqlReplicationReporter;
use Wikimedia\TestingAccessWrapper;
/**
* @covers \Wikimedia\Rdbms\DatabaseMySQL
*/
class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
private function getMockForViews(): IMaintainableDatabase {
$db = $this->getMockBuilder( DatabaseMySQL::class )
->disableOriginalConstructor()
->onlyMethods( [ 'query', 'getDBname' ] )
->getMock();
$db->method( 'query' )
->with( $this->anything() )
->willReturn( new FakeResultWrapper( [
(object)[ 'Tables_in_' => 'view1' ],
(object)[ 'Tables_in_' => 'view2' ],
(object)[ 'Tables_in_' => 'myview' ]
] ) );
$db->method( 'getDBname' )->willReturn( '' );
return $db;
}
public function testListviews() {
$db = $this->getMockForViews();
$this->assertEquals( [ 'view1', 'view2', 'myview' ],
$db->listViews() );
// Prefix filtering
$this->assertEquals( [ 'view1', 'view2' ],
$db->listViews( 'view' ) );
$this->assertEquals( [ 'myview' ],
$db->listViews( 'my' ) );
$this->assertEquals( [],
$db->listViews( 'UNUSED_PREFIX' ) );
$this->assertEquals( [ 'view1', 'view2', 'myview' ],
$db->listViews( '' ) );
}
/**
* @covers \Wikimedia\Rdbms\MySQLPrimaryPos
*/
public function testBinLogName() {
$pos = new MySQLPrimaryPos( "db1052.2424/4643", 1 );
$this->assertEquals( "db1052.2424", $pos->getLogFile() );
$this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() );
}
/**
* @dataProvider provideComparePositions
* @covers \Wikimedia\Rdbms\MySQLPrimaryPos
*/
public function testHasReached(
MySQLPrimaryPos $lowerPos, MySQLPrimaryPos $higherPos, $match, $hetero
) {
if ( $match ) {
if ( $hetero ) {
// Each position is has one channel higher than the other
$this->assertFalse( $higherPos->hasReached( $lowerPos ) );
} else {
$this->assertTrue( $higherPos->hasReached( $lowerPos ) );
}
$this->assertTrue( $lowerPos->hasReached( $lowerPos ) );
$this->assertTrue( $higherPos->hasReached( $higherPos ) );
$this->assertFalse( $lowerPos->hasReached( $higherPos ) );
} else { // channels don't match
$this->assertFalse( $higherPos->hasReached( $lowerPos ) );
$this->assertFalse( $lowerPos->hasReached( $higherPos ) );
}
}
public static function provideComparePositions() {
$now = microtime( true );
return [
// Binlog style
[
new MySQLPrimaryPos( 'db1034-bin.000976/843431247', $now ),
new MySQLPrimaryPos( 'db1034-bin.000976/843431248', $now ),
true,
false
],
[
new MySQLPrimaryPos( 'db1034-bin.000976/999', $now ),
new MySQLPrimaryPos( 'db1034-bin.000976/1000', $now ),
true,
false
],
[
new MySQLPrimaryPos( 'db1034-bin.000976/999', $now ),
new MySQLPrimaryPos( 'db1035-bin.000976/1000', $now ),
false,
false
],
// MySQL GTID style
[
new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
true,
false
],
[
new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
true,
false
],
[
new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
new MySQLPrimaryPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
false,
false
],
// MariaDB GTID style
[
new MySQLPrimaryPos( '255-11-23', $now ),
new MySQLPrimaryPos( '255-11-24', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-99', $now ),
new MySQLPrimaryPos( '255-11-100', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-999', $now ),
new MySQLPrimaryPos( '254-11-1000', $now ),
false,
false
],
[
new MySQLPrimaryPos( '255-11-23,256-12-50', $now ),
new MySQLPrimaryPos( '255-11-24', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-99,256-12-50,257-12-50', $now ),
new MySQLPrimaryPos( '255-11-1000', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-23,256-12-50', $now ),
new MySQLPrimaryPos( '255-11-24,155-52-63', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-99,256-12-50,257-12-50', $now ),
new MySQLPrimaryPos( '255-11-1000,256-12-51', $now ),
true,
false
],
[
new MySQLPrimaryPos( '255-11-99,256-12-50', $now ),
new MySQLPrimaryPos( '255-13-1000,256-14-49', $now ),
true,
true
],
[
new MySQLPrimaryPos( '253-11-999,255-11-999', $now ),
new MySQLPrimaryPos( '254-11-1000', $now ),
false,
false
],
];
}
/**
* @dataProvider provideCommonDomainGTIDs
* @covers \Wikimedia\Rdbms\MySQLPrimaryPos
*/
public function testGetRelevantActiveGTIDs( MySQLPrimaryPos $pos, MySQLPrimaryPos $ref, $gtids ) {
$this->assertEquals( $gtids, MySQLPrimaryPos::getRelevantActiveGTIDs( $pos, $ref ) );
}
public static function provideCommonDomainGTIDs() {
return [
[
new MySQLPrimaryPos( '255-13-99,256-12-50,257-14-50', 1 ),
new MySQLPrimaryPos( '255-11-1000', 1 ),
[ '255-13-99' ]
],
[
( new MySQLPrimaryPos( '255-13-99,256-12-50,257-14-50', 1 ) )
->setActiveDomain( 257 ),
new MySQLPrimaryPos( '255-11-1000,257-14-30', 1 ),
[ '257-14-50' ]
],
[
new MySQLPrimaryPos(
'2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
'3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
'7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
1
),
new MySQLPrimaryPos(
'1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
'3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
1
),
[ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ]
]
];
}
/**
* @dataProvider provideLagAmounts
*/
public function testPtHeartbeat( $lag ) {
/** @var IDatabase $db */
$db = $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()
->getMock();
$db->setLBInfo( 'replica', true );
$replicationReporter = $this->getMockBuilder( MysqlReplicationReporter::class )
->disableOriginalConstructor()
->onlyMethods( [ 'fetchSecondsSinceHeartbeat' ] )
->getMock();
TestingAccessWrapper::newFromObject( $replicationReporter )->lagDetectionMethod = 'pt-heartbeat';
$replicationReporter->method( 'fetchSecondsSinceHeartbeat' )->willReturn( $lag );
$lagEst = $replicationReporter->getLag( $db );
$this->assertGreaterThan( $lag - 0.010, $lagEst, "Correct heatbeat lag" );
$this->assertLessThan( $lag + 0.010, $lagEst, "Correct heatbeat lag" );
}
public static function provideLagAmounts() {
return [
[ 0 ],
[ 0.3 ],
[ 6.5 ],
[ 10.1 ],
[ 200.2 ],
[ 400.7 ],
[ 600.22 ],
[ 1000.77 ],
];
}
/**
* @dataProvider provideGtidData
* @covers \Wikimedia\Rdbms\MySQLPrimaryPos
* @covers \Wikimedia\Rdbms\DatabaseMySQL
*/
public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) {
$db = $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()
->getMock();
$replicationReporter = $this->getMockBuilder( MysqlReplicationReporter::class )
->disableOriginalConstructor()
->onlyMethods( [
'useGTIDs',
'getServerGTIDs',
'getServerRoleStatus',
'getServerId',
'getServerUUID'
] )
->getMock();
$replicationReporter->method( 'useGTIDs' )->willReturn( true );
$replicationReporter->method( 'getServerGTIDs' )->willReturn( $gtable );
$replicationReporter->method( 'getServerRoleStatus' )->willReturnCallback(
static function ( $db, $role ) use ( $rBLtable, $mBLtable ) {
if ( $role === 'SLAVE' ) {
return $rBLtable;
} elseif ( $role === 'MASTER' ) {
return $mBLtable;
}
return null;
}
);
$replicationReporter->method( 'getServerId' )->willReturn( 1 );
$replicationReporter->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' );
/** @var DatabaseMySQL $replicationReporter */
if ( is_array( $rGTIDs ) ) {
$this->assertEquals( $rGTIDs, $replicationReporter->getReplicaPos( $db )->getGTIDs() );
} else {
$this->assertFalse( $replicationReporter->getReplicaPos( $db ) );
}
if ( is_array( $mGTIDs ) ) {
$this->assertEquals( $mGTIDs, $replicationReporter->getPrimaryPos( $db )->getGTIDs() );
} else {
$this->assertFalse( $replicationReporter->getPrimaryPos( $db ) );
}
}
public static function provideGtidData() {
return [
// MariaDB
[
[
'gtid_domain_id' => 100,
'gtid_current_pos' => '100-13-77',
'gtid_binlog_pos' => '100-13-77',
'gtid_slave_pos' => null // master
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[
'File' => 'host.1600',
'Position' => '77'
],
[],
[ '100' => '100-13-77' ]
],
[
[
'gtid_domain_id' => 100,
'gtid_current_pos' => '100-13-77',
'gtid_binlog_pos' => '100-13-77',
'gtid_slave_pos' => '100-13-77' // replica
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[],
[ '100' => '100-13-77' ],
[ '100' => '100-13-77' ]
],
[
[
'gtid_current_pos' => '100-13-77',
'gtid_binlog_pos' => '100-13-77',
'gtid_slave_pos' => '100-13-77' // replica
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[],
[ '100' => '100-13-77' ],
[ '100' => '100-13-77' ]
],
// MySQL
[
[
'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77'
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[], // only a replica
[ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
=> '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
// replica/master use same var
[ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
=> '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
],
[
[
'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' .
'2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77'
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[], // only a replica
[ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
=> '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
// replica/master use same var
[ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
=> '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
],
[
[
'gtid_executed' => null, // not enabled?
'gtid_binlog_pos' => null
],
[
'Relay_Master_Log_File' => 'host.1600',
'Exec_Master_Log_Pos' => '77'
],
[], // only a replica
[], // binlog fallback
false
],
[
[
'gtid_executed' => null, // not enabled?
'gtid_binlog_pos' => null
],
[], // no replication
[], // no replication
false,
false
]
];
}
/**
* @covers \Wikimedia\Rdbms\MySQLPrimaryPos
*/
public function testSerialize() {
$pos = new MySQLPrimaryPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', 53636363 );
$roundtripPos = unserialize( serialize( $pos ) );
$this->assertEquals( $pos, $roundtripPos );
$pos = new MySQLPrimaryPos( '255-11-23', 53636363 );
$roundtripPos = unserialize( serialize( $pos ) );
$this->assertEquals( $pos, $roundtripPos );
}
public function testBuildIntegerCast() {
$db = $this->createPartialMock( DatabaseMySQL::class, [] );
TestingAccessWrapper::newFromObject( $db )->platform = new MySQLPlatform( new AddQuoterMock() );
/** @var IDatabase $db */
$output = $db->buildIntegerCast( 'fieldName' );
$this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
}
/**
* @covers \Wikimedia\Rdbms\Platform\MySQLPlatform
*/
public function testNormalizeJoinType() {
$platform = new MySQLPlatform( new AddQuoterMock() );
$sql = $platform->selectSQLText(
[ 'a', 'b' ],
'aa',
[],
'',
[],
[ 'b' => [ 'STRAIGHT_JOIN', 'bb=aa' ] ]
);
$this->assertSame(
'SELECT aa FROM `a` STRAIGHT_JOIN `b` ON ((bb=aa)) ',
$sql
);
}
/**
* @covers \Wikimedia\Rdbms\Platform\MySQLPlatform
*/
public function testNormalizeJoinTypeSqb() {
$db = $this->createPartialMock( DatabaseMySQL::class, [] );
TestingAccessWrapper::newFromObject( $db )->currentDomain =
new DatabaseDomain( null, null, '' );
TestingAccessWrapper::newFromObject( $db )->platform =
new MySQLPlatform( new AddQuoterMock() );
/** @var IDatabase $db */
$sql = $db->newSelectQueryBuilder()
->select( 'aa' )
->from( 'a' )
->straightJoin( 'b', null, [ 'bb=aa' ] )
->getSQL();
$this->assertSame(
'SELECT aa FROM `a` STRAIGHT_JOIN `b` ON ((bb=aa)) ',
$sql
);
}
/**
* @covers \Wikimedia\Rdbms\Database
* @covers \Wikimedia\Rdbms\DatabaseMySQL
* @covers \Wikimedia\Rdbms\Platform\MySQLPlatform
*/
public function testIndexAliases() {
$db = $this->getMockBuilder( DatabaseMySQL::class )
->disableOriginalConstructor()
->onlyMethods( [ 'strencode', 'dbSchema', 'tablePrefix' ] )
->getMock();
$db->method( 'strencode' )->willReturnCallback(
static function ( $s ) {
return str_replace( "'", "\\'", $s );
}
);
$wdb = TestingAccessWrapper::newFromObject( $db );
$wdb->platform = new MySQLPlatform( new AddQuoterMock() );
/** @var IDatabase $db */
$db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] );
$sql = $db->newSelectQueryBuilder()
->select( 'field' )
->from( 'zend' )
->where( [ 'a' => 'x' ] )
->useIndex( 'a_b_idx' )
->caller( __METHOD__ )->getSQL();
$this->assertSameSql(
"SELECT field FROM `zend` FORCE INDEX (a_c_idx) WHERE a = 'x' ",
$sql
);
$db->setIndexAliases( [] );
$sql = $db->newSelectQueryBuilder()
->select( 'field' )
->from( 'zend' )
->where( [ 'a' => 'x' ] )
->useIndex( 'a_b_idx' )
->caller( __METHOD__ )->getSQL();
$this->assertSameSql(
"SELECT field FROM `zend` FORCE INDEX (a_b_idx) WHERE a = 'x'",
$sql
);
}
/**
* @covers \Wikimedia\Rdbms\Database
* @covers \Wikimedia\Rdbms\Platform\SQLPlatform
* @covers \Wikimedia\Rdbms\Platform\MySQLPlatform
*/
public function testTableAliases() {
$db = $this->getMockBuilder( DatabaseMySQL::class )
->disableOriginalConstructor()
->onlyMethods( [ 'strencode', 'dbSchema', 'tablePrefix' ] )
->getMock();
$db->method( 'strencode' )->willReturnCallback(
static function ( $s ) {
return str_replace( "'", "\\'", $s );
}
);
$wdb = TestingAccessWrapper::newFromObject( $db );
$wdb->platform = new MySQLPlatform( new AddQuoterMock() );
/** @var IDatabase $db */
$db->setTableAliases( [
'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ]
] );
$sql = $db->newSelectQueryBuilder()
->select( 'field' )
->from( 'meow' )
->where( [ 'a' => 'x' ] )
->caller( __METHOD__ )->getSQL();
$this->assertSameSql(
"SELECT field FROM `feline`.`cat_meow` WHERE a = 'x' ",
$sql
);
$db->setTableAliases( [] );
$sql = $db->newSelectQueryBuilder()
->select( 'field' )
->from( 'meow' )
->where( [ 'a' => 'x' ] )
->caller( __METHOD__ )->getSQL();
$this->assertSameSql(
"SELECT field FROM `meow` WHERE a = 'x' ",
$sql
);
}
/**
* @covers \Wikimedia\Rdbms\DatabaseMySQL
* @covers \Wikimedia\Rdbms\Platform\SQLPlatform
* @covers \Wikimedia\Rdbms\Platform\MySQLPlatform
*/
public function testMaxExecutionTime() {
$db = $this->getMockBuilder( DatabaseMySQL::class )
->disableOriginalConstructor()
->onlyMethods( [ 'getServerVersion', 'dbSchema', 'tablePrefix' ] )
->getMock();
$db->method( 'getServerVersion' )->willReturn( '10.4.28-MariaDB-log' );
$wdb = TestingAccessWrapper::newFromObject( $db );
$wdb->platform = new MySQLPlatform( new AddQuoterMock() );
/** @var IDatabase $db */
$sql = $wdb->selectSQLText( 'image',
'img_metadata',
'*',
'',
[ 'MAX_EXECUTION_TIME' => 1 ]
);
$this->assertSameSql(
"SET STATEMENT max_statement_time=0.001 FOR SELECT img_metadata FROM `image` ",
$sql
);
}
/**
* @covers \Wikimedia\Rdbms\Database
* @covers \Wikimedia\Rdbms\DatabaseMySQL
*/
public function testStreamStatementEnd() {
/** @var DatabaseMySQL $db */
$db = $this->getMockForAbstractClass( DatabaseMySQL::class, [], '', false );
$sql = '';
$newLine = "delimiter\n!! ?";
$this->assertFalse( $db->streamStatementEnd( $sql, $newLine ) );
$this->assertSame( '', $newLine );
$newLine = 'JUST A TEST!!!';
$this->assertTrue( $db->streamStatementEnd( $sql, $newLine ) );
$this->assertSame( 'JUST A TEST!', $newLine );
}
private function assertSameSql( $expected, $actual, $message = '' ) {
$this->assertSame( trim( $expected ), trim( $actual ), $message );
}
}
rdbms/querybuilder/InsertQueryBuilderTest.php 0000666 00000007550 15133512024 0015550 0 ustar 00 <?php
/**
* @covers \Wikimedia\Rdbms\InsertQueryBuilder
*/
class InsertQueryBuilderTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
/** @var DatabaseTestHelper */
private $db;
/** @var InsertQueryBuilderTest */
private $iqb;
protected function setUp(): void {
$this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
$this->iqb = $this->db->newInsertQueryBuilder();
}
private function assertSQL( $expected, $fname ) {
$this->iqb->caller( $fname )->execute();
$actual = $this->db->getLastSqls();
$actual = preg_replace( '/ +/', ' ', $actual );
$actual = rtrim( $actual, " " );
$this->assertEquals( $expected, $actual );
}
public function testSimpleInsert() {
$this->iqb
->insertInto( 'a' )
->row( [ 'f' => 'g', 'd' => 'l' ] );
$this->assertSQL( "INSERT INTO a (f,d) VALUES ('g','l')", __METHOD__ );
}
public function testIgnore() {
$this->iqb
->insertInto( 'a' )
->ignore()
->row( [ 'f' => 'g', 'd' => 'l' ] );
$this->assertSQL( "INSERT IGNORE INTO a (f,d) VALUES ('g','l')", __METHOD__ );
}
public function testUpsert() {
$this->iqb
->insertInto( 'a' )
->row( [ 'f' => 'g', 'd' => 'l' ] )
->onDuplicateKeyUpdate()
->uniqueIndexFields( [ 'd' ] )
->set( [ 'f' => 'm' ] );
$this->assertSQL(
"BEGIN; UPDATE a SET f = 'm' WHERE (d = 'l'); INSERT INTO a (f,d) VALUES ('g','l'); COMMIT",
__METHOD__
);
}
public function testUpsertWithStringKey() {
$this->iqb
->insertInto( 'a' )
->row( [ 'f' => 'g', 'd' => 'l' ] )
->onDuplicateKeyUpdate()
->uniqueIndexFields( 'd' )
->set( [ 'f' => 'm' ] );
$this->assertSQL(
"BEGIN; UPDATE a SET f = 'm' WHERE (d = 'l'); INSERT INTO a (f,d) VALUES ('g','l'); COMMIT",
__METHOD__
);
}
public function testOption() {
$this->iqb
->insertInto( 't' )
->row( [ 'f' => 'g' ] )
->option( 'IGNORE' );
$this->assertSQL( "INSERT IGNORE INTO t (f) VALUES ('g')", __METHOD__ );
}
public function testOptions() {
$this->iqb
->insertInto( 't' )
->row( [ 'f' => 'g' ] )
->options( [ 'IGNORE' ] );
$this->assertSQL( "INSERT IGNORE INTO t (f) VALUES ('g')", __METHOD__ );
}
public function testExecute() {
$this->iqb->insertInto( 't' )->rows( [ 'a' => 'b' ] )->caller( __METHOD__ );
$this->iqb->execute();
$this->assertEquals( "INSERT INTO t (a) VALUES ('b')", $this->db->getLastSqls() );
}
public function testGetQueryInfo() {
$this->iqb
->insertInto( 't' )
->ignore()
->row( [ 'a' => 'b', 'd' => 'l' ] )
->caller( 'foo' );
$this->assertEquals(
[
'table' => 't' ,
'rows' => [ [ 'a' => 'b', 'd' => 'l' ] ],
'options' => [ 'IGNORE' ],
'upsert' => false,
'set' => [],
'uniqueIndexFields' => [],
'caller' => 'foo',
],
$this->iqb->getQueryInfo() );
}
public function testGetQueryInfoUpsert() {
$this->iqb
->insertInto( 't' )
->row( [ 'f' => 'g', 'd' => 'l' ] )
->onDuplicateKeyUpdate()
->uniqueIndexFields( [ 'd' ] )
->set( [ 'f' => 'm' ] )
->caller( 'foo' );
$this->assertEquals(
[
'table' => 't' ,
'rows' => [ [ 'f' => 'g', 'd' => 'l' ] ],
'upsert' => true,
'set' => [ 'f' => 'm' ],
'uniqueIndexFields' => [ 'd' ],
'options' => [],
'caller' => 'foo'
],
$this->iqb->getQueryInfo() );
}
public function testQueryInfo() {
$this->iqb->queryInfo(
[
'table' => 't',
'rows' => [ [ 'f' => 'g', 'd' => 'l' ] ],
'options' => [ 'IGNORE' ],
]
);
$this->assertSQL( "INSERT IGNORE INTO t (f,d) VALUES ('g','l')", __METHOD__ );
}
public function testQueryInfoUpsert() {
$this->iqb->queryInfo(
[
'table' => 't',
'rows' => [ [ 'f' => 'g', 'd' => 'l' ] ],
'upsert' => true,
'set' => [ 'f' => 'm' ],
'uniqueIndexFields' => [ 'd' ],
]
);
$this->assertSQL(
"BEGIN; UPDATE t SET f = 'm' WHERE (d = 'l'); INSERT INTO t (f,d) VALUES ('g','l'); COMMIT",
__METHOD__
);
}
}
rdbms/querybuilder/DeleteQueryBuilderTest.php 0000666 00000003776 15133512024 0015514 0 ustar 00 <?php
use Wikimedia\Rdbms\DeleteQueryBuilder;
use Wikimedia\Rdbms\Platform\ISQLPlatform;
/**
* @covers \Wikimedia\Rdbms\DeleteQueryBuilder
*/
class DeleteQueryBuilderTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
/** @var DatabaseTestHelper */
private $db;
/** @var DeleteQueryBuilder */
private $dqb;
protected function setUp(): void {
$this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
$this->dqb = $this->db->newDeleteQueryBuilder();
}
private function assertSQL( $expected, $fname ) {
$this->dqb->caller( $fname )->execute();
$actual = $this->db->getLastSqls();
$actual = preg_replace( '/ +/', ' ', $actual );
$actual = rtrim( $actual, " " );
$this->assertEquals( $expected, $actual );
}
public function testCondsEtc() {
$this->dqb
->table( 'a' )
->where( '1' )
->andWhere( '2' )
->conds( '3' );
$this->assertSQL( 'DELETE FROM a WHERE (1) AND (2) AND (3)', __METHOD__ );
}
public function testConflictingConds() {
$this->dqb
->deleteFrom( '1' )
->where( [ 'k' => 'v1' ] )
->andWhere( [ 'k' => 'v2' ] );
$this->assertSQL( 'DELETE FROM 1 WHERE k = \'v1\' AND (k = \'v2\')', __METHOD__ );
}
public function testCondsAllRows() {
$this->dqb
->table( 'a' )
->where( ISQLPlatform::ALL_ROWS );
$this->assertSQL( 'DELETE FROM a', __METHOD__ );
}
public function testExecute() {
$this->dqb->deleteFrom( 't' )->where( 'c' )->caller( __METHOD__ );
$this->dqb->execute();
$this->assertEquals( 'DELETE FROM t WHERE (c)', $this->db->getLastSqls() );
}
public function testGetQueryInfo() {
$this->dqb
->deleteFrom( 't' )
->where( [ 'a' => 'b' ] )
->caller( 'foo' );
$this->assertEquals(
[
'table' => 't' ,
'conds' => [ 'a' => 'b' ],
'caller' => 'foo',
],
$this->dqb->getQueryInfo() );
}
public function testQueryInfo() {
$this->dqb->queryInfo(
[
'table' => 't',
'conds' => [ 'a' => 'b' ],
]
);
$this->assertSQL( "DELETE FROM t WHERE a = 'b'", __METHOD__ );
}
}
Back to Directory
File Manager