Viewing File: /home/omtekel/www/wp-content/upgrade/backup/ParamValidator.tar

TypeDef/UserDef.php000066600000025704151334737450010202 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWiki\Title\MalformedTitleException;
use MediaWiki\Title\TitleParser;
use MediaWiki\User\ExternalUserNames;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityValue;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserRigorOptions;
use Wikimedia\IPUtils;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;

/**
 * Type definition for user types
 *
 * Failure codes:
 *  - 'baduser': The value was not a valid MediaWiki user. No data.
 *
 * @since 1.35
 */
class UserDef extends TypeDef {

	/**
	 * (string[]) Allowed types of user.
	 *
	 * One or more of the following values:
	 * - 'name': User names are allowed.
	 * - 'ip': IP ("anon") usernames are allowed.
	 * - 'cidr': IP ranges are allowed.
	 * - 'interwiki': Interwiki usernames are allowed.
	 * - 'id': Allow specifying user IDs, formatted like "#123".
	 *
	 * Default is `[ 'name', 'ip', 'cidr', 'interwiki' ]`.
	 *
	 * Avoid combining 'id' with PARAM_ISMULTI, as it may result in excessive
	 * DB lookups. If you do combine them, consider setting low values for
	 * PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
	 */
	public const PARAM_ALLOWED_USER_TYPES = 'param-allowed-user-types';

	/**
	 * (bool) Whether to return a UserIdentity object.
	 *
	 * If false, the validated user name is returned as a string. Default is false.
	 *
	 * Avoid setting true with PARAM_ISMULTI, as it may result in excessive DB
	 * lookups. If you do combine them, consider setting low values for
	 * PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
	 */
	public const PARAM_RETURN_OBJECT = 'param-return-object';

	/** @var UserIdentityLookup */
	private $userIdentityLookup;

	/** @var TitleParser */
	private $titleParser;

	/** @var UserNameUtils */
	private $userNameUtils;

	/**
	 * @param Callbacks $callbacks
	 * @param UserIdentityLookup $userIdentityLookup
	 * @param TitleParser $titleParser
	 * @param UserNameUtils $userNameUtils
	 */
	public function __construct(
		Callbacks $callbacks,
		UserIdentityLookup $userIdentityLookup,
		TitleParser $titleParser,
		UserNameUtils $userNameUtils
	) {
		parent::__construct( $callbacks );
		$this->userIdentityLookup = $userIdentityLookup;
		$this->titleParser = $titleParser;
		$this->userNameUtils = $userNameUtils;
	}

	public function validate( $name, $value, array $settings, array $options ) {
		[ $type, $user ] = $this->processUser( $value );

		if ( !$user || !in_array( $type, $settings[self::PARAM_ALLOWED_USER_TYPES], true ) ) {
			// Message used: paramvalidator-baduser
			$this->failure( 'baduser', $name, $value, $settings, $options );
		}

		return empty( $settings[self::PARAM_RETURN_OBJECT] ) ? $user->getName() : $user;
	}

