From 68762df9031e353fe33e9b19d3c52bd9c71d2644 Mon Sep 17 00:00:00 2001 From: Shaharia Azam Date: Sat, 21 Dec 2019 03:35:56 +0600 Subject: [PATCH 01/33] adrift URL updated Previous adrift URL was obsolete. It has been updated and fixed --- test/File/_files/magic.mime | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/File/_files/magic.mime b/test/File/_files/magic.mime index f1d242d72..40b1370e4 100644 --- a/test/File/_files/magic.mime +++ b/test/File/_files/magic.mime @@ -167,7 +167,7 @@ # depending on version magic continues with 0x93453E6139FA (V 4.0) # 0x9445376139FA (V 3.90) # 0x9445366139FA (V 3.80) -# this is from source (http://www.adrift.org.uk/) and I have some taf +# this is from source (http://www.adrift.co/) and I have some taf # files, and checked them. #0 belong 0x3C423FC9 #>4 belong 0x6A87C2CF Adrift game file @@ -1055,7 +1055,7 @@ >0 byte x GameCube movie, >0x34 ubeshort x %d x >0x36 ubeshort x %d, ->0x26 ubeshort x %dµs, +>0x26 ubeshort x %dµs, >0x42 ubeshort 0 no audio >0x42 ubeshort >0 %dHz audio @@ -1612,7 +1612,7 @@ # AMGC 0 string \xad6" AMGC archive data # NuLIB -0 string NõFélå NuLIB archive data +0 string NõFélÃ¥ NuLIB archive data # PakLeo 0 string LEOLZW PAKLeo archive data # ChArc @@ -1624,7 +1624,7 @@ # Freeze 0 string \x1f\x9f\x4a\x10\x0a Freeze archive data # KBoom -0 string ¨MP¨ KBoom archive data +0 string ¨MP¨ KBoom archive data # NSQ, must go after CDC Codec 0 string \x76\xff NSQ archive data # DPA @@ -1662,7 +1662,7 @@ # MP3 (archiver, not lossy audio compression) 0 string MP3\x1a MP3-Archiver archive data # ZET -0 string OZÝ ZET archive data +0 string OZÝ ZET archive data # TSComp 0 string \x65\x5d\x13\x8c\x08\x01\x03\x00 TSComp archive data # ARQ @@ -1693,7 +1693,7 @@ # Xtreme 0 string ULEB\0 Xtreme archive data # Pack Magic -0 string @â\1\0 Pack Magic archive data +0 string @â\1\0 Pack Magic archive data # BTS 0 belong&0xfeffffff 0x1a034465 BTS archive data # ELI 5750 @@ -1829,7 +1829,7 @@ # XPack Data 0 string xpa XPack archive data # XPack Single Data -0 string Í\ jm XPack single archive data +0 string Í\ jm XPack single archive data # TODO: missing due to unknown magic/magic at end of file: #DWC @@ -2814,7 +2814,7 @@ >>12 ulelong x \b, sample rate %d # adlib sound files -# From Gürkan Sengün , http://www.linuks.mine.nu +# From Gürkan Sengün , http://www.linuks.mine.nu 0 string RAWADATA RdosPlay RAW 1068 string RoR AMUSIC Adlib Tracker @@ -3972,7 +3972,7 @@ >113 string x (%s) #------------------------------------------------------------------------------ -# Microsoft Xbox executables .xbe (Esa Hyytiä ) +# Microsoft Xbox executables .xbe (Esa Hyytiä ) 0 string XBEH XBE, Microsoft Xbox executable # probabilistic checks whether signed or not >0x0004 ulelong =0x0 @@ -4008,7 +4008,7 @@ # From: Serge van den Boom 0 string \x01ZZZZZ\x01 3DO "Opera" file system -# From Gürkan Sengün , www.linuks.mine.nu +# From Gürkan Sengün , www.linuks.mine.nu 0 string GBS Nintendo Gameboy Music/Audio Data 12 string GameBoy\ Music\ Module Nintendo Gameboy Music Module @@ -6468,8 +6468,8 @@ # 10 SS, 8 SPT # 11 DS, 8 SPT # -# 11111001 Double density 3 floppy disk, high density 5 -# 11110000 High density 3 floppy disk +# 11111001 Double density 3½ floppy disk, high density 5¼ +# 11110000 High density 3½ floppy disk # 11111000 Hard disk any format # @@ -6887,7 +6887,7 @@ 0 string OTTO OpenType font data !:mime application/vnd.ms-opentype -# Gürkan Sengün , www.linuks.mine.nu +# Gürkan Sengün , www.linuks.mine.nu 0 string SplineFontDB: Spline Font Database !:mime application/vnd.font-fontforge-sfd >14 string x version %s @@ -8641,7 +8641,7 @@ >5 byte 0x00 (white background) >5 byte 0xFF (black background) -# Gürkan Sengün , www.linuks.mine.nu +# Gürkan Sengün , www.linuks.mine.nu # http://www.atarimax.com/jindroush.atari.org/afmtatr.html 0 leshort 0x0296 Atari ATR image @@ -9039,7 +9039,7 @@ # rom: file(1) magic for BIOS ROM Extensions found in intel machines # mapped into memory between 0xC0000 and 0xFFFFF -# From Gürkan Sengün , www.linuks.mine.nu +# From Gürkan Sengün , www.linuks.mine.nu 0 beshort 0x55AA BIOS (ia32) ROM Ext. >5 string USB USB >7 string LDR UNDI image @@ -9504,7 +9504,7 @@ # # Linux kernel boot images, from Albert Cahalan # and others such as Axel Kohlmeyer -# and Nicols Lichtmaier +# and Nicolás Lichtmaier # All known start with: b8 c0 07 8e d8 b8 00 90 8e c0 b9 00 01 29 f6 29 # Linux kernel boot images (i386 arch) (Wolfram Kleff) 514 string HdrS Linux kernel @@ -9527,10 +9527,10 @@ >0x1e3 string Loading version 1.3.79 or older >0x1e9 string Loading from prehistoric times -# System.map files - Nicols Lichtmaier +# System.map files - Nicolás Lichtmaier 8 search/1 \ A\ _text Linux kernel symbol map text -# LSM entries - Nicols Lichtmaier +# LSM entries - Nicolás Lichtmaier 0 search/1 Begin3 Linux Software Map entry text 0 search/1 Begin4 Linux Software Map entry text (new format) @@ -11718,7 +11718,7 @@ # natinst: file(1) magic for National Instruments Code Files # -# From Enrique Gmez-Flores +# From Enrique Gámez-Flores # version 1 # Many formats still missing, we use, for the moment LabVIEW # We guess VXI format file. VISA, LabWindowsCVI, BridgeVIEW, etc, are missing @@ -12933,7 +12933,7 @@ >4 belong =2 \b, version 2 # Type: Git index file -# From: Frédéric Brière +# From: Frédéric Brière 0 string DIRC Git index >4 belong >0 \b, version %d >>8 belong >0 \b, %d entries @@ -12962,7 +12962,7 @@ # # http://www.seanet.com/users/matts/riffmci/riffmci.htm # -# AVI section extended by Patrik Rdman +# AVI section extended by Patrik Rådman # 0 string RIFF RIFF (little-endian) data # RIFF Palette format @@ -15809,7 +15809,7 @@ 512 string R\0o\0o\0t\0 Hangul (Korean) Word Processor File 2000 !:mime application/x-hwp -# CosmicBook, from Benot Rouits +# CosmicBook, from Benoît Rouits 0 string CSBK Ted Neslson's CosmicBook hypertext file 2 string EYWR AmigaWriter file From 294e523fc76d6efdede0bc2cbb7f7601bc31ed54 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 17:53:37 -0600 Subject: [PATCH 02/33] docs: adds CHANGELOG entry for #279 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e21b0a3..65cb888e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file, in reverse ### Changed -- Nothing. +- [#279](https://github.com/zendframework/zend-validator/pull/279) updates the `magic.mime` file used for file validations. ### Deprecated From cf217e00fd47e575e9a28a811d159d5402747e19 Mon Sep 17 00:00:00 2001 From: Erik Dannenberg Date: Tue, 9 Apr 2019 18:53:10 +0200 Subject: [PATCH 03/33] fix: Make File\Extension usable for non-existing files Currently the validator fails if the given filename is not readable. This prevents valid use cases like checking if a user configurable filename has an allowed file extension before creating said file. As the validator only operates on the given string there is no need for the current readable check. Also fixes File\ExcludeExtension which suffers from the same issue. --- src/File/ExcludeExtension.php | 6 ------ src/File/Extension.php | 6 ------ 2 files changed, 12 deletions(-) diff --git a/src/File/ExcludeExtension.php b/src/File/ExcludeExtension.php index 9f8d3e034..7dd89aaab 100644 --- a/src/File/ExcludeExtension.php +++ b/src/File/ExcludeExtension.php @@ -47,12 +47,6 @@ public function isValid($value, $file = null) $this->setValue($fileInfo['filename']); - // Is file readable ? - if (empty($fileInfo['file']) || false === is_readable($fileInfo['file'])) { - $this->error(self::NOT_FOUND); - return false; - } - $extension = substr($fileInfo['filename'], strrpos($fileInfo['filename'], '.') + 1); $extensions = $this->getExtension(); diff --git a/src/File/Extension.php b/src/File/Extension.php index 0e1cd9ed0..63767619f 100644 --- a/src/File/Extension.php +++ b/src/File/Extension.php @@ -184,12 +184,6 @@ public function isValid($value, $file = null) $this->setValue($fileInfo['filename']); - // Is file readable ? - if (empty($fileInfo['file']) || false === is_readable($fileInfo['file'])) { - $this->error(self::NOT_FOUND); - return false; - } - $extension = substr($fileInfo['filename'], strrpos($fileInfo['filename'], '.') + 1); $extensions = $this->getExtension(); From f27a0c39727066784075000ca9ff967b3f54896b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 18:46:11 -0600 Subject: [PATCH 04/33] feat: adds allowNonExistentFile option to Extension and ExcludeExtension validators This builds on the original patch. However, to avoid a BC break, this patch adds a new option to each of the Extension and ExcludeExtension file validators, `allowNonExistentFile`. By default, this flag is `false`, preserving the existing behavior where a file must exist in order to be validated. When `true`, the validator does not automatically return `false` for non-existent files, and will continue to test its extension. --- src/File/ExcludeExtension.php | 12 ++++++++++++ src/File/Extension.php | 31 ++++++++++++++++++++++++++++++ test/File/ExcludeExtensionTest.php | 1 + test/File/ExtensionTest.php | 3 +++ 4 files changed, 47 insertions(+) diff --git a/src/File/ExcludeExtension.php b/src/File/ExcludeExtension.php index 7dd89aaab..60a88f7df 100644 --- a/src/File/ExcludeExtension.php +++ b/src/File/ExcludeExtension.php @@ -45,6 +45,16 @@ public function isValid($value, $file = null) { $fileInfo = $this->getFileInfo($value, $file); + // Is file readable ? + if (! $this->getAllowNonExistentFile() + && (empty($fileInfo['file']) || false === is_readable($fileInfo['file'])) + ) { + if (preg_match('/nofile\.mo$/', $fileInfo['file'])) { + } + $this->error(self::NOT_FOUND); + return false; + } + $this->setValue($fileInfo['filename']); $extension = substr($fileInfo['filename'], strrpos($fileInfo['filename'], '.') + 1); @@ -55,6 +65,8 @@ public function isValid($value, $file = null) } elseif (! $this->getCase()) { foreach ($extensions as $ext) { if (strtolower($ext) == strtolower($extension)) { + if (preg_match('/nofile\.mo$/', $fileInfo['file'])) { + } $this->error(self::FALSE_EXTENSION); return false; } diff --git a/src/File/Extension.php b/src/File/Extension.php index 63767619f..875a48bbc 100644 --- a/src/File/Extension.php +++ b/src/File/Extension.php @@ -44,6 +44,7 @@ class Extension extends AbstractValidator protected $options = [ 'case' => false, // Validate case sensitive 'extension' => '', // List of extensions + 'allowNonExistentFile' => false, // Allow validation even if file does not exist ]; /** @@ -170,6 +171,28 @@ public function addExtension($extension) return $this; } + /** + * Returns whether or not to allow validation of non-existent files. + * + * @return bool + */ + public function getAllowNonExistentFile() + { + return $this->options['allowNonExistentFile']; + } + + /** + * Sets the flag indicating whether or not to allow validation of non-existent files. + * + * @param bool $flag Whether or not to allow validation of non-existent files. + * @return self Provides a fluent interface + */ + public function setAllowNonExistentFile($flag) + { + $this->options['allowNonExistentFile'] = (bool) $flag; + return $this; + } + /** * Returns true if and only if the file extension of $value is included in the * set extension list @@ -182,6 +205,14 @@ public function isValid($value, $file = null) { $fileInfo = $this->getFileInfo($value, $file); + // Is file readable ? + if (! $this->getAllowNonExistentFile() + && (empty($fileInfo['file']) || false === is_readable($fileInfo['file'])) + ) { + $this->error(self::NOT_FOUND); + return false; + } + $this->setValue($fileInfo['filename']); $extension = substr($fileInfo['filename'], strrpos($fileInfo['filename'], '.') + 1); diff --git a/test/File/ExcludeExtensionTest.php b/test/File/ExcludeExtensionTest.php index 5ac2a2243..b7eeb344e 100644 --- a/test/File/ExcludeExtensionTest.php +++ b/test/File/ExcludeExtensionTest.php @@ -38,6 +38,7 @@ public function basicBehaviorDataProvider() $noFileTests = [ // Options, isValid Param, Expected value, message ['mo', $testFile, false, 'fileExcludeExtensionNotFound'], + [['extension' => 'gif', 'allowNonExistentFile' => true], $testFile, true, ''], ]; // Dupe data in File Upload format diff --git a/test/File/ExtensionTest.php b/test/File/ExtensionTest.php index 3b6231f19..01cb69e38 100644 --- a/test/File/ExtensionTest.php +++ b/test/File/ExtensionTest.php @@ -38,6 +38,7 @@ public function basicBehaviorDataProvider() $noFileTests = [ // Options, isValid Param, Expected value, message ['mo', $testFile, false, 'fileExtensionNotFound'], + [['extension' => 'mo', 'allowNonExistentFile' => true], $testFile, true, ''], ]; // Dupe data in File Upload format @@ -49,6 +50,8 @@ public function basicBehaviorDataProvider() ]; $testData[] = [$data[0], $fileUpload, $data[2], $data[3]]; } + + return $testData; } From 5f339ffff2557f17cc8bfe5bf12bcd09739de8c4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 18:51:51 -0600 Subject: [PATCH 05/33] docs: adds documentation for new allowNonExistentFile option --- docs/book/validators/file/extension.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/book/validators/file/extension.md b/docs/book/validators/file/extension.md index e53e21162..bcf89de1b 100644 --- a/docs/book/validators/file/extension.md +++ b/docs/book/validators/file/extension.md @@ -14,6 +14,9 @@ The following set of options are supported: against which to test. - `case`: Boolean indicating whether or not extensions should match case sensitively; defaults to `false` (case-insensitive). +- `allowNonExistentFile`: (**Since 2.13.0**) Boolean indicating whether or not + to allow validating a filename for a non-existent file. Defaults to `false` + (will not validate non-existent files). ## Usage Examples From cc8805d9b075b03bccebf33c9ce55075a3570c06 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 18:53:32 -0600 Subject: [PATCH 06/33] docs: adds CHANGELOG entry for #266 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cb888e9..fd729331a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#266](https://github.com/zendframework/zend-validator/pull/266) adds a new option to the `File\Extension` and `File\ExcludeExtension` validators, `allowNonExistentFile`. When set to `true`, the validators will continue validating the extension of the filename given even if the file does not exist. The default is `false`, to preserve backwards compatibility with previous versions. ### Changed From 2ffb409112e43b10b8e5f803b95414a53795fb7c Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Fri, 12 Apr 2019 03:38:14 +0200 Subject: [PATCH 07/33] Adding psr/http-client as suggested package To make this validator PSR18 compliant we are implementing the `psr/http-client` interfaces. --- composer.json | 3 +- composer.lock | 437 +++++++++++++++++++++++++++++++------------------- 2 files changed, 275 insertions(+), 165 deletions(-) diff --git a/composer.json b/composer.json index 89fa4c7da..4d7ca9190 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "zendframework/zend-math": "^2.6", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", "zendframework/zend-session": "^2.8", - "zendframework/zend-uri": "^2.5" + "zendframework/zend-uri": "^2.5", + "psr/http-client": "^1.0" }, "suggest": { "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators", diff --git a/composer.lock b/composer.lock index aa75f44d4..dc2e1db30 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1a7fd31aa89bfed8ec74f609189ec441", + "content-hash": "e7e27b65bbd5272f78747508fd0122f0", "packages": [ { "name": "container-interop/container-interop", @@ -35,6 +35,7 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", + "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, { @@ -136,27 +137,29 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", "shasum": "" }, "require": { "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { @@ -181,25 +184,25 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2019-10-21T16:45:58+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.8.1", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7", + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7", "shasum": "" }, "require": { @@ -234,7 +237,7 @@ "object", "object graph" ], - "time": "2018-06-11T23:09:50+00:00" + "time": "2019-12-15T19:12:40+00:00" }, { "name": "phar-io/manifest", @@ -340,35 +343,33 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "~6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -390,30 +391,30 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", + "version": "4.3.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + "reference": "2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62", + "reference": "2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", + "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", "phpunit/phpunit": "^6.4" }, @@ -441,41 +442,40 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" + "time": "2019-12-20T13:40:23+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -488,42 +488,43 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpspec/prophecy", - "version": "1.8.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", + "phpspec/phpspec": "^2.5 || ^3.2", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev" + "dev-master": "1.10.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -551,7 +552,7 @@ "spy", "stub" ], - "time": "2018-08-05T17:53:17+00:00" + "time": "2019-12-22T21:05:45+00:00" }, { "name": "phpunit/php-code-coverage", @@ -804,16 +805,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.13", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -884,7 +885,7 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -992,6 +993,55 @@ ], "time": "2016-08-06T20:24:11+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/496a823ef742b632934724bf769560c2a5c7c44e", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "time": "2018-10-30T23:29:13+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -1303,16 +1353,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -1339,6 +1389,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1347,17 +1401,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -1366,7 +1416,7 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/global-state", @@ -1727,18 +1777,76 @@ ], "time": "2018-11-07T22:31:41+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-11-27T13:56:44+00:00" + }, { "name": "theseer/tokenizer", - "version": "1.1.0", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { @@ -1765,35 +1873,33 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" + "time": "2019-06-13T22:48:21+00:00" }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "vimeo/psalm": "<3.6.0" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -1815,20 +1921,20 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2019-11-24T13:36:37+00:00" }, { "name": "zendframework/zend-cache", - "version": "2.8.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-cache.git", - "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3" + "reference": "cffd54a2dc4db094976d3b3f05e418a047cc9110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/4983dff629956490c78b88adcc8ece4711d7d8a3", - "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/cffd54a2dc4db094976d3b3f05e418a047cc9110", + "reference": "cffd54a2dc4db094976d3b3f05e418a047cc9110", "shasum": "" }, "require": { @@ -1837,7 +1943,7 @@ "psr/simple-cache": "^1.0", "zendframework/zend-eventmanager": "^2.6.3 || ^3.2", "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", - "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + "zendframework/zend-stdlib": "^3.2.1" }, "provide": { "psr/cache-implementation": "1.0", @@ -1870,8 +1976,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Cache", @@ -1898,7 +2004,7 @@ "psr-6", "zf" ], - "time": "2018-05-01T21:58:00+00:00" + "time": "2019-08-29T18:30:41+00:00" }, { "name": "zendframework/zend-coding-standard", @@ -1987,16 +2093,16 @@ }, { "name": "zendframework/zend-db", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-db.git", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9" + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-db/zipball/5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", + "url": "https://api.github.com/repos/zendframework/zend-db/zipball/77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", "shasum": "" }, "require": { @@ -2007,7 +2113,7 @@ "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -2041,20 +2147,20 @@ "db", "zf" ], - "time": "2018-04-09T13:21:36+00:00" + "time": "2019-02-25T11:37:45+00:00" }, { "name": "zendframework/zend-escaper", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-escaper.git", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074" + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/31d8aafae982f9568287cb4dce987e6aff8fd074", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074", + "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", "shasum": "" }, "require": { @@ -2086,7 +2192,7 @@ "escaper", "zf" ], - "time": "2018-04-25T15:48:53+00:00" + "time": "2019-09-05T20:03:20+00:00" }, { "name": "zendframework/zend-eventmanager", @@ -2144,16 +2250,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.9.0", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "875da9790e5cb16b9a12f41453d5f7c441452daf" + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/875da9790e5cb16b9a12f41453d5f7c441452daf", - "reference": "875da9790e5cb16b9a12f41453d5f7c441452daf", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/d78f2cdde1c31975e18b2a0753381ed7b61118ef", + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef", "shasum": "" }, "require": { @@ -2199,32 +2305,32 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a set of commonly needed data filters", + "description": "Programmatically filter and normalize data and files", "keywords": [ "ZendFramework", "filter", "zf" ], - "time": "2018-12-12T23:14:25+00:00" + "time": "2019-08-19T07:08:04+00:00" }, { "name": "zendframework/zend-http", - "version": "2.8.2", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b" + "reference": "76000da8490b8685d63ff6f6ff8eefa459f6e9e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/2c8aed3d25522618573194e7cc51351f8cd4a45b", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/76000da8490b8685d63ff6f6ff8eefa459f6e9e7", + "reference": "76000da8490b8685d63ff6f6ff8eefa459f6e9e7", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-loader": "^2.5.1", - "zendframework/zend-stdlib": "^3.1 || ^2.7.7", + "zendframework/zend-stdlib": "^3.2.1", "zendframework/zend-uri": "^2.5.2", "zendframework/zend-validator": "^2.10.1" }, @@ -2239,8 +2345,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" } }, "autoload": { @@ -2260,28 +2366,32 @@ "zend", "zf" ], - "time": "2018-08-13T18:47:03+00:00" + "time": "2019-12-04T23:02:34+00:00" }, { "name": "zendframework/zend-i18n", - "version": "2.9.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" + "reference": "84038e6a1838b611dcc491b1c40321fa4c3a123c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/84038e6a1838b611dcc491b1c40321fa4c3a123c", + "reference": "84038e6a1838b611dcc491b1c40321fa4c3a123c", "shasum": "" }, "require": { + "ext-intl": "*", "php": "^5.6 || ^7.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, "require-dev": { - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -2292,7 +2402,6 @@ "zendframework/zend-view": "^2.6.3" }, "suggest": { - "ext-intl": "Required for most features of Zend\\I18n; included in default builds of PHP", "zendframework/zend-cache": "Zend\\Cache component", "zendframework/zend-config": "Zend\\Config component", "zendframework/zend-eventmanager": "You should install this package to use the events in the translator", @@ -2305,8 +2414,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", - "dev-develop": "2.10.x-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" }, "zf": { "component": "Zend\\I18n", @@ -2328,20 +2437,20 @@ "i18n", "zf" ], - "time": "2018-05-16T16:39:13+00:00" + "time": "2019-12-12T14:08:22+00:00" }, { "name": "zendframework/zend-loader", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-loader.git", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c" + "reference": "91da574d29b58547385b2298c020b257310898c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/78f11749ea340f6ca316bca5958eef80b38f9b6c", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c", + "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/91da574d29b58547385b2298c020b257310898c6", + "reference": "91da574d29b58547385b2298c020b257310898c6", "shasum": "" }, "require": { @@ -2373,7 +2482,7 @@ "loader", "zf" ], - "time": "2018-04-30T15:20:54+00:00" + "time": "2019-09-04T19:38:14+00:00" }, { "name": "zendframework/zend-math", @@ -2427,23 +2536,23 @@ }, { "name": "zendframework/zend-servicemanager", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42" + "reference": "a1ed6140d0d3ee803fec96582593ed024950067b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42", - "reference": "9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/a1ed6140d0d3ee803fec96582593ed024950067b", + "reference": "a1ed6140d0d3ee803fec96582593ed024950067b", "shasum": "" }, "require": { "container-interop/container-interop": "^1.2", "php": "^5.6 || ^7.0", "psr/container": "^1.0", - "zendframework/zend-stdlib": "^3.1" + "zendframework/zend-stdlib": "^3.2.1" }, "provide": { "container-interop/container-interop-implementation": "^1.2", @@ -2491,32 +2600,32 @@ "servicemanager", "zf" ], - "time": "2018-01-29T16:48:37+00:00" + "time": "2018-12-22T06:05:09+00:00" }, { "name": "zendframework/zend-session", - "version": "2.8.5", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-session.git", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b" + "reference": "c289c4d733ec23a389e25c7c451f4d062088511f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-session/zipball/2cfd90e1a2f6b066b9f908599251d8f64f07021b", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b", + "url": "https://api.github.com/repos/zendframework/zend-session/zipball/c289c4d733ec23a389e25c7c451f4d062088511f", + "reference": "c289c4d733ec23a389e25c7c451f4d062088511f", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "container-interop/container-interop": "^1.1", "mongodb/mongodb": "^1.0.1", "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", - "phpunit/phpunit": "^5.7.5 || >=6.0.13 <6.5.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.7", @@ -2535,8 +2644,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev", - "dev-develop": "2.9-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Session", @@ -2552,26 +2661,26 @@ "license": [ "BSD-3-Clause" ], - "description": "manage and preserve session data, a logical complement of cookie data, across multiple page requests by the same client", + "description": "Object-oriented interface to PHP sessions and storage", "keywords": [ "ZendFramework", "session", "zf" ], - "time": "2018-02-22T16:33:54+00:00" + "time": "2019-10-28T19:40:43+00:00" }, { "name": "zendframework/zend-uri", - "version": "2.6.1", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f" + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/3b6463645c6766f78ce537c70cb4fdabee1e725f", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/bfc4a5b9a309711e968d7c72afae4ac50c650083", + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083", "shasum": "" }, "require": { @@ -2586,8 +2695,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -2605,7 +2714,7 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-10-07T13:35:33+00:00" } ], "aliases": [], From 31d52d7052a1021eeba4ea303544ef5269d73ad1 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Fri, 12 Apr 2019 03:39:46 +0200 Subject: [PATCH 08/33] Writing out the validator for undisclosed passwords We can now validate if a password has been seen in a password breach. --- src/UndisclosedPassword.php | 186 +++++++++++++++++++++++++++++++ test/UndisclosedPasswordTest.php | 167 +++++++++++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 src/UndisclosedPassword.php create mode 100644 test/UndisclosedPasswordTest.php diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php new file mode 100644 index 000000000..0fb2cd21d --- /dev/null +++ b/src/UndisclosedPassword.php @@ -0,0 +1,186 @@ + 'The provided password was used by others', + self::WRONG_INPUT => 'The provided password failed to be correctly hashed, please verify your input', + self::CONNECTION_FAILURE => 'Unable to reach HIBP service, please try again later', + self::UNKNOWN_ERROR => 'Something happened beyond our control, error reporting should give more details', + ]; + + /** + * @var ClientInterface + */ + private $httpClient; + + /** + * @var RequestInterface + */ + private $httpRequest; + + /** + * @var ResponseInterface + */ + private $httpResponse; + + /** + * @var int + */ + private $count = 0; + + /** + * PasswordBreach constructor. + * + * @param ClientInterface $httpClient + * @param RequestInterface $httpRequest + * @param ResponseInterface $httpResponse + */ + public function __construct( + ClientInterface $httpClient, + RequestInterface $httpRequest, + ResponseInterface $httpResponse + ) { + $this->httpClient = $httpClient; + $this->httpRequest = $httpRequest; + $this->httpResponse = $httpResponse; + } + + /** + * @inheritDoc + */ + public function isValid($value) + { + if (! is_string($value)) { + $this->error(self::WRONG_INPUT); + return false; + } + try { + $isPwnd = $this->isPwnedPassword($value); + } catch (Exception\InvalidArgumentException $invalidArgumentException) { + $this->error(self::WRONG_INPUT); + return false; + } catch (Exception\RuntimeException $runtimeException) { + $this->error(self::CONNECTION_FAILURE); + return false; + } catch (\Exception $exception) { + $this->error(self::UNKNOWN_ERROR); + return false; + } + if ($isPwnd) { + $this->error(self::PASSWORD_BREACHED); + return false; + } + return true; + } + + private function isPwnedPassword($password) + { + $sha1Hash = $this->hashPassword($password); + $rangeHash = $this->getRangeHash($sha1Hash); + $hashList = $this->retrieveHashList($rangeHash); + return $this->hashInResponse($sha1Hash, $hashList); + } + + /** + * We use a SHA1 hashed password for checking it against + * the breached data set of HIBP. + * + * @param string $password + * @return string + * @throws Exception\InvalidArgumentException + */ + private function hashPassword($password) + { + $hashedPassword = \sha1($password); + if (self::SHA1_LENGTH !== strlen($hashedPassword)) { + throw new Exception\InvalidArgumentException($this->messageTemplates[self::WRONG_INPUT]); + } + return strtoupper($hashedPassword); + } + + /** + * Creates a hash range that will be send to HIBP API + * applying K-Anonymity + * + * @param string $passwordHash + * @return string + * @see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-by-exclusively-supporting-anonymity/ + */ + private function getRangeHash($passwordHash) + { + $range = substr($passwordHash, self::HIBP_RANGE_BASE, self::HIBP_RANGE_LENGTH); + return $range; + } + + /** + * Making a connection to the HIBP API to retrieve a + * list of hashes that all have the same range as we + * provided. + * + * @param string $passwordRange + * @return string + * @throws Exception\RuntimeException + */ + private function retrieveHashList($passwordRange) + { + $requestClass = get_class($this->httpRequest); + $request = new $requestClass('GET', '/range/' . $passwordRange); + + try { + $response = $this->httpClient->sendRequest($request); + } catch (ClientExceptionInterface $connectException) { + throw new Exception\RuntimeException($this->messageTemplates[self::CONNECTION_FAILURE]); + } + return (string) $response->getBody(); + } + + /** + * Checks if the password is in the response from HIBP + * + * @param string $sha1Hash + * @param string $resultStream + * @return bool + */ + private function hashInResponse($sha1Hash, $resultStream) + { + $data = explode("\r\n", $resultStream); + $totalCount = self::HIBP_COUNT_BASE; + $hashes = array_filter($data, function ($value) use ($sha1Hash, &$totalCount) { + list($hash, $count) = explode(':', $value); + if (0 === strcmp($hash, substr($sha1Hash, self::HIBP_RANGE_LENGTH))) { + $totalCount = (int) $count; + return true; + } + return false; + }); + if ([] === $hashes) { + return false; + } + $this->count = $totalCount; + return true; + } +} \ No newline at end of file diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php new file mode 100644 index 000000000..5ee75542e --- /dev/null +++ b/test/UndisclosedPasswordTest.php @@ -0,0 +1,167 @@ +httpClient = $this->getMockBuilder(ClientInterface::class) + ->getMockForAbstractClass(); + $this->httpRequest = $this->getMockBuilder(RequestInterface::class) + ->getMockForAbstractClass(); + $this->httpResponse = $this->getMockBuilder(ResponseInterface::class) + ->getMockForAbstractClass(); + + $this->validator = new UndisclosedPassword( + $this->httpClient, + $this->httpRequest, + $this->httpResponse + ); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + $this->httpClient = null; + } + + /** + * Data provider returning good, strong and unseen + * passwords to be used in the validator. + * + * @return array + */ + public function goodPasswordProvider() + { + return [ + ['ABi$B47es.Pfg3n9PjPi'], + ['potence tipple would frisk shoofly'], + ]; + } + + /** + * Data provider for most common used passwords + * + * @return array + * @see https://en.wikipedia.org/wiki/List_of_the_most_common_passwords + */ + public function seenPasswordProvider() + { + return [ + ['123456'], + ['password'], + ['123456789'], + ['12345678'], + ['12345'], + ]; + } + + /** + * Test that a given password was not found in the HIBP + * API service. + * + * @param string $password + * + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @dataProvider goodPasswordProvider + */ + public function testStrongUnseenPasswordsPassValidation($password) + { + $this->httpResponse->method('getBody') + ->will($this->returnCallback(function () use ($password) { + $hash = \sha1('zend-validator'); + return sprintf( + '%s:%d', + strtoupper(substr($hash, UndisclosedPassword::HIBP_RANGE_LENGTH)), + rand(0, 100000) + ); + })); + $this->httpClient->method('sendRequest') + ->will($this->returnValue($this->httpResponse)); + + $this->assertTrue($this->validator->isValid($password)); + } + + /** + * Test that a given password was already seen in the HIBP + * AP service. + * + * @param string $password + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @dataProvider seenPasswordProvider + */ + public function testBreachedPasswordsDoNotPassValidation($password) + { + $this->httpResponse->method('getBody') + ->will($this->returnCallback(function () use ($password) { + $hash = \sha1($password); + return sprintf( + '%s:%d', + strtoupper(substr($hash, UndisclosedPassword::HIBP_RANGE_LENGTH)), + rand(0, 100000) + ); + })); + $this->httpClient->method('sendRequest') + ->will($this->returnValue($this->httpResponse)); + + $this->assertFalse($this->validator->isValid($password)); + } + + /** + * Testing we are setting error messages when a password was found + * in the breach database. + * + * @param string $password + * @depends testBreachedPasswordsDoNotPassValidation + * @dataProvider seenPasswordProvider + */ + public function testBreachedPasswordReturnErrorMessages($password) + { + $this->httpClient->method('sendRequest') + ->will($this->throwException(new \Exception())); + + $this->validator->isValid($password); + $this->assertCount(1, $this->validator->getMessages()); + } +} From 24f0da005d89b48bddd0623b4e60425bfc14eb3b Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Fri, 12 Apr 2019 03:44:08 +0200 Subject: [PATCH 09/33] Fixing coding style violations Running CodeSniffer indicated some issues --- src/UndisclosedPassword.php | 3 +-- test/UndisclosedPasswordTest.php | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 0fb2cd21d..e0f8cb0ea 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -3,7 +3,6 @@ namespace Zend\Validator; - use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; @@ -183,4 +182,4 @@ private function hashInResponse($sha1Hash, $resultStream) $this->count = $totalCount; return true; } -} \ No newline at end of file +} diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php index 5ee75542e..e603c358c 100644 --- a/test/UndisclosedPasswordTest.php +++ b/test/UndisclosedPasswordTest.php @@ -3,7 +3,6 @@ namespace ZendTest\Validator; - use PHPUnit\Framework\MockObject\Generator; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientExceptionInterface; From ccf548a8844ab4347d731bcbfdca4c2bff06e00b Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sat, 13 Apr 2019 19:02:18 +0200 Subject: [PATCH 10/33] Creating a HttpClientException for testing purposes Mocking an interface implementing \Throwable was not possible, creating a dummy \Exception class implementing the interface is the easiest solution without complexity requiring further testing. --- test/TestAsset/HttpClientException.php | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 test/TestAsset/HttpClientException.php diff --git a/test/TestAsset/HttpClientException.php b/test/TestAsset/HttpClientException.php new file mode 100644 index 000000000..414c6c65b --- /dev/null +++ b/test/TestAsset/HttpClientException.php @@ -0,0 +1,11 @@ + Date: Sat, 13 Apr 2019 19:04:49 +0200 Subject: [PATCH 11/33] Removing unreached exceptions The input is verified at an earlier stage so no need for throwing an exception for a condition that never will be reached. --- src/UndisclosedPassword.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index e0f8cb0ea..172ced8e1 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -79,9 +79,6 @@ public function isValid($value) } try { $isPwnd = $this->isPwnedPassword($value); - } catch (Exception\InvalidArgumentException $invalidArgumentException) { - $this->error(self::WRONG_INPUT); - return false; } catch (Exception\RuntimeException $runtimeException) { $this->error(self::CONNECTION_FAILURE); return false; @@ -110,14 +107,10 @@ private function isPwnedPassword($password) * * @param string $password * @return string - * @throws Exception\InvalidArgumentException */ private function hashPassword($password) { $hashedPassword = \sha1($password); - if (self::SHA1_LENGTH !== strlen($hashedPassword)) { - throw new Exception\InvalidArgumentException($this->messageTemplates[self::WRONG_INPUT]); - } return strtoupper($hashedPassword); } From f779428e4ba3a0d0935a5af43e395629dc04dd24 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sat, 13 Apr 2019 19:06:59 +0200 Subject: [PATCH 12/33] Improving tests Adding coverage and more precise testing we can now achieve higher quality with less maintenance. --- test/UndisclosedPasswordTest.php | 75 ++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php index e603c358c..0ebc11006 100644 --- a/test/UndisclosedPasswordTest.php +++ b/test/UndisclosedPasswordTest.php @@ -3,13 +3,13 @@ namespace ZendTest\Validator; -use PHPUnit\Framework\MockObject\Generator; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Zend\Validator\UndisclosedPassword; +use ZendTest\Validator\TestAsset\HttpClientException; class UndisclosedPasswordTest extends TestCase { @@ -96,13 +96,35 @@ public function seenPasswordProvider() ]; } + /** + * Testing that we reject invalid password types + * + * @covers \Zend\Validator\UndisclosedPassword::__construct + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @covers \Zend\Validator\AbstractValidator::createMessage + * @covers \Zend\Validator\AbstractValidator::error + * @todo Can be replaced by a \TypeError being thrown in PHP 7.0 or up + */ + public function testValidationFailsForInvalidInput() + { + $this->assertFalse($this->validator->isValid(true)); + $this->assertFalse($this->validator->isValid((new \stdClass()))); + $this->assertFalse($this->validator->isValid(['foo'])); + } + /** * Test that a given password was not found in the HIBP * API service. * * @param string $password * + * @covers \Zend\Validator\UndisclosedPassword::__construct * @covers \Zend\Validator\UndisclosedPassword::isValid + * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword + * @covers \Zend\Validator\UndisclosedPassword::getRangeHash + * @covers \Zend\Validator\UndisclosedPassword::hashInResponse + * @covers \Zend\Validator\UndisclosedPassword::hashPassword + * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList * @dataProvider goodPasswordProvider */ public function testStrongUnseenPasswordsPassValidation($password) @@ -127,8 +149,16 @@ public function testStrongUnseenPasswordsPassValidation($password) * AP service. * * @param string $password - * @covers \Zend\Validator\UndisclosedPassword::isValid * @dataProvider seenPasswordProvider + * @covers \Zend\Validator\UndisclosedPassword::__construct + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword + * @covers \Zend\Validator\UndisclosedPassword::getRangeHash + * @covers \Zend\Validator\UndisclosedPassword::hashInResponse + * @covers \Zend\Validator\UndisclosedPassword::hashPassword + * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList + * @covers \Zend\Validator\AbstractValidator::createMessage + * @covers \Zend\Validator\AbstractValidator::error */ public function testBreachedPasswordsDoNotPassValidation($password) { @@ -154,11 +184,50 @@ public function testBreachedPasswordsDoNotPassValidation($password) * @param string $password * @depends testBreachedPasswordsDoNotPassValidation * @dataProvider seenPasswordProvider + * @covers \Zend\Validator\UndisclosedPassword::__construct + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword + * @covers \Zend\Validator\UndisclosedPassword::getRangeHash + * @covers \Zend\Validator\UndisclosedPassword::hashInResponse + * @covers \Zend\Validator\UndisclosedPassword::hashPassword + * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList + * @covers \Zend\Validator\AbstractValidator::createMessage + * @covers \Zend\Validator\AbstractValidator::error + * @covers \Zend\Validator\AbstractValidator::getMessages */ public function testBreachedPasswordReturnErrorMessages($password) { $this->httpClient->method('sendRequest') - ->will($this->throwException(new \Exception())); + ->will($this->throwException(new \Exception('foo'))); + + $this->validator->isValid($password); + $this->assertCount(1, $this->validator->getMessages()); + } + + /** + * Testing that we capture any failures when trying to connect with + * the HIBP web service. + * + * @param string $password + * @depends testBreachedPasswordsDoNotPassValidation + * @dataProvider seenPasswordProvider + * @covers \Zend\Validator\UndisclosedPassword::__construct + * @covers \Zend\Validator\UndisclosedPassword::isValid + * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword + * @covers \Zend\Validator\UndisclosedPassword::getRangeHash + * @covers \Zend\Validator\UndisclosedPassword::hashInResponse + * @covers \Zend\Validator\UndisclosedPassword::hashPassword + * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList + * @covers \Zend\Validator\AbstractValidator::createMessage + * @covers \Zend\Validator\AbstractValidator::error + * @covers \Zend\Validator\AbstractValidator::getMessages + */ + public function testValidationDegradesGracefullyWhenNoConnectionCanBeMade($password) + { + $clientException = $this->getMockBuilder(HttpClientException::class) + ->getMock(); + $this->httpClient->method('sendRequest') + ->will($this->throwException($clientException)); $this->validator->isValid($password); $this->assertCount(1, $this->validator->getMessages()); From c69f9993aadf060c637be4da25b8547e7be7d238 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sat, 13 Apr 2019 22:34:57 +0200 Subject: [PATCH 13/33] Implementing PSR-17 for making requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of recreating a new request object, I'm using the request factory to build a new request object. This was indirectly suggested by @azjezz via Twitter (https://twitter.com/azjezz/status/1117112058032656384). One thing is for sure: I ❤️ the PHP community for they give me so much wisdom and make me a smarter person 🙂 --- composer.json | 3 +- composer.lock | 54 +++++++++++++++++++++++++++++++- src/UndisclosedPassword.php | 19 ++++++----- test/UndisclosedPasswordTest.php | 14 ++++----- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index 4d7ca9190..9ee1bedc8 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", "zendframework/zend-session": "^2.8", "zendframework/zend-uri": "^2.5", - "psr/http-client": "^1.0" + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" }, "suggest": { "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators", diff --git a/composer.lock b/composer.lock index dc2e1db30..5357bc6a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e7e27b65bbd5272f78747508fd0122f0", + "content-hash": "c42cc2a1634daf96c8546bea68178d13", "packages": [ { "name": "container-interop/container-interop", @@ -1042,6 +1042,58 @@ ], "time": "2018-10-30T23:29:13+00:00" }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 172ced8e1..5b39d6cea 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -5,8 +5,8 @@ use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class UndisclosedPassword extends AbstractValidator { @@ -37,12 +37,12 @@ final class UndisclosedPassword extends AbstractValidator private $httpClient; /** - * @var RequestInterface + * @var RequestFactoryInterface */ private $httpRequest; /** - * @var ResponseInterface + * @var ResponseFactoryInterface */ private $httpResponse; @@ -55,13 +55,13 @@ final class UndisclosedPassword extends AbstractValidator * PasswordBreach constructor. * * @param ClientInterface $httpClient - * @param RequestInterface $httpRequest - * @param ResponseInterface $httpResponse + * @param RequestFactoryInterface $httpRequest + * @param ResponseFactoryInterface $httpResponse */ public function __construct( ClientInterface $httpClient, - RequestInterface $httpRequest, - ResponseInterface $httpResponse + RequestFactoryInterface $httpRequest, + ResponseFactoryInterface $httpResponse ) { $this->httpClient = $httpClient; $this->httpRequest = $httpRequest; @@ -139,8 +139,7 @@ private function getRangeHash($passwordHash) */ private function retrieveHashList($passwordRange) { - $requestClass = get_class($this->httpRequest); - $request = new $requestClass('GET', '/range/' . $passwordRange); + $request = $this->httpRequest->createRequest('GET', '/range/' . $passwordRange); try { $response = $this->httpClient->sendRequest($request); diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php index 0ebc11006..2b5d9a9d1 100644 --- a/test/UndisclosedPasswordTest.php +++ b/test/UndisclosedPasswordTest.php @@ -4,9 +4,10 @@ namespace ZendTest\Validator; use PHPUnit\Framework\TestCase; -use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Zend\Validator\UndisclosedPassword; use ZendTest\Validator\TestAsset\HttpClientException; @@ -28,11 +29,6 @@ class UndisclosedPasswordTest extends TestCase */ private $httpResponse; - /** - * @var ClientExceptionInterface - */ - private $httpClientException; - /** * @var UndisclosedPassword */ @@ -45,15 +41,17 @@ public function setUp() { $this->httpClient = $this->getMockBuilder(ClientInterface::class) ->getMockForAbstractClass(); - $this->httpRequest = $this->getMockBuilder(RequestInterface::class) + $this->httpRequest = $this->getMockBuilder(RequestFactoryInterface::class) ->getMockForAbstractClass(); $this->httpResponse = $this->getMockBuilder(ResponseInterface::class) ->getMockForAbstractClass(); + $responseFactoryInterface = $this->getMockBuilder(ResponseFactoryInterface::class) + ->getMockForAbstractClass(); $this->validator = new UndisclosedPassword( $this->httpClient, $this->httpRequest, - $this->httpResponse + $responseFactoryInterface ); } From 4bd88612b4de4e239dd6188b6d447262ca21782e Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sat, 13 Apr 2019 23:43:38 +0200 Subject: [PATCH 14/33] Moving PSR components in composer.json As suggested by @Ocramius in PR #264 I'm moving added PSR components to the existing PSR requirement in composer.json --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 9ee1bedc8..ac522f028 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,8 @@ "require-dev": { "phpunit/phpunit": "^6.0.8 || ^5.7.15", "psr/http-message": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -33,9 +35,7 @@ "zendframework/zend-math": "^2.6", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", "zendframework/zend-session": "^2.8", - "zendframework/zend-uri": "^2.5", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "zendframework/zend-uri": "^2.5" }, "suggest": { "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators", From fc86ec2110adf29b33226d026dc8fd45e9a22525 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sun, 14 Apr 2019 00:35:00 +0200 Subject: [PATCH 15/33] Processing feedback from PR #264 Applying the following changes: - Improving error messages in the validator - Improving naming conventions for variable names and constants - Bubbling up important Exceptions to be taken care of higher up the stream - Removing unused or arbitrary variables, messages and constants - Simplifying coverage declarations in unit tests --- src/UndisclosedPassword.php | 70 ++++++++++++-------------------- test/UndisclosedPasswordTest.php | 59 +++++++-------------------- 2 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 5b39d6cea..d700464ba 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -11,24 +11,21 @@ final class UndisclosedPassword extends AbstractValidator { const HIBP_API_URI = 'https://api.pwnedpasswords.com'; - const HIBP_API_TIMEOUT = 300; - const HIBP_CLIENT_UA = 'zend-validator'; - const HIBP_CLIENT_ACCEPT = 'application/vnd.haveibeenpwned.v2+json'; - const HIBP_RANGE_LENGTH = 5; - const HIBP_RANGE_BASE = 0; - const HIBP_COUNT_BASE = 0; - const SHA1_LENGTH = 40; + const HIBP_API_REQUEST_TIMEOUT = 300; + const HIBP_CLIENT_USER_AGENT_STRING = 'zend-validator'; + const HIBP_CLIENT_ACCEPT_HEADER = 'application/vnd.haveibeenpwned.v2+json'; + const HIBP_K_ANONYMITY_HASH_RANGE_LENGTH = 5; + const HIBP_K_ANONYMITY_HASH_RANGE_BASE = 0; + const HIBP_FOUND_PASSWORD_COUNT_BASE = 0; + const SHA1_STRING_LENGTH = 40; const PASSWORD_BREACHED = 'passwordBreached'; - const WRONG_INPUT = 'wrongInput'; - const CONNECTION_FAILURE = 'connFail'; - const UNKNOWN_ERROR = 'unknownError'; + const NOT_A_STRING = 'wrongInput'; protected $messageTemplates = [ - self::PASSWORD_BREACHED => 'The provided password was used by others', - self::WRONG_INPUT => 'The provided password failed to be correctly hashed, please verify your input', - self::CONNECTION_FAILURE => 'Unable to reach HIBP service, please try again later', - self::UNKNOWN_ERROR => 'Something happened beyond our control, error reporting should give more details', + self::PASSWORD_BREACHED => + 'The provided password was found in previous breaches, please create another password', + self::NOT_A_STRING => 'The provided password is not a string, please provide a correct password', ]; /** @@ -39,12 +36,12 @@ final class UndisclosedPassword extends AbstractValidator /** * @var RequestFactoryInterface */ - private $httpRequest; + private $makeHttpRequest; /** * @var ResponseFactoryInterface */ - private $httpResponse; + private $makeHttpResponse; /** * @var int @@ -55,17 +52,17 @@ final class UndisclosedPassword extends AbstractValidator * PasswordBreach constructor. * * @param ClientInterface $httpClient - * @param RequestFactoryInterface $httpRequest - * @param ResponseFactoryInterface $httpResponse + * @param RequestFactoryInterface $makeHttpRequest + * @param ResponseFactoryInterface $makeHttpResponse */ public function __construct( ClientInterface $httpClient, - RequestFactoryInterface $httpRequest, - ResponseFactoryInterface $httpResponse + RequestFactoryInterface $makeHttpRequest, + ResponseFactoryInterface $makeHttpResponse ) { $this->httpClient = $httpClient; - $this->httpRequest = $httpRequest; - $this->httpResponse = $httpResponse; + $this->makeHttpRequest = $makeHttpRequest; + $this->makeHttpResponse = $makeHttpResponse; } /** @@ -74,18 +71,10 @@ public function __construct( public function isValid($value) { if (! is_string($value)) { - $this->error(self::WRONG_INPUT); - return false; - } - try { - $isPwnd = $this->isPwnedPassword($value); - } catch (Exception\RuntimeException $runtimeException) { - $this->error(self::CONNECTION_FAILURE); - return false; - } catch (\Exception $exception) { - $this->error(self::UNKNOWN_ERROR); + $this->error(self::NOT_A_STRING); return false; } + $isPwnd = $this->isPwnedPassword($value); if ($isPwnd) { $this->error(self::PASSWORD_BREACHED); return false; @@ -124,8 +113,7 @@ private function hashPassword($password) */ private function getRangeHash($passwordHash) { - $range = substr($passwordHash, self::HIBP_RANGE_BASE, self::HIBP_RANGE_LENGTH); - return $range; + return substr($passwordHash, self::HIBP_K_ANONYMITY_HASH_RANGE_BASE, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH); } /** @@ -135,17 +123,13 @@ private function getRangeHash($passwordHash) * * @param string $passwordRange * @return string - * @throws Exception\RuntimeException + * @throws ClientExceptionInterface */ private function retrieveHashList($passwordRange) { - $request = $this->httpRequest->createRequest('GET', '/range/' . $passwordRange); + $request = $this->makeHttpRequest->createRequest('GET', '/range/' . $passwordRange); - try { - $response = $this->httpClient->sendRequest($request); - } catch (ClientExceptionInterface $connectException) { - throw new Exception\RuntimeException($this->messageTemplates[self::CONNECTION_FAILURE]); - } + $response = $this->httpClient->sendRequest($request); return (string) $response->getBody(); } @@ -159,10 +143,10 @@ private function retrieveHashList($passwordRange) private function hashInResponse($sha1Hash, $resultStream) { $data = explode("\r\n", $resultStream); - $totalCount = self::HIBP_COUNT_BASE; + $totalCount = self::HIBP_FOUND_PASSWORD_COUNT_BASE; $hashes = array_filter($data, function ($value) use ($sha1Hash, &$totalCount) { list($hash, $count) = explode(':', $value); - if (0 === strcmp($hash, substr($sha1Hash, self::HIBP_RANGE_LENGTH))) { + if (0 === strcmp($hash, substr($sha1Hash, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH))) { $totalCount = (int) $count; return true; } diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php index 2b5d9a9d1..db72a5783 100644 --- a/test/UndisclosedPasswordTest.php +++ b/test/UndisclosedPasswordTest.php @@ -4,6 +4,7 @@ namespace ZendTest\Validator; use PHPUnit\Framework\TestCase; +use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; @@ -97,10 +98,8 @@ public function seenPasswordProvider() /** * Testing that we reject invalid password types * - * @covers \Zend\Validator\UndisclosedPassword::__construct - * @covers \Zend\Validator\UndisclosedPassword::isValid - * @covers \Zend\Validator\AbstractValidator::createMessage - * @covers \Zend\Validator\AbstractValidator::error + * @covers \Zend\Validator\UndisclosedPassword + * @covers \Zend\Validator\AbstractValidator * @todo Can be replaced by a \TypeError being thrown in PHP 7.0 or up */ public function testValidationFailsForInvalidInput() @@ -116,13 +115,7 @@ public function testValidationFailsForInvalidInput() * * @param string $password * - * @covers \Zend\Validator\UndisclosedPassword::__construct - * @covers \Zend\Validator\UndisclosedPassword::isValid - * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword - * @covers \Zend\Validator\UndisclosedPassword::getRangeHash - * @covers \Zend\Validator\UndisclosedPassword::hashInResponse - * @covers \Zend\Validator\UndisclosedPassword::hashPassword - * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList + * @covers \Zend\Validator\UndisclosedPassword * @dataProvider goodPasswordProvider */ public function testStrongUnseenPasswordsPassValidation($password) @@ -132,7 +125,7 @@ public function testStrongUnseenPasswordsPassValidation($password) $hash = \sha1('zend-validator'); return sprintf( '%s:%d', - strtoupper(substr($hash, UndisclosedPassword::HIBP_RANGE_LENGTH)), + strtoupper(substr($hash, UndisclosedPassword::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH)), rand(0, 100000) ); })); @@ -148,15 +141,8 @@ public function testStrongUnseenPasswordsPassValidation($password) * * @param string $password * @dataProvider seenPasswordProvider - * @covers \Zend\Validator\UndisclosedPassword::__construct - * @covers \Zend\Validator\UndisclosedPassword::isValid - * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword - * @covers \Zend\Validator\UndisclosedPassword::getRangeHash - * @covers \Zend\Validator\UndisclosedPassword::hashInResponse - * @covers \Zend\Validator\UndisclosedPassword::hashPassword - * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList - * @covers \Zend\Validator\AbstractValidator::createMessage - * @covers \Zend\Validator\AbstractValidator::error + * @covers \Zend\Validator\UndisclosedPassword + * @covers \Zend\Validator\AbstractValidator */ public function testBreachedPasswordsDoNotPassValidation($password) { @@ -165,7 +151,7 @@ public function testBreachedPasswordsDoNotPassValidation($password) $hash = \sha1($password); return sprintf( '%s:%d', - strtoupper(substr($hash, UndisclosedPassword::HIBP_RANGE_LENGTH)), + strtoupper(substr($hash, UndisclosedPassword::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH)), rand(0, 100000) ); })); @@ -182,24 +168,16 @@ public function testBreachedPasswordsDoNotPassValidation($password) * @param string $password * @depends testBreachedPasswordsDoNotPassValidation * @dataProvider seenPasswordProvider - * @covers \Zend\Validator\UndisclosedPassword::__construct - * @covers \Zend\Validator\UndisclosedPassword::isValid - * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword - * @covers \Zend\Validator\UndisclosedPassword::getRangeHash - * @covers \Zend\Validator\UndisclosedPassword::hashInResponse - * @covers \Zend\Validator\UndisclosedPassword::hashPassword - * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList - * @covers \Zend\Validator\AbstractValidator::createMessage - * @covers \Zend\Validator\AbstractValidator::error - * @covers \Zend\Validator\AbstractValidator::getMessages + * @covers \Zend\Validator\UndisclosedPassword */ public function testBreachedPasswordReturnErrorMessages($password) { $this->httpClient->method('sendRequest') ->will($this->throwException(new \Exception('foo'))); + $this->expectException(\Exception::class); $this->validator->isValid($password); - $this->assertCount(1, $this->validator->getMessages()); + $this->fail('Excpected exception was not thrown'); } /** @@ -209,16 +187,7 @@ public function testBreachedPasswordReturnErrorMessages($password) * @param string $password * @depends testBreachedPasswordsDoNotPassValidation * @dataProvider seenPasswordProvider - * @covers \Zend\Validator\UndisclosedPassword::__construct - * @covers \Zend\Validator\UndisclosedPassword::isValid - * @covers \Zend\Validator\UndisclosedPassword::isPwnedPassword - * @covers \Zend\Validator\UndisclosedPassword::getRangeHash - * @covers \Zend\Validator\UndisclosedPassword::hashInResponse - * @covers \Zend\Validator\UndisclosedPassword::hashPassword - * @covers \Zend\Validator\UndisclosedPassword::retrieveHashList - * @covers \Zend\Validator\AbstractValidator::createMessage - * @covers \Zend\Validator\AbstractValidator::error - * @covers \Zend\Validator\AbstractValidator::getMessages + * @covers \Zend\Validator\UndisclosedPassword */ public function testValidationDegradesGracefullyWhenNoConnectionCanBeMade($password) { @@ -227,7 +196,9 @@ public function testValidationDegradesGracefullyWhenNoConnectionCanBeMade($passw $this->httpClient->method('sendRequest') ->will($this->throwException($clientException)); + $this->expectException(ClientExceptionInterface::class); + $this->validator->isValid($password); - $this->assertCount(1, $this->validator->getMessages()); + $this->fail('Expected ClientException was not thrown'); } } From 7d6377808256baf36c1e71857e9ffec218b80704 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sun, 14 Apr 2019 00:43:36 +0200 Subject: [PATCH 16/33] Removing password found count After reviewing the feedback provided by @Ocramius on PR #264 I've agreed with him that keeping the count of found passwords has no meaning in a validation context and have removed it from the code base. --- src/UndisclosedPassword.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index d700464ba..d70fd8741 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -16,7 +16,6 @@ final class UndisclosedPassword extends AbstractValidator const HIBP_CLIENT_ACCEPT_HEADER = 'application/vnd.haveibeenpwned.v2+json'; const HIBP_K_ANONYMITY_HASH_RANGE_LENGTH = 5; const HIBP_K_ANONYMITY_HASH_RANGE_BASE = 0; - const HIBP_FOUND_PASSWORD_COUNT_BASE = 0; const SHA1_STRING_LENGTH = 40; const PASSWORD_BREACHED = 'passwordBreached'; @@ -43,11 +42,6 @@ final class UndisclosedPassword extends AbstractValidator */ private $makeHttpResponse; - /** - * @var int - */ - private $count = 0; - /** * PasswordBreach constructor. * @@ -143,11 +137,9 @@ private function retrieveHashList($passwordRange) private function hashInResponse($sha1Hash, $resultStream) { $data = explode("\r\n", $resultStream); - $totalCount = self::HIBP_FOUND_PASSWORD_COUNT_BASE; - $hashes = array_filter($data, function ($value) use ($sha1Hash, &$totalCount) { + $hashes = array_filter($data, function ($value) use ($sha1Hash) { list($hash, $count) = explode(':', $value); if (0 === strcmp($hash, substr($sha1Hash, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH))) { - $totalCount = (int) $count; return true; } return false; @@ -155,7 +147,6 @@ private function hashInResponse($sha1Hash, $resultStream) if ([] === $hashes) { return false; } - $this->count = $totalCount; return true; } } From e3c334f9e63d13af7c7e192cf0e4039e6605d7a8 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Sun, 14 Apr 2019 22:34:19 +0200 Subject: [PATCH 17/33] Adding documentation for UndisclosedPassword validator No feature is complete if there's not some user documentation available, so with this I've provided a basic introduction on how to use the validator. --- doc/book/validators/undisclosed-password.md | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/book/validators/undisclosed-password.md diff --git a/doc/book/validators/undisclosed-password.md b/doc/book/validators/undisclosed-password.md new file mode 100644 index 000000000..96e0b7633 --- /dev/null +++ b/doc/book/validators/undisclosed-password.md @@ -0,0 +1,38 @@ +# Undisclosed Password Validator + +`Zend\Validator\UndisclosedPassword` allows you to validate if a given password was found in data breaches using the service [Have I Been Pwned?](https://www.haveibeenpwned.com), in a secure, anonymous way using [K-Anonymity](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2) to ensure passwords are not send in full over the wire. + +> ## Installation requirements +> +> This validator needs to make a request over HTTP, therefor it requires an HTTP client of your choice that implements [PSR-18](https://www.php-fig.org/psr/psr-18/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) request and response factories. +> +> Make sure you have it installed before using this validator: +> +> ```bash +> $ composer require psr/http-client +> $ composer require psr/http-factory +> ``` + +## Basic usage + +To validate if a password was disclosed in a known data breach, you need to provide a HTTP Client that implements `psr\http\ClientInterface`, a `Psr\Http\Message\RequestFactoryInterface` and a `Psr\Http\Message\ResponseFactoryInterface` to the constructor and validate the password you want to check. + +If the password was found via the service, `isValid` will return FALSE. If the password was not found, `isValid` will return TRUE. + +```php +$validator = new Zend\Validator\UndisclosedPassword( + $httpClient, // a PSR-18 HttpClientInterface + $requestFactory, // a PSR-17 RequestFactoryInterface + $responseFactory // a PSR-17 ResponseFactoryInterface +); +``` +```php +$result = $validator->isValid('password'); +// $result is FALSE because "password" was found in a data breach +``` + +```php +$result = $validator->isValid('8aDk=XiW2E.77tLfuAcB'); +// $result is TRUE because "8aDk=XiW2E.77tLfuAcB" was not found in a data breach +``` + From ce3eaa9a4b597cbade1caf616c71a24795aeffbb Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Mon, 15 Apr 2019 11:51:50 +0200 Subject: [PATCH 18/33] Processing minor feedback points in UndisclosedPassword documentation I'm processing the easy to fix documentation issues pointed out by @froschdesign in PR #264 --- doc/book/validators/undisclosed-password.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/book/validators/undisclosed-password.md b/doc/book/validators/undisclosed-password.md index 96e0b7633..80e50d7be 100644 --- a/doc/book/validators/undisclosed-password.md +++ b/doc/book/validators/undisclosed-password.md @@ -2,7 +2,7 @@ `Zend\Validator\UndisclosedPassword` allows you to validate if a given password was found in data breaches using the service [Have I Been Pwned?](https://www.haveibeenpwned.com), in a secure, anonymous way using [K-Anonymity](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2) to ensure passwords are not send in full over the wire. -> ## Installation requirements +> ### Installation requirements > > This validator needs to make a request over HTTP, therefor it requires an HTTP client of your choice that implements [PSR-18](https://www.php-fig.org/psr/psr-18/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) request and response factories. > @@ -15,9 +15,9 @@ ## Basic usage -To validate if a password was disclosed in a known data breach, you need to provide a HTTP Client that implements `psr\http\ClientInterface`, a `Psr\Http\Message\RequestFactoryInterface` and a `Psr\Http\Message\ResponseFactoryInterface` to the constructor and validate the password you want to check. +To validate if a password was disclosed in a known data breach, you need to provide a HTTP Client that implements `Psr\Http\Client\ClientInterface`, a `Psr\Http\Message\RequestFactoryInterface` and a `Psr\Http\Message\ResponseFactoryInterface` to the constructor and validate the password you want to check. -If the password was found via the service, `isValid` will return FALSE. If the password was not found, `isValid` will return TRUE. +If the password was found via the service, `isValid` will return `false`. If the password was not found, `isValid` will return `true`. ```php $validator = new Zend\Validator\UndisclosedPassword( From c709e512c91d91d152b82ad079c6840b5a1b1887 Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Thu, 18 Apr 2019 00:24:46 +0200 Subject: [PATCH 19/33] Creating a full qualified URI Since we have no knowledge how the client is configured, I'm using a fully qualified URI pointing to the HIBP API service. This will prevent issues making a connection. --- src/UndisclosedPassword.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index d70fd8741..20110f770 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -121,7 +121,10 @@ private function getRangeHash($passwordHash) */ private function retrieveHashList($passwordRange) { - $request = $this->makeHttpRequest->createRequest('GET', '/range/' . $passwordRange); + $request = $this->makeHttpRequest->createRequest( + 'GET', + self::HIBP_API_URI . '/range/' . $passwordRange + ); $response = $this->httpClient->sendRequest($request); return (string) $response->getBody(); From 3095cd3161899bc671969fb5ed7ed6d9f6cb1abd Mon Sep 17 00:00:00 2001 From: Michelangelo van Dam Date: Thu, 18 Apr 2019 00:37:59 +0200 Subject: [PATCH 20/33] Providing a working example documentation To make it simple for users to make use of this validation feature, I created a working example people can use. --- doc/book/validators/undisclosed-password.md | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/doc/book/validators/undisclosed-password.md b/doc/book/validators/undisclosed-password.md index 80e50d7be..a0fc65b7f 100644 --- a/doc/book/validators/undisclosed-password.md +++ b/doc/book/validators/undisclosed-password.md @@ -36,3 +36,53 @@ $result = $validator->isValid('8aDk=XiW2E.77tLfuAcB'); // $result is TRUE because "8aDk=XiW2E.77tLfuAcB" was not found in a data breach ``` +## A simple command line example + +In this example I'm using `zendframework/zend-diactoros` for HTTP messaging and `php-http/curl-client` as the HTTP client. Let's begin with installation of all required packages: + +```bash +$ composer require \ + php-http/message \ + php-http/message-factory \ + php-http/discovery \ + php-http/curl-client \ + zendframework/zend-diactoros \ + zendframework/zend-validator +``` + +Next thing is I create a file `undisclosed.php` where I will put in my code. + +```php +isValid('password') ? 'not disclosed' : 'disclosed') . PHP_EOL; +echo 'Password "NVt3MpvQ" is ' . ($undisclosedPassword->isValid('NVt3MpvQ') ? 'not disclosed' : 'disclosed') . PHP_EOL; +``` + +To run it, I use PHP on the command line: + +```bash +$ php undisclosed.php +``` + +And it will give me the following output: + +```bash +Password "password" is disclosed +Password "NVt3MpvQ" is not disclosed +``` From e4c1b55f878ceedc819e9a1179945831cb64bc41 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:07:54 -0600 Subject: [PATCH 21/33] fixup: move and edit docs --- .../book/validators/undisclosed-password.md | 33 +++++++++++-------- mkdocs.yml | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) rename {doc => docs}/book/validators/undisclosed-password.md (60%) diff --git a/doc/book/validators/undisclosed-password.md b/docs/book/validators/undisclosed-password.md similarity index 60% rename from doc/book/validators/undisclosed-password.md rename to docs/book/validators/undisclosed-password.md index a0fc65b7f..badaad823 100644 --- a/doc/book/validators/undisclosed-password.md +++ b/docs/book/validators/undisclosed-password.md @@ -1,12 +1,14 @@ # Undisclosed Password Validator +- **Since 2.13.0** + `Zend\Validator\UndisclosedPassword` allows you to validate if a given password was found in data breaches using the service [Have I Been Pwned?](https://www.haveibeenpwned.com), in a secure, anonymous way using [K-Anonymity](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2) to ensure passwords are not send in full over the wire. > ### Installation requirements > -> This validator needs to make a request over HTTP, therefor it requires an HTTP client of your choice that implements [PSR-18](https://www.php-fig.org/psr/psr-18/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) request and response factories. +> This validator needs to make a request over HTTP; therefore it requires an HTTP client. The validator provides support only for HTTP clients implementing [PSR-18](https://www.php-fig.org/psr/psr-18/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) request and response factories. > -> Make sure you have it installed before using this validator: +> To ensure you have these installed before using this validator, run the following: > > ```bash > $ composer require psr/http-client @@ -15,9 +17,15 @@ ## Basic usage -To validate if a password was disclosed in a known data breach, you need to provide a HTTP Client that implements `Psr\Http\Client\ClientInterface`, a `Psr\Http\Message\RequestFactoryInterface` and a `Psr\Http\Message\ResponseFactoryInterface` to the constructor and validate the password you want to check. +The validator has three required constructor arguments: + +- an HTTP Client that implements `Psr\Http\Client\ClientInterface` +- a `Psr\Http\Message\RequestFactoryInterface` instance +- a `Psr\Http\Message\ResponseFactoryInterface` instance -If the password was found via the service, `isValid` will return `false`. If the password was not found, `isValid` will return `true`. +Once you have an instance, you can then pass a password to its `isValid()` method to determine if it has been disclosed in a known data breach. + +If the password was found via the service, `isValid()` will return `false`. If the password was not found, `isValid()` will return `true`. ```php $validator = new Zend\Validator\UndisclosedPassword( @@ -25,20 +33,17 @@ $validator = new Zend\Validator\UndisclosedPassword( $requestFactory, // a PSR-17 RequestFactoryInterface $responseFactory // a PSR-17 ResponseFactoryInterface ); -``` -```php -$result = $validator->isValid('password'); + +$result = $validator->isValid('password'); // $result is FALSE because "password" was found in a data breach -``` -```php $result = $validator->isValid('8aDk=XiW2E.77tLfuAcB'); // $result is TRUE because "8aDk=XiW2E.77tLfuAcB" was not found in a data breach ``` ## A simple command line example -In this example I'm using `zendframework/zend-diactoros` for HTTP messaging and `php-http/curl-client` as the HTTP client. Let's begin with installation of all required packages: +In this example, I'm using `zendframework/zend-diactoros` to provide HTTP messages, and `php-http/curl-client` as the HTTP client. Let's begin with installation of all required packages: ```bash $ composer require \ @@ -47,10 +52,10 @@ $ composer require \ php-http/discovery \ php-http/curl-client \ zendframework/zend-diactoros \ - zendframework/zend-validator + zendframework/zend-validator ``` -Next thing is I create a file `undisclosed.php` where I will put in my code. +Next, I create a file, `undisclosed.php`, where I put my code: ```php isValid('password') ? 'n echo 'Password "NVt3MpvQ" is ' . ($undisclosedPassword->isValid('NVt3MpvQ') ? 'not disclosed' : 'disclosed') . PHP_EOL; ``` -To run it, I use PHP on the command line: +To run it, I use the PHP command line interpreter: ```bash $ php undisclosed.php ``` -And it will give me the following output: +And it gives me the following output: ```bash Password "password" is disclosed diff --git a/mkdocs.yml b/mkdocs.yml index d7a0d4849..927f7da53 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - Step: validators/step.md - StringLength: validators/string-length.md - Timezone: validators/timezone.md + - UndisclosedPassword: validators/undisclosed-password.md - Uri: validators/uri.md - Uuid: validators/uuid.md - "File Validators": From 425d608a4c40c92bf8d55e400601441bd3bf66e2 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:24:30 -0600 Subject: [PATCH 22/33] qa: make constants private Adds license docblock to new class files. Makes constants in `UndisclosedPassword` private. Updates `UndisclosedPasswordTest` to use `ReflectionClass` when doing constant comparisons. --- src/UndisclosedPassword.php | 26 +++++++++++++++----------- test/UndisclosedPasswordTest.php | 27 ++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 20110f770..4fe63a185 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -1,5 +1,9 @@ diff --git a/test/UndisclosedPasswordTest.php b/test/UndisclosedPasswordTest.php index db72a5783..7de06f680 100644 --- a/test/UndisclosedPasswordTest.php +++ b/test/UndisclosedPasswordTest.php @@ -1,5 +1,9 @@ httpClient = null; } + /** + * @param string|object $classOrInstance + * @return mixed + */ + public function getConstant(string $constant, $classOrInstance) + { + $r = new ReflectionClass($classOrInstance); + return $r->getConstant($constant); + } + /** * Data provider returning good, strong and unseen * passwords to be used in the validator. @@ -125,7 +140,10 @@ public function testStrongUnseenPasswordsPassValidation($password) $hash = \sha1('zend-validator'); return sprintf( '%s:%d', - strtoupper(substr($hash, UndisclosedPassword::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH)), + strtoupper(substr($hash, $this->getConstant( + 'HIBP_K_ANONYMITY_HASH_RANGE_LENGTH', + UndisclosedPassword::class + ))), rand(0, 100000) ); })); @@ -151,7 +169,10 @@ public function testBreachedPasswordsDoNotPassValidation($password) $hash = \sha1($password); return sprintf( '%s:%d', - strtoupper(substr($hash, UndisclosedPassword::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH)), + strtoupper(substr($hash, $this->getConstant( + 'HIBP_K_ANONYMITY_HASH_RANGE_LENGTH', + UndisclosedPassword::class + ))), rand(0, 100000) ); })); From 07e51da459523db5a1141a44a97d5d247ee1797b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:27:36 -0600 Subject: [PATCH 23/33] feat: bump PHP requirement to 7.1, and support 7.4 Bumps the minimum supported PHP version to 7.1, to allow us to use private constants (and reduce BC breaks going forwards). Adds support for PHP 7.4 to the test matrix. --- .travis.yml | 32 +++++++++++--------------------- composer.json | 2 +- composer.lock | 4 ++-- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 429f28631..aa6d577a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,33 +18,14 @@ env: matrix: include: - - php: 5.6 - env: - - DEPS=lowest - - php: 5.6 - env: - - DEPS=locked - - EXECUTE_HOSTNAME_CHECK=true - - TEST_COVERAGE=true - - php: 5.6 - env: - - DEPS=latest - - php: 7 - env: - - DEPS=lowest - - php: 7 - env: - - DEPS=locked - - CS_CHECK=true - - php: 7 - env: - - DEPS=latest - php: 7.1 env: - DEPS=lowest - php: 7.1 env: - DEPS=locked + - EXECUTE_HOSTNAME_CHECK=true + - TEST_COVERAGE=true - php: 7.1 env: - DEPS=latest @@ -66,6 +47,15 @@ matrix: - php: 7.3 env: - DEPS=latest + - php: 7.4 + env: + - DEPS=lowest + - php: 7.4 + env: + - DEPS=locked + - php: 7.4 + env: + - DEPS=latest before_install: - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi diff --git a/composer.json b/composer.json index ac522f028..a0e60940a 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "forum": "https://discourse.zendframework.com/c/questions/components" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1", "zendframework/zend-stdlib": "^3.2.1", "container-interop/container-interop": "^1.1" }, diff --git a/composer.lock b/composer.lock index 5357bc6a6..087764e79 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c42cc2a1634daf96c8546bea68178d13", + "content-hash": "177c10deb5d622d2cc787d8aec3c8d4c", "packages": [ { "name": "container-interop/container-interop", @@ -2775,7 +2775,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "platform-dev": [] } From 89a6646e0037dc2c72e5bc6b7071f03e4a0acc16 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:28:51 -0600 Subject: [PATCH 24/33] fix: runs update_hostname_validator to update hostname list And hopefully fix Travis test errors. --- src/Hostname.php | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Hostname.php b/src/Hostname.php index 6823eab12..5b4fdcdc8 100644 --- a/src/Hostname.php +++ b/src/Hostname.php @@ -69,7 +69,7 @@ class Hostname extends AbstractValidator /** * Array of valid top-level-domains - * IanaVersion 2019081200 + * IanaVersion 2019122700 * * @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 @@ -287,7 +287,6 @@ class Hostname extends AbstractValidator 'career', 'careers', 'cars', - 'cartier', 'casa', 'case', 'caseih', @@ -320,7 +319,6 @@ class Hostname extends AbstractValidator 'chintai', 'christmas', 'chrome', - 'chrysler', 'church', 'ci', 'cipriani', @@ -372,6 +370,7 @@ class Hostname extends AbstractValidator 'coupon', 'coupons', 'courses', + 'cpa', 'cr', 'credit', 'creditcard', @@ -433,7 +432,6 @@ class Hostname extends AbstractValidator 'do', 'docs', 'doctor', - 'dodge', 'dog', 'domains', 'dot', @@ -443,7 +441,6 @@ class Hostname extends AbstractValidator 'dubai', 'duck', 'dunlop', - 'duns', 'dupont', 'durban', 'dvag', @@ -479,7 +476,6 @@ class Hostname extends AbstractValidator 'eurovision', 'eus', 'events', - 'everbank', 'exchange', 'expert', 'exposed', @@ -769,12 +765,10 @@ class Hostname extends AbstractValidator 'kz', 'la', 'lacaixa', - 'ladbrokes', 'lamborghini', 'lamer', 'lancaster', 'lancia', - 'lancome', 'land', 'landrover', 'lanxess', @@ -814,6 +808,7 @@ class Hostname extends AbstractValidator 'lixil', 'lk', 'llc', + 'llp', 'loan', 'loans', 'locker', @@ -889,7 +884,6 @@ class Hostname extends AbstractValidator 'mo', 'mobi', 'mobile', - 'mobily', 'moda', 'moe', 'moi', @@ -897,7 +891,6 @@ class Hostname extends AbstractValidator 'monash', 'money', 'monster', - 'mopar', 'mormon', 'mortgage', 'moscow', @@ -905,7 +898,6 @@ class Hostname extends AbstractValidator 'motorcycles', 'mov', 'movie', - 'movistar', 'mp', 'mq', 'mr', @@ -1025,7 +1017,6 @@ class Hostname extends AbstractValidator 'photography', 'photos', 'physio', - 'piaget', 'pics', 'pictet', 'pictures', @@ -1227,7 +1218,6 @@ class Hostname extends AbstractValidator 'spreadbetting', 'sr', 'srl', - 'srt', 'ss', 'st', 'stada', @@ -1280,7 +1270,6 @@ class Hostname extends AbstractValidator 'tech', 'technology', 'tel', - 'telefonica', 'temasek', 'tennis', 'teva', @@ -1340,7 +1329,6 @@ class Hostname extends AbstractValidator 'ua', 'ubank', 'ubs', - 'uconnect', 'ug', 'uk', 'unicom', @@ -1393,7 +1381,6 @@ class Hostname extends AbstractValidator 'walter', 'wang', 'wanggou', - 'warman', 'watch', 'watches', 'weather', @@ -1524,7 +1511,6 @@ class Hostname extends AbstractValidator 'موريتانيا', 'پاکستان', 'الاردن', - 'موبايلي', 'بارت', 'بھارت', 'المغرب', @@ -1556,6 +1542,7 @@ class Hostname extends AbstractValidator '大拿', 'みんな', 'グーグル', + 'ευ', 'ελ', '世界', '書籍', From 46393e965f870b0aad0f2451bae82d78c18f052b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:33:19 -0600 Subject: [PATCH 25/33] fix: add typehints to UndisclosedPassword On all internal methods. --- src/UndisclosedPassword.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 4fe63a185..2cb16c01c 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -48,10 +48,6 @@ final class UndisclosedPassword extends AbstractValidator /** * PasswordBreach constructor. - * - * @param ClientInterface $httpClient - * @param RequestFactoryInterface $makeHttpRequest - * @param ResponseFactoryInterface $makeHttpResponse */ public function __construct( ClientInterface $httpClient, @@ -80,7 +76,7 @@ public function isValid($value) return true; } - private function isPwnedPassword($password) + private function isPwnedPassword(string $password) : bool { $sha1Hash = $this->hashPassword($password); $rangeHash = $this->getRangeHash($sha1Hash); @@ -95,7 +91,7 @@ private function isPwnedPassword($password) * @param string $password * @return string */ - private function hashPassword($password) + private function hashPassword(string $password) : string { $hashedPassword = \sha1($password); return strtoupper($hashedPassword); @@ -109,7 +105,7 @@ private function hashPassword($password) * @return string * @see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-by-exclusively-supporting-anonymity/ */ - private function getRangeHash($passwordHash) + private function getRangeHash(string $passwordHash) : string { return substr($passwordHash, self::HIBP_K_ANONYMITY_HASH_RANGE_BASE, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH); } @@ -123,7 +119,7 @@ private function getRangeHash($passwordHash) * @return string * @throws ClientExceptionInterface */ - private function retrieveHashList($passwordRange) + private function retrieveHashList(string $passwordRange) : string { $request = $this->makeHttpRequest->createRequest( 'GET', @@ -141,7 +137,7 @@ private function retrieveHashList($passwordRange) * @param string $resultStream * @return bool */ - private function hashInResponse($sha1Hash, $resultStream) + private function hashInResponse(string $sha1Hash, string $resultStream) : bool { $data = explode("\r\n", $resultStream); $hashes = array_filter($data, function ($value) use ($sha1Hash) { From 66d9b3eda3f18ee0b11ac765fd3f4b5fb73771c6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:34:14 -0600 Subject: [PATCH 26/33] qa: remove intermediate variable --- src/UndisclosedPassword.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UndisclosedPassword.php b/src/UndisclosedPassword.php index 2cb16c01c..00c305bfe 100644 --- a/src/UndisclosedPassword.php +++ b/src/UndisclosedPassword.php @@ -68,11 +68,12 @@ public function isValid($value) $this->error(self::NOT_A_STRING); return false; } - $isPwnd = $this->isPwnedPassword($value); - if ($isPwnd) { + + if ($this->isPwnedPassword($value)) { $this->error(self::PASSWORD_BREACHED); return false; } + return true; } From d3355e92008ae178686aa1e0b8b6302790a11bbb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:38:07 -0600 Subject: [PATCH 27/33] docs: adds CHANGELOG entry for #264 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd729331a..a2ece1317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,14 @@ All notable changes to this project will be documented in this file, in reverse ### Added +- [#264](https://github.com/zendframework/zend-validator/pull/264) adds `Zend\Validator\UndisclosedPassword`, which can be used to determine if a password has been exposed in a known data breach as reported on the [Have I Been Pwned?](https://www.haveibeenpwned.com) website. [Documentation](https://docs.zendframework.com/zend-validator/validators/undisclosed-password/) + - [#266](https://github.com/zendframework/zend-validator/pull/266) adds a new option to the `File\Extension` and `File\ExcludeExtension` validators, `allowNonExistentFile`. When set to `true`, the validators will continue validating the extension of the filename given even if the file does not exist. The default is `false`, to preserve backwards compatibility with previous versions. ### Changed +- [#264](https://github.com/zendframework/zend-validator/pull/264) bumps the minimum supported PHP version to 7.1.0. + - [#279](https://github.com/zendframework/zend-validator/pull/279) updates the `magic.mime` file used for file validations. ### Deprecated @@ -18,7 +22,7 @@ All notable changes to this project will be documented in this file, in reverse ### Removed -- Nothing. +- [#264](https://github.com/zendframework/zend-validator/pull/264) removes support for PHP versions prior to 7.1.0. ### Fixed From eb9a7e71dd42959d35dc8b25af9af442ef40b4ef Mon Sep 17 00:00:00 2001 From: webimpress Date: Sat, 12 Oct 2019 13:41:36 +0100 Subject: [PATCH 28/33] Strict mode for Date validator Input must be always in the defined format and must be the same as output of format method of DateTime. Strict mode is set to `false` by default to keep BC. Resolve #33 Fix zendframework/zendframework#6407 --- src/Date.php | 39 ++++++++++++++++++++- test/DateTest.php | 88 +++++++++++++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/Date.php b/src/Date.php index bd2fd9070..461874360 100644 --- a/src/Date.php +++ b/src/Date.php @@ -56,6 +56,11 @@ class Date extends AbstractValidator */ protected $format = self::FORMAT_DEFAULT; + /** + * @var bool + */ + protected $strict = false; + /** * Sets validator options * @@ -100,6 +105,32 @@ public function setFormat($format = self::FORMAT_DEFAULT) return $this; } + /** + * @param bool $strict + * @return $this + */ + public function setStrict($strict) + { + if (! is_bool($strict)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected boolean value; %s received', + is_object($strict) ? get_class($strict) : gettype($strict) + )); + } + + $this->strict = $strict; + + return $this; + } + + /** + * @return bool + */ + public function isStrict() + { + return $this->strict; + } + /** * Returns true if $value is a DateTime instance or can be converted into one. * @@ -110,11 +141,17 @@ public function isValid($value) { $this->setValue($value); - if (! $this->convertToDateTime($value)) { + $date = $this->convertToDateTime($value); + if (! $date) { $this->error(self::INVALID_DATE); return false; } + if ($this->isStrict() && $date->format($this->getFormat()) !== $value) { + $this->error(self::FALSEFORMAT); + return false; + } + return true; } diff --git a/test/DateTest.php b/test/DateTest.php index c4398d9ca..285483d27 100644 --- a/test/DateTest.php +++ b/test/DateTest.php @@ -44,48 +44,53 @@ public function testSetFormatIgnoresNull() public function datesDataProvider() { return [ - // date format isValid - ['2007-01-01', null, true], - ['2007-02-28', null, true], - ['2007-02-29', null, false], - ['2008-02-29', null, true], - ['2007-02-30', null, false], - ['2007-02-99', null, false], - ['2007-02-99', 'Y-m-d', false], - ['9999-99-99', null, false], - ['9999-99-99', 'Y-m-d', false], - ['Jan 1 2007', null, false], - ['Jan 1 2007', 'M j Y', true], - ['asdasda', null, false], - ['sdgsdg', null, false], - ['2007-01-01something', null, false], - ['something2007-01-01', null, false], - ['10.01.2008', 'd.m.Y', true], - ['01 2010', 'm Y', true], - ['2008/10/22', 'd/m/Y', false], - ['22/10/08', 'd/m/y', true], - ['22/10', 'd/m/Y', false], + // date format isValid isValid Strict + ['2007-01-01', null, true, true], + ['2007-02-28', null, true, true], + ['2007-02-29', null, false, false], + ['2008-02-29', null, true, true], + ['2007-02-30', null, false, false], + ['2007-02-99', null, false, false], + ['2007-02-99', 'Y-m-d', false, false], + ['9999-99-99', null, false, false], + ['9999-99-99', 'Y-m-d', false, false], + ['Jan 1 2007', null, false, false], + ['Jan 1 2007', 'M j Y', true, true], + ['asdasda', null, false, false], + ['sdgsdg', null, false, false], + ['2007-01-01something', null, false, false], + ['something2007-01-01', null, false, false], + ['10.01.2008', 'd.m.Y', true, true], + ['01 2010', 'm Y', true, true], + ['2008/10/22', 'd/m/Y', false, false], + ['22/10/08', 'd/m/y', true, true], + ['22/10', 'd/m/Y', false, false], // time - ['2007-01-01T12:02:55Z', DateTime::ISO8601, true], - ['12:02:55', 'H:i:s', true], - ['25:02:55', 'H:i:s', false], + ['2007-01-01T12:02:55Z', DateTime::ISO8601, true, false], + ['2007-01-01T12:02:55+0000', DateTime::ISO8601, true, true], + ['12:02:55', 'H:i:s', true, true], + ['25:02:55', 'H:i:s', false, false], // int - [0, null, true], - [1340677235, null, true], + [0, null, true, false], + [6, 'd', true, false], + ['6', 'd', true, false], + ['06', 'd', true, true], + [123, null, true, false], + [1340677235, null, true, false], // 32bit version of php will convert this to double - [999999999999, null, true], + [999999999999, null, true, false], // double - [12.12, null, false], + [12.12, null, false, false], // array - [['2012', '06', '25'], null, true], + [['2012', '06', '25'], null, true, false], // 0012-06-25 is a valid date, if you want 2012, use 'y' instead of 'Y' - [['12', '06', '25'], null, true], - [['2012', '06', '33'], null, false], - [[1 => 1], null, false], + [['12', '06', '25'], null, true, false], + [['2012', '06', '33'], null, false, false], + [[1 => 1], null, false, false], // DateTime - [new DateTime(), null, true], + [new DateTime(), null, true, false], // invalid obj - [new stdClass(), null, false], + [new stdClass(), null, false, false], ]; } @@ -100,6 +105,21 @@ public function testBasic($input, $format, $result) $this->assertEquals($result, $this->validator->isValid($input)); } + /** + * @dataProvider datesDataProvider + * + * @param mixed $input + * @param string|null $format + * @param bool $result + * @param bool $resultStrict + */ + public function testBasicStrictMode($input, $format, $result, $resultStrict) + { + $this->validator->setStrict(true); + $this->validator->setFormat($format); + $this->assertSame($resultStrict, $this->validator->isValid($input)); + } + public function testDateTimeImmutable() { $this->assertTrue($this->validator->isValid(new DateTimeImmutable())); From 1fd37b924bdd6b0a0a1b66a6532850914ec8aea1 Mon Sep 17 00:00:00 2001 From: webimpress Date: Mon, 14 Oct 2019 14:07:36 +0100 Subject: [PATCH 29/33] Adds example with Unix timestamp format - per @svycka --- test/DateTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/DateTest.php b/test/DateTest.php index 285483d27..d93f99c2a 100644 --- a/test/DateTest.php +++ b/test/DateTest.php @@ -77,6 +77,8 @@ public function datesDataProvider() ['06', 'd', true, true], [123, null, true, false], [1340677235, null, true, false], + [1340677235, 'U', true, false], + ['1340677235', 'U', true, true], // 32bit version of php will convert this to double [999999999999, null, true, false], // double From 5dfcb5fee0976994654d7785929751e5a41de538 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 21:48:22 -0600 Subject: [PATCH 30/33] qa: use 7.1 features on new code Updates new methods of Date validator and its tests to use typehints. Updates license docblock year range. --- src/Date.php | 21 +++------------------ test/DateTest.php | 7 ++----- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/Date.php b/src/Date.php index 461874360..c096c9653 100644 --- a/src/Date.php +++ b/src/Date.php @@ -3,7 +3,7 @@ * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository - * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @copyright Copyright (c) 2005-2019 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ @@ -105,28 +105,13 @@ public function setFormat($format = self::FORMAT_DEFAULT) return $this; } - /** - * @param bool $strict - * @return $this - */ - public function setStrict($strict) + public function setStrict(bool $strict) : self { - if (! is_bool($strict)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Expected boolean value; %s received', - is_object($strict) ? get_class($strict) : gettype($strict) - )); - } - $this->strict = $strict; - return $this; } - /** - * @return bool - */ - public function isStrict() + public function isStrict() : bool { return $this->strict; } diff --git a/test/DateTest.php b/test/DateTest.php index d93f99c2a..cd92484a2 100644 --- a/test/DateTest.php +++ b/test/DateTest.php @@ -3,7 +3,7 @@ * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository - * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @copyright Copyright (c) 2005-2019 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ @@ -111,11 +111,8 @@ public function testBasic($input, $format, $result) * @dataProvider datesDataProvider * * @param mixed $input - * @param string|null $format - * @param bool $result - * @param bool $resultStrict */ - public function testBasicStrictMode($input, $format, $result, $resultStrict) + public function testBasicStrictMode($input, ?string $format, bool $result, bool $resultStrict) : void { $this->validator->setStrict(true); $this->validator->setFormat($format); From 8bdb9db06ad5b7111a7a06e3a6e3a63a5ad7b747 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 22:00:31 -0600 Subject: [PATCH 31/33] docs: documents Date validator strict mode --- docs/book/validators/date.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/book/validators/date.md b/docs/book/validators/date.md index 83de95a40..fe484722e 100644 --- a/docs/book/validators/date.md +++ b/docs/book/validators/date.md @@ -33,3 +33,24 @@ $validator = new Zend\Validator\Date(['format' => 'Y']); $validator->isValid('2010'); // returns true $validator->isValid('May'); // returns false ``` + +## Strict mode + +- **Since 2.13.0** + +By default, `Zend\Validator\Date` only validates that it can convert the +provided value to a valid `DateTime` value. + +If you want to require that the date is specified in a specific format, you can +provide both the [date format](#specifying-a-date-format) and the `strict` +options. In such a scenario, the value must both be covertable to a `DateTime` +value **and** be in the same format as provided to the validator. (Generally, +this will mean the value must be a string.) + +```php +$validator = new Zend\Validator\Date(['format' => 'Y-m-d', 'strict' => true]); + +$validator->isValid('2010-10-10'); // returns true +$validator->isValid(new DateTime('2010-10-10)); // returns false; value is not a string +$validator->isValid('2010.10.10'); // returns false; format differs +``` From 757fcf8c163393b75441370b5ac83858339b8147 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 22:04:46 -0600 Subject: [PATCH 32/33] docs: adds CHANGELOG entry for #275 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ece1317..9a3be8458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file, in reverse ### Added +- [#275](https://github.com/zendframework/zend-validator/pull/275) adds a new `strict` option to `Zend\Validator\Date`; when `true`, the value being validated must both be a date AND in the same format as provided via the `format` option. + - [#264](https://github.com/zendframework/zend-validator/pull/264) adds `Zend\Validator\UndisclosedPassword`, which can be used to determine if a password has been exposed in a known data breach as reported on the [Have I Been Pwned?](https://www.haveibeenpwned.com) website. [Documentation](https://docs.zendframework.com/zend-validator/validators/undisclosed-password/) - [#266](https://github.com/zendframework/zend-validator/pull/266) adds a new option to the `File\Extension` and `File\ExcludeExtension` validators, `allowNonExistentFile`. When set to `true`, the validators will continue validating the extension of the filename given even if the file does not exist. The default is `false`, to preserve backwards compatibility with previous versions. From 13f1d29ab20b10a3885b8d861c2f77d031942eb3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 27 Dec 2019 22:06:22 -0600 Subject: [PATCH 33/33] 2.13.0 readiness - Updates branch aliases - dev-master => 2.13.x-dev - dev-develop => 2.14.x-dev - Updates CHANGELOG - Removes empty 2.12.3 stub - Sets release date for 2.13.0 --- CHANGELOG.md | 24 +----------------------- composer.json | 4 ++-- composer.lock | 2 +- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3be8458..fa92841ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. -## 2.13.0 - TBD +## 2.13.0 - 2019-12-27 ### Added @@ -30,28 +30,6 @@ All notable changes to this project will be documented in this file, in reverse - Nothing. -## 2.12.3 - TBD - -### Added - -- Nothing. - -### Changed - -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- Nothing. - -### Fixed - -- Nothing. - ## 2.12.2 - 2019-10-29 ### Added diff --git a/composer.json b/composer.json index a0e60940a..076b1c724 100644 --- a/composer.json +++ b/composer.json @@ -63,8 +63,8 @@ }, "extra": { "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" + "dev-master": "2.13.x-dev", + "dev-develop": "2.14.x-dev" }, "zf": { "component": "Zend\\Validator", diff --git a/composer.lock b/composer.lock index 087764e79..512cece00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "177c10deb5d622d2cc787d8aec3c8d4c", + "content-hash": "8e8923c7e2ce24bb8e5adf1c8fada887", "packages": [ { "name": "container-interop/container-interop",