Generator::generateString()   C
last analyzed

Complexity

Conditions 8
Paths 14

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 1
Metric Value
c 5
b 0
f 1
dl 0
loc 38
rs 5.3846
cc 8
eloc 19
nc 14
nop 2
1
<?php
2
3
/*
4
 * The RandomLib library for securely generating random numbers and strings in PHP
5
 *
6
 * @author     Anthony Ferrara <[email protected]>
7
 * @copyright  2011 The Authors
8
 * @license    http://www.opensource.org/licenses/mit-license.html  MIT License
9
 * @version    Build @@version@@
10
 */
11
12
/**
13
 * The Random Number Generator Class
14
 *
15
 * Use this factory to generate cryptographic quality random numbers (strings)
16
 *
17
 * PHP version 5.3
18
 *
19
 * @category   PHPPasswordLib
20
 * @package    Random
21
 *
22
 * @author     Anthony Ferrara <[email protected]>
23
 * @author     Timo Hamina
24
 * @copyright  2011 The Authors
25
 * @license    http://www.opensource.org/licenses/mit-license.html  MIT License
26
 *
27
 * @version    Build @@version@@
28
 */
29
namespace RandomLib;
30
31
/**
32
 * The Random Number Generator Class
33
 *
34
 * Use this factory to generate cryptographic quality random numbers (strings)
35
 *
36
 * @category   PHPPasswordLib
37
 * @package    Random
38
 *
39
 * @author     Anthony Ferrara <[email protected]>
40
 * @author     Timo Hamina
41
 */