	public function normalizeSettings( array $settings ) {
		if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
			$settings[self::PARAM_ALLOWED_USER_TYPES] = array_values( array_intersect(
				[ 'name', 'ip', 'cidr', 'interwiki', 'id' ],
				$settings[self::PARAM_ALLOWED_USER_TYPES]
			) );
		}
		if ( empty( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
			$settings[self::PARAM_ALLOWED_USER_TYPES] = [ 'name', 'ip', 'cidr', 'interwiki' ];
		}

		return parent::normalizeSettings( $settings );
	}

	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_ALLOWED_USER_TYPES, self::PARAM_RETURN_OBJECT,
		] );

		if ( !is_bool( $settings[self::PARAM_RETURN_OBJECT] ?? false ) ) {
			$ret['issues'][self::PARAM_RETURN_OBJECT] = 'PARAM_RETURN_OBJECT must be boolean, got '
				. gettype( $settings[self::PARAM_RETURN_OBJECT] );
		}

		$hasId = false;
		if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
			if ( !is_array( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
				$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES must be an array, '
					. 'got ' . gettype( $settings[self::PARAM_ALLOWED_USER_TYPES] );
			} elseif ( $settings[self::PARAM_ALLOWED_USER_TYPES] === [] ) {
				$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES cannot be empty';
			} else {
				$bad = array_diff(
					$settings[self::PARAM_ALLOWED_USER_TYPES],
					[ 'name', 'ip', 'cidr', 'interwiki', 'id' ]
				);
				if ( $bad ) {
					$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] =
						'PARAM_ALLOWED_USER_TYPES contains invalid values: ' . implode( ', ', $bad );
				}

				$hasId = in_array( 'id', $settings[self::PARAM_ALLOWED_USER_TYPES], true );
			}
		}

		if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
			( $hasId || !empty( $settings[self::PARAM_RETURN_OBJECT] ) ) &&
			(
				( $settings[ParamValidator::PARAM_ISMULTI_LIMIT1] ?? 100 ) > 10 ||
				( $settings[ParamValidator::PARAM_ISMULTI_LIMIT2] ?? 100 ) > 10
			)
		) {
			$ret['issues'][] = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
				. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
				. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
				. 'once we have a real use case to look at.)';
		}

		return $ret;
	}

	/**
	 * Process $value to a UserIdentity, if possible
	 * @param string $value
	 * @return array [ string $type, UserIdentity|null $user ]
	 * @phan-return array{0:string,1:UserIdentity|null}
	 */
	private function processUser( string $value ): array {
		// A user ID?
		if ( preg_match( '/^#(\d+)$/D', $value, $m ) ) {
			// This used to use the IP address of the current request if the
			// id was 0, to match the behavior of User objects, but was switched
			// to "Unknown user" because the former behavior is likely unexpected.
			// If the id corresponds to a user in the database, use that user, otherwise
			// return a UserIdentityValue with id 0 (regardless of the input id) and
			// the name "Unknown user"
			$userId = (int)$m[1];
			if ( $userId !== 0 ) {
				// Check the database.
				$userIdentity = $this->userIdentityLookup->getUserIdentityByUserId( $userId );
				if ( $userIdentity ) {
					return [ 'id', $userIdentity ];
				}
			}
			// Fall back to "Unknown user"
			return [
				'id',
				new UserIdentityValue( 0, "Unknown user" )
			];
		}

		// An interwiki username?
		if ( ExternalUserNames::isExternal( $value ) ) {
			$name = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_NONE );
			// UserIdentityValue has the username which includes the > separating the external
			// wiki database and the actual name, but is created for the *local* wiki, like
			// for User objects (local is the default, but we specify it anyway to show
			// that its intentional even though the username is for a different wiki)
			// NOTE: We deliberately use the raw $value instead of the canonical $name
			// to avoid converting the first character of the interwiki prefix to uppercase
			$user = $name !== false ? new UserIdentityValue( 0, $value, UserIdentityValue::LOCAL ) : null;
			return [ 'interwiki', $user ];
		}

		// A valid user name?
		// Match behavior of UserFactory::newFromName with RIGOR_VALID and User::getId()
		// we know that if there is a canonical form from UserNameUtils then this can't
		// look like an IP, and since we checked for external user names above it isn't
		// that either, so if this is a valid user name then we check the database for
		// the id, and if there is no user with this name the id is 0
		$canonicalName = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_VALID );
		if ( $canonicalName !== false ) {
			$userIdentity = $this->userIdentityLookup->getUserIdentityByName( $canonicalName );
			if ( $userIdentity ) {
				return [ 'name', $userIdentity ];
			}
			// Fall back to id 0
			return [
				'name',
				new UserIdentityValue( 0, $canonicalName )
			];
		}

		// (T232672) Reproduce the normalization applied in UserNameUtils::getCanonical() when
		// performing the checks below.
		if ( strpos( $value, '#' ) !== false ) {
			return [ '', null ];
		}

		try {
			$t = $this->titleParser->parseTitle( $value );
		} catch ( MalformedTitleException $_ ) {
			$t = null;
		}
		if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { // likely
			try {
				$t = $this->titleParser->parseTitle( "User:$value" );
			} catch ( MalformedTitleException $_ ) {
				$t = null;
			}
		}
		if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
			// If it wasn't a valid User-namespace title, fail.
			return [ '', null ];
		}
		$value = $t->getText();

		// An IP?
		$b = IPUtils::RE_IP_BYTE;
		if ( IPUtils::isValid( $value ) ||
			// See comment for UserNameUtils::isIP. We don't just call that function
			// here because it also returns true for things like
			// 300.300.300.300 that are neither valid usernames nor valid IP
			// addresses.
			preg_match( "/^$b\.$b\.$b\.xxx$/D", $value )
		) {
			$name = IPUtils::sanitizeIP( $value );
			// We don't really need to use UserNameUtils::getCanonical() because for anonymous
			// users the only validation is that there is no `#` (which is already the case if its
			// a valid IP or matches the regex) and the only normalization is making the first
			// character uppercase (doesn't matter for numbers) and replacing underscores with
			// spaces (doesn't apply to IPs). But, better safe than sorry?
			$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
			return [ 'ip', UserIdentityValue::newAnonymous( $name ) ];
		}

		// A range?
		if ( IPUtils::isValidRange( $value ) ) {
			$name = IPUtils::sanitizeIP( $value );
			// Per above, the UserNameUtils call isn't strictly needed, but doesn't hurt
			$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
			return [ 'cidr', UserIdentityValue::newAnonymous( $name ) ];
		}

		// Fail.
		return [ '', null ];
	}

	public function getParamInfo( $name, array $settings, array $options ) {
		$info = parent::getParamInfo( $name, $settings, $options );

		$info['subtypes'] = $settings[self::PARAM_ALLOWED_USER_TYPES];

		return $info;
	}

	public function getHelpInfo( $name, array $settings, array $options ) {
		$info = parent::getParamInfo( $name, $settings, $options );

		$isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );

		$subtypes = [];
		foreach ( $settings[self::PARAM_ALLOWED_USER_TYPES] as $st ) {
			// Messages: paramvalidator-help-type-user-subtype-name,
			// paramvalidator-help-type-user-subtype-ip, paramvalidator-help-type-user-subtype-cidr,
			// paramvalidator-help-type-user-subtype-interwiki, paramvalidator-help-type-user-subtype-id
			$subtypes[] = MessageValue::new( "paramvalidator-help-type-user-subtype-$st" );
		}
		$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-user' )
			->params( $isMulti ? 2 : 1 )
			->textListParams( $subtypes )
			->numParams( count( $subtypes ) );

		return $info;
	}

}
TypeDef/NamespaceDef.php000066600000006346151334737450011161 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use ApiResult;
use MediaWiki\Title\NamespaceInfo;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\EnumDef;

/**
 * Type definition for namespace types
 *
 * A namespace type is an enum type that accepts MediaWiki namespace IDs.
 *
 * @since 1.35
 */
class NamespaceDef extends EnumDef {

	/**
	 * (int[]) Additional namespace IDs to recognize.
	 *
	 * Generally this will be used to include NS_SPECIAL and/or NS_MEDIA.
	 */
	public const PARAM_EXTRA_NAMESPACES = 'param-extra-namespaces';

	/** @var NamespaceInfo */
	private $nsInfo;

	public function __construct( Callbacks $callbacks, NamespaceInfo $nsInfo ) {
		parent::__construct( $callbacks );
		$this->nsInfo = $nsInfo;
	}

	public function validate( $name, $value, array $settings, array $options ) {
		if ( !is_int( $value ) && preg_match( '/^[+-]?\d+$/D', $value ) ) {
			// Convert to int since that's what getEnumValues() returns.
			$value = (int)$value;
		}

		return parent::validate( $name, $value, $settings, $options );
	}

	public function getEnumValues( $name, array $settings, array $options ) {
		$namespaces = $this->nsInfo->getValidNamespaces();
		$extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
		if ( is_array( $extra ) && $extra !== [] ) {
			$namespaces = array_merge( $namespaces, $extra );
		}
		sort( $namespaces );
		return $namespaces;
	}

	public function normalizeSettings( array $settings ) {
		// Force PARAM_ALL
		if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) ) {
			$settings[ParamValidator::PARAM_ALL] = true;
		}
		return parent::normalizeSettings( $settings );
	}

	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_EXTRA_NAMESPACES,
		] );

		if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
			( $settings[ParamValidator::PARAM_ALL] ?? true ) !== true &&
			!isset( $ret['issues'][ParamValidator::PARAM_ALL] )
		) {
			$ret['issues'][ParamValidator::PARAM_ALL] =
				'PARAM_ALL cannot be false or a string for namespace-type parameters';
		}

		$ns = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
		if ( !is_array( $ns ) ) {
			$type = gettype( $ns );
		} elseif ( $ns === [] ) {
			$type = 'integer[]';
		} else {
			$types = array_unique( array_map( 'gettype', $ns ) );
			$type = implode( '|', $types );
			$type = count( $types ) > 1 ? "($type)[]" : "{$type}[]";
		}
		if ( $type !== 'integer[]' ) {
			$ret['issues'][self::PARAM_EXTRA_NAMESPACES] =
				"PARAM_EXTRA_NAMESPACES must be an integer[], got $type";
		}

		return $ret;
	}

	public function getParamInfo( $name, array $settings, array $options ) {
		$info = parent::getParamInfo( $name, $settings, $options );

		$info['type'] = 'namespace';
		$extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
		if ( is_array( $extra ) && $extra !== [] ) {
			$info['extranamespaces'] = array_values( $extra );
			if ( isset( $options['module'] ) ) {
				// ApiResult metadata when used with the Action API.
				ApiResult::setIndexedTagName( $info['extranamespaces'], 'ns' );
			}
		}

		return $info;
	}

}
TypeDef/TagsDef.php000066600000004446151334737450010162 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use ChangeTags;
use MediaWiki\ChangeTags\ChangeTagsStore;
use MediaWiki\Message\Converter as MessageConverter;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\ValidationException;

/**
 * Type definition for tags type
 *
 * A tags type is an enum type for selecting MediaWiki change tags.
 *
 * Failure codes:
 *  - 'badtags': The value was not a valid set of tags. Data:
 *    - 'disallowedtags': The tags that were disallowed.
 *
 * @since 1.35
 */
class TagsDef extends EnumDef {

	private ChangeTagsStore $changeTagsStore;

	/** @var MessageConverter */
	private $messageConverter;

	public function __construct( Callbacks $callbacks, ChangeTagsStore $changeTagsStore ) {
		parent::__construct( $callbacks );
		$this->changeTagsStore = $changeTagsStore;
		$this->messageConverter = new MessageConverter();
	}

	public function validate( $name, $value, array $settings, array $options ) {
		// Validate the full list of tags at once, because the caller will
		// *probably* stop at the first exception thrown.
		if ( isset( $options['values-list'] ) ) {
			$ret = $value;
			$tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $options['values-list'] );
		} else {
			// The 'tags' type always returns an array.
			$ret = [ $value ];
			$tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $ret );
		}

		if ( !$tagsStatus->isGood() ) {
			$msg = $this->messageConverter->convertMessage( $tagsStatus->getMessage() );
			$data = [];
			if ( $tagsStatus->value ) {
				// Specific tags are not allowed.
				$data['disallowedtags'] = $tagsStatus->value;
			// @codeCoverageIgnoreStart
			} else {
				// All are disallowed, I guess
				$data['disallowedtags'] = $settings['values-list'] ?? $ret;
			}
			// @codeCoverageIgnoreEnd

			// Only throw if $value is among the disallowed tags
			if ( in_array( $value, $data['disallowedtags'], true ) ) {
				throw new ValidationException(
					DataMessageValue::new( $msg->getKey(), $msg->getParams(), 'badtags', $data ),
					$name, $value, $settings
				);
			}
		}

		return $ret;
	}

	public function getEnumValues( $name, array $settings, array $options ) {
		return $this->changeTagsStore->listExplicitlyDefinedTags();
	}

}
TypeDef/TitleDef.php000066600000011123151334737450010333 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWiki\Linker\LinkTarget;
use MediaWiki\Title\TitleFactory;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;

/**
 * Type definition for page titles.
 *
 * Failure codes:
 * - 'badtitle': invalid title (e.g. containing disallowed characters). No data.
 * - 'missingtitle': the page with this title does not exist (when PARAM_MUST_EXIST
 *   was specified). No data.
 *
 * @since 1.36
 */
class TitleDef extends TypeDef {

	/**
	 * (bool) Whether the page with the given title needs to exist.
	 *
	 * Defaults to false.
	 */
	public const PARAM_MUST_EXIST = 'param-must-exist';

	/**
	 * (bool) Whether to return a LinkTarget.
	 *
	 * If false, the validated title is returned as a string (in getPrefixedText() format).
	 * Default is false.
	 *
	 * Avoid setting true with PARAM_ISMULTI, as it may result in excessive DB
	 * lookups. If you do combine them, consider setting low values for
	 * PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
	 */
	public const PARAM_RETURN_OBJECT = 'param-return-object';

	/** @var TitleFactory */
	private $titleFactory;

	/**
	 * @param Callbacks $callbacks
	 * @param TitleFactory $titleFactory
	 */
	public function __construct( Callbacks $callbacks, TitleFactory $titleFactory ) {
		parent::__construct( $callbacks );
		$this->titleFactory = $titleFactory;
	}

	/**
	 * @inheritDoc
	 * @return string|LinkTarget Depending on the PARAM_RETURN_OBJECT setting.
	 */
	public function validate( $name, $value, array $settings, array $options ) {
		$mustExist = !empty( $settings[self::PARAM_MUST_EXIST] );
		$returnObject = !empty( $settings[self::PARAM_RETURN_OBJECT] );

		$title = $this->titleFactory->newFromText( $value );

		if ( !$title ) {
			// Message used: paramvalidator-badtitle
			$this->failure( 'badtitle', $name, $value, $settings, $options );
		} elseif ( $mustExist && !$title->exists() ) {
			// Message used: paramvalidator-missingtitle
			$this->failure( 'missingtitle', $name, $value, $settings, $options );
		}

		if ( $returnObject ) {
			return $title->getTitleValue();
		} else {
			return $title->getPrefixedText();
		}
	}

	/** @inheritDoc */
	public function stringifyValue( $name, $value, array $settings, array $options ) {
		if ( $value instanceof LinkTarget ) {
			return $this->titleFactory->newFromLinkTarget( $value )->getPrefixedText();
		}
		return parent::stringifyValue( $name, $value, $settings, $options );
	}

