diff --git a/.travis.yml b/.travis.yml index 53cbbd2ca..e1faebc8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,7 @@ branches: cache: directories: - - $HOME/.composer/cache - - $HOME/.local - - zf-mkdoc-theme + - $HOME/.composer/ env: global: @@ -19,11 +17,6 @@ env: - COVERAGE_DEPS="satooshi/php-coveralls" - LEGACY_DEPS="phpunit/phpunit" - TESTS_ZEND_VALIDATOR_ONLINE_ENABLED=true - - SITE_URL=https://zendframework.github.io/zend-validator - - GH_USER_NAME="Matthew Weier O'Phinney" - - GH_USER_EMAIL=matthew@weierophinney.net - - GH_REF=github.com/zendframework/zend-validator.git - - secure="SoUsUxBFCuC0rVQyDJ/+IB38glC5WeWvg0XxtNj79di7wsQ92Jofp6Uu3NJBB8H1+at1pHetphRm4N+GPQmZGMFTG7LyF5u8duV8t4nDpAz5WfoP1y0IyacP6IrWzANeszOTZ04dlHu3dBdHusNpNxxUHl97bSx4XQUAm2GUTqNkuXNgQJFAAxx91jb5txG4W8KeMnfRm9jeDHP17BCnBMaSkYEXeLpHkYa9wA4lBJ7ZD6LuSC+MhrJCtREBTsWKLJY6xeBjRorUug+uCrNyArPtcOAaOLMSDJ1XIi3L5/Q7HdoldV7aC3V5HjNlpdIEFl33IGiCOyictFCpT1KaKx7TL8zDTMCiqe0cCyfTnq28lzULz2hXg0Kov7BFcRr2Ht/1f96RgrakWQiYTmk+C3YYYA16Fb+MndkMI3WH7WI0suC+5nhPdGl53MCWsd5x2+dDk/ifB/VvxHdGhhgxzAxsYJ41gV/LlzjbCQJNDCnTaL/GHCTUGJEPgwLrn2W52uZx6VggE9wl5z4XkiPqBy6zAAdwF55RRJgCxFttGOMVGdegFLHTf6+13S4sEImNmyVTeuJBZEHxaYRJ21wweOocjC2StKC9V54uPysDcEYwhu8WOsYU34fQdpMx3OHfPmXvhNGqoZ1rVsd5HM0QZZMT+7SI0r3UNKxrPC8LEAU=" matrix: include: @@ -35,12 +28,6 @@ matrix: - DEPS=locked - EXECUTE_HOSTNAME_CHECK=true - TEST_COVERAGE=true - - DEPLOY_DOCS="$(if [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then echo -n 'true' ; else echo -n 'false' ; fi)" - - PATH="$HOME/.local/bin:$PATH" - - php: 5.6 - env: - - DEPS=locked - - SERVICE_MANAGER_VERSION="^2.7.5" - php: 5.6 env: - DEPS=latest @@ -51,10 +38,6 @@ matrix: env: - DEPS=locked - CS_CHECK=true - - php: 7 - env: - - DEPS=locked - - SERVICE_MANAGER_VERSION="^2.7.5" - php: 7 env: - DEPS=latest @@ -67,21 +50,17 @@ matrix: - php: 7.1 env: - DEPS=latest - - php: hhvm + - php: nightly env: - DEPS=lowest - - php: hhvm + - php: nightly env: - DEPS=locked - - php: hhvm + - php: nightly env: - DEPS=latest - - php: hhvm - env: - - DEPS=locked - - SERVICE_MANAGER_VERSION="^2.7.5" allow_failures: - - php: hhvm + - php: nightly before_install: - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi @@ -92,8 +71,6 @@ install: - if [[ $TRAVIS_PHP_VERSION =~ ^5.6 ]]; then travis_retry composer update $COMPOSER_ARGS --with-dependencies $LEGACY_DEPS ; fi - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi - - if [[ $SERVICE_MANAGER_VERSION != '' ]]; then travis_retry composer require --dev --no-update $COMPOSER_ARGS "zendframework/zend-servicemanager:$SERVICE_MANAGER_VERSION" ; fi - - if [[ $SERVICE_MANAGER_VERSION == '' ]]; then travis_retry composer require --dev --no-update $COMPOSER_ARGS "zendframework/zend-servicemanager:^3.0.3" ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi - stty cols 120 - COLUMNS=120 composer show @@ -102,14 +79,9 @@ script: - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi - if [[ $EXECUTE_HOSTNAME_CHECK == "true" && $TRAVIS_PULL_REQUEST == "false" ]]; then php bin/update_hostname_validator.php --check-only; fi - - if [[ $DEPLOY_DOCS == "true" && "$TRAVIS_TEST_RESULT" == "0" ]]; then wget -O theme-installer.sh "https://raw.githubusercontent.com/zendframework/zf-mkdoc-theme/master/theme-installer.sh" ; chmod 755 theme-installer.sh ; ./theme-installer.sh ; fi after_script: - - if [[ $TEST_COVERAGE == 'true' ]]; then composer upload-coverage ; fi - -after_success: - - if [[ $DEPLOY_DOCS == "true" ]]; then echo "Preparing to build and deploy documentation" ; ./zf-mkdoc-theme/deploy.sh ; echo "Completed deploying documentation" ; fi + - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer upload-coverage ; fi notifications: - irc: "irc.freenode.org#zftalk.dev" email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index a14e811a3..0a90c6fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,28 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#175](https://github.com/zendframework/zend-validator/pull/175) adds support + for PHP 7.2 (conditionally, as PHP 7.2 is currently in beta1). + +- [#157](https://github.com/zendframework/zend-validator/pull/157) adds a new + validator, `IsCountable`, which allows validating: + - if a value is countable + - if a countable value exactly matches a configured count + - if a countable value is greater than a configured minimum count + - if a countable value is less than a configured maximum count + - if a countable value is between configured minimum and maximum counts + +### Changed + +- [#169](https://github.com/zendframework/zend-validator/pull/169) modifies how + the various `File` validators check for readable files. Previously, they used + `stream_resolve_include_path`, which led to false negative checks when the + files did not exist within an `include_path` (which is often the case within a + web application). These now use `is_readable()` instead. + +- [#185](https://github.com/zendframework/zend-validator/pull/185) updates the + zend-session requirement (during development, and in the suggestions) to 2.8+, + to ensure compatibility with the upcoming PHP 7.2 release. ### Deprecated @@ -14,13 +35,14 @@ All notable changes to this project will be documented in this file, in reverse ### Removed -- Nothing. +- [#175](https://github.com/zendframework/zend-validator/pull/175) removes + support for HHVM. ### Fixed - Nothing. -## 2.9.1 - TBD +## 2.9.2 - 2017-07-20 ### Added @@ -36,8 +58,49 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed +- [#180](https://github.com/zendframework/zend-validator/pull/180) fixes how + `Zend\Validator\File\MimeType` "closes" the open FileInfo handle for the file + being validated, using `unset()` instead of `finfo_close()`; this resolves a + segfault that occurs on older PHP versions. +- [#174](https://github.com/zendframework/zend-validator/pull/174) fixes how + `Zend\Validator\Between` handles two situations: (1) when a non-numeric value + is validated against numeric min/max values, and (2) when a numeric value is + validated against non-numeric min/max values. Previously, these incorrectly + validated as true; now they are marked invalid. + +## 2.9.1 - 2017-05-17 + +### Added + - Nothing. +### Changes + +- [#154](https://github.com/zendframework/zend-validator/pull/154) updates the + `CreditCard` validator to allow 19 digit Discover card values, and 13 and 19 + digit Visa card values, which are now allowed (see + https://en.wikipedia.org/wiki/Payment_card_number). +- [#162](https://github.com/zendframework/zend-validator/pull/162) updates the + `Hostname` validator to support `.hr` (Croatia) IDN domains. +- [#163](https://github.com/zendframework/zend-validator/pull/163) updates the + `Iban` validator to support Belarus. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#168](https://github.com/zendframework/zend-validator/pull/168) fixes how the + `ValidatorPluginManagerFactory` factory initializes the plugin manager instance, + ensuring it is injecting the relevant configuration from the `config` service + and thus seeding it with configured validator services. This means + that the `validators` configuration will now be honored in non-zend-mvc contexts. + ## 2.9.0 - 2017-03-17 ### Added diff --git a/bin/update_hostname_validator.php b/bin/update_hostname_validator.php index fab52c24e..69043d681 100644 --- a/bin/update_hostname_validator.php +++ b/bin/update_hostname_validator.php @@ -177,7 +177,9 @@ function getNewValidTlds($string) function getPunycodeDecoder() { if (function_exists('idn_to_utf8')) { - return 'idn_to_utf8'; + return function ($domain) { + return idn_to_utf8($domain, 0, INTL_IDNA_VARIANT_UTS46); + }; } $hostnameValidator = new Hostname(); diff --git a/composer.json b/composer.json index 560ed56bb..6b52eca70 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "zendframework/zend-i18n": "^2.6", "zendframework/zend-math": "^2.6", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-session": "^2.6.2", + "zendframework/zend-session": "^2.8", "zendframework/zend-uri": "^2.5", "phpunit/PHPUnit": "^6.0.8 || ^5.7.15", "zendframework/zend-coding-standard": "~1.0.0" @@ -38,7 +38,7 @@ "zendframework/zend-math": "Zend\\Math component, required by the Csrf validator", "zendframework/zend-i18n-resources": "Translations of validator messages", "zendframework/zend-servicemanager": "Zend\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", - "zendframework/zend-session": "Zend\\Session component, required by the Csrf validator", + "zendframework/zend-session": "Zend\\Session component, ^2.8; required by the Csrf validator", "zendframework/zend-uri": "Zend\\Uri component, required by the Uri and Sitemap\\Loc validators" }, "minimum-stability": "dev", diff --git a/composer.lock b/composer.lock index dca2092e7..e47398518 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "6926a47b4f62c328d7c9d74767654cb9", + "content-hash": "592dc52b9037dcdb0de561eced941e30", "packages": [ { "name": "container-interop/container-interop", @@ -2169,29 +2169,29 @@ }, { "name": "zendframework/zend-session", - "version": "2.7.3", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-session.git", - "reference": "346e9709657b81a5d53d70ce754730a26d1f02f2" + "reference": "b1486c382decc241de8b1c7778eaf2f0a884f67d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-session/zipball/346e9709657b81a5d53d70ce754730a26d1f02f2", - "reference": "346e9709657b81a5d53d70ce754730a26d1f02f2", + "url": "https://api.github.com/repos/zendframework/zend-session/zipball/b1486c382decc241de8b1c7778eaf2f0a884f67d", + "reference": "b1486c382decc241de8b1c7778eaf2f0a884f67d", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", + "php": "^7.0 || ^5.6", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { "container-interop/container-interop": "^1.1", - "fabpot/php-cs-fixer": "1.7.*", "mongodb/mongodb": "^1.0.1", - "phpunit/phpunit": "~4.0", + "phpunit/phpunit": "^6.0.8 || ^5.7.15", "zendframework/zend-cache": "^2.6.1", + "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.7", "zendframework/zend-http": "^2.5.4", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", @@ -2208,8 +2208,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.8-dev", + "dev-develop": "2.9-dev" }, "zf": { "component": "Zend\\Session", @@ -2231,7 +2231,7 @@ "session", "zf2" ], - "time": "2016-07-05T18:32:50+00:00" + "time": "2017-06-19T21:31:39+00:00" }, { "name": "zendframework/zend-uri", diff --git a/doc/book/validators/date.md b/doc/book/validators/date.md index 133792677..83de95a40 100644 --- a/doc/book/validators/date.md +++ b/doc/book/validators/date.md @@ -25,7 +25,7 @@ $validator->isValid('10.10.2000'); // returns false `Zend\Validator\Date` also supports custom date formats. When you want to validate such a date, use the `format` option. This option accepts any format -allowed by the PHP [date()](http://php.net/date) function. +allowed by the PHP [DateTime::createFromFormat()](http://php.net/manual/en/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters) method. ```php $validator = new Zend\Validator\Date(['format' => 'Y']); diff --git a/doc/book/validators/is-countable.md b/doc/book/validators/is-countable.md new file mode 100644 index 000000000..bfd66c123 --- /dev/null +++ b/doc/book/validators/is-countable.md @@ -0,0 +1,107 @@ +# IsCountable Validator + +- **Since 2.10.0** + +`Zend\Validator\IsCountable` allows you to validate that a value can be counted +(i.e., it's an array or implements `Countable`), and, optionally: + +- the exact count of the value +- the minimum count of the value +- the maximum count of the value + +Specifying either of the latter two is inconsistent with the first, and, as +such, the validator does not allow setting both a count and a minimum or maximum +value. You may, however specify both minimum and maximum values, in which case +the validator operates similar to the [Between validator](between.md). + +## Supported options + +The following options are supported for `Zend\Validator\IsCountable`: + +- `count`: Defines if the validation should look for a specific, exact count for + the value provided. +- `max`: Sets the maximum value for the validation; if the count of the value is + greater than the maximum, validation fails.. +- `min`: Sets the minimum value for the validation; if the count of the value is + lower than the minimum, validation fails. + +## Default behaviour + +Given no options, the validator simply tests to see that the value may be +counted (i.e., it's an array or `Countable` instance): + +```php +$validator = new Zend\Validator\IsCountable(); + +$validator->isValid(10); // false; not an array or Countable +$validator->isValid([10]); // true; value is an array +$validator->isValid(new ArrayObject([10])); // true; value is Countable +$validator->isValid(new stdClass); // false; value is not Countable +``` + +## Specifying an exact count + +You can also specify an exact count; if the value is countable, and its count +matches, the the value is valid. + +```php +$validator = new Zend\Validator\IsCountable(['count' => 3]); + +$validator->isValid([1, 2, 3]); // true; countable, and count is 3 +$validator->isValid(new ArrayObject([1, 2, 3])); // true; countable, and count is 3 +$validator->isValid([1]); // false; countable, but count is 1 +$validator->isValid(new ArrayObject([1])); // false; countable, but count is 1 +``` + +## Specifying a minimum count + +You may specify a minimum count. When you do, the value must be countable, and +greater than or equal to the minimum count you specify in order to be valid. + +```php +$validator = new Zend\Validator\IsCountable(['min' => 2]); + +$validator->isValid([1, 2, 3]); // true; countable, and count is 3 +$validator->isValid(new ArrayObject([1, 2, 3])); // true; countable, and count is 3 +$validator->isValid([1, 2]); // true; countable, and count is 2 +$validator->isValid(new ArrayObject([1, 2])); // true; countable, and count is 2 +$validator->isValid([1]); // false; countable, but count is 1 +$validator->isValid(new ArrayObject([1])); // false; countable, but count is 1 +``` + +## Specifying a maximum count + +You may specify a maximum count. When you do, the value must be countable, and +less than or equal to the maximum count you specify in order to be valid. + +```php +$validator = new Zend\Validator\IsCountable(['max' => 2]); + +$validator->isValid([1, 2, 3]); // false; countable, but count is 3 +$validator->isValid(new ArrayObject([1, 2, 3])); // false; countable, but count is 3 +$validator->isValid([1, 2]); // true; countable, and count is 2 +$validator->isValid(new ArrayObject([1, 2])); // true; countable, and count is 2 +$validator->isValid([1]); // true; countable, and count is 1 +$validator->isValid(new ArrayObject([1])); // true; countable, and count is 1 +``` + +## Specifying both minimum and maximum + +If you specify both a minimum and maximum, the count must be _between_ the two, +inclusively (i.e., it may be the minimum or maximum, and any value between). + +```php +$validator = new Zend\Validator\IsCountable([ + 'min' => 3, + 'max' => 5, +]); + +$validator->isValid([1, 2, 3]); // true; countable, and count is 3 +$validator->isValid(new ArrayObject([1, 2, 3])); // true; countable, and count is 3 +$validator->isValid(range(1, 5)); // true; countable, and count is 5 +$validator->isValid(new ArrayObject(range(1, 5))); // true; countable, and count is 5 +$validator->isValid([1, 2]); // false; countable, and count is 2 +$validator->isValid(new ArrayObject([1, 2])); // false; countable, and count is 2 +$validator->isValid(range(1, 6)); // false; countable, and count is 6 +$validator->isValid(new ArrayObject(range(1, 6))); // false; countable, and count is 6 +``` diff --git a/mkdocs.yml b/mkdocs.yml index f72bf9afb..277caf7ff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,7 @@ pages: - InArray: validators/in-array.md - Ip: validators/ip.md - Isbn: validators/isbn.md + - IsCountable: validators/is-countable.md - IsInstanceOf: validators/isinstanceof.md - LessThan: validators/less-than.md - NotEmpty: validators/not-empty.md diff --git a/src/Between.php b/src/Between.php index 386393dbd..05a74bcca 100644 --- a/src/Between.php +++ b/src/Between.php @@ -16,6 +16,15 @@ class Between extends AbstractValidator { const NOT_BETWEEN = 'notBetween'; const NOT_BETWEEN_STRICT = 'notBetweenStrict'; + const VALUE_NOT_NUMERIC = 'valueNotNumeric'; + const VALUE_NOT_STRING = 'valueNotString'; + + /** + * Retain if min and max are numeric values. Allow to not compare string and numeric types + * + * @var boolean + */ + private $numeric; /** * Validation failure message template definitions @@ -24,7 +33,10 @@ class Between extends AbstractValidator */ protected $messageTemplates = [ self::NOT_BETWEEN => "The input is not between '%min%' and '%max%', inclusively", - self::NOT_BETWEEN_STRICT => "The input is not strictly between '%min%' and '%max%'" + self::NOT_BETWEEN_STRICT => "The input is not strictly between '%min%' and '%max%'", + self::VALUE_NOT_NUMERIC => "The min ('%min%') and max ('%max%') values are numeric, but the input is not", + self::VALUE_NOT_STRING => "The min ('%min%') and max ('%max%') values are non-numeric strings, " + . "but the input is not a string", ]; /** @@ -81,7 +93,17 @@ public function __construct($options = null) if (count($options) !== 2 && (! array_key_exists('min', $options) || ! array_key_exists('max', $options)) ) { - throw new Exception\InvalidArgumentException("Missing option. 'min' and 'max' have to be given"); + throw new Exception\InvalidArgumentException("Missing option: 'min' and 'max' have to be given"); + } + + if (is_numeric($options['min']) && is_numeric($options['max'])) { + $this->numeric = true; + } elseif (is_string($options['min']) && is_string($options['max'])) { + $this->numeric = false; + } else { + throw new Exception\InvalidArgumentException( + "Invalid options: 'min' and 'max' should be of the same scalar type" + ); } parent::__construct($options); @@ -164,6 +186,15 @@ public function isValid($value) { $this->setValue($value); + if ($this->numeric && ! is_numeric($value)) { + $this->error(self::VALUE_NOT_NUMERIC); + return false; + } + if (! $this->numeric && ! is_string($value)) { + $this->error(self::VALUE_NOT_STRING); + return false; + } + if ($this->getInclusive()) { if ($this->getMin() > $value || $value > $this->getMax()) { $this->error(self::NOT_BETWEEN); diff --git a/src/CreditCard.php b/src/CreditCard.php index 4d36759da..6bf514bdd 100644 --- a/src/CreditCard.php +++ b/src/CreditCard.php @@ -83,14 +83,14 @@ class CreditCard extends AbstractValidator self::AMERICAN_EXPRESS => [15], self::DINERS_CLUB => [14], self::DINERS_CLUB_US => [16], - self::DISCOVER => [16], + self::DISCOVER => [16, 19], self::JCB => [15, 16], self::LASER => [16, 17, 18, 19], self::MAESTRO => [12, 13, 14, 15, 16, 17, 18, 19], self::MASTERCARD => [16], self::SOLO => [16, 18, 19], self::UNIONPAY => [16, 17, 18, 19], - self::VISA => [16], + self::VISA => [13, 16, 19], ]; /** diff --git a/src/EmailAddress.php b/src/EmailAddress.php index 7f06c2267..bd58018dc 100644 --- a/src/EmailAddress.php +++ b/src/EmailAddress.php @@ -9,6 +9,8 @@ namespace Zend\Validator; +use UConverter; + class EmailAddress extends AbstractValidator { const INVALID = 'emailAddressInvalid'; @@ -342,6 +344,8 @@ protected function validateLocalPart() $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e'; if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->localPart)) { $result = true; + } elseif ($this->validateInternationalizedLocalPart($this->localPart)) { + $result = true; } else { // Try quoted string format (RFC 5321 Chapter 4.1.2) @@ -360,6 +364,26 @@ protected function validateLocalPart() return $result; } + /** + * @param string $localPart Address local part to validate. + * @return bool + */ + protected function validateInternationalizedLocalPart($localPart) + { + if (extension_loaded('intl') + && false === UConverter::transcode($localPart, 'UTF-8', 'UTF-8') + ) { + // invalid utf? + return false; + } + + $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e'; + // RFC 6532 extends atext to include non-ascii utf + // @see https://tools.ietf.org/html/rfc6532#section-3.1 + $uatext = $atext . '\x{80}-\x{FFFF}'; + return (bool) preg_match('/^[' . $uatext . ']+(\x2e+[' . $uatext . ']+)*$/u', $localPart); + } + /** * Returns the found MX Record information after validation including weight for further processing * @@ -511,6 +535,7 @@ public function isValid($value) } // Match hostname part + $hostname = false; if ($this->options['useDomainCheck']) { $hostname = $this->validateHostnamePart(); } @@ -518,13 +543,7 @@ public function isValid($value) $local = $this->validateLocalPart(); // If both parts valid, return true - if ($local && $length) { - if (($this->options['useDomainCheck'] && $hostname) || ! $this->options['useDomainCheck']) { - return true; - } - } - - return false; + return ($local && $length) && (! $this->options['useDomainCheck'] || $hostname); } /** @@ -535,7 +554,7 @@ public function isValid($value) protected function idnToAscii($email) { if (extension_loaded('intl')) { - return (idn_to_ascii($email) ?: $email); + return (idn_to_ascii($email, 0, INTL_IDNA_VARIANT_UTS46) ?: $email); } return $email; } @@ -547,6 +566,10 @@ protected function idnToAscii($email) */ protected function idnToUtf8($email) { + if (strlen($email) == 0) { + return $email; + } + if (extension_loaded('intl')) { // The documentation does not clarify what kind of failure // can happen in idn_to_utf8. One can assume if the source @@ -554,7 +577,7 @@ protected function idnToUtf8($email) // the source string in those cases. // But not when the source string is long enough. // Thus we default to source string ourselves. - return idn_to_utf8($email) ?: $email; + return idn_to_utf8($email, 0, INTL_IDNA_VARIANT_UTS46) ?: $email; } return $email; } diff --git a/src/File/Crc32.php b/src/File/Crc32.php index db8aac806..126d63d56 100644 --- a/src/File/Crc32.php +++ b/src/File/Crc32.php @@ -56,7 +56,7 @@ public function getCrc32() * Sets the crc32 hash for one or multiple files * * @param string|array $options - * @return Crc32 Provides a fluent interface + * @return self Provides a fluent interface */ public function setCrc32($options) { @@ -68,7 +68,7 @@ public function setCrc32($options) * Adds the crc32 hash for one or multiple files * * @param string|array $options - * @return Crc32 Provides a fluent interface + * @return self Provides a fluent interface */ public function addCrc32($options) { @@ -104,12 +104,12 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } - $hashes = array_unique(array_keys($this->getHash())); + $hashes = array_unique(array_keys($this->getHash())); $filehash = hash_file('crc32', $file); if ($filehash === false) { $this->error(self::NOT_DETECTED); diff --git a/src/File/ExcludeExtension.php b/src/File/ExcludeExtension.php index e06830788..1d1cb4218 100644 --- a/src/File/ExcludeExtension.php +++ b/src/File/ExcludeExtension.php @@ -59,7 +59,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } diff --git a/src/File/ExcludeMimeType.php b/src/File/ExcludeMimeType.php index 2f6b5771e..66c8eadb6 100644 --- a/src/File/ExcludeMimeType.php +++ b/src/File/ExcludeMimeType.php @@ -63,7 +63,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_READABLE); return false; } diff --git a/src/File/Extension.php b/src/File/Extension.php index 6f2287840..e502dc993 100644 --- a/src/File/Extension.php +++ b/src/File/Extension.php @@ -100,7 +100,7 @@ public function getCase() * Sets the case to use * * @param bool $case - * @return Extension Provides a fluent interface + * @return self Provides a fluent interface */ public function setCase($case) { @@ -124,7 +124,7 @@ public function getExtension() * Sets the file extensions * * @param string|array $extension The extensions to validate - * @return Extension Provides a fluent interface + * @return self Provides a fluent interface */ public function setExtension($extension) { @@ -137,7 +137,7 @@ public function setExtension($extension) * Adds the file extensions * * @param string|array $extension The extensions to add for validation - * @return Extension Provides a fluent interface + * @return self Provides a fluent interface */ public function addExtension($extension) { @@ -196,7 +196,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } diff --git a/src/File/FilesSize.php b/src/File/FilesSize.php index 237f417cd..079c7a291 100644 --- a/src/File/FilesSize.php +++ b/src/File/FilesSize.php @@ -103,12 +103,12 @@ public function isValid($value, $file = null) 'Value array must be in $_FILES format' ); } - $file = $files; + $file = $files; $files = $files['tmp_name']; } // Is file readable ? - if (empty($files) || false === stream_resolve_include_path($files)) { + if (empty($files) || false === is_readable($files)) { $this->throwError($file, self::NOT_READABLE); continue; } @@ -128,10 +128,10 @@ public function isValid($value, $file = null) if (($max !== null) && ($max < $size)) { if ($this->getByteString()) { $this->options['max'] = $this->toByteString($max); - $this->size = $this->toByteString($size); + $this->size = $this->toByteString($size); $this->throwError($file, self::TOO_BIG); $this->options['max'] = $max; - $this->size = $size; + $this->size = $size; } else { $this->throwError($file, self::TOO_BIG); } @@ -142,10 +142,10 @@ public function isValid($value, $file = null) if (($min !== null) && ($size < $min)) { if ($this->getByteString()) { $this->options['min'] = $this->toByteString($min); - $this->size = $this->toByteString($size); + $this->size = $this->toByteString($size); $this->throwError($file, self::TOO_SMALL); $this->options['min'] = $min; - $this->size = $size; + $this->size = $size; } else { $this->throwError($file, self::TOO_SMALL); } diff --git a/src/File/Hash.php b/src/File/Hash.php index e28e8afb6..f48c2c921 100644 --- a/src/File/Hash.php +++ b/src/File/Hash.php @@ -76,7 +76,7 @@ public function getHash() * Sets the hash for one or multiple files * * @param string|array $options - * @return Hash Provides a fluent interface + * @return self Provides a fluent interface */ public function setHash($options) { @@ -90,8 +90,8 @@ public function setHash($options) * Adds the hash for one or multiple files * * @param string|array $options - * @return Hash Provides a fluent interface * @throws Exception\InvalidArgumentException + * @return self Provides a fluent interface */ public function addHash($options) { @@ -148,7 +148,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } diff --git a/src/File/ImageSize.php b/src/File/ImageSize.php index 023a86b40..62e1d1f7a 100644 --- a/src/File/ImageSize.php +++ b/src/File/ImageSize.php @@ -124,8 +124,8 @@ public function getMinWidth() * Sets the minimum allowed width * * @param int $minWidth - * @return ImageSize Provides a fluid interface * @throws Exception\InvalidArgumentException When minwidth is greater than maxwidth + * @return self Provides a fluid interface */ public function setMinWidth($minWidth) { @@ -154,8 +154,8 @@ public function getMaxWidth() * Sets the maximum allowed width * * @param int $maxWidth - * @return ImageSize Provides a fluid interface * @throws Exception\InvalidArgumentException When maxwidth is less than minwidth + * @return self Provides a fluid interface */ public function setMaxWidth($maxWidth) { @@ -184,8 +184,8 @@ public function getMinHeight() * Sets the minimum allowed height * * @param int $minHeight - * @return ImageSize Provides a fluid interface * @throws Exception\InvalidArgumentException When minheight is greater than maxheight + * @return self Provides a fluid interface */ public function setMinHeight($minHeight) { @@ -214,8 +214,8 @@ public function getMaxHeight() * Sets the maximum allowed height * * @param int $maxHeight - * @return ImageSize Provides a fluid interface * @throws Exception\InvalidArgumentException When maxheight is less than minheight + * @return self Provides a fluid interface */ public function setMaxHeight($maxHeight) { @@ -274,7 +274,7 @@ public function getImageHeight() * Sets the minimum image size * * @param array $options The minimum image dimensions - * @return ImageSize Provides a fluent interface + * @return self Provides a fluent interface */ public function setImageMin($options) { @@ -286,7 +286,7 @@ public function setImageMin($options) * Sets the maximum image size * * @param array|\Traversable $options The maximum image dimensions - * @return ImageSize Provides a fluent interface + * @return self Provides a fluent interface */ public function setImageMax($options) { @@ -298,7 +298,7 @@ public function setImageMax($options) * Sets the minimum and maximum image width * * @param array $options The image width dimensions - * @return ImageSize Provides a fluent interface + * @return self Provides a fluent interface */ public function setImageWidth($options) { @@ -312,7 +312,7 @@ public function setImageWidth($options) * Sets the minimum and maximum image height * * @param array $options The image height dimensions - * @return ImageSize Provides a fluent interface + * @return self Provides a fluent interface */ public function setImageHeight($options) { @@ -351,7 +351,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_READABLE); return false; } diff --git a/src/File/Md5.php b/src/File/Md5.php index 2a88e4084..c36b8a457 100644 --- a/src/File/Md5.php +++ b/src/File/Md5.php @@ -104,12 +104,12 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } - $hashes = array_unique(array_keys($this->getHash())); + $hashes = array_unique(array_keys($this->getHash())); $filehash = hash_file('md5', $file); if ($filehash === false) { $this->error(self::NOT_DETECTED); diff --git a/src/File/MimeType.php b/src/File/MimeType.php index 3c09469a5..3a9d42e6f 100644 --- a/src/File/MimeType.php +++ b/src/File/MimeType.php @@ -176,10 +176,10 @@ public function getMagicFile() * if false, the default MAGIC file from PHP will be used * * @param string $file - * @return MimeType Provides fluid interface * @throws Exception\RuntimeException When finfo can not read the magicfile * @throws Exception\InvalidArgumentException * @throws Exception\InvalidMagicMimeFileException + * @return self Provides fluid interface */ public function setMagicFile($file) { @@ -216,7 +216,7 @@ public function setMagicFile($file) * Disables usage of MagicFile * * @param $disable boolean False disables usage of magic file - * @return MimeType Provides fluid interface + * @return self Provides fluid interface */ public function disableMagicFile($disable) { @@ -249,7 +249,7 @@ public function getHeaderCheck() * Note that this is unsafe and therefor the default value is false * * @param bool $headerCheck - * @return MimeType Provides fluid interface + * @return self Provides fluid interface */ public function enableHeaderCheck($headerCheck = true) { @@ -278,7 +278,7 @@ public function getMimeType($asArray = false) * Sets the mimetypes * * @param string|array $mimetype The mimetypes to validate - * @return MimeType Provides a fluent interface + * @return self Provides a fluent interface */ public function setMimeType($mimetype) { @@ -291,8 +291,8 @@ public function setMimeType($mimetype) * Adds the mimetypes * * @param string|array $mimetype The mimetypes to add for validation - * @return MimeType Provides a fluent interface * @throws Exception\InvalidArgumentException + * @return self Provides a fluent interface */ public function addMimeType($mimetype) { @@ -363,7 +363,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(static::NOT_READABLE); return false; } @@ -385,6 +385,7 @@ public function isValid($value, $file = null) $this->type = null; if (! empty($this->finfo)) { $this->type = finfo_file($this->finfo, $file); + unset($this->finfo); } } diff --git a/src/File/Sha1.php b/src/File/Sha1.php index 2a59039c2..bc3929d1f 100644 --- a/src/File/Sha1.php +++ b/src/File/Sha1.php @@ -104,12 +104,12 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } - $hashes = array_unique(array_keys($this->getHash())); + $hashes = array_unique(array_keys($this->getHash())); $filehash = hash_file('sha1', $file); if ($filehash === false) { $this->error(self::NOT_DETECTED); diff --git a/src/File/Size.php b/src/File/Size.php index d8ac7481b..80a9701b0 100644 --- a/src/File/Size.php +++ b/src/File/Size.php @@ -136,8 +136,8 @@ public function getMin($raw = false) * For example: 2000, 2MB, 0.2GB * * @param int|string $min The minimum file size - * @return Size Provides a fluent interface * @throws Exception\InvalidArgumentException When min is greater than max + * @return self Provides a fluent interface */ public function setMin($min) { @@ -181,8 +181,8 @@ public function getMax($raw = false) * For example: 2000, 2MB, 0.2GB * * @param int|string $max The maximum file size - * @return Size Provides a fluent interface * @throws Exception\InvalidArgumentException When max is smaller than min + * @return self Provides a fluent interface */ public function setMax($max) { @@ -216,7 +216,7 @@ protected function getSize() * Set current size * * @param int $size - * @return Size + * @return self */ protected function setSize($size) { @@ -253,7 +253,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } @@ -270,10 +270,10 @@ public function isValid($value, $file = null) if (($min !== null) && ($size < $min)) { if ($this->getByteString()) { $this->options['min'] = $this->toByteString($min); - $this->size = $this->toByteString($size); + $this->size = $this->toByteString($size); $this->error(self::TOO_SMALL); $this->options['min'] = $min; - $this->size = $size; + $this->size = $size; } else { $this->error(self::TOO_SMALL); } @@ -283,10 +283,10 @@ public function isValid($value, $file = null) if (($max !== null) && ($max < $size)) { if ($this->getByteString()) { $this->options['max'] = $this->toByteString($max); - $this->size = $this->toByteString($size); + $this->size = $this->toByteString($size); $this->error(self::TOO_BIG); $this->options['max'] = $max; - $this->size = $size; + $this->size = $size; } else { $this->error(self::TOO_BIG); } diff --git a/src/File/Upload.php b/src/File/Upload.php index 55025f633..33b664b3a 100644 --- a/src/File/Upload.php +++ b/src/File/Upload.php @@ -9,6 +9,7 @@ namespace Zend\Validator\File; +use Countable; use Zend\Validator\AbstractValidator; use Zend\Validator\Exception; @@ -109,7 +110,10 @@ public function getFiles($file = null) */ public function setFiles($files = []) { - if (count($files) === 0) { + if (null === $files + || ((is_array($files) || $files instanceof Countable) + && count($files) === 0) + ) { $this->options['files'] = $_FILES; } else { $this->options['files'] = $files; diff --git a/src/File/WordCount.php b/src/File/WordCount.php index 61145597a..cbe9ce451 100644 --- a/src/File/WordCount.php +++ b/src/File/WordCount.php @@ -28,8 +28,8 @@ class WordCount extends AbstractValidator * @var array Error message templates */ protected $messageTemplates = [ - self::TOO_MUCH => "Too many words, maximum '%max%' are allowed but '%count%' were counted", - self::TOO_LESS => "Too few words, minimum '%min%' are expected but '%count%' were counted", + self::TOO_MUCH => "Too many words, maximum '%max%' are allowed but '%count%' were counted", + self::TOO_LESS => "Too few words, minimum '%min%' are expected but '%count%' were counted", self::NOT_FOUND => "File is not readable or does not exist", ]; @@ -75,7 +75,7 @@ class WordCount extends AbstractValidator public function __construct($options = null) { if (1 < func_num_args()) { - $args = func_get_args(); + $args = func_get_args(); $options = [ 'min' => array_shift($args), 'max' => array_shift($args), @@ -103,8 +103,8 @@ public function getMin() * Sets the minimum word count * * @param int|array $min The minimum word count - * @return WordCount Provides a fluent interface * @throws Exception\InvalidArgumentException When min is greater than max + * @return self Provides a fluent interface */ public function setMin($min) { @@ -141,8 +141,8 @@ public function getMax() * Sets the maximum file count * * @param int|array $max The maximum word count - * @return WordCount Provides a fluent interface * @throws Exception\InvalidArgumentException When max is smaller than min + * @return self Provides a fluent interface */ public function setMax($max) { @@ -194,7 +194,7 @@ public function isValid($value, $file = null) $this->setValue($filename); // Is file readable ? - if (empty($file) || false === stream_resolve_include_path($file)) { + if (empty($file) || false === is_readable($file)) { $this->error(self::NOT_FOUND); return false; } diff --git a/src/Hostname.php b/src/Hostname.php index 4fa57ce35..1e88b9894 100644 --- a/src/Hostname.php +++ b/src/Hostname.php @@ -69,7 +69,7 @@ class Hostname extends AbstractValidator /** * Array of valid top-level-domains - * IanaVersion 2017031600 + * IanaVersion 2017072500 * * @see ftp://data.iana.org/TLD/tlds-alpha-by-domain.txt List of all TLDs by domain * @see http://www.iana.org/domains/root/db/ Official list of supported TLDs @@ -144,6 +144,7 @@ class Hostname extends AbstractValidator 'aq', 'aquarelle', 'ar', + 'arab', 'aramco', 'archi', 'army', @@ -479,6 +480,7 @@ class Hostname extends AbstractValidator 'estate', 'esurance', 'et', + 'etisalat', 'eu', 'eurovision', 'eus', @@ -613,6 +615,7 @@ class Hostname extends AbstractValidator 'gratis', 'green', 'gripe', + 'grocery', 'group', 'gs', 'gt', @@ -662,6 +665,7 @@ class Hostname extends AbstractValidator 'hosting', 'hot', 'hoteles', + 'hotels', 'hotmail', 'house', 'how', @@ -852,6 +856,7 @@ class Hostname extends AbstractValidator 'man', 'management', 'mango', + 'map', 'market', 'marketing', 'markets', @@ -875,6 +880,7 @@ class Hostname extends AbstractValidator 'men', 'menu', 'meo', + 'merckmsd', 'metlife', 'mg', 'mh', @@ -920,7 +926,6 @@ class Hostname extends AbstractValidator 'msd', 'mt', 'mtn', - 'mtpc', 'mtr', 'mu', 'museum', @@ -1003,7 +1008,6 @@ class Hostname extends AbstractValidator 'orange', 'org', 'organic', - 'orientexpress', 'origins', 'osaka', 'otsuka', @@ -1029,6 +1033,7 @@ class Hostname extends AbstractValidator 'pg', 'ph', 'pharmacy', + 'phd', 'philips', 'phone', 'photo', @@ -1132,6 +1137,7 @@ class Hostname extends AbstractValidator 'rs', 'rsvp', 'ru', + 'rugby', 'ruhr', 'run', 'rw', @@ -1173,6 +1179,7 @@ class Hostname extends AbstractValidator 'scot', 'sd', 'se', + 'search', 'seat', 'secure', 'security', @@ -1444,13 +1451,16 @@ class Hostname extends AbstractValidator 'कॉम', 'セール', '佛山', + 'ಭಾರತ', '慈善', '集团', '在线', '한국', + 'ଭାରତ', '大众汽车', '点看', 'คอม', + 'ভাৰত', 'ভারত', '八卦', 'موقع', @@ -1504,7 +1514,9 @@ class Hostname extends AbstractValidator 'クラウド', 'ભારત', '通販', + 'भारतम्', 'भारत', + 'भारोत', '网店', 'संगठन', '餐厅', @@ -1525,15 +1537,18 @@ class Hostname extends AbstractValidator 'ارامكو', 'ایران', 'العليان', + 'اتصالات', 'امارات', 'بازار', 'پاکستان', 'الاردن', 'موبايلي', + 'بارت', 'بھارت', 'المغرب', 'ابوظبي', 'السعودية', + 'ڀارت', 'كاثوليك', 'سودان', 'همراه', @@ -1544,6 +1559,7 @@ class Hostname extends AbstractValidator '政府', 'شبكة', 'بيتك', + 'عرب', 'გე', '机构', '组织机构', @@ -1560,6 +1576,7 @@ class Hostname extends AbstractValidator 'ελ', '世界', '書籍', + 'ഭാരതം', 'ਭਾਰਤ', '网址', '닷넷', @@ -1629,6 +1646,7 @@ class Hostname extends AbstractValidator * (.ES) Spain https://www.nic.es/media/2008-05/1210147705287.pdf * (.FI) Finland http://www.ficora.fi/en/index/palvelut/fiverkkotunnukset/aakkostenkaytto.html * (.GR) Greece https://grweb.ics.forth.gr/CharacterTable1_en.jsp + * (.HR) Croatia https://www.dns.hr/en/portal/files/Odluka-1,2alfanum-dijak.pdf * (.HU) Hungary http://www.domain.hu/domain/English/szabalyzat/szabalyzat.html * (.IL) Israel http://www.isoc.org.il/domains/il-domain-rules.html * (.INFO) International http://www.nic.info/info/idn @@ -1688,6 +1706,7 @@ class Hostname extends AbstractValidator 'FI' => [1 => '/^[\x{002d}0-9a-zäåö]{1,63}$/iu'], 'GR' => [1 => '/^[\x{002d}0-9a-zΆΈΉΊΌΎ-ΡΣ-ώἀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼῂῃῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲῳῴῶ-ῼ]{1,63}$/iu'], 'HK' => 'Hostname/Cn.php', + 'HR' => [1 => '/^[\x{002d}0-9a-zžćčđš]{1,63}$/iu'], 'HU' => [1 => '/^[\x{002d}0-9a-záéíóöúüőű]{1,63}$/iu'], 'IL' => [1 => '/^[\x{002d}0-9\x{05D0}-\x{05EA}]{1,63}$/iu', 2 => '/^[\x{002d}0-9a-z]{1,63}$/i'], diff --git a/src/Iban.php b/src/Iban.php index cbc35b757..926573b13 100644 --- a/src/Iban.php +++ b/src/Iban.php @@ -75,6 +75,7 @@ class Iban extends AbstractValidator 'BG' => 'BG[0-9]{2}[A-Z]{4}[0-9]{4}[0-9]{2}[A-Z0-9]{8}', 'BH' => 'BH[0-9]{2}[A-Z]{4}[A-Z0-9]{14}', 'BR' => 'BR[0-9]{2}[0-9]{8}[0-9]{5}[0-9]{10}[A-Z][A-Z0-9]', + 'BY' => 'BY[0-9]{2}[A-Z0-9]{4}[0-9]{4}[A-Z0-9]{16}', 'CH' => 'CH[0-9]{2}[0-9]{5}[A-Z0-9]{12}', 'CR' => 'CR[0-9]{2}[0-9]{3}[0-9]{14}', 'CY' => 'CY[0-9]{2}[0-9]{3}[0-9]{5}[A-Z0-9]{16}', diff --git a/src/IsCountable.php b/src/IsCountable.php new file mode 100644 index 000000000..ab553f83f --- /dev/null +++ b/src/IsCountable.php @@ -0,0 +1,197 @@ + "The input must be an array or an instance of \\Countable", + self::NOT_EQUALS => "The input count must equal '%count%'", + self::GREATER_THAN => "The input count must be less than '%max%', inclusively", + self::LESS_THAN => "The input count must be greater than '%min%', inclusively", + ]; + + /** + * Additional variables available for validation failure messages + * + * @var array + */ + protected $messageVariables = [ + 'count' => ['options' => 'count'], + 'min' => ['options' => 'min'], + 'max' => ['options' => 'max'], + ]; + + /** + * Options for the between validator + * + * @var array + */ + protected $options = [ + 'count' => null, + 'min' => null, + 'max' => null, + ]; + + public function setOptions($options = []) + { + foreach (['count', 'min', 'max'] as $option) { + if (! is_array($options) || ! isset($options[$option])) { + continue; + } + + $method = sprintf('set%s', ucfirst($option)); + $this->$method($options[$option]); + unset($options[$option]); + } + + return parent::setOptions($options); + } + + /** + * Returns true if and only if $value is countable (and the count validates against optional values). + * + * @param iterable $value + * @return bool + */ + public function isValid($value) + { + if (! (is_array($value) || $value instanceof Countable)) { + $this->error(self::NOT_COUNTABLE); + return false; + } + + $count = count($value); + + if (is_numeric($this->getCount())) { + if ($count != $this->getCount()) { + $this->error(self::NOT_EQUALS); + return false; + } + + return true; + } + + if (is_numeric($this->getMax()) && $count > $this->getMax()) { + $this->error(self::GREATER_THAN); + return false; + } + + if (is_numeric($this->getMin()) && $count < $this->getMin()) { + $this->error(self::LESS_THAN); + return false; + } + + return true; + } + + /** + * Returns the count option + * + * @return mixed + */ + public function getCount() + { + return $this->options['count']; + } + + /** + * Returns the min option + * + * @return mixed + */ + public function getMin() + { + return $this->options['min']; + } + + /** + * Returns the max option + * + * @return mixed + */ + public function getMax() + { + return $this->options['max']; + } + + /** + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException if either a min or max option + * was previously set. + */ + private function setCount($value) + { + if (isset($this->options['min']) || isset($this->options['max'])) { + throw new Exception\InvalidArgumentException( + 'Cannot set count; conflicts with either a min or max option previously set' + ); + } + $this->options['count'] = $value; + } + + /** + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException if either a count or max option + * was previously set. + */ + private function setMin($value) + { + if (isset($this->options['count'])) { + throw new Exception\InvalidArgumentException( + 'Cannot set count; conflicts with either a count option previously set' + ); + } + $this->options['min'] = $value; + } + + /** + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException if either a count or min option + * was previously set. + */ + private function setMax($value) + { + if (isset($this->options['count'])) { + throw new Exception\InvalidArgumentException( + 'Cannot set count; conflicts with either a count option previously set' + ); + } + $this->options['max'] = $value; + } +} diff --git a/src/ValidatorPluginManagerFactory.php b/src/ValidatorPluginManagerFactory.php index 2a3ffd4e6..e3575af90 100644 --- a/src/ValidatorPluginManagerFactory.php +++ b/src/ValidatorPluginManagerFactory.php @@ -8,6 +8,7 @@ namespace Zend\Validator; use Interop\Container\ContainerInterface; +use Zend\ServiceManager\Config; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; @@ -27,7 +28,30 @@ class ValidatorPluginManagerFactory implements FactoryInterface */ public function __invoke(ContainerInterface $container, $name, array $options = null) { - return new ValidatorPluginManager($container, $options ?: []); + $pluginManager = new ValidatorPluginManager($container, $options ?: []); + + // If this is in a zend-mvc application, the ServiceListener will inject + // merged configuration during bootstrap. + if ($container->has('ServiceListener')) { + return $pluginManager; + } + + // If we do not have a config service, nothing more to do + if (! $container->has('config')) { + return $pluginManager; + } + + $config = $container->get('config'); + + // If we do not have validators configuration, nothing more to do + if (! isset($config['validators']) || ! is_array($config['validators'])) { + return $pluginManager; + } + + // Wire service configuration for validators + (new Config($config['validators']))->configureServiceManager($pluginManager); + + return $pluginManager; } /** diff --git a/test/BetweenTest.php b/test/BetweenTest.php index bf86e4e5f..478c12c96 100644 --- a/test/BetweenTest.php +++ b/test/BetweenTest.php @@ -18,38 +18,178 @@ */ class BetweenTest extends TestCase { + public function providerBasic() + { + return [ + 'inclusive-int-valid-floor' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => true, + 'value' => 1, + ], + 'inclusive-int-valid-between' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => true, + 'value' => 10, + ], + 'inclusive-int-valid-ceiling' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => true, + 'value' => 100, + ], + 'inclusive-int-invaild-below' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => false, + 'value' => 0, + ], + 'inclusive-int-invalid-below-fractional' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => false, + 'value' => 0.99, + ], + 'inclusive-int-invalid-above-fractional' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => false, + 'value' => 100.01, + ], + 'inclusive-int-invalid-above' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => true, + 'expected' => false, + 'value' => 101, + ], + 'exclusive-int-invalid-below' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => false, + 'expected' => false, + 'value' => 0, + ], + 'exclusive-int-invalid-floor' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => false, + 'expected' => false, + 'value' => 1, + ], + 'exclusive-int-invalid-ceiling' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => false, + 'expected' => false, + 'value' => 100, + ], + 'exclusive-int-invalid-above' => [ + 'min' => 1, + 'max' => 100, + 'inclusive' => false, + 'expected' => false, + 'value' => 101, + ], + 'inclusive-string-valid-floor' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => true, + 'expected' => true, + 'value' => 'a', + ], + 'inclusive-string-valid-between' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => true, + 'expected' => true, + 'value' => 'm', + ], + 'inclusive-string-valid-ceiling' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => true, + 'expected' => true, + 'value' => 'z', + ], + 'exclusive-string-invalid-out-of-range' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => false, + 'expected' => false, + 'value' => '!', + ], + 'exclusive-string-invalid-floor' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => false, + 'expected' => false, + 'value' => 'a', + ], + 'exclusive-string-invalid-ceiling' => [ + 'min' => 'a', + 'max' => 'z', + 'inclusive' => false, + 'expected' => false, + 'value' => 'z', + ], + 'inclusive-int-invalid-string' => [ + 'min' => 0, + 'max' => 99999999, + 'inclusive' => true, + 'expected' => false, + 'value' => 'asdasd', + ], + 'inclusive-int-invalid-char' => [ + 'min' => 0, + 'max' => 99999999, + 'inclusive' => true, + 'expected' => false, + 'value' => 'q', + ], + 'inclusive-string-invalid-zero' => [ + 'min' => 'a', + 'max' => 'zzzzz', + 'inclusive' => true, + 'expected' => false, + 'value' => 0, + ], + 'inclusive-string-invalid-non-zero' => [ + 'min' => 'a', + 'max' => 'zzzzz', + 'inclusive' => true, + 'expected' => false, + 'value' => 10, + ], + ]; + } /** * Ensures that the validator follows expected behavior * + * @dataProvider providerBasic + * @param int|float|string $min + * @param int|float|string $max + * @param bool $inclusive + * @param bool $expected + * @param mixed $value * @return void */ - public function testBasic() + public function testBasic($min, $max, $inclusive, $expected, $value) { - /** - * The elements of each array are, in order: - * - minimum - * - maximum - * - inclusive - * - expected validation result - * - array of test input values - */ - $valuesExpected = [ - [1, 100, true, true, [1, 10, 100]], - [1, 100, true, false, [0, 0.99, 100.01, 101]], - [1, 100, false, false, [0, 1, 100, 101]], - ['a', 'z', true, true, ['a', 'b', 'y', 'z']], - ['a', 'z', false, false, ['!', 'a', 'z']] - ]; - foreach ($valuesExpected as $element) { - $validator = new Between(['min' => $element[0], 'max' => $element[1], 'inclusive' => $element[2]]); - foreach ($element[4] as $input) { - $this->assertEquals( - $element[3], - $validator->isValid($input), - 'Failed values: ' . $input . ":" . implode("\n", $validator->getMessages()) - ); - } - } + $validator = new Between(['min' => $min, 'max' => $max, 'inclusive' => $inclusive]); + + $this->assertSame( + $expected, + $validator->isValid($value), + 'Failed value: ' . $value . ':' . implode("\n", $validator->getMessages()) + ); } /** @@ -117,7 +257,7 @@ public function testEqualsMessageVariables() public function testMissingMinOrMax(array $args) { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Missing option. 'min' and 'max' have to be given"); + $this->expectExceptionMessage("Missing option: 'min' and 'max' have to be given"); new Between($args); } @@ -140,7 +280,7 @@ public function testConstructorCanAcceptInclusiveParameter() $this->assertFalse($validator->getInclusive()); } - public function testConstructWithTravesableOptions() + public function testConstructWithTraversableOptions() { $options = new \ArrayObject(['min' => 1, 'max' => 10, 'inclusive' => false]); $validator = new Between($options); @@ -148,4 +288,26 @@ public function testConstructWithTravesableOptions() $this->assertTrue($validator->isValid(5)); $this->assertFalse($validator->isValid(10)); } + + public function testStringValidatedAgainstNumericMinAndMaxIsInvalidAndReturnsAFailureMessage() + { + $validator = new Between(['min' => 1, 'max' => 10]); + $this->assertFalse($validator->isValid('a')); + $messages = $validator->getMessages(); + $this->assertContains( + 'The min (\'1\') and max (\'10\') values are numeric, but the input is not', + $messages + ); + } + + public function testNumericValidatedAgainstStringMinAndMaxIsInvalidAndReturnsAFailureMessage() + { + $validator = new Between(['min' => 'a', 'max' => 'z']); + $this->assertFalse($validator->isValid(10)); + $messages = $validator->getMessages(); + $this->assertContains( + 'The min (\'a\') and max (\'z\') values are non-numeric strings, but the input is not a string', + $messages + ); + } } diff --git a/test/EmailAddressTest.php b/test/EmailAddressTest.php index a13f3e0df..40b37ea43 100644 --- a/test/EmailAddressTest.php +++ b/test/EmailAddressTest.php @@ -235,6 +235,9 @@ public function validEmailAddresses() ]; if (extension_loaded('intl')) { + $return['иван@письмо.рф'] = ['иван@письмо.рф']; + $return['öäü@ä-umlaut.de'] = ['öäü@ä-umlaut.de']; + $return['frédéric@domain.com'] = ['frédéric@domain.com']; $return['bob@тест.рф'] = ['bob@тест.рф']; $return['bob@xn--e1aybc.xn--p1ai'] = ['bob@xn--e1aybc.xn--p1ai']; } @@ -277,7 +280,6 @@ public function invalidEmailAddresses() 'bob @ domain.com' => ['bob @ domain.com'], 'Abc..123@example.com' => ['Abc..123@example.com'], '"bob%jones@domain.com' => ['"bob%jones@domain.com'], - 'иван@письмо.рф' => ['иван@письмо.рф'], 'multiline' => ['bob @domain.com'], @@ -685,6 +687,11 @@ public function testNotSetHostnameValidator() $this->assertInstanceOf(Hostname::class, $hostname); } + public function testIsMxSupported() + { + $validator = new EmailAddress(['useMxCheck' => true, 'allow' => Hostname::ALLOW_ALL]); + $this->assertInternalType('bool', $validator->isMxSupported()); + } /** * Test getMXRecord */ @@ -692,7 +699,6 @@ public function testGetMXRecord() { $this->skipIfOnlineTestsDisabled(); - $validator = new EmailAddress(['useMxCheck' => true, 'allow' => Hostname::ALLOW_ALL]); $validator = new EmailAddress(['useMxCheck' => true, 'allow' => Hostname::ALLOW_ALL]); if (! $validator->isMxSupported()) { diff --git a/test/IsCountableTest.php b/test/IsCountableTest.php new file mode 100644 index 000000000..2084b52c7 --- /dev/null +++ b/test/IsCountableTest.php @@ -0,0 +1,141 @@ + [['count' => 10, 'min' => 1]], + 'count-max' => [['count' => 10, 'max' => 10]], + ]; + } + + /** + * @dataProvider conflictingOptionsProvider + */ + public function testConstructorRaisesExceptionWhenProvidedConflictingOptions(array $options) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('conflicts'); + new IsCountable($options); + } + + public function conflictingSecondaryOptionsProvider() + { + return [ + 'count-min' => [['count' => 10], ['min' => 1]], + 'count-max' => [['count' => 10], ['max' => 10]], + ]; + } + + /** + * @dataProvider conflictingSecondaryOptionsProvider + */ + public function testSetOptionsRaisesExceptionWhenProvidedOptionConflictingWithCurrentSettings( + array $originalOptions, + array $secondaryOptions + ) { + $validator = new IsCountable($originalOptions); + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('conflicts'); + $validator->setOptions($secondaryOptions); + } + + public function testArrayIsValid() + { + $sut = new IsCountable([ + 'min' => 1, + 'max' => 10, + ]); + + $this->assertTrue($sut->isValid(['Foo']), json_encode($sut->getMessages())); + $this->assertCount(0, $sut->getMessages()); + } + + public function testIteratorIsValid() + { + $sut = new IsCountable(); + + $this->assertTrue($sut->isValid(new \SplQueue()), json_encode($sut->getMessages())); + $this->assertCount(0, $sut->getMessages()); + } + + public function testValidEquals() + { + $sut = new IsCountable([ + 'count' => 1, + ]); + + $this->assertTrue($sut->isValid(['Foo'])); + $this->assertCount(0, $sut->getMessages()); + } + + public function testValidMax() + { + $sut = new IsCountable([ + 'max' => 1, + ]); + + $this->assertTrue($sut->isValid(['Foo'])); + $this->assertCount(0, $sut->getMessages()); + } + + public function testValidMin() + { + $sut = new IsCountable([ + 'min' => 1, + ]); + + $this->assertTrue($sut->isValid(['Foo'])); + $this->assertCount(0, $sut->getMessages()); + } + + public function testInvalidNotEquals() + { + $sut = new IsCountable([ + 'count' => 2, + ]); + + $this->assertFalse($sut->isValid(['Foo'])); + $this->assertCount(1, $sut->getMessages()); + } + + public function testInvalidType() + { + $sut = new IsCountable(); + + $this->assertFalse($sut->isValid(new \stdClass())); + $this->assertCount(1, $sut->getMessages()); + } + + public function testInvalidExceedsMax() + { + $sut = new IsCountable([ + 'max' => 1, + ]); + + $this->assertFalse($sut->isValid(['Foo', 'Bar'])); + $this->assertCount(1, $sut->getMessages()); + } + + public function testInvalidExceedsMin() + { + $sut = new IsCountable([ + 'min' => 2, + ]); + + $this->assertFalse($sut->isValid(['Foo'])); + $this->assertCount(1, $sut->getMessages()); + } +} diff --git a/test/ValidatorPluginManagerFactoryTest.php b/test/ValidatorPluginManagerFactoryTest.php index 06ac39b5b..e91e43465 100644 --- a/test/ValidatorPluginManagerFactoryTest.php +++ b/test/ValidatorPluginManagerFactoryTest.php @@ -9,6 +9,7 @@ use Interop\Container\ContainerInterface; use PHPUnit\Framework\TestCase; +use Zend\Validator\Digits; use Zend\Validator\ValidatorInterface; use Zend\Validator\ValidatorPluginManager; use Zend\Validator\ValidatorPluginManagerFactory; @@ -70,4 +71,103 @@ public function testFactoryConfiguresPluginManagerUnderServiceManagerV2() $validators = $factory->createService($container->reveal()); $this->assertSame($validator, $validators->get('test')); } + + public function testConfiguresValidatorServicesWhenFound() + { + $validator = $this->prophesize(ValidatorInterface::class)->reveal(); + $config = [ + 'validators' => [ + 'aliases' => [ + 'test' => Digits::class, + ], + 'factories' => [ + 'test-too' => function ($container) use ($validator) { + return $validator; + }, + ], + ], + ]; + + $container = $this->prophesize(ServiceLocatorInterface::class); + $container->willImplement(ContainerInterface::class); + + $container->has('ServiceListener')->willReturn(false); + $container->has('config')->willReturn(true); + $container->get('config')->willReturn($config); + $container->has('MvcTranslator')->willReturn(false); // necessary due to default initializers + + $factory = new ValidatorPluginManagerFactory(); + $validators = $factory($container->reveal(), 'ValidatorManager'); + + $this->assertInstanceOf(ValidatorPluginManager::class, $validators); + $this->assertTrue($validators->has('test')); + $this->assertInstanceOf(Digits::class, $validators->get('test')); + $this->assertTrue($validators->has('test-too')); + $this->assertSame($validator, $validators->get('test-too')); + } + + public function testDoesNotConfigureValidatorServicesWhenServiceListenerPresent() + { + $validator = $this->prophesize(ValidatorInterface::class)->reveal(); + $config = [ + 'validators' => [ + 'aliases' => [ + 'test' => Digits::class, + ], + 'factories' => [ + 'test-too' => function ($container) use ($validator) { + return $validator; + }, + ], + ], + ]; + + $container = $this->prophesize(ServiceLocatorInterface::class); + $container->willImplement(ContainerInterface::class); + + $container->has('ServiceListener')->willReturn(true); + $container->has('config')->shouldNotBeCalled(); + $container->get('config')->shouldNotBeCalled(); + $container->has('MvcTranslator')->willReturn(false); // necessary due to default initializers + + $factory = new ValidatorPluginManagerFactory(); + $validators = $factory($container->reveal(), 'ValidatorManager'); + + $this->assertInstanceOf(ValidatorPluginManager::class, $validators); + $this->assertFalse($validators->has('test')); + $this->assertFalse($validators->has('test-too')); + } + + public function testDoesNotConfigureValidatorServicesWhenConfigServiceNotPresent() + { + $container = $this->prophesize(ServiceLocatorInterface::class); + $container->willImplement(ContainerInterface::class); + + $container->has('ServiceListener')->willReturn(false); + $container->has('config')->willReturn(false); + $container->get('config')->shouldNotBeCalled(); + $container->has('MvcTranslator')->willReturn(false); // necessary due to default initializers + + $factory = new ValidatorPluginManagerFactory(); + $validators = $factory($container->reveal(), 'ValidatorManager'); + + $this->assertInstanceOf(ValidatorPluginManager::class, $validators); + } + + public function testDoesNotConfigureValidatorServicesWhenConfigServiceDoesNotContainValidatorsConfig() + { + $container = $this->prophesize(ServiceLocatorInterface::class); + $container->willImplement(ContainerInterface::class); + + $container->has('ServiceListener')->willReturn(false); + $container->has('config')->willReturn(true); + $container->get('config')->willReturn(['foo' => 'bar']); + $container->has('MvcTranslator')->willReturn(false); // necessary due to default initializers + + $factory = new ValidatorPluginManagerFactory(); + $validators = $factory($container->reveal(), 'ValidatorManager'); + + $this->assertInstanceOf(ValidatorPluginManager::class, $validators); + $this->assertFalse($validators->has('foo')); + } }