mirror of
https://github.com/processwire/processwire.git
synced 2025-08-17 20:11:46 +02:00
Move random generation functions from Password to new WireRandom class, and add several new methods for generating random strings, numbers, arrays, etc.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* Class to hold combined password/salt info. Uses Blowfish when possible.
|
||||
* Specially used by FieldtypePassword.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method setPass($value)
|
||||
@@ -23,6 +23,12 @@ class Password extends Wire {
|
||||
'hash' => '',
|
||||
);
|
||||
|
||||
/**
|
||||
* @var WireRandom|null
|
||||
*
|
||||
*/
|
||||
protected $random = null;
|
||||
|
||||
/**
|
||||
* Does this Password match the given string?
|
||||
*
|
||||
@@ -159,9 +165,7 @@ class Password extends Wire {
|
||||
/**
|
||||
* Generate a truly random base64 string of a certain length
|
||||
*
|
||||
* This is largely taken from Anthony Ferrara's password_compat library:
|
||||
* https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
|
||||
* Modified for camelCase, variable names, and function-based context by Ryan.
|
||||
* See WireRandom::base64() for details
|
||||
*
|
||||
* @param int $requiredLength Length of string you want returned (default=22)
|
||||
* @param array|bool $options Specify array of options or boolean to specify only `fast` option.
|
||||
@@ -172,143 +176,7 @@ class Password extends Wire {
|
||||
*
|
||||
*/
|
||||
public function randomBase64String($requiredLength = 22, $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'fast' => false,
|
||||
'test' => false,
|
||||
);
|
||||
|
||||
if(is_array($options)) {
|
||||
$options = array_merge($defaults, $options);
|
||||
} else {
|
||||
if(is_bool($options)) $defaults['fast'] = $options;
|
||||
$options = $defaults;
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
$valid = false;
|
||||
$tests = array();
|
||||
$test = $options['test'];
|
||||
|
||||
if($options['fast'] && !$test) {
|
||||
// fast mode for non-password use, uses only mt_rand() generated characters
|
||||
$rawLength = $requiredLength;
|
||||
|
||||
} else {
|
||||
// for password use, slower
|
||||
$rawLength = (int) ($requiredLength * 3 / 4 + 1);
|
||||
|
||||
// mcrypt_create_iv
|
||||
if((!$valid || $test) && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
|
||||
// @operator added for PHP 7.1 which throws deprecated notice on this function call
|
||||
$buffer = @mcrypt_create_iv($rawLength, MCRYPT_DEV_URANDOM);
|
||||
if($buffer) $valid = true;
|
||||
if($test) $tests['mcrypt_create_iv'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['mcrypt_create_iv'] = '';
|
||||
}
|
||||
|
||||
// PHP7 random_bytes
|
||||
if((!$valid || $test) && function_exists('random_bytes')) {
|
||||
try {
|
||||
$buffer = random_bytes($rawLength);
|
||||
if($buffer) $valid = true;
|
||||
} catch(\Exception $e) {
|
||||
$valid = false;
|
||||
}
|
||||
if($test) $tests['random_bytes'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['random_bytes'] = '';
|
||||
}
|
||||
|
||||
// openssl_random_pseudo_bytes
|
||||
if((!$valid || $test) && function_exists('openssl_random_pseudo_bytes')) {
|
||||
$good = false;
|
||||
$buffer = openssl_random_pseudo_bytes($rawLength, $good);
|
||||
if($test) $tests['openssl_random_pseudo_bytes'] = $buffer . "\tNOTE=" . ($good ? 'strong' : 'NOT strong');
|
||||
if(!$good) $buffer = '';
|
||||
if($buffer) $valid = true;
|
||||
} else if($test) {
|
||||
$tests['openssl_random_pseudo_bytes'] = '';
|
||||
}
|
||||
|
||||
// read from /dev/urandom
|
||||
if((!$valid || $test) && @is_readable('/dev/urandom')) {
|
||||
$f = fopen('/dev/urandom', 'r');
|
||||
$readLength = 0;
|
||||
if($test) $buffer = '';
|
||||
while($readLength < $rawLength) {
|
||||
$buffer .= fread($f, $rawLength - $readLength);
|
||||
$readLength = $this->_strlen($buffer);
|
||||
}
|
||||
fclose($f);
|
||||
if($readLength >= $rawLength) $valid = true;
|
||||
if($test) $tests['/dev/urandom'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['/dev/urandom'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$bufferLength = $this->_strlen($buffer);
|
||||
|
||||
// mt_rand() fast
|
||||
if(!$valid || $test || $bufferLength < $rawLength) {
|
||||
for($i = 0; $i < $rawLength; $i++) {
|
||||
if($i < $bufferLength) {
|
||||
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
|
||||
} else {
|
||||
$buffer .= chr(mt_rand(0, 255));
|
||||
}
|
||||
}
|
||||
if($test) $tests['mt_rand'] = $buffer;
|
||||
}
|
||||
|
||||
if($test) {
|
||||
// test mode
|
||||
$salt = '';
|
||||
foreach($tests as $name => $value) {
|
||||
$note = '';
|
||||
if(strpos($value, "\tNOTE=")) list($value, $note) = explode("\tNOTE=", $value);
|
||||
$value = empty($value) ? 'N/A' : $this->randomBufferToSalt($value, $requiredLength);
|
||||
$_name = str_pad($name, 28, ' ', STR_PAD_LEFT);
|
||||
$tests[$name] = $value;
|
||||
$salt .= "\n$_name: $value $note";
|
||||
}
|
||||
$salt = is_array($test) ? $tests : ltrim($salt, "\n");
|
||||
} else {
|
||||
// regular random string mode
|
||||
$salt = $this->randomBufferToSalt($buffer, $requiredLength);
|
||||
}
|
||||
|
||||
return $salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given random buffer string of bytes return base64 encoded salt
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param int $requiredLength
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function randomBufferToSalt($buffer, $requiredLength) {
|
||||
$c1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // base64
|
||||
$c2 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; // bcrypt64
|
||||
$salt = rtrim(base64_encode($buffer), '=');
|
||||
$salt = strtr($salt, $c1, $c2);
|
||||
$salt = substr($salt, 0, $requiredLength);
|
||||
return $salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string length, using mb_strlen() when available, or strlen() when not
|
||||
*
|
||||
* @param string $s
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
function _strlen($s) {
|
||||
return function_exists('mb_strlen') ? mb_strlen($s, '8bit') : strlen($s);
|
||||
return $this->random()->base64($requiredLength, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,6 +264,7 @@ class Password extends Wire {
|
||||
* @param bool $alphanumeric Specify true to allow digits in return value
|
||||
* @param array $disallow Characters that may not be used in return value
|
||||
* @return string
|
||||
* @deprecated use WireRandom::alpha() instead
|
||||
*
|
||||
*/
|
||||
public function randomAlpha($qty = 1, $alphanumeric = false, $disallow = array()) {
|
||||
@@ -417,203 +286,16 @@ class Password extends Wire {
|
||||
/**
|
||||
* Return cryptographically secure random alphanumeric, alpha or numeric string
|
||||
*
|
||||
* This method does essentially the same thing as the randomAlpha() method except
|
||||
* that it is alphanumeric by default, it uses a more cryptographically secure
|
||||
* method by default (and thus can be slower), and it provides for more $options.
|
||||
*
|
||||
* **Note about the `allow` option:**
|
||||
* If this option is used, it overrides the `alpha` and `numeric` options and creates a
|
||||
* string that has only the given characters. If given characters are not ASCII alpha or
|
||||
* numeric, then the `fast` option is always used, as the crypto-secure option does not
|
||||
* support non-alphanumeric characters. When the `allow` option is used, the `strict`
|
||||
* option does not apply.
|
||||
*
|
||||
* @param int $length Required length of string, or 0 for random length
|
||||
* @param array $options Options to modify default behavior:
|
||||
* - `alpha` (bool): Allow ASCII alphabetic characters? (default=true)
|
||||
* - `upper` (bool): Allow uppercase ASCII alphabetic characters? (default=true)
|
||||
* - `lower` (bool): Allow lowercase ASCII alphabetic characters? (default=true)
|
||||
* - `numeric` (bool): Allow numeric characters 0123456789? (default=true)
|
||||
* - `strict` (bool): Require that at least 1 character representing each true option above is present? (default=false)
|
||||
* - `allow` (array|string): Only allow these ASCII alpha or digit characters, see notes. (default='')
|
||||
* - `disallow` (array|string): Do not allow these characters. (default='')
|
||||
* - `require` (array|string): Require that these character(s) are present. (default='')
|
||||
* - `extras` (array|string): Also allow these non-alphanumeric extra characters. (default='')
|
||||
* - `minLength` (int): If $length argument is 0, minimum length of returned string. (default=10)
|
||||
* - `maxLength` (int): If $length argument is 0, maximum length of returned string. (default=40)
|
||||
* - `noRepeat` (bool): Prevent same character from appearing more than once in sequence? (default=false)
|
||||
* - `noStart` (string|array): Do not start string with these characters. (default='')
|
||||
* - 'noEnd` (string|array): Do not end string with these characters. (default='')
|
||||
* - `fast` (bool): Use fast, non-cryptographically secure method instead? (default=false)
|
||||
* @param array $options See WireRandom::alphanumeric() for options
|
||||
* @return string
|
||||
* @throws WireException
|
||||
* @since 3.0.109
|
||||
* @deprecated use WireRandom::alphanumeric() instead
|
||||
*
|
||||
*/
|
||||
public function randomAlnum($length = 0, array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'alpha' => true,
|
||||
'upper' => true,
|
||||
'lower' => true,
|
||||
'numeric' => true,
|
||||
'strict' => false,
|
||||
'allow' => '',
|
||||
'disallow' => array(),
|
||||
'extras' => array(),
|
||||
'require' => array(),
|
||||
'minLength' => 10,
|
||||
'maxLength' => 40,
|
||||
'noRepeat' => false,
|
||||
'noStart' => array(),
|
||||
'noEnd' => array(),
|
||||
'fast' => false,
|
||||
);
|
||||
|
||||
$alphaUpperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$alphaLowerChars = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$numericChars = '0123456789';
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$allowed = '';
|
||||
$value = '';
|
||||
|
||||
if($length < 1) {
|
||||
$length = mt_rand($options['minLength'], $options['maxLength']);
|
||||
}
|
||||
|
||||
// some options can be specified as strings, but we want them as arrays
|
||||
foreach(array('disallow', 'extras', 'require', 'noStart', 'noEnd') as $name) {
|
||||
$val = $options[$name];
|
||||
if(is_array($val)) continue;
|
||||
if(strlen($val)) {
|
||||
$options[$name] = str_split($val);
|
||||
} else {
|
||||
$options[$name] = array();
|
||||
}
|
||||
}
|
||||
|
||||
// some options can be specified as arrays, but we want them as strings
|
||||
foreach(array('allow', 'noStart', 'noEnd') as $name) {
|
||||
if(is_array($options[$name])) $options[$name] = implode('', $options[$name]);
|
||||
}
|
||||
|
||||
if(strlen($options['allow'])) {
|
||||
// only fast option supports non-alphanumeric characters specified in allow option
|
||||
if(!ctype_alnum($options['allow'])) $options['fast'] = true;
|
||||
$allowed = $options['allow'];
|
||||
|
||||
} else {
|
||||
if($options['alpha']) {
|
||||
if($options['upper']) $allowed .= $alphaUpperChars;
|
||||
if($options['lower']) $allowed .= $alphaLowerChars;
|
||||
}
|
||||
if($options['numeric']) {
|
||||
$allowed .= $numericChars;
|
||||
}
|
||||
}
|
||||
|
||||
$numExtras = count($options['extras']);
|
||||
|
||||
if($numExtras) {
|
||||
$allowed .= implode('', $options['extras']);
|
||||
}
|
||||
|
||||
if(count($options['disallow'])) {
|
||||
$allowed = str_replace($options['disallow'], '', $allowed);
|
||||
}
|
||||
|
||||
foreach($options['require'] as $c) {
|
||||
if(strpos($allowed, $c) === false) $allowed = '';
|
||||
}
|
||||
|
||||
if(!strlen($allowed)) {
|
||||
throw new WireException("Specified options prevent any alnum string from being created");
|
||||
}
|
||||
|
||||
do {
|
||||
if($options['fast']) {
|
||||
// fast method
|
||||
$lastChar = '';
|
||||
for($x = 0; $x < $length; $x++) {
|
||||
$n = mt_rand(0, strlen($allowed) - 1);
|
||||
$c = $allowed[$n];
|
||||
if($options['noRepeat'] && $c === $lastChar) {
|
||||
$x--;
|
||||
continue;
|
||||
}
|
||||
$value .= $c;
|
||||
$lastChar = $c;
|
||||
}
|
||||
} else {
|
||||
// slower but more cryptographically secure method
|
||||
$qty = 0;
|
||||
do {
|
||||
$baseLen = strlen($allowed) < 50 ? $length * 3 : $length * 2;
|
||||
$baseStr = $this->randomBase64String($baseLen);
|
||||
if($numExtras && !ctype_alnum($baseStr)) {
|
||||
// base64 string includes "/" or "." characters and we have substitutions (extras)
|
||||
$base64Extras = array('slash' => '/', 'period' => '.');
|
||||
$r = 0; // non-zero if we need to perform replacements at the ed
|
||||
foreach($base64Extras as $name => $c) {
|
||||
while(strpos($baseStr, $c) !== false) {
|
||||
list($a, $b) = explode($c, $baseStr, 2);
|
||||
$n = $numExtras > 1 ? mt_rand(0, $numExtras-1) : 0;
|
||||
$x = $options['extras'][$n];
|
||||
if(in_array($x, $base64Extras)) {
|
||||
$x = $name;
|
||||
$r++;
|
||||
}
|
||||
$baseStr = $a . $x . $b;
|
||||
}
|
||||
}
|
||||
if($r) {
|
||||
$baseStr = str_replace(array_keys($base64Extras), array_values($base64Extras), $baseStr);
|
||||
}
|
||||
unset($a, $b, $c, $r, $x);
|
||||
}
|
||||
if($options['alpha']) {
|
||||
if($options['lower'] && !$options['upper']) {
|
||||
$baseStr = strtolower($baseStr);
|
||||
} else if($options['upper'] && !$options['lower']) {
|
||||
$baseStr = strtoupper($baseStr);
|
||||
}
|
||||
}
|
||||
$lastChar = '';
|
||||
for($n = 0; $n < strlen($baseStr); $n++) {
|
||||
$c = $baseStr[$n];
|
||||
if(strpos($allowed, $c) === false) continue;
|
||||
if($options['noRepeat'] && $c === $lastChar) continue;
|
||||
$value .= $c;
|
||||
$lastChar = $c;
|
||||
if(++$qty >= $length) break;
|
||||
}
|
||||
} while($qty < $length);
|
||||
}
|
||||
|
||||
// check that all required characters are present
|
||||
if(count($options['require'])) {
|
||||
$n = 0;
|
||||
foreach($options['require'] as $c) {
|
||||
if(strpos($value, $c) === false) $n++;
|
||||
}
|
||||
if($n) continue;
|
||||
}
|
||||
|
||||
// enforce returned value having at least one of each requested type (alpha, upper, lower, numeric)
|
||||
if($options['strict'] && !strlen($options['allow'])) {
|
||||
if($options['alpha'] && $options['upper'] && !preg_match('/[A-Z]/', $value)) continue;
|
||||
if($options['alpha'] && $options['lower'] && !preg_match('/[a-z]/', $value)) continue;
|
||||
if($options['numeric'] && !preg_match('/[0-9]/', $value)) continue;
|
||||
}
|
||||
|
||||
if(strlen($value) > $length) $value = substr($value, 0, $length);
|
||||
if(strlen($options['noStart'])) $value = ltrim($value, $options['noStart']);
|
||||
if(strlen($options['noEnd'])) $value = rtrim($value, $options['noEnd']);
|
||||
|
||||
} while(strlen($value) < $length);
|
||||
|
||||
return $value;
|
||||
return $this->random()->alphanumeric($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,189 +305,47 @@ class Password extends Wire {
|
||||
* @param array $options See options for randomAlnum() method
|
||||
* @return string
|
||||
* @since 3.0.109
|
||||
* @deprecated use WireRandom::alpha() instead.
|
||||
*
|
||||
*/
|
||||
public function randomLetters($length = 0, array $options = array()) {
|
||||
if(!isset($options['numeric'])) $options['numeric'] = false;
|
||||
return $this->randomAlnum($length, $options);
|
||||
return $this->random()->alpha($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string of random digits
|
||||
*
|
||||
* @param int $length Required length of string or 0 for random length
|
||||
* @param array $options See options for randomAlnum() method
|
||||
* @param array $options See WireRandom::numeric() method
|
||||
* @return string
|
||||
* @since 3.0.109
|
||||
* @deprecated Use WireRandom::numeric() instead
|
||||
*
|
||||
*/
|
||||
public function randomDigits($length = 0, array $options = array()) {
|
||||
$options['alpha'] = false;
|
||||
return $this->randomAlnum($length, $options);
|
||||
return $this->random()->numeric($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and return a random password
|
||||
*
|
||||
* Default settings of this method are to generate a random but readable password without characters that
|
||||
* tend to have readability issues, and using only ASCII characters (for broadest keyboard compatibility).
|
||||
* See WireRandom::pass() method for details.
|
||||
*
|
||||
* @param array $options Specify any of the following options (all optional):
|
||||
* - `minLength` (int): Minimum lenth of returned value (default=7).
|
||||
* - `maxLength` (int): Maximum lenth of returned value, will be exceeded if needed to meet other options (default=15).
|
||||
* - `minLower` (int): Minimum number of lowercase characters required (default=1).
|
||||
* - `minUpper` (int): Minimum number of uppercase characters required (default=1).
|
||||
* - `maxUpper` (int): Maximum number of uppercase characters allowed (0=any, -1=none, default=3).
|
||||
* - `minDigits` (int): Minimum number of digits required (default=1).
|
||||
* - `maxDigits` (int): Maximum number of digits allowed (0=any, -1=none, default=0).
|
||||
* - `minSymbols` (int): Minimum number of non-alpha, non-digit symbols required (default=0).
|
||||
* - `maxSymbols` (int): Maximum number of non-alpha, non-digit symbols to allow (0=any, -1=none, default=3).
|
||||
* - `useSymbols` (array): Array of characters to use as "symbols" in returned value (see method for default).
|
||||
* - `disallow` (array): Disallowed characters that may be confused with others (default=O,0,I,1,l).
|
||||
*
|
||||
* @param array $options See WireRandom::pass() for options
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function randomPass(array $options = array()) {
|
||||
return $this->random()->pass($options);
|
||||
}
|
||||
|
||||
$defaults = array(
|
||||
'minLength' => 7,
|
||||
'maxLength' => 15,
|
||||
'minUpper' => 1,
|
||||
'maxUpper' => 3,
|
||||
'minLower' => 1,
|
||||
'minDigits' => 1,
|
||||
'maxDigits' => 0,
|
||||
'minSymbols' => 0,
|
||||
'maxSymbols' => 3,
|
||||
'useSymbols' => array('@', '#', '$', '%', '^', '*', '_', '-', '+', '?', '(', ')', '!', '.', '=', '/'),
|
||||
'disallow' => array('O', '0', 'I', '1', 'l'),
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$length = mt_rand($options['minLength'], $options['maxLength']);
|
||||
$base64Symbols = array('/' , '.');
|
||||
$_disallow = array(); // with both upper and lower versions
|
||||
|
||||
foreach($options['disallow'] as $c) {
|
||||
$c = strtolower($c);
|
||||
$_disallow[$c] = $c;
|
||||
$c = strtoupper($c);
|
||||
$_disallow[$c] = $c;
|
||||
}
|
||||
|
||||
// build foundation of password using base64 string
|
||||
do {
|
||||
$value = $this->randomBase64String($length);
|
||||
$valid = preg_match('/[A-Z]/i', $value) && preg_match('/[0-9]/', $value);
|
||||
} while(!$valid);
|
||||
|
||||
// limit amount of characters that are too common in base64 string
|
||||
foreach($base64Symbols as $char) {
|
||||
if(strpos($value, $char) === false) continue;
|
||||
$c = $this->randomAlpha(1, true, $options['disallow']);
|
||||
$value = str_replace($char, $c, $value);
|
||||
}
|
||||
|
||||
// manage quantity of symbols
|
||||
if($options['maxSymbols'] > -1) {
|
||||
// ensure there are a certain quantity of symbols present
|
||||
if($options['maxSymbols'] === 0) {
|
||||
$numSymbols = mt_rand($options['minSymbols'], floor(strlen($value) / 2));
|
||||
} else {
|
||||
$numSymbols = mt_rand($options['minSymbols'], $options['maxSymbols']);
|
||||
}
|
||||
$symbols = $options['useSymbols'];
|
||||
shuffle($symbols);
|
||||
for($n = 0; $n < $numSymbols; $n++) {
|
||||
$symbol = array_shift($symbols);
|
||||
$value .= $symbol;
|
||||
}
|
||||
} else {
|
||||
// no symbols, remove those commonly added in base64 string
|
||||
$options['disallow'] = array_merge($options['disallow'], $base64Symbols);
|
||||
}
|
||||
|
||||
// manage quantity of uppercase characters
|
||||
if($options['maxUpper'] > 0 || ($options['minUpper'] > 0 && $options['maxUpper'] > -1)) {
|
||||
// limit or establish the number of uppercase characters
|
||||
if(!$options['maxUpper']) $options['maxUpper'] = floor(strlen($value) / 2);
|
||||
$numUpper = mt_rand($options['minUpper'], $options['maxUpper']);
|
||||
if($numUpper) {
|
||||
$value = strtolower($value);
|
||||
$test = $this->wire('sanitizer')->alpha($value);
|
||||
if(strlen($test) < $numUpper) {
|
||||
// there aren't enough characters present to meet requirements, so add some
|
||||
$value .= $this->randomAlpha($numUpper - strlen($test), false, $_disallow);
|
||||
}
|
||||
for($i = 0; $i < strlen($value); $i++) {
|
||||
$c = strtoupper($value[$i]);
|
||||
if(in_array($c, $options['disallow'])) continue;
|
||||
if($c !== $value[$i]) $value[$i] = $c;
|
||||
if($c >= 'A' && $c <= 'Z') $numUpper--;
|
||||
if(!$numUpper) break;
|
||||
}
|
||||
// still need more? append new characters as needed
|
||||
if($numUpper) $value .= strtoupper($this->randomAlpha($numUpper, false, $_disallow));
|
||||
}
|
||||
|
||||
} else if($options['maxUpper'] < 0) {
|
||||
// disallow upper
|
||||
$value = strtolower($value);
|
||||
}
|
||||
|
||||
// manage quantity of lowercase characters
|
||||
if($options['minLower'] > 0) {
|
||||
$test = preg_replace('/[^a-z]/', '', $value);
|
||||
if(strlen($test) < $options['minLower']) {
|
||||
// needs more lowercase
|
||||
$value .= strtolower($this->randomAlpha($options['minLower'] - strlen($test), false, $_disallow));
|
||||
}
|
||||
}
|
||||
|
||||
// manage quantity of required digits
|
||||
if($options['minDigits'] > 0) {
|
||||
$test = $this->wire('sanitizer')->digits($value);
|
||||
$test = str_replace($options['disallow'], '', $test);
|
||||
$numDigits = $options['minDigits'] - strlen($test);
|
||||
if($numDigits > 0) {
|
||||
$value .= $this->randomAlpha($numDigits, 1, $options['disallow']);
|
||||
}
|
||||
}
|
||||
if($options['maxDigits'] > 0 || $options['maxDigits'] == -1) {
|
||||
// a maximum number of digits specified
|
||||
$numDigits = 0;
|
||||
for($n = 0; $n < strlen($value); $n++) {
|
||||
$c = $value[$n];
|
||||
$isDigit = ctype_digit($c);
|
||||
if($isDigit) $numDigits++;
|
||||
if($isDigit && $numDigits > $options['maxDigits']) {
|
||||
// convert digit to alpha
|
||||
$value[$n] = strtolower($this->randomAlpha(1, false, $_disallow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace any disallowed characters
|
||||
foreach($options['disallow'] as $char) {
|
||||
$pos = strpos($value, $char);
|
||||
if($pos === false) continue;
|
||||
if(ctype_digit($char)) {
|
||||
$c = $this->randomAlpha(1, 1, $_disallow);
|
||||
} else if(strtoupper($char) === $char) {
|
||||
$c = strtoupper($this->randomAlpha(1, false, $_disallow));
|
||||
} else {
|
||||
$c = strtolower($this->randomAlpha(1, false, $_disallow));
|
||||
}
|
||||
$value = str_replace($char, $c, $value);
|
||||
}
|
||||
|
||||
// randomize, in case any operations above need it
|
||||
$value = str_split($value);
|
||||
shuffle($value);
|
||||
$value = implode('', $value);
|
||||
|
||||
return $value;
|
||||
/**
|
||||
* @return WireRandom
|
||||
*
|
||||
*/
|
||||
protected function random() {
|
||||
if($this->random === null) $this->random = $this->wire(new WireRandom());
|
||||
return $this->random;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
|
944
wire/core/WireRandom.php
Normal file
944
wire/core/WireRandom.php
Normal file
@@ -0,0 +1,944 @@
|
||||
<?php namespace ProcessWire;
|
||||
|
||||
/**
|
||||
* Random generators for ProcessWire
|
||||
*
|
||||
* Includes methods for random strings, numbers, arrays and passwords.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @since 3.0.111
|
||||
*
|
||||
* Usage example
|
||||
* ~~~~~
|
||||
* $random = new WireRandom();
|
||||
* $s = $random->alphanumeric(10);
|
||||
* $i = $random->integer(0, 10);
|
||||
* ~~~~~
|
||||
*
|
||||
*/
|
||||
|
||||
class WireRandom extends Wire {
|
||||
|
||||
/**
|
||||
* Return random alphanumeric, alpha or numeric string
|
||||
*
|
||||
* This method uses cryptographically secure random generation unless you specify `true` for
|
||||
* the `fast` option, in which case it will use cryptographically secure method only if PHP is
|
||||
* version 7+ or the mcrypt library is available.
|
||||
*
|
||||
* **Note about the `allow` option:**
|
||||
* If this option is used, it overrides the `alpha` and `numeric` options and creates a
|
||||
* string that has only the given characters. If given characters are not ASCII alpha or
|
||||
* numeric, then the `fast` option is always used, as the crypto-secure option does not
|
||||
* support non-alphanumeric characters. When the `allow` option is used, the `strict`
|
||||
* option does not apply.
|
||||
*
|
||||
* @param int $length Required length of string, or 0 for random length
|
||||
* @param array $options Options to modify default behavior:
|
||||
* - `alpha` (bool): Allow ASCII alphabetic characters? (default=true)
|
||||
* - `upper` (bool): Allow uppercase ASCII alphabetic characters? (default=true)
|
||||
* - `lower` (bool): Allow lowercase ASCII alphabetic characters? (default=true)
|
||||
* - `numeric` (bool): Allow numeric characters 0123456789? (default=true)
|
||||
* - `strict` (bool): Require that at least 1 character representing each true option above is present? (default=false)
|
||||
* - `allow` (array|string): Only allow these ASCII alpha or digit characters, see notes. (default='')
|
||||
* - `disallow` (array|string): Do not allow these characters. (default='')
|
||||
* - `require` (array|string): Require that these character(s) are present. (default='')
|
||||
* - `extras` (array|string): Also allow these non-alphanumeric extra characters. (default='')
|
||||
* - `minLength` (int): If $length argument is 0, minimum length of returned string. (default=10)
|
||||
* - `maxLength` (int): If $length argument is 0, maximum length of returned string. (default=40)
|
||||
* - `noRepeat` (bool): Prevent same character from appearing more than once in sequence? (default=false)
|
||||
* - `noStart` (string|array): Do not start string with these characters. (default='')
|
||||
* - 'noEnd` (string|array): Do not end string with these characters. (default='')
|
||||
* - `fast` (bool): Use faster method? (default=true if PHP7 or mcrypt available, false if not)
|
||||
* @return string
|
||||
* @throws WireException
|
||||
* @since 3.0.111
|
||||
*
|
||||
*/
|
||||
public function alphanumeric($length = 0, array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'alpha' => true,
|
||||
'upper' => true,
|
||||
'lower' => true,
|
||||
'numeric' => true,
|
||||
'strict' => false,
|
||||
'allow' => '',
|
||||
'disallow' => array(),
|
||||
'extras' => array(),
|
||||
'require' => array(),
|
||||
'minLength' => 10,
|
||||
'maxLength' => 40,
|
||||
'noRepeat' => false,
|
||||
'noStart' => array(),
|
||||
'noEnd' => array(),
|
||||
'fast' => $this->cryptoSecure(),
|
||||
);
|
||||
|
||||
$alphaUpperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$alphaLowerChars = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$numericChars = '0123456789';
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$allowed = '';
|
||||
|
||||
if($length < 1) {
|
||||
$length = $this->integer($options['minLength'], $options['maxLength']);
|
||||
}
|
||||
|
||||
// some options can be specified as strings, but we want them as arrays
|
||||
foreach(array('disallow', 'extras', 'require', 'noStart', 'noEnd') as $name) {
|
||||
$val = $options[$name];
|
||||
if(is_array($val)) continue;
|
||||
if(strlen($val)) {
|
||||
$options[$name] = str_split($val);
|
||||
} else {
|
||||
$options[$name] = array();
|
||||
}
|
||||
}
|
||||
|
||||
// some options can be specified as arrays, but we want them as strings
|
||||
foreach(array('allow', 'noStart', 'noEnd') as $name) {
|
||||
if(is_array($options[$name])) $options[$name] = implode('', $options[$name]);
|
||||
}
|
||||
|
||||
if(strlen($options['allow'])) {
|
||||
// only fast option supports non-alphanumeric characters specified in allow option
|
||||
if(!ctype_alnum($options['allow'])) $options['fast'] = true;
|
||||
$allowed = $options['allow'];
|
||||
|
||||
} else {
|
||||
if($options['alpha']) {
|
||||
if($options['upper']) $allowed .= $alphaUpperChars;
|
||||
if($options['lower']) $allowed .= $alphaLowerChars;
|
||||
}
|
||||
if($options['numeric']) {
|
||||
$allowed .= $numericChars;
|
||||
}
|
||||
}
|
||||
|
||||
if(count($options['extras'])) {
|
||||
$allowed .= implode('', $options['extras']);
|
||||
}
|
||||
|
||||
if(count($options['disallow'])) {
|
||||
$allowed = str_replace($options['disallow'], '', $allowed);
|
||||
}
|
||||
|
||||
foreach($options['require'] as $c) {
|
||||
if(strpos($allowed, $c) === false) $allowed = '';
|
||||
}
|
||||
|
||||
if(!strlen($allowed)) {
|
||||
throw new WireException("Specified options prevent any alphanumeric string from being created");
|
||||
}
|
||||
|
||||
do {
|
||||
if($options['fast']) {
|
||||
$value = $this->string1($length, $allowed, $options);
|
||||
} else {
|
||||
$value = $this->string2($length, $allowed, $options);
|
||||
}
|
||||
|
||||
// check that all required characters are present
|
||||
if(count($options['require'])) {
|
||||
$n = 0;
|
||||
foreach($options['require'] as $c) {
|
||||
if(strpos($value, $c) === false) $n++;
|
||||
}
|
||||
if($n) continue;
|
||||
}
|
||||
|
||||
// enforce returned value having at least one of each requested type (alpha, upper, lower, numeric)
|
||||
if($options['strict'] && !strlen($options['allow'])) {
|
||||
if($options['alpha'] && $options['upper'] && !preg_match('/[A-Z]/', $value)) continue;
|
||||
if($options['alpha'] && $options['lower'] && !preg_match('/[a-z]/', $value)) continue;
|
||||
if($options['numeric'] && !preg_match('/[0-9]/', $value)) continue;
|
||||
}
|
||||
|
||||
if(strlen($value) > $length) $value = substr($value, 0, $length);
|
||||
if(strlen($options['noStart'])) $value = ltrim($value, $options['noStart']);
|
||||
if(strlen($options['noEnd'])) $value = rtrim($value, $options['noEnd']);
|
||||
|
||||
} while(strlen($value) < $length);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string using faster method
|
||||
*
|
||||
* @param int $length Required length
|
||||
* @param string $allowed Characters allowed
|
||||
* @param array $options
|
||||
* - `noRepeat` (bool): True if two of the same character may not be repeated in sequence.
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function string1($length, $allowed, array $options) {
|
||||
$defaults = array(
|
||||
'noRepeat' => false,
|
||||
);
|
||||
$options = array_merge($defaults, $options);
|
||||
$value = '';
|
||||
$lastChar = '';
|
||||
for($x = 0; $x < $length; $x++) {
|
||||
$n = $this->integer(0, strlen($allowed) - 1);
|
||||
$c = $allowed[$n];
|
||||
if($options['noRepeat'] && $c === $lastChar) {
|
||||
$x--;
|
||||
continue;
|
||||
}
|
||||
$value .= $c;
|
||||
$lastChar = $c;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random string using method that pulls from the base64 method
|
||||
*
|
||||
* @param int $length Required length
|
||||
* @param string $allowed Allowed characters
|
||||
* @param array $options See options for alphanumeric() method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function string2($length, $allowed, array $options) {
|
||||
|
||||
$defaults = array(
|
||||
'extras' => array(),
|
||||
'alpha' => true,
|
||||
'lower' => true,
|
||||
'upper' => true,
|
||||
'noRepeat' => false,
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$qty = 0;
|
||||
$value = '';
|
||||
$numExtras = count($options['extras']);
|
||||
$base64Extras = array('slash' => '/', 'period' => '.');
|
||||
|
||||
do {
|
||||
$baseLen = strlen($allowed) < 50 ? $length * 3 : $length * 2;
|
||||
$baseStr = $this->base64($baseLen);
|
||||
|
||||
if($numExtras && !ctype_alnum($baseStr)) {
|
||||
// base64 string includes "/" or "." characters and we have substitutions (extras)
|
||||
$r = 0; // non-zero if we need to perform replacements at the ed
|
||||
foreach($base64Extras as $name => $c) {
|
||||
while(strpos($baseStr, $c) !== false) {
|
||||
list($a, $b) = explode($c, $baseStr, 2);
|
||||
$n = $numExtras > 1 ? $this->integer(0, $numExtras-1) : 0;
|
||||
$x = $options['extras'][$n];
|
||||
if(in_array($x, $base64Extras)) {
|
||||
$x = $name;
|
||||
$r++;
|
||||
}
|
||||
$baseStr = $a . $x . $b;
|
||||
}
|
||||
}
|
||||
|
||||
if($r) {
|
||||
// replacements necessary
|
||||
$baseStr = str_replace(array_keys($base64Extras), array_values($base64Extras), $baseStr);
|
||||
}
|
||||
|
||||
unset($a, $b, $c, $r, $x);
|
||||
}
|
||||
|
||||
if($options['alpha']) {
|
||||
if($options['lower'] && !$options['upper']) {
|
||||
$baseStr = strtolower($baseStr);
|
||||
} else if($options['upper'] && !$options['lower']) {
|
||||
$baseStr = strtoupper($baseStr);
|
||||
}
|
||||
}
|
||||
|
||||
$lastChar = '';
|
||||
for($n = 0; $n < strlen($baseStr); $n++) {
|
||||
$c = $baseStr[$n];
|
||||
if(strpos($allowed, $c) === false) continue;
|
||||
if($options['noRepeat'] && $c === $lastChar) continue;
|
||||
$value .= $c;
|
||||
$lastChar = $c;
|
||||
if(++$qty >= $length) break;
|
||||
}
|
||||
|
||||
} while($qty < $length);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string of random ASCII alphabetical letters
|
||||
*
|
||||
* @param int $length Required length of string or 0 for random length
|
||||
* @param array $options See options for alphanumeric() method
|
||||
* @return string
|
||||
* @since 3.0.111
|
||||
*
|
||||
*/
|
||||
public function alpha($length = 0, array $options = array()) {
|
||||
if(!isset($options['numeric'])) $options['numeric'] = false;
|
||||
return $this->alphanumeric($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string of random numbers/digits
|
||||
*
|
||||
* @param int $length Required length of string or 0 for random length
|
||||
* @param array $options See options for alphanumeric() method
|
||||
* @return string
|
||||
* @since 3.0.111
|
||||
*
|
||||
*/
|
||||
public function numeric($length = 0, array $options = array()) {
|
||||
$options['alpha'] = false;
|
||||
return $this->alphanumeric($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random integer
|
||||
*
|
||||
* @param int $min Minimum allowed value (default=0).
|
||||
* @param int $max Maximum allowed value (default=PHP_INT_MAX).
|
||||
* @param array $options
|
||||
* - `info` (bool): Return array of [value, type] indicating what type of random generator was used? (default=false).
|
||||
* - `cryptoSecure` (bool): Throw WireException if cryptographically secure type not available (default=false).
|
||||
* @return int|array Returns integer, or will return array if $info option specified.
|
||||
* @throws WireException
|
||||
*
|
||||
*/
|
||||
public function integer($min = 0, $max = PHP_INT_MAX, array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'info' => false,
|
||||
'cryptoSecure' => false,
|
||||
);
|
||||
|
||||
if(is_array($min)) {
|
||||
$options = $min;
|
||||
$min = isset($options['min']) ? (int) $options['min'] : 0;
|
||||
} else if(is_array($max)) {
|
||||
$options = $max;
|
||||
$max = isset($options['max']) ? (int) $options['max'] : PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
|
||||
if($max == $min) return $max;
|
||||
if($max < $min) throw new WireException('Max may not be less than min');
|
||||
|
||||
if(function_exists('random_int')) {
|
||||
// PHP 7 has random_int, previous versions do not
|
||||
$value = random_int($min, $max);
|
||||
$type = 'random_int';
|
||||
|
||||
} else if(function_exists('mcrypt_create_iv')) {
|
||||
// via user contributed notes at: http://php.net/manual/en/function.random-int.php
|
||||
$range = $counter = $max - $min;
|
||||
$bits = 1;
|
||||
while($counter >>= 1) ++$bits;
|
||||
$bytes = (int) max(ceil($bits / 8), 1);
|
||||
$bitmask = pow(2, $bits) - 1;
|
||||
if($bitmask >= PHP_INT_MAX) $bitmask = PHP_INT_MAX;
|
||||
do {
|
||||
$result = hexdec(bin2hex(mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM))) & $bitmask;
|
||||
} while($result > $range);
|
||||
$value = $result + $min;
|
||||
$type = 'mcrypt';
|
||||
|
||||
} else if($options['cryptoSecure']) {
|
||||
throw new WireException('cryptoSecure required and neither PHP7 random_int() or mcrypt available');
|
||||
|
||||
} else {
|
||||
// mt_rand (not cryptographically secure)
|
||||
$value = mt_rand($min, $max);
|
||||
$type = 'mt_rand';
|
||||
}
|
||||
|
||||
if($options['info']) return array($value, $type);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random item (or items, key or keys) from the given array
|
||||
*
|
||||
* - Given array may be regular or associative.
|
||||
*
|
||||
* - If given a `qty` other than 1 (default) then the `getArray` option is assumed true, unless a
|
||||
* different value for the `getArray` option was manually specified.
|
||||
*
|
||||
* - When using the `getArray` option, returned array will have keys retained, except when `qty`
|
||||
* option exceeds the number of items in given array `$a`, then keys will not be retained.
|
||||
*
|
||||
* @param array $a Array to get random item from
|
||||
* @param array $options Options to modify behavior:
|
||||
* - `qty` (int): Return this quantity of item(s) (default=1).
|
||||
* - `getKey` (bool): Return item key(s) rather than values.
|
||||
* - `getArray` (bool): Return array (with original keys) rather than value (default=false if qty==1, true if not).
|
||||
* @return mixed|array|null
|
||||
*
|
||||
*/
|
||||
protected function arrayItem(array $a, array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'qty' => 1,
|
||||
'getKey' => false,
|
||||
'getArray' => null, // null=not specified
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$count = count($a);
|
||||
$keys = array_keys($a);
|
||||
$items = array();
|
||||
$item = null;
|
||||
$keepKeys = true;
|
||||
|
||||
// if getArray option not specified, auto determine from qty
|
||||
if($options['getArray'] === null) {
|
||||
$options['getArray'] = $options['qty'] === 1 ? false : true;
|
||||
}
|
||||
|
||||
// if given an empty array, return an empty value
|
||||
if(!$count) return $options['getArray'] ? array() : null;
|
||||
|
||||
// if impossible qty requested, adjust according to what is present
|
||||
if($options['qty'] < 1) $options['qty'] = $count;
|
||||
|
||||
do {
|
||||
$keysIndex = $this->integer(0, count($keys) - 1);
|
||||
$key = $keys[$keysIndex];
|
||||
$item = $options['getKey'] ? $key : $a[$key];
|
||||
if($keepKeys) {
|
||||
// if getting more than one item, ensure it’s not the same one we already got
|
||||
if(array_key_exists($key, $items)) continue;
|
||||
$items[$key] = $item;
|
||||
} else {
|
||||
// they are requesting a quantity larger than what’s in the array, so disregard keys or duplicates
|
||||
$items[] = $item;
|
||||
}
|
||||
// if more items requested than in original array, and we’ve got all of them, stop keeping track of keys
|
||||
if($options['qty'] > $count && count($items) === $count) {
|
||||
$keepKeys = false;
|
||||
$items = array_values($items);
|
||||
}
|
||||
} while(count($items) < $options['qty']);
|
||||
|
||||
if($options['getArray']) {
|
||||
// if requesting a qty greater than what’s in array, the first $count items will be unique
|
||||
// so run them through a shuffle() to prevent that predictable behavior
|
||||
if($options['qty'] > $count) shuffle($items);
|
||||
return $items;
|
||||
} else {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random value from given array
|
||||
*
|
||||
* @param array $a Array to get random value from
|
||||
* @return mixed|null
|
||||
*
|
||||
*/
|
||||
public function arrayValue(array $a) {
|
||||
return $this->arrayItem($a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random version of given array or a quantity of random items
|
||||
*
|
||||
* Array keys are retained in return value, unless requested $qty exceeds
|
||||
* the quantity of items in given array.
|
||||
*
|
||||
* @param array $a Array to get random items from.
|
||||
* @param int $qty Quantity of items, or 0 to return all (default=0).
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function arrayValues(array $a, $qty = 0) {
|
||||
return $this->arrayItem($a, array('getArray' => true, 'qty' => $qty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random key from given array
|
||||
*
|
||||
* @param array $a
|
||||
* @return string|int
|
||||
*
|
||||
*/
|
||||
public function arrayKey(array $a) {
|
||||
$options['getKey'] = true;
|
||||
return $this->arrayItem($a, array('getKey' => true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random version of all keys in given array (or a specified quantity of them)
|
||||
*
|
||||
* @param array $a Array to get random keys from.
|
||||
* @param int $qty Quantity of unique keys to return or 0 for all (default=0)
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function arrayKeys(array $a, $qty = 0) {
|
||||
return $this->arrayItem($a, array('getKey' => true, 'getArray' => true, 'qty' => $qty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle a string or an array
|
||||
*
|
||||
* Unlike PHP’s shuffle() function, this method:
|
||||
*
|
||||
* - Accepts strings or arrays and returns the same type.
|
||||
* - Maintains array keys, if given an array.
|
||||
* - Returns a copy of the value rather than modifying the given value directly.
|
||||
* - Is cryptographically secure if PHP7 or mcrypt available.
|
||||
*
|
||||
* @param string|array $value
|
||||
* @return string|array
|
||||
*
|
||||
*/
|
||||
public function shuffle($value) {
|
||||
|
||||
$isArray = is_array($value);
|
||||
|
||||
if(!$isArray) {
|
||||
if(function_exists('mb_substr')) {
|
||||
$a = array();
|
||||
for($n = 0; $n < mb_strlen($value); $n++) {
|
||||
$c = mb_substr($value, $n, 1);
|
||||
$a[] = $c;
|
||||
}
|
||||
$value = $a;
|
||||
} else {
|
||||
$value = str_split((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
$value = $this->arrayValues($value);
|
||||
if(!$isArray) $value = implode('', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and return a random password
|
||||
*
|
||||
* Default settings of this method are to generate a random but readable password without characters that
|
||||
* tend to have readability issues, and using only ASCII characters (for broadest keyboard compatibility).
|
||||
*
|
||||
* @param array $options Specify any of the following options (all optional):
|
||||
* - `minLength` (int): Minimum lenth of returned value (default=7).
|
||||
* - `maxLength` (int): Maximum lenth of returned value, will be exceeded if needed to meet other options (default=15).
|
||||
* - `minLower` (int): Minimum number of lowercase characters required (default=1).
|
||||
* - `minUpper` (int): Minimum number of uppercase characters required (default=1).
|
||||
* - `maxUpper` (int): Maximum number of uppercase characters allowed (0=any, -1=none, default=3).
|
||||
* - `minDigits` (int): Minimum number of digits required (default=1).
|
||||
* - `maxDigits` (int): Maximum number of digits allowed (0=any, -1=none, default=0).
|
||||
* - `minSymbols` (int): Minimum number of non-alpha, non-digit symbols required (default=0).
|
||||
* - `maxSymbols` (int): Maximum number of non-alpha, non-digit symbols to allow (0=any, -1=none, default=3).
|
||||
* - `useSymbols` (array): Array of characters to use as "symbols" in returned value (see method for default).
|
||||
* - `disallow` (array): Disallowed characters that may be confused with others (default=O,0,I,1,l).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function pass(array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'minLength' => 7,
|
||||
'maxLength' => 15,
|
||||
'minUpper' => 1,
|
||||
'maxUpper' => 3,
|
||||
'minLower' => 1,
|
||||
'minDigits' => 1,
|
||||
'maxDigits' => 0,
|
||||
'minSymbols' => 0,
|
||||
'maxSymbols' => 3,
|
||||
'useSymbols' => array('@', '#', '$', '%', '^', '*', '_', '-', '+', '?', '(', ')', '!', '.', '=', '/'),
|
||||
'disallow' => array('O', '0', 'I', '1', 'l'),
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
|
||||
// check if we need to increase maxLength to accommodate given options
|
||||
$minLength = $options['minUpper'] + $options['minLower'] + $options['minDigits'] + $options['minSymbols'];
|
||||
if($minLength > $options['maxLength']) $options['maxLength'] = $minLength;
|
||||
$value = $this->passCreate($options);
|
||||
if(strlen($value) > $options['maxLength']) $value = $this->passTrunc($value, $options);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a password (for password method)
|
||||
*
|
||||
* @param array $options
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function passCreate(array $options) {
|
||||
|
||||
$length = $this->integer($options['minLength'], $options['maxLength']);
|
||||
$base64Symbols = array('/' , '.');
|
||||
$disallow = $options['disallow'];
|
||||
$disallowCase = array(); // with both upper and lower versions
|
||||
|
||||
foreach($disallow as $c) {
|
||||
$c = strtolower($c);
|
||||
$disallowCase[$c] = $c;
|
||||
$c = strtoupper($c);
|
||||
$disallowCase[$c] = $c;
|
||||
}
|
||||
|
||||
// build foundation of password using base64 string
|
||||
do {
|
||||
$value = $this->base64($length);
|
||||
$valid = preg_match('/[A-Z]/i', $value) && preg_match('/[0-9]/', $value);
|
||||
} while(!$valid);
|
||||
|
||||
// limit amount of characters that are too common in base64 string
|
||||
foreach($base64Symbols as $char) {
|
||||
do {
|
||||
$pos = strpos($value, $char);
|
||||
if($pos === false) break;
|
||||
$value[$pos] = $this->alphanumeric(1, array('disallow' => $disallow));
|
||||
} while(1);
|
||||
}
|
||||
|
||||
// manage quantity of symbols
|
||||
if($options['maxSymbols'] > -1) {
|
||||
// ensure there are a certain quantity of symbols present
|
||||
if($options['maxSymbols'] === 0) {
|
||||
$numSymbols = $this->integer($options['minSymbols'], floor(strlen($value) / 2));
|
||||
} else {
|
||||
$numSymbols = $this->integer($options['minSymbols'], $options['maxSymbols']);
|
||||
}
|
||||
$symbols = $options['useSymbols'];
|
||||
shuffle($symbols);
|
||||
for($n = 0; $n < $numSymbols; $n++) {
|
||||
$symbol = array_shift($symbols);
|
||||
$value .= $symbol;
|
||||
}
|
||||
} else {
|
||||
// no symbols, remove those commonly added in base64 string
|
||||
foreach($base64Symbols as $char) {
|
||||
$disallow[] = $char;
|
||||
$disallowCase[$char] = $char;
|
||||
}
|
||||
}
|
||||
|
||||
// manage quantity of uppercase characters
|
||||
if($options['maxUpper'] > 0 || ($options['minUpper'] > 0 && $options['maxUpper'] > -1)) {
|
||||
// limit or establish the number of uppercase characters
|
||||
if(!$options['maxUpper']) $options['maxUpper'] = floor(strlen($value) / 2);
|
||||
$numUpper = $this->integer($options['minUpper'], $options['maxUpper']);
|
||||
if($numUpper) {
|
||||
$value = strtolower($value);
|
||||
$test = $this->wire('sanitizer')->alpha($value);
|
||||
if(strlen($test) < $numUpper) {
|
||||
// there aren't enough characters present to meet requirements, so add some
|
||||
$value .= $this->alpha($numUpper - strlen($test), array('disallow' => $disallow));
|
||||
}
|
||||
for($i = 0; $i < strlen($value); $i++) {
|
||||
$c = strtoupper($value[$i]);
|
||||
if(in_array($c, $disallow)) continue;
|
||||
if($c !== $value[$i]) $value[$i] = $c;
|
||||
if($c >= 'A' && $c <= 'Z') $numUpper--;
|
||||
if(!$numUpper) break;
|
||||
}
|
||||
// still need more? append new characters as needed
|
||||
if($numUpper) $value .= strtoupper($this->alpha($numUpper, array('disallow' => $disallowCase)));
|
||||
}
|
||||
|
||||
} else if($options['maxUpper'] < 0) {
|
||||
// disallow upper
|
||||
$value = strtolower($value);
|
||||
}
|
||||
|
||||
// manage quantity of lowercase characters
|
||||
if($options['minLower'] > 0) {
|
||||
$test = preg_replace('/[^a-z]/', '', $value);
|
||||
if(strlen($test) < $options['minLower']) {
|
||||
// needs more lowercase
|
||||
$value .= strtolower($this->alpha($options['minLower'] - strlen($test), array('disallow' => $disallowCase)));
|
||||
}
|
||||
}
|
||||
|
||||
// manage quantity of required digits
|
||||
if($options['minDigits'] > 0) {
|
||||
$test = $this->wire('sanitizer')->digits($value);
|
||||
$test = str_replace($options['disallow'], '', $test);
|
||||
$numDigits = $options['minDigits'] - strlen($test);
|
||||
if($numDigits > 0) {
|
||||
$value .= $this->numeric($numDigits, array('disallow' => $disallow));
|
||||
}
|
||||
}
|
||||
|
||||
if($options['maxDigits'] > 0 || $options['maxDigits'] == -1) {
|
||||
// a maximum number of digits specified
|
||||
$numDigits = 0;
|
||||
for($n = 0; $n < strlen($value); $n++) {
|
||||
$c = $value[$n];
|
||||
$isDigit = ctype_digit($c);
|
||||
if($isDigit) $numDigits++;
|
||||
if($isDigit && $numDigits > $options['maxDigits']) {
|
||||
// convert digit to alpha
|
||||
$value[$n] = strtolower($this->alpha(1, array('disallow' => $disallowCase)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace any disallowed characters
|
||||
foreach($disallow as $char) {
|
||||
$n = 0;
|
||||
do {
|
||||
$pos = strpos($value, $char);
|
||||
if($pos === false) break;
|
||||
if(ctype_digit($char)) {
|
||||
// find a different digit
|
||||
$c = $this->numeric(1, array('disallow' => $disallow));
|
||||
} else if(strtoupper($char) === $char) {
|
||||
// find a different uppercase char
|
||||
$c = strtoupper($this->alpha(1, array('disallow' => $disallowCase)));
|
||||
} else {
|
||||
// find a different lowercase char
|
||||
$c = strtolower($this->alpha(1, array('disallow' => $disallowCase)));
|
||||
}
|
||||
while(in_array($c, $disallow)) {
|
||||
// insurance fallback, not likely (impossible?) to occur
|
||||
$c = $this->alphanumeric(1);
|
||||
}
|
||||
$value[$pos] = $c;
|
||||
} while(++$n < 100);
|
||||
}
|
||||
|
||||
// randomize, in case any operations above need it
|
||||
$value = $this->shuffle($value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate password to requested maxLength without removing required options (for password method)
|
||||
*
|
||||
* @param string $value
|
||||
* @param array $options See options from password() method
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function passTrunc($value, array $options) {
|
||||
|
||||
$chars = array(
|
||||
'minLower' => array(),
|
||||
'minUpper' => array(),
|
||||
'minDigits' => array(),
|
||||
'minSymbols' => array(),
|
||||
);
|
||||
|
||||
$value = str_split($value);
|
||||
|
||||
for($n = 0; $n < count($value); $n++) {
|
||||
$c = $value[$n];
|
||||
if($c >= 'a' && $c <= 'z') {
|
||||
$chars['minLower'][$n] = $c;
|
||||
} else if($c >= 'A' && $c <= 'Z') {
|
||||
$chars['minUpper'][$n] = $c;
|
||||
} else if($c >= '0' && $c <= '9') {
|
||||
$chars['minDigits'][$n] = $c;
|
||||
} else if(in_array($c, $options['useSymbols'])) {
|
||||
$chars['minSymbols'][$n] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
$cnt = 0;
|
||||
$max = 100;
|
||||
|
||||
while(count($value) > $options['maxLength'] && ++$cnt <= $max) {
|
||||
$key = $this->arrayKey($chars);
|
||||
if(count($chars[$key]) > $options[$key]) {
|
||||
$n = $this->arrayKey($chars[$key]);
|
||||
unset($chars[$key][$n]);
|
||||
unset($value[$n]);
|
||||
}
|
||||
}
|
||||
|
||||
if($cnt >= $max) {
|
||||
// impossible to accommodate length request with given options
|
||||
}
|
||||
|
||||
return implode('', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a truly random base64 string of a certain length
|
||||
*
|
||||
* This is largely taken from Anthony Ferrara's password_compat library:
|
||||
* https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
|
||||
* Modified for camelCase, variable names, and function-based context by Ryan.
|
||||
*
|
||||
* @param int $requiredLength Length of string you want returned (default=22)
|
||||
* @param array|bool $options Specify array of options or boolean to specify only `fast` option.
|
||||
* - `fast` (bool): Use fastest, not cryptographically secure method (default=false).
|
||||
* - `test` (bool|array): Return tests in a string (bool true), or specify array(true) to return tests array (default=false).
|
||||
* Note that if the test option is used, then the fast option is disabled.
|
||||
* @return string|array Returns only array if you specify array for $test argument, otherwise returns string
|
||||
*
|
||||
*/
|
||||
public function base64($requiredLength = 22, $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'fast' => false,
|
||||
'test' => false,
|
||||
);
|
||||
|
||||
if(is_array($options)) {
|
||||
$options = array_merge($defaults, $options);
|
||||
} else {
|
||||
if(is_bool($options)) $defaults['fast'] = $options;
|
||||
$options = $defaults;
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
$valid = false;
|
||||
$tests = array();
|
||||
$test = $options['test'];
|
||||
|
||||
if($options['fast'] && !$test) {
|
||||
// fast mode for non-password use, uses only mt_rand() generated characters
|
||||
$rawLength = $requiredLength;
|
||||
|
||||
} else {
|
||||
// for password use, slower
|
||||
$rawLength = (int) ($requiredLength * 3 / 4 + 1);
|
||||
|
||||
// mcrypt_create_iv
|
||||
if((!$valid || $test) && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
|
||||
// @operator added for PHP 7.1 which throws deprecated notice on this function call
|
||||
$buffer = @mcrypt_create_iv($rawLength, MCRYPT_DEV_URANDOM);
|
||||
if($buffer) $valid = true;
|
||||
if($test) $tests['mcrypt_create_iv'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['mcrypt_create_iv'] = '';
|
||||
}
|
||||
|
||||
// PHP7 random_bytes
|
||||
if((!$valid || $test) && function_exists('random_bytes')) {
|
||||
try {
|
||||
$buffer = random_bytes($rawLength);
|
||||
if($buffer) $valid = true;
|
||||
} catch(\Exception $e) {
|
||||
$valid = false;
|
||||
}
|
||||
if($test) $tests['random_bytes'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['random_bytes'] = '';
|
||||
}
|
||||
|
||||
// openssl_random_pseudo_bytes
|
||||
if((!$valid || $test) && function_exists('openssl_random_pseudo_bytes')) {
|
||||
$good = false;
|
||||
$buffer = openssl_random_pseudo_bytes($rawLength, $good);
|
||||
if($test) $tests['openssl_random_pseudo_bytes'] = $buffer . "\tNOTE=" . ($good ? 'strong' : 'NOT strong');
|
||||
if(!$good) $buffer = '';
|
||||
if($buffer) $valid = true;
|
||||
} else if($test) {
|
||||
$tests['openssl_random_pseudo_bytes'] = '';
|
||||
}
|
||||
|
||||
// read from /dev/urandom
|
||||
if((!$valid || $test) && @is_readable('/dev/urandom')) {
|
||||
$f = fopen('/dev/urandom', 'r');
|
||||
$readLength = 0;
|
||||
if($test) $buffer = '';
|
||||
while($readLength < $rawLength) {
|
||||
$buffer .= fread($f, $rawLength - $readLength);
|
||||
$readLength = $this->_strlen($buffer);
|
||||
}
|
||||
fclose($f);
|
||||
if($readLength >= $rawLength) $valid = true;
|
||||
if($test) $tests['/dev/urandom'] = $buffer;
|
||||
} else if($test) {
|
||||
$tests['/dev/urandom'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$bufferLength = $this->_strlen($buffer);
|
||||
|
||||
// randomInteger or mt_rand() fast
|
||||
if(!$valid || $test || $bufferLength < $rawLength) {
|
||||
for($i = 0; $i < $rawLength; $i++) {
|
||||
if($i < $bufferLength) {
|
||||
$buffer[$i] = $buffer[$i] ^ chr($this->integer(0, 255));
|
||||
} else {
|
||||
$buffer .= chr($this->integer(0, 255));
|
||||
}
|
||||
}
|
||||
if($test) $tests['randomInteger'] = $buffer;
|
||||
}
|
||||
|
||||
if($test) {
|
||||
// test mode
|
||||
$salt = '';
|
||||
foreach($tests as $name => $value) {
|
||||
$note = '';
|
||||
if(strpos($value, "\tNOTE=")) list($value, $note) = explode("\tNOTE=", $value);
|
||||
$value = empty($value) ? 'N/A' : $this->randomBufferToSalt($value, $requiredLength);
|
||||
$_name = str_pad($name, 28, ' ', STR_PAD_LEFT);
|
||||
$tests[$name] = $value;
|
||||
$salt .= "\n$_name: $value $note";
|
||||
}
|
||||
$salt = is_array($test) ? $tests : ltrim($salt, "\n");
|
||||
} else {
|
||||
// regular random string mode
|
||||
$salt = $this->randomBufferToSalt($buffer, $requiredLength);
|
||||
}
|
||||
|
||||
return $salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given random buffer string of bytes return base64 encoded salt
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param int $requiredLength
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function randomBufferToSalt($buffer, $requiredLength) {
|
||||
$c1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // base64
|
||||
$c2 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; // bcrypt64
|
||||
$salt = rtrim(base64_encode($buffer), '=');
|
||||
$salt = strtr($salt, $c1, $c2);
|
||||
$salt = substr($salt, 0, $requiredLength);
|
||||
return $salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a crypto secure method of generating numbers available?
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function cryptoSecure() {
|
||||
return function_exists('random_int') || function_exists('mcrypt_create_iv');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string length, using mb_strlen() when available, or strlen() when not
|
||||
*
|
||||
* @param string $s
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
protected function _strlen($s) {
|
||||
return function_exists('mb_strlen') ? mb_strlen($s, '8bit') : strlen($s);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user