	/** @inheritDoc */
	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_MUST_EXIST, self::PARAM_RETURN_OBJECT,
		] );

		if ( !is_bool( $settings[self::PARAM_MUST_EXIST] ?? false ) ) {
			$ret['issues'][self::PARAM_MUST_EXIST] = 'PARAM_MUST_EXIST must be boolean, got '
				. gettype( $settings[self::PARAM_MUST_EXIST] );
		}

		if ( !is_bool( $settings[self::PARAM_RETURN_OBJECT] ?? false ) ) {
			$ret['issues'][self::PARAM_RETURN_OBJECT] = 'PARAM_RETURN_OBJECT must be boolean, got '
				. gettype( $settings[self::PARAM_RETURN_OBJECT] );
		}

		if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
			!empty( $settings[self::PARAM_RETURN_OBJECT] ) &&
			(
				( $settings[ParamValidator::PARAM_ISMULTI_LIMIT1] ?? 100 ) > 10 ||
				( $settings[ParamValidator::PARAM_ISMULTI_LIMIT2] ?? 100 ) > 10
			)
		) {
			$ret['issues'][] = 'Multi-valued title-type parameters with PARAM_RETURN_OBJECT '
				. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
				. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
				. 'once we have a real use case to look at.)';
		}

		return $ret;
	}

	/** @inheritDoc */
	public function getParamInfo( $name, array $settings, array $options ) {
		$info = parent::getParamInfo( $name, $settings, $options );

		$info['mustExist'] = !empty( $settings[self::PARAM_MUST_EXIST] );

		return $info;
	}

	/** @inheritDoc */
	public function getHelpInfo( $name, array $settings, array $options ) {
		$info = parent::getParamInfo( $name, $settings, $options );

		$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-title' );

		$mustExist = !empty( $settings[self::PARAM_MUST_EXIST] );
		$info[self::PARAM_MUST_EXIST] = $mustExist
			? MessageValue::new( 'paramvalidator-help-type-title-must-exist' )
			: MessageValue::new( 'paramvalidator-help-type-title-no-must-exist' );

		return $info;
	}

}
TypeDef/EnumDef.php000066600000016715151335037510010161 0ustar00<?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;
	}

}
TypeDef/PresenceBooleanDef.php000066600000004611151335037510012311 0ustar00<?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;
	}

}
TypeDef/BooleanDef.php000066600000003671151335037510010631 0ustar00<?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;
	}

}
TypeDef/LimitDef.php000066600000004202151335037510010317 0ustar00<?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;
	}

}
TypeDef/FloatDef.php000066600000003564151335037510010320 0ustar00<?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;
	}

}
TypeDef/NumericDef.php000066600000015205151335037510010650 0ustar00<?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;
	}

}
TypeDef/UploadDef.php000066600000011677151335037510010503 0ustar00<?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;
	}

}
TypeDef/TimestampDef.php000066600000011311151335037510011203 0ustar00<?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;
	}

}
TypeDef/StringDef.php000066600000012170151335037510010512 0ustar00<?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;
	}

}
TypeDef/ExpiryDef.php000066600000012731151335037510010527 0ustar00<?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;
	}

}
TypeDef/PasswordDef.php000066600000001624151335037510011050 0ustar00<?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;
	}

}
TypeDef/IntegerDef.php000066600000003311151335037510010636 0ustar00<?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 );
	}

}
TypeDef.php000066600000021471151335037510006631 0ustar00<?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 [];
	}

}
TypeDef/TagsDefTest.php000066600000010250151335113370010776 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use ChangeTags;
use MediaWiki\ChangeTags\ChangeTagsStore;
use MediaWikiIntegrationTestCase;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;

/**
 * @group Database
 * @covers MediaWiki\ParamValidator\TypeDef\TagsDef
 */
class TagsDefTest extends MediaWikiIntegrationTestCase {

	protected static $testClass = TagsDef::class;

	protected function setUp(): void {
		parent::setUp();

		ChangeTags::defineTag( 'tag1' );
		ChangeTags::defineTag( 'tag2' );

		$this->tablesUsed[] = 'change_tag_def';

		// Since the type def shouldn't care about the specific user,
		// remove the right from relevant groups to ensure that it's not
		// checking.
		$this->setGroupPermissions( [
			'*' => [ 'applychangetags' => false ],
			'user' => [ 'applychangetags' => false ],
		] );
	}

	/**
	 * @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 conditions reported. Each array is
	 *  `[ $ex->getFailureCode() ] + $ex->getFailureData()`.
	 */
	public function testValidate(
		$value, $expect, array $settings = [], array $options = [], array $expectConds = []
	) {
		$callbacks = new SimpleCallbacks( [ 'test' => $value ] );
		$typeDef = new TagsDef(
			$callbacks,
			$this->getServiceContainer()->getChangeTagsStore()
		);
		$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 );
	}

	public static function provideValidate() {
		$settings = [
			ParamValidator::PARAM_TYPE => 'tags',
			ParamValidator::PARAM_ISMULTI => true,
		];
		$valuesList = [ 'values-list' => [ 'tag1', 'doesnotexist', 'doesnotexist2' ] ];

		return [
			'Basic' => [ 'tag1', [ 'tag1' ] ],
			'Bad tag' => [
				'doesnotexist',
				new ValidationException(
					DataMessageValue::new( 'paramvalidator-badtags', [], 'badtags', [
						'disallowedtags' => [ 'doesnotexist' ],
					] ),
					'test', 'doesnotexist', []
				),
			],
			'Multi' => [ 'tag1', 'tag1', $settings, [ 'values-list' => [ 'tag1', 'tag2' ] ] ],
			'Multi with bad tag (but not the tag)' => [
				'tag1', 'tag1', $settings, $valuesList
			],
			'Multi with bad tag' => [
				'doesnotexist',
				new ValidationException(
					DataMessageValue::new( 'paramvalidator-badtags', [], 'badtags', [
						'disallowedtags' => [ 'doesnotexist', 'doesnotexist2' ],
					] ),
					'test', 'doesnotexist', $settings
				),
				$settings, $valuesList
			],
		];
	}

	public function testGetEnumValues() {
		$explicitlyDefinedTags = [ 'foo', 'bar', 'baz' ];
		$changeTagsStore = $this->createNoOpMock(
			ChangeTagsStore::class,
			[ 'listExplicitlyDefinedTags' ]
		);
		$changeTagsStore->method( 'listExplicitlyDefinedTags' )
			->willReturn( $explicitlyDefinedTags );

		$typeDef = new TagsDef( new SimpleCallbacks( [] ), $changeTagsStore );
		$this->assertSame(
			$explicitlyDefinedTags,
			$typeDef->getEnumValues( 'test', [], [] )
		);
	}

}
TypeDef/TitleDefTest.php000066600000007505151335113370011172 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Title\TitleValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;

/**
 * @covers \MediaWiki\ParamValidator\TypeDef\TitleDef
 * @group Database
 */
class TitleDefTest extends TypeDefIntegrationTestCase {
	protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
		$this->overrideConfigValue( MainConfigNames::LanguageCode, 'en' );
		return new TitleDef(
			$callbacks,
			MediaWikiServices::getInstance()->getTitleFactory()
		);
	}

	/**
	 * @inheritDoc
	 * @dataProvider provideValidate
	 */
	public function testValidate(
		$value, $expect, array $settings = [], array $options = [], array $expectConds = []
	) {
		if ( $this->dataName() === 'must exist (success)' ) {
			$status = $this->editPage( Title::makeTitle( NS_MAIN, 'Exists' ), 'exists' );
			$this->assertTrue( $status->isOK() );
		}
		parent::testValidate( $value, $expect, $settings, $options, $expectConds );
	}

	public function provideValidate() {
		return [
			'plain' => [
				'value' => 'Foo',
				'expect' => 'Foo',
				'settings' => [],
			],
			'normalization' => [
				'value' => 'foo_bar',
				'expect' => 'Foo bar',
				'settings' => [],
			],
			'bad title' => [
				'value' => '<script>',
				'expect' => $this->getValidationException( 'badtitle', '<script>' ),
				'settings' => [],
			],
			'as object' => [
				'value' => 'Foo',
				'expect' => new TitleValue( NS_MAIN, 'Foo' ),
				'settings' => [ TitleDef::PARAM_RETURN_OBJECT => true ],
			],
			'as object, with namespace' => [
				'value' => 'User:Foo',
				'expect' => new TitleValue( NS_USER, 'Foo' ),
				'settings' => [ TitleDef::PARAM_RETURN_OBJECT => true ],
			],
			'object normalization' => [
				'value' => 'foo_bar',
				'expect' => new TitleValue( NS_MAIN, 'Foo bar' ),
				'settings' => [ TitleDef::PARAM_RETURN_OBJECT => true ],
			],
			'must exist (success)' => [
				'value' => 'Exists',
				'expect' => 'Exists',
				'settings' => [ TitleDef::PARAM_MUST_EXIST => true ],
			],
			'must exist (failure)' => [
				'value' => 'does not exist',
				'expect' => $this->getValidationException( 'missingtitle', 'does not exist',
					[ TitleDef::PARAM_MUST_EXIST => true ] ),
				'settings' => [ TitleDef::PARAM_MUST_EXIST => true ],
			],
		];
	}

	public function provideStringifyValue() {
		return [
			// Underscore-to-space conversion not happening here but later in validate().
			'String' => [ 'User:John_Doe', 'User:John_Doe' ],
			'TitleValue' => [ new TitleValue( NS_USER, 'John_Doe' ), 'User:John Doe' ],
			'Title' => [ Title::makeTitle( NS_USER, 'John_Doe' ), 'User:John Doe' ],
		];
	}

	public function provideCheckSettings() {
		// checkSettings() is itself used in tests. Testing it is a waste of time,
		// just provide the minimum required.
		return [
			'Basic test' => [ [], self::STDRET, array_merge_recursive( self::STDRET, [
				'allowedKeys' => [ TitleDef::PARAM_MUST_EXIST, TitleDef::PARAM_RETURN_OBJECT ],
			] ) ],
		];
	}

	public function provideGetInfo() {
		return [
			'no mustExist' => [
				'settings' => [],
				'expectParamInfo' => [ 'mustExist' => false ],
				'expectHelpInfo' => [
					ParamValidator::PARAM_TYPE =>
						'<message key="paramvalidator-help-type-title"></message>',
					TitleDef::PARAM_MUST_EXIST =>
						'<message key="paramvalidator-help-type-title-no-must-exist"></message>'
				],
			],
			'mustExist' => [
				'settings' => [ TitleDef::PARAM_MUST_EXIST => true ],
				'expectParamInfo' => [ 'mustExist' => true ],
				'expectHelpInfo' => [
					ParamValidator::PARAM_TYPE =>
						'<message key="paramvalidator-help-type-title"></message>',
					TitleDef::PARAM_MUST_EXIST =>
						'<message key="paramvalidator-help-type-title-must-exist"></message>'
				],
			],
		];
	}

}
TypeDef/TypeDefIntegrationTestCase.php000066600000000631151335113370014023 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWikiIntegrationTestCase;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCaseTrait;

abstract class TypeDefIntegrationTestCase extends MediaWikiIntegrationTestCase {
	use TypeDefTestCaseTrait;

	/** Standard "$ret" array for provideCheckSettings */
	protected const STDRET =
		[ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
}
TypeDef/UserDefTest.php000066600000035523151335117610011031 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;

/**
 * @covers \MediaWiki\ParamValidator\TypeDef\UserDef
 */
class UserDefTest extends TypeDefUnitTestCase {
	use DummyServicesTrait;

	protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
		// The UserIdentityLookup that we have knows about 5 users, with ids
		// 1 through 5 and names starting with the first 5 letters of the alphabet:
		$namesToIds = [
			'Adam Smith' => 1,
			'Becca' => 2,
			'Charlie' => 3,
			'Danny' => 4,
			'Emma' => 5,
		];
		$userIdentityLookup = $this->createMock( UserIdentityLookup::class );
		$userIdentityLookup->method( 'getUserIdentityByName' )->willReturnCallback(
			static function ( $name, $flags ) use ( $namesToIds ) {
				if ( isset( $namesToIds[$name] ) ) {
					return new UserIdentityValue( $namesToIds[$name], $name );
				}
				return null;
			}
		);
		$userIdentityLookup->method( 'getUserIdentityByUserId' )->willReturnCallback(
			static function ( $id, $flags ) use ( $namesToIds ) {
				$idsToNames = array_flip( $namesToIds );
				if ( isset( $idsToNames[$id] ) ) {
					return new UserIdentityValue( $id, $idsToNames[$id] );
				}
				return null;
			}
		);

		// DummyServicesTrait will call $this->createHookContainer() if we didn't pass
		// one, but that method is only available from MediaWikiTestCaseTrait - just
		// create a simple mock that doesn't do anything, because we
		// don't care about hooks here
		$hookContainer = $this->createMock( HookContainer::class );
		$hookContainer->method( 'run' )->willReturn( true );
		// We can throw mock exceptions because the UserDef code doesn't care about
		// the messages in the exceptions, just if they are thrown
		$titleParser = $this->getDummyTitleParser( [
			'validInterwikis' => [ 'interwiki' ],
			'throwMockExceptions' => true,
			'hookContainer' => $hookContainer, // for the NamespaceInfo
		] );
		$userNameUtils = $this->getDummyUserNameUtils( [
			'titleParser' => $titleParser, // don't create a new one
			'hookContainer' => $hookContainer,
		] );
		return new UserDef(
			$callbacks,
			$userIdentityLookup,
			$titleParser,
			$userNameUtils
		);
	}

	public function provideValidate() {
		// General tests of string inputs
		$data = [
			'Basic' => [ 'name', 'Adam Smith', 'Adam Smith' ],
			'Normalized' => [ 'name', 'adam_Smith', 'Adam Smith' ],
			'External' => [ 'interwiki', 'm>some_user', 'm>some_user' ],
			'IPv4' => [ 'ip', '192.168.0.1', '192.168.0.1' ],
			'IPv4, normalized' => [ 'ip', '192.168.000.001', '192.168.0.1' ],
			'IPv6' => [ 'ip', '2001:DB8:0:0:0:0:0:0', '2001:DB8:0:0:0:0:0:0' ],
			'IPv6, normalized' => [ 'ip', '2001:0db8::', '2001:DB8:0:0:0:0:0:0' ],
			'IPv6, with leading ::' => [ 'ip', '::1', '0:0:0:0:0:0:0:1' ],
			'IPv4 range' => [ 'cidr', '192.168.000.000/16', '192.168.0.0/16' ],
			'IPv6 range' => [ 'cidr', '2001:0DB8::/64', '2001:DB8:0:0:0:0:0:0/64' ],
			'Usemod IP' => [ 'ip', '192.168.0.xxx', '192.168.0.xxx' ],
			'Bogus IP' => [ '', '192.168.0.256', null ],
			'Bogus Usemod IP' => [ '', '192.268.0.xxx', null ],
			'Usemod IP as range' => [ '', '192.168.0.xxx/16', null ],
			'Bad username' => [ '', '[[Foo]]', null ],
			'No namespaces' => [ '', 'Talk:Foo', null ],
			'No namespaces (2)' => [ '', 'Help:Foo', null ],
			'No namespaces (except User is ok)' => [ 'name', 'User:Adam_Smith', 'Adam Smith' ],
			'No namespaces (except User is ok) (IPv6)' => [ 'ip', 'User:::1', '0:0:0:0:0:0:0:1' ],
			'No interwiki prefixes' => [ '', 'interwiki:Foo', null ],
			'No fragment in IP' => [ '', '192.168.0.256#', null ],
		];
		foreach ( $data as $key => [ $type, $input, $expect ] ) {
			$ex = new ValidationException(
				DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
				'test', $input, []
			);
			if ( $type === '' ) {
				yield $key => [ $input, $ex ];
				continue;
			}

			yield $key => [ $input, $expect ];

			yield "$key, only '$type' allowed" => [
				$input,
				$expect,
				[ UserDef::PARAM_ALLOWED_USER_TYPES => [ $type ] ],
			];

			$types = array_diff( [ 'name', 'ip', 'cidr', 'interwiki' ], [ $type ] );
			yield "$key, without '$type' allowed" => [
				$input,
				$ex,
				[ UserDef::PARAM_ALLOWED_USER_TYPES => $types ],
			];
			if ( $type === 'ip'
				|| $type === 'interwiki'
				|| $type === 'cidr'
			) {
				// For all of these the UserIdentity returned will be a
				// UserIdentityValue object since the name and id are both
				// known (id is 0 for all)
				$obj = UserIdentityValue::newAnonymous( $expect );
			} else {
				// Creating from name, we are only testing for "Adam Smith"
				// so the id will be 1
				$obj = new UserIdentityValue( 1, $expect );
			}

			yield "$key, returning object" => [ $input, $obj, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
		}

		// Test input by user ID
		// Since this user isn't in our mock UserIdentityLookup, the name is "Unknown user"
		// and the id is switched to just 0. We cover the case of existing ids in
		// testProcessUser()
		$input = '#1234';
		$ex = new ValidationException(
			DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
			'test', $input, []
		);
		yield 'User ID' => [ $input, $ex, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
		yield 'User ID, with \'id\' allowed, returning object' => [
			$input,
			new UserIdentityValue( 0, "Unknown user" ),
			[ UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ], UserDef::PARAM_RETURN_OBJECT => true ],
		];

		// Tests for T232672 (consistent treatment of whitespace and BIDI characters)
		$data = [
			'name' => [ 'Emma', [ 1 ], 'Emma' ],
			'interwiki' => [ 'm>some_user', [ 1, 2, 6 ], null ],
			'ip (v4)' => [ '192.168.0.1', [ 1, 3, 4 ], '192.168.0.1' ],
			'ip (v6)' => [ '2001:DB8:0:0:0:0:0:0', [ 2, 5, 6 ], '2001:DB8:0:0:0:0:0:0' ],
			'ip (v6, colons)' => [ '::1', [ 1, 2 ], '0:0:0:0:0:0:0:1' ],
			'cidr (v4)' => [ '192.168.0.0/16', [ 1, 3, 4, 11, 12, 13 ], '192.168.0.0/16' ],
			'cidr (v6)' => [ '2001:db8::/64', [ 2, 5, 6, 20, 21, 22 ], '2001:DB8:0:0:0:0:0:0/64' ],
		];
		foreach ( $data as $key => [ $name, $positions, $expect ] ) {
			$input = " $name ";
			yield "T232672: leading/trailing whitespace for $key" => [ $input, $expect ?? $input ];

			$input = "_{$name}_";
			yield "T232672: leading/trailing underscores for $key" => [ $input, $expect ?? $input ];

			$positions = array_merge( [ 0, strlen( $name ) ], $positions );
			foreach ( $positions as $i ) {
				$input = substr_replace( $name, "\u{200E}", $i, 0 );
				yield "T232672: U+200E at position $i for $key" => [ $input, $expect ?? $input ];
			}
		}
	}

	public function provideNormalizeSettings() {
		return [
			'Basic test' => [
				[ 'param-foo' => 'bar' ],
				[
					'param-foo' => 'bar',
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'interwiki' ],
				],
			],
			'Types not overridden' => [
				[
					'param-foo' => 'bar',
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
				],
				[
					'param-foo' => 'bar',
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
				],
			],
		];
	}

	public function provideCheckSettings() {
		$keys = [ 'Y', UserDef::PARAM_ALLOWED_USER_TYPES, UserDef::PARAM_RETURN_OBJECT ];
		$ismultiIssue = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
			. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
			. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
			. 'once we have a real use case to look at.)';

		return [
			'Basic test' => [
				[],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Test with everything' => [
				[
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
					UserDef::PARAM_RETURN_OBJECT => true,
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Bad types' => [
				[
					UserDef::PARAM_ALLOWED_USER_TYPES => 'name',
					UserDef::PARAM_RETURN_OBJECT => 1,
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						UserDef::PARAM_RETURN_OBJECT => 'PARAM_RETURN_OBJECT must be boolean, got integer',
						UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES must be an array, got string',
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALLOWED_USER_TYPES cannot be empty' => [
				[
					UserDef::PARAM_ALLOWED_USER_TYPES => [],
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES cannot be empty',
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALLOWED_USER_TYPES invalid values' => [
				[
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id', 'ssn', 'Q-number' ],
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						UserDef::PARAM_ALLOWED_USER_TYPES
							=> 'PARAM_ALLOWED_USER_TYPES contains invalid values: ssn, Q-number',
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI generally ok' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI with ID not ok (1)' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
				],
				self::STDRET,
				[
					'issues' => [ 'X', $ismultiIssue ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI with ID not ok (2)' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
					ParamValidator::PARAM_ISMULTI_LIMIT2 => 11,
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
				],
				self::STDRET,
				[
					'issues' => [ 'X', $ismultiIssue ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI with ID ok with low limits' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
					ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI with RETURN_OBJECT also not ok' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					UserDef::PARAM_RETURN_OBJECT => true,
				],
				self::STDRET,
				[
					'issues' => [ 'X', $ismultiIssue ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'ISMULTI with RETURN_OBJECT also ok with low limits' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
					ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
					UserDef::PARAM_RETURN_OBJECT => true,
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
		];
	}

	public function provideGetInfo() {
		return [
			'Basic test' => [
				[],
				[
					'subtypes' => [ 'name', 'ip', 'cidr', 'interwiki' ],
				],
				[
					ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>1</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-ip"></message></text><text><message key="paramvalidator-help-type-user-subtype-cidr"></message></text><text><message key="paramvalidator-help-type-user-subtype-interwiki"></message></text></list><num>4</num></message>',
				],
			],
			'Specific types' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
					UserDef::PARAM_RETURN_OBJECT => true,
				],
				[
					'subtypes' => [ 'name', 'id' ],
				],
				[
					ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>2</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-id"></message></text></list><num>2</num></message>',
				],
			],
		];
	}

	private function assertUserIdentity( $actual, $expectId, $expectName ) {
		// Can't use UserIdentity::equals() since that only checks the name
		$this->assertInstanceOf( UserIdentity::class, $actual );
		$this->assertSame( $expectId, $actual->getId() );
		$this->assertSame( $expectName, $actual->getName() );
	}

	/**
	 * @dataProvider provideMissingId
	 */
	public function testProcessUser_missingId( $missingId ) {
		// User created by id, does not exist, falls back to "Unknown user"
		// See our mock UserIdentityLookup for which ids and names exist
		$userDef = $this->getInstance( new SimpleCallbacks( [] ), [] );
		$res = $userDef->validate(
			'', // $name, unused here
			"#$missingId",
			[
				UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
				UserDef::PARAM_RETURN_OBJECT => true,
			], // $settings
			[] // $options, unused here
		);
		// Even though we created with $missingId, the resulting UserIdentity has
		// an id of 0 because the user does not exist
		$this->assertUserIdentity( $res, 0, "Unknown user" );
	}

	public static function provideMissingId() {
		yield "0 no longer matches request ip" => [ 0 ];
		yield "Id with no user" => [ 6 ];
	}

	public function testProcessUser_validId() {
		// User created by id, does exist
		// See our mock UserIdentityLookup for which ids and names exist
		$userDef = $this->getInstance( new SimpleCallbacks( [] ), [] );
		$res = $userDef->validate(
			'', // $name, unused here
			"#5",
			[
				UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
				UserDef::PARAM_RETURN_OBJECT => true,
			], // $settings
			[] // $options, unused here
		);
		$this->assertUserIdentity( $res, 5, 'Emma' );
	}

	public function testProcessUser_missingName() {
		// Created by name, does not exist
		// Already in the canonical form
		// See our mock UserIdentityLookup for which ids and names exist
		$userName = 'UserDefTest-processUser-missing';

		$userDef = $this->getInstance( new SimpleCallbacks( [] ), [] );
		$res = $userDef->validate(
			'', // $name, unused here
			$userName,
			[
				UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
				UserDef::PARAM_RETURN_OBJECT => true,
			], // $settings
			[] // $options, unused here
		);

		$this->assertUserIdentity( $res, 0, $userName );
	}

	public function testProcessUser_0() {
		$userName = '0';

		$userDef = $this->getInstance( new SimpleCallbacks( [] ), [] );
		$res = $userDef->validate(
			'', // $name, unused here
			$userName,
			[
				UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
				UserDef::PARAM_RETURN_OBJECT => true,
			], // $settings
			[] // $options, unused here
		);

		$this->assertUserIdentity( $res, 0, $userName );
	}

}
TypeDef/NamespaceDefTest.php000066600000020661151335117610012004 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use ApiResult;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\ValidationException;

/**
 * @covers \MediaWiki\ParamValidator\TypeDef\NamespaceDef
 */
class NamespaceDefTest extends TypeDefUnitTestCase {
	use DummyServicesTrait;

	private function getNamespaceInfo() {
		// DummyServicesTrait::getDummyNamespaceInfo() would call
		// $this->createHookContainer() if we didn't pass one, but that
		// method is only available from MediaWikiTestCaseTrait - just
		// create a simple mock that doesn't do anything, because we
		// don't care about hooks here
		$hookContainer = $this->createMock( HookContainer::class );
		$hookContainer->method( 'run' )->willReturn( true );
		return $this->getDummyNamespaceInfo( [ 'hookContainer' => $hookContainer ] );
	}

	protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
		return new NamespaceDef(
			$callbacks,
			$this->getNamespaceInfo()
		);
	}

	private function getNamespaces( $extra = [] ) {
		$namespaces = array_merge(
			$this->getNamespaceInfo()->getValidNamespaces(),
			$extra
		);
		sort( $namespaces );
		return $namespaces;
	}

	public function provideValidate() {
		$settings = [
			ParamValidator::PARAM_TYPE => 'namespace',
		];
		$extraSettings = [
			ParamValidator::PARAM_TYPE => 'namespace',
			NamespaceDef::PARAM_EXTRA_NAMESPACES => [ -5 ],
		];

		return [
			'Basic' => [ '0', 0, $settings ],
			'Bad namespace' => [
				'x',
				new ValidationException(
					DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', 'x', $settings
				),
				$settings
			],
			'Unknown namespace' => [
				'x',
				new ValidationException(
					DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', '-1', []
				),
			],
			'Extra namespaces' => [ '-5', -5, $extraSettings ],
		];
	}

	public function provideGetEnumValues() {
		return [
			'Basic test' => [
				[ ParamValidator::PARAM_TYPE => 'namespace' ],
				$this->getNamespaces(),
			],
			'Extra namespaces' => [
				[
					ParamValidator::PARAM_TYPE => 'namespace',
					NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ]
				],
				$this->getNamespaces( [ NS_SPECIAL, NS_MEDIA ] ),
			],
		];
	}

	public function provideNormalizeSettings() {
		return [
			'Basic test' => [ [], [] ],
			'Add PARAM_ALL' => [
				[ ParamValidator::PARAM_ISMULTI => true ],
				[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
			],
			'Force PARAM_ALL' => [
				[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => false ],
				[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
			],
			'Force PARAM_ALL (2)' => [
				[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => 'all' ],
				[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
			],
		];
	}

	public function provideCheckSettings() {
		$keys = [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES, NamespaceDef::PARAM_EXTRA_NAMESPACES ];
		return [
			'Basic test' => [
				[],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Test with everything' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ALL => true,
					NamespaceDef::PARAM_EXTRA_NAMESPACES => [ -1, -2 ],
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALL cannot be false' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ALL => false,
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						ParamValidator::PARAM_ALL
							=> 'PARAM_ALL cannot be false or a string for namespace-type parameters',
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALL cannot be a string' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ALL => 'all',
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						ParamValidator::PARAM_ALL
							=> 'PARAM_ALL cannot be false or a string for namespace-type parameters',
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALL ignored without PARAM_ISMULTI' => [
				[
					ParamValidator::PARAM_ALL => 'all',
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'PARAM_ALL cannot be a string, but another PARAM_ALL issue was already logged' => [
				[
					ParamValidator::PARAM_ISMULTI => true,
					ParamValidator::PARAM_ALL => 'all',
				],
				[
					'issues' => [ ParamValidator::PARAM_ALL => 'XXX' ],
					'allowedKeys' => [ 'Y' ],
					'messages' => [],
				],
				[
					'issues' => [ ParamValidator::PARAM_ALL => 'XXX' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Bad type for PARAM_EXTRA_NAMESPACES' => [
				[
					NamespaceDef::PARAM_EXTRA_NAMESPACES => -1,
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						NamespaceDef::PARAM_EXTRA_NAMESPACES
							=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got integer'
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Empty array for PARAM_EXTRA_NAMESPACES ok' => [
				[
					NamespaceDef::PARAM_EXTRA_NAMESPACES => [],
				],
				self::STDRET,
				[
					'issues' => [ 'X' ],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Bad value types for PARAM_EXTRA_NAMESPACES' => [
				[
					NamespaceDef::PARAM_EXTRA_NAMESPACES => [ '-1' ],
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						NamespaceDef::PARAM_EXTRA_NAMESPACES
							=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got string[]'
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
			'Bad value types for PARAM_EXTRA_NAMESPACES (2)' => [
				[
					NamespaceDef::PARAM_EXTRA_NAMESPACES => [ 0, '-1', '-2' ],
				],
				self::STDRET,
				[
					'issues' => [
						'X',
						NamespaceDef::PARAM_EXTRA_NAMESPACES
							=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got (integer|string)[]'
					],
					'allowedKeys' => $keys,
					'messages' => [],
				],
			],
		];
	}

	public function provideStringifyValue() {
		return [
			'Basic test' => [ 123, '123' ],
			'Array' => [ [ 1, 2, 3 ], '1|2|3' ],
		];
	}

	public function provideGetInfo() {
		yield 'Basic test' => [
			[],
			[ 'type' => 'namespace' ],
			[
				ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>16</num></message>',
				ParamValidator::PARAM_ISMULTI => null,
			],
		];

		yield 'Extra namespaces' => [
			[
				ParamValidator::PARAM_DEFAULT => 0,
				NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ]
			],
			[ 'type' => 'namespace', 'extranamespaces' => [ NS_SPECIAL, NS_MEDIA ] ],
			[
				ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>-1</text><text>-2</text><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>18</num></message>',
				ParamValidator::PARAM_ISMULTI => null,
			],
		];

		yield 'Extra namespaces, for Action API' => [
			[ NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ] ],
			[
				'type' => 'namespace',
				'extranamespaces' => [
					NS_SPECIAL, NS_MEDIA,
					ApiResult::META_INDEXED_TAG_NAME => 'ns',
				],
			],
			[
				ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>-1</text><text>-2</text><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>18</num></message>',
				ParamValidator::PARAM_ISMULTI => null,
			],
			[ 'module' => (object)[] ],
		];
	}

}
TypeDef/TypeDefUnitTestCase.php000066600000000604151335117610012460 0ustar00<?php

namespace MediaWiki\ParamValidator\TypeDef;

use MediaWikiUnitTestCase;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCaseTrait;

abstract class TypeDefUnitTestCase extends MediaWikiUnitTestCase {
	use TypeDefTestCaseTrait;

	/** Standard "$ret" array for provideCheckSettings */
	protected const STDRET =
		[ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
}
TypeDefTest.php000066600000011755151335123350007473 0ustar00<?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', [], []
			],
		];
	}

}
TypeDef/EnumDefTest.php000066600000015231151335123350011007 0ustar00<?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,
				],
			],
		];
	}

}
TypeDef/UploadDefTest.php000066600000016302151335123350011327 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/TypeDefTestCase.php000066600000000713151335123350011617 0ustar00<?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' => [] ];
}
TypeDef/TimestampDefTest.php000066600000015210151335123350012043 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/FloatDefTest.php000066600000011436151335123350011153 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/LimitDefTest.php000066600000010300151335123350011151 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/IntegerDefTest.php000066600000023447151335123350011510 0ustar00<?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 ],
			],
		];
	}

}
TypeDef/ExpiryDefTest.php000066600000012067151335123350011367 0ustar00<?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>&quot;infinite&quot;</text><text>&quot;indefinite&quot;</text><text>&quot;infinity&quot;</text><text>&quot;never&quot;</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 );
	}
}
TypeDef/BooleanDefTest.php000066600000003412151335123350011460 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/StringDefTest.php000066600000012531151335123350011351 0ustar00<?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>',
				],
			],
		];
	}

}
TypeDef/PresenceBooleanDefTest.php000066600000006253151335123350013153 0ustar00<?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,
				],
			],
		];
	}

}
TypeDef/PasswordDefTest.php000066600000003070151335123350011703 0ustar00<?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' => [],
			],
		];
	}

}
TypeDef/TypeDefTestCaseTrait.php000066600000016142151335123350012626 0ustar00<?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 );
	}

}
Back to Directory File Manager