42
class Generator
43
{
44
45
    /**
46
     * @const Flag for uppercase letters
47
     */
48
    const CHAR_UPPER = 1;
49
50
    /**
51
     * @const Flag for lowercase letters
52
     */
53
    const CHAR_LOWER = 2;
54
55
    /**
56
     * @const Flag for alpha characters (combines UPPER + LOWER)
57
     */
58
    const CHAR_ALPHA = 3; // CHAR_UPPER | CHAR_LOWER
59
60
    /**
61
     * @const Flag for digits
62
     */
63
    const CHAR_DIGITS = 4;
64
65
    /**
66
     * @const Flag for alpha numeric characters
67
     */
68
    const CHAR_ALNUM = 7; // CHAR_ALPHA | CHAR_DIGITS
69
70
    /**
71
     * @const Flag for uppercase hexadecimal symbols
72
     */
73
    const CHAR_UPPER_HEX = 12; // 8 | CHAR_DIGITS
74
75
    /**
76
     * @const Flag for lowercase hexidecimal symbols
77
     */
78
    const CHAR_LOWER_HEX = 20; // 16 | CHAR_DIGITS
79
80
    /**
81
     * @const Flag for base64 symbols
82
     */
83
    const CHAR_BASE64 = 39; // 32 | CHAR_ALNUM
84
85
    /**
86
     * @const Flag for additional symbols accessible via the keyboard
87
     */
88
    const CHAR_SYMBOLS = 64;
89
90
    /**
91
     * @const Flag for brackets
92
     */
93
    const CHAR_BRACKETS = 128;
94
95
    /**
96
     * @const Flag for punctuation marks
97
     */
98
    const CHAR_PUNCT = 256;
99
100
    /**
101
     * @const Flag for upper/lower-case and digits but without "B8G6I1l|0OQDS5Z2"
102
     */
103
    const EASY_TO_READ = 512;
104
105
    /**
106
     * @var Mixer The mixing strategy to use for this generator instance
107
     */
108
    protected $mixer = null;
109
110
    /**
111
     * @var array An array of random number sources to use for this generator
112
     */
113
    protected $sources = array();
114
115
    /**
116
     * @var array The different characters, by Flag
117
     */
118
    protected $charArrays = array(
119
        self::CHAR_UPPER     => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
120
        self::CHAR_LOWER     => 'abcdefghijklmnopqrstuvwxyz',
121
        self::CHAR_DIGITS    => '0123456789',
122
        self::CHAR_UPPER_HEX => 'ABCDEF',
123
        self::CHAR_LOWER_HEX => 'abcdef',
124
        self::CHAR_BASE64    => '+/',
125
        self::CHAR_SYMBOLS   => '!"#$%&\'()* +,-./:;<=>?@[\]^_`{|}~',
126
        self::CHAR_BRACKETS  => '()[]{}<>',
127
        self::CHAR_PUNCT     => ',.;:',
128
    );
129
130
    /**
131
     * @internal
132
     * @private
133
     * @const string Ambiguous characters for "Easy To Read" sets
134
     */
135
    const AMBIGUOUS_CHARS = 'B8G6I1l|0OQDS5Z2()[]{}:;,.';
136
137
    /**
138
     * Build a new instance of the generator
139
     *
140
     * @param array $sources An array of random data sources to use
141
     * @param Mixer $mixer   The mixing strategy to use for this generator
142
     */
143
    public function __construct(array $sources, Mixer $mixer)
144
    {
145
        foreach ($sources as $source) {
146
            $this->addSource($source);
147
        }
148
        $this->mixer = $mixer;
149
    }
150
151
    /**
152
     * Add a random number source to the generator
153
     *
154
     * @param Source $source The random number source to add
155
     *
156
     * @return Generator $this The current generator instance
157
     */
158
    public function addSource(Source $source)
159
    {
160
        $this->sources[] = $source;
161
162
        return $this;
163
    }
164
165
    /**
166
     * Generate a random number (string) of the requested size
167
     *
168
     * @param int $size The size of the requested random number
169
     *
170
     * @return string The generated random number (string)
171
     */
172
    public function generate($size)
173
    {
174
        $seeds = array();
175
        foreach ($this->sources as $source) {
176
            $seeds[] = $source->generate($size);
177
        }
178
179
        return $this->mixer->mix($seeds);
180
    }
181
182
    /**
183
     * Generate a random integer with the given range
184
     *
185
     * @param int $min The lower bound of the range to generate
186
     * @param int $max The upper bound of the range to generate
187
     *
188
     * @return int The generated random number within the range
189
     */
190
    public function generateInt($min = 0, $max = PHP_INT_MAX)
191
    {
192
        $tmp   = (int) max($max, $min);
193
        $min   = (int) min($max, $min);
194
        $max   = $tmp;
195
        $range = $max - $min;
196
        if ($range == 0) {
197
            return $max;
198
        } elseif ($range > PHP_INT_MAX || is_float($range) || $range < 0) {
199
            /**
200
             * This works, because PHP will auto-convert it to a float at this point,
201
             * But on 64 bit systems, the float won't have enough precision to
202
             * actually store the difference, so we need to check if it's a float
203
             * and hence auto-converted...
204
             */
205
            throw new \RangeException(
206
                'The supplied range is too great to generate'
207
            );
208
        }
209
210
        $bits  = $this->countBits($range) + 1;
211
        $bytes = (int) max(ceil($bits / 8), 1);
212
        if ($bits == 63) {
213
            /**
214
             * Fixes issue #22
215
             *
216
             * @see https://github.com/ircmaxell/RandomLib/issues/22
217
             */
218
            $mask = 0x7fffffffffffffff;
219
        } else {
220
            $mask = (int) (pow(2, $bits) - 1);
221
        }
222
223
        /**
224
         * The mask is a better way of dropping unused bits.  Basically what it does
225
         * is to set all the bits in the mask to 1 that we may need.  Since the max
226
         * range is PHP_INT_MAX, we will never need negative numbers (which would
227
         * have the MSB set on the max int possible to generate).  Therefore we
228
         * can just mask that away.  Since pow returns a float, we need to cast
229
         * it back to an int so the mask will work.
230
         *
231
         * On a 64 bit platform, that means that PHP_INT_MAX is 2^63 - 1.  Which
232
         * is also the mask if 63 bits are needed (by the log(range, 2) call).
233
         * So if the computed result is negative (meaning the 64th bit is set), the
234
         * mask will correct that.
235
         *
236
         * This turns out to be slightly better than the shift as we don't need to
237
         * worry about "fixing" negative values.
238
         */
239
        do {
240
            $test   = $this->generate($bytes);
241
            $result = hexdec(bin2hex($test)) & $mask;
242
        } while ($result > $range);
243
244
        return $result + $min;
245
    }
246
247
    /**
248
     * Generate a random string of specified length.
249
     *
250
     * This uses the supplied character list for generating the new result
251
     * string.
252
     *
253
     * @param int   $length     The length of the generated string
254
     * @param mixed $characters String: An optional list of characters to use
255
     *                          Integer: Character flags
256
     *
257
     * @return string The generated random string
258
     */
259
    public function generateString($length, $characters = '')
260
    {
261
        if (is_int($characters)) {
262
            // Combine character sets
263
            $characters = $this->expandCharacterSets($characters);
264
        }
265
        if ($length == 0 || strlen($characters) == 1) {
266
            return '';
267
        } elseif (empty($characters)) {
268
            // Default to base 64
269
            $characters = $this->expandCharacterSets(self::CHAR_BASE64);
270
        }
271
272
        // determine how many bytes to generate
273
        // This is basically doing floor(log(strlen($characters)))
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
274
        // But it's fixed to work properly for all numbers
275
        $len   = strlen($characters);
276
277
        // The max call here fixes an issue where we under-generate in cases
278
        // where less than 8 bits are needed to represent $len
279
        $bytes = $length * ceil(($this->countBits($len)) / 8);
280
281
        // determine mask for valid characters
282
        $mask   = 256 - (256 % $len);
283
284
        $result = '';
285
        do {
286
            $rand = $this->generate($bytes);
287
            for ($i = 0; $i < $bytes; $i++) {
288
                if (ord($rand[$i]) >= $mask) {
289
                    continue;
290
                }
291
                $result .= $characters[ord($rand[$i]) % $len];
292
            }
293
        } while (strlen($result) < $length);
294
        // We may over-generate, since we always use the entire buffer
295
        return substr($result, 0, $length);
296
    }
297
298
    /**
299
     * Get the Mixer used for this instance
300
     *
301
     * @return Mixer the current mixer
302
     */
303
    public function getMixer()
304
    {
305
        return $this->mixer;
306
    }
307
308
    /**
309
     * Get the Sources used for this instance
310
     *
311
     * @return Source[] the current mixer
312
     */
313
    public function getSources()
314
    {
315
        return $this->sources;
316
    }
317
318
    /**
319
     * Count the minimum number of bits to represent the provided number
320
     *
321
     * This is basically floor(log($number, 2))
322
     * But avoids float precision issues
323
     *
324
     * @param int $number The number to count
325
     *
326
     * @return int The number of bits
327
     */
328
    protected function countBits($number)
329
    {
330
        $log2 = 0;
331
        while ($number >>= 1) {
332
            $log2++;
333
        }
334
335
        return $log2;
336
    }
337
338
    /**
339
     * Expand a character set bitwise spec into a string character set
340
     *
341
     * This will also replace EASY_TO_READ characters if the flag is set
342
     *
343
     * @param int $spec The spec to expand (bitwise combination of flags)
344
     *
345
     * @return string The expanded string
346
     */
347
    protected function expandCharacterSets($spec)
348
    {
349
        $combined = '';
350
        if ($spec == self::EASY_TO_READ) {
351
            $spec |= self::CHAR_ALNUM;
352
        }
353
        foreach ($this->charArrays as $flag => $chars) {
354
            if ($flag == self::EASY_TO_READ) {
355
                // handle this later
356
                continue;
357
            }
358
            if (($spec & $flag) === $flag) {
359
                $combined .= $chars;
360
            }
361
        }
362
        if ($spec & self::EASY_TO_READ) {
363
            // remove ambiguous characters
364
            $combined = str_replace(str_split(self::AMBIGUOUS_CHARS), '', $combined);
365
        }
366
367
        return count_chars($combined, 3);
368
    }
369
}
370