diff --git a/CHANGELOG.md b/CHANGELOG.md index c573cd3a93d..f4c4abff6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Added a new `Phalcon\Mvc\Model\Binder::findBoundModel` method. Params fetched from cache are being added to `internalCache` class property in `Phalcon\Mvc\Model\Binder::getParamsFromCache` - Added `Phalcon\Mvc\Model\Criteria::createBuilder` to create a query builder from criteria - Added `dispatcher::beforeForward` event to allow forwarding request to the separated module [#121](https://github.com/phalcon/cphalcon/issues/121), [#12417](https://github.com/phalcon/cphalcon/issues/12417) +- Added `Phalcon\Security\Random:base62` to provide the largest value that can safely be used in URLs without needing to take extra characters into consideration [#12105](https://github.com/phalcon/cphalcon/issues/12105) - Fixed Dispatcher forwarding when handling exception [#11819](https://github.com/phalcon/cphalcon/issues/11819), [#12154](https://github.com/phalcon/cphalcon/issues/12154) - Fixed params view scope for PHP 7 [#12648](https://github.com/phalcon/cphalcon/issues/12648) - Fixed `Phalcon\Mvc\Micro::handle` to prevent attemps to send response twice [#12668](https://github.com/phalcon/cphalcon/pull/12668) diff --git a/phalcon/security/random.zep b/phalcon/security/random.zep index 719ea8a8a38..17b17761e77 100644 --- a/phalcon/security/random.zep +++ b/phalcon/security/random.zep @@ -52,6 +52,9 @@ namespace Phalcon\Security; * echo $random->hex(12); // 95469d667475125208be45c4 * echo $random->hex(13); // 05475e8af4a34f8f743ab48761 * + * // Random base62 string + * echo $random->base62(); // z0RkwHfh8ErDM1xw + * * // Random base64 string * echo $random->base64(12); // XfIN81jGGuKkcE1E * echo $random->base64(12); // 3rcq39QzGK9fUqh8 @@ -172,37 +175,44 @@ class Random * If $len is not specified, 16 is assumed. It may be larger in future. * The result may contain alphanumeric characters except 0, O, I and l. * - * It is similar to Base64 but has been modified to avoid both non-alphanumeric + * It is similar to `Phalcon\Security\Random:base64` but has been modified to avoid both non-alphanumeric * characters and letters which might look ambiguous when printed. * - * + * * $random = new \Phalcon\Security\Random(); * * echo $random->base58(); // 4kUgL2pdQMSCQtjE - * + * * - * @link https://en.wikipedia.org/wiki/Base58 + * @see \Phalcon\Security\Random:base64 + * @link https://en.wikipedia.org/wiki/Base58 * @throws Exception If secure random number generator is not available or unexpected partial read */ - public function base58(n = null) -> string + public function base58(int len = null) -> string { - var bytes, idx; - string byteString = "", - alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - - let bytes = unpack("C*", this->bytes(n)); - - for idx in bytes { - let idx = idx % 64; - - if idx >= 58 { - let idx = this->number(57); - } - - let byteString .= alphabet[(int) idx]; - } + return this->base("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", 58, len); + } - return byteString; + /** + * Generates a random base62 string + * + * If $len is not specified, 16 is assumed. It may be larger in future. + * + * It is similar to `Phalcon\Security\Random:base58` but has been modified to provide the largest value that can + * safely be used in URLs without needing to take extra characters into consideration because it is [A-Za-z0-9]. + * + *< code> + * $random = new \Phalcon\Security\Random(); + * + * echo $random->base62(); // z0RkwHfh8ErDM1xw + * + * + * @see \Phalcon\Security\Random:base58 + * @throws Exception If secure random number generator is not available or unexpected partial read + */ + public function base62(int len = null) -> string + { + return this->base("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 62, len); } /** @@ -210,7 +220,7 @@ class Random * * If $len is not specified, 16 is assumed. It may be larger in future. * The length of the result string is usually greater of $len. - * Size formula: 4 *( $len / 3) and this need to be rounded up to a multiple of 4. + * Size formula: 4 * ($len / 3) and this need to be rounded up to a multiple of 4. * * * $random = new \Phalcon\Security\Random(); @@ -343,4 +353,31 @@ class Random return hexdec(array_shift(ret)); } + + /** + * Generates a random string based on the number ($base) of characters ($alphabet). + * + * If $n is not specified, 16 is assumed. It may be larger in future. + * + * @throws Exception If secure random number generator is not available or unexpected partial read + */ + protected function base(string alphabet, int base, n = null) -> string + { + var bytes, idx; + string byteString = ""; + + let bytes = unpack("C*", this->bytes(n)); + + for idx in bytes { + let idx = idx % 64; + + if idx >= base { + let idx = this->number(base - 1); + } + + let byteString .= alphabet[(int) idx]; + } + + return byteString; + } } diff --git a/tests/unit/Security/RandomTest.php b/tests/unit/Security/RandomTest.php index 66afd68ab85..9f6badc6fef 100644 --- a/tests/unit/Security/RandomTest.php +++ b/tests/unit/Security/RandomTest.php @@ -106,16 +106,7 @@ public function testRandomBase58() { $this->specify( "base58 does not generate a valid string", - function () { - $lens = [ - 2, - 12, - 16, - 24, - 48, - 100 - ]; - + function ($len) { $random = new Random(); $isValid = function ($base58) { @@ -131,17 +122,69 @@ function () { return (preg_match('#^[^'.join('', $alphabet).']+$#i', $base58) === 0); }; - foreach ($lens as $len) { - $actual = $random->base58($len); + $actual = $random->base58($len); + if ($len === null) { + expect(strlen($actual))->equals(16); + } else { expect(strlen($actual))->equals($len); - expect($isValid($actual))->true(); } - $actual = $random->base58(); - expect(strlen($actual))->equals(16); expect($isValid($actual))->true(); - } + }, + [ + 'examples' => [ + [null], + [2], + [12], + [16], + [24], + [48], + [100], + ] + ] + ); + } + + /** + * Tests the random base62 generation + * + * @issue 12105 + * @author Serghei Iakovlev + * @since 2017-05-21 + */ + public function testRandomBase62() + { + $this->specify( + 'base62 does not generate a valid string', + function ($len) { + $random = new Random(); + + $isValid = function ($base62) { + return (preg_match("#^[^a-z0-9]+$#i", $base62) === 0); + }; + + $actual = $random->base62($len); + + if ($len === null) { + expect(strlen($actual))->equals(16); + } else { + expect(strlen($actual))->equals($len); + } + + expect($isValid($actual))->true(); + }, + [ + 'examples' => [ + [null], + [2], + [12], + [16], + [24], + [48], + [100], + ] + ] ); } @@ -155,40 +198,34 @@ public function testRandomBase64() { $this->specify( "base64 does not generate a valid string", - function () { - $lens = [ - 2, - 12, - 16, - 24, - 48, - 100 - ]; - + function ($len) { $random = new Random(); - $checkSize = function ($base64, $len) { - // Size formula: 4 *( $len / 3) and this need to be rounded up to a multiple of 4. - $formula = (round(4*($len/3))%4 === 0) ? round(4*($len/3)) : round((4*($len/3)+4/2)/4)*4; - - return strlen($base64) == $formula; - }; - $isValid = function ($base64) { return (preg_match("#[^a-z0-9+_=/-]+#i", $base64) === 0); }; - foreach ($lens as $len) { - $actual = $random->base64($len); + $actual = $random->base64($len); - expect($checkSize($actual, $len))->true(); - expect($isValid($actual))->true(); + if ($len === null) { + expect($this->checkSize($actual, 16))->true(); + } else { + expect($this->checkSize($actual, $len))->true(); } - $actual = $random->base64(); - expect($checkSize($actual, 16))->true(); expect($isValid($actual))->true(); - } + }, + [ + 'examples' => [ + [null], + [2], + [12], + [16], + [24], + [48], + [100], + ] + ] ); } @@ -202,34 +239,34 @@ public function testRandomBase64Safe() { $this->specify( "base64Safe does not generate a valid string", - function () { - $lens = [ - 2, - 12, - 16, - 24, - 48, - 100 - ]; - + function ($len, $padding, $pattern) { $random = new Random(); - $isValid = function ($base64, $padding = false) { - $pattern = $padding ? "a-z0-9_=-" : "a-z0-9_-"; + $isValid = function ($base64) use ($pattern) { return (preg_match("#[^$pattern]+#i", $base64) === 0); }; - foreach ($lens as $len) { - $actual = $random->base64Safe($len); - expect($isValid($actual))->true(); - } - - $actual = $random->base64Safe(); + $actual = $random->base64Safe($len, $padding); expect($isValid($actual))->true(); - - $actual = $random->base64Safe(null, true); - expect($isValid($actual, true))->true(); - } + }, + [ + 'examples' => [ + [null, false, 'a-z0-9_-' ], + [null, true, 'a-z0-9_=-'], + [2, false, 'a-z0-9_-' ], + [2, true, 'a-z0-9_=-'], + [12, false, 'a-z0-9_-' ], + [12, true, 'a-z0-9_=-'], + [16, false, 'a-z0-9_-' ], + [16, true, 'a-z0-9_=-'], + [24, false, 'a-z0-9_-' ], + [24, true, 'a-z0-9_=-'], + [48, false, 'a-z0-9_-' ], + [48, true, 'a-z0-9_=-'], + [100, false, 'a-z0-9_-' ], + [100, true, 'a-z0-9_=-'], + ] + ] ); } @@ -317,4 +354,23 @@ function () { } ); } + + /** + * Size formula: 4 * ($n / 3) and this need to be rounded up to a multiple of 4. + * + * @param string $string + * @param int $n + * + * @return bool + */ + protected function checkSize($string, $n) + { + if (round(4 * ($n / 3)) % 4 === 0) { + $len = round(4 * ($n / 3)); + } else { + $len = round((4 * ($n / 3) + 4 / 2) / 4) * 4; + } + + return strlen($string) == $len; + } }