Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.52% covered (warning)
52.52%
73 / 139
36.84% covered (danger)
36.84%
7 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
UUID
52.52% covered (warning)
52.52%
73 / 139
36.84% covered (danger)
36.84%
7 / 19
729.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
22.05
 __sleep
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __wakeup
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
 create
71.05% covered (warning)
71.05%
27 / 38
0.00% covered (danger)
0.00%
0 / 1
29.70
 __toString
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 serializeForApiResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBinary
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
5.50
 getHex
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 getAlphadecimal
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
5.16
 getTimestampObj
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 getTimestamp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 convertUUIDs
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
240
 equals
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getComparisonUUID
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 bin2hex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 alnum2hex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hex2bin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hex2alnum
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hex2timestamp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Flow\Model;
4
5use ApiSerializable;
6use Flow\Data\ObjectManager;
7use Flow\Exception\FlowException;
8use Flow\Exception\InvalidInputException;
9use Flow\Exception\InvalidParameterException;
10use MediaWiki\Utils\MWTimestamp;
11use Wikimedia\Rdbms\Blob;
12use Wikimedia\Timestamp\TimestampException;
13
14/**
15 * Immutable class modeling timestamped UUID's from
16 * the core UIDGenerator.
17 *
18 * @todo probably should be UID since these dont match the UUID standard
19 */
20class UUID implements ApiSerializable {
21    /**
22     * @var UUID[][]
23     */
24    private static $instances;
25
26    /**
27     * binary UUID string
28     *
29     * @var string
30     */
31    protected $binaryValue;
32
33    /**
34     * base16 representation
35     *
36     * @var string|null
37     */
38    protected $hexValue;
39
40    /**
41     * base36 representation
42     *
43     * @var string|null
44     */
45    protected $alphadecimalValue;
46
47    /**
48     * Timestamp uuid was created
49     *
50     * @var MWTimestamp|null
51     */
52    protected $timestamp;
53
54    /**
55     * Acceptable input values for constructor.
56     * Values are the property names the input data will be saved to.
57     */
58    private const INPUT_BIN = 'binaryValue',
59        INPUT_HEX = 'hexValue',
60        INPUT_ALNUM = 'alphadecimalValue';
61
62    // UUID length in hex, always padded
63    public const HEX_LEN = 22;
64    // UUID length in binary, always padded
65    public const BIN_LEN = 11;
66    // UUID length in base36, with padding
67    public const ALNUM_LEN = 19;
68    // unpadded base36 input string
69    public const MIN_ALNUM_LEN = 16;
70
71    // 126 bit binary length
72    public const OLD_BIN_LEN = 16;
73    // 128 bit hex length
74    public const OLD_HEX_LEN = 32;
75
76    /**
77     * Constructs a UUID object based on either the binary, hex or alphanumeric
78     * representation.
79     *
80     * @param string $value UUID value
81     * @param string $format UUID format (static::INPUT_BIN, static::input_HEX
82     *  or static::input_ALNUM)
83     * @throws InvalidParameterException On logic error, or for an invalid UUID string
84     *  in a format not used directly by end-users
85     * @throws InvalidInputException
86     */
87    protected function __construct( $value, $format ) {
88        if ( !in_array( $format, [ static::INPUT_BIN, static::INPUT_HEX, static::INPUT_ALNUM ] ) ) {
89            throw new InvalidParameterException( 'Invalid UUID input format: ' . $format );
90        }
91
92        // doublecheck validity of inputs, based on pre-determined lengths
93        $len = strlen( $value );
94        if ( $format === static::INPUT_BIN && $len !== self::BIN_LEN ) {
95            throw new InvalidInputException( 'Expected ' . self::BIN_LEN .
96                ' char binary string, got: ' . $value, 'invalid-input' );
97        } elseif ( $format === static::INPUT_HEX && $len !== self::HEX_LEN ) {
98            throw new InvalidInputException( 'Expected ' . self::HEX_LEN .
99                ' char hex string, got: ' . $value, 'invalid-input' );
100        } elseif ( $format === static::INPUT_ALNUM &&
101            ( $len < self::MIN_ALNUM_LEN || $len > self::ALNUM_LEN || !ctype_alnum( $value ) )
102        ) {
103            throw new InvalidInputException( 'Expected ' . self::MIN_ALNUM_LEN . ' to ' .
104                self::ALNUM_LEN . ' char alphanumeric string, got: ' . $value, 'invalid-input' );
105        }
106
107        // If this is not a binary UUID, reject any string containing upper case characters.
108        if ( $format !== self::INPUT_BIN && $value !== strtolower( $value ) ) {
109            throw new InvalidInputException( 'Input UUID strings must be lowercase', 'invalid-input' );
110        }
111        self::$instances[$format][$value] = $this;
112        $this->{$format} = $value;
113    }
114
115    /**
116     * Alphanumeric value is all we need to construct a UUID object; saving
117     * anything more is just wasted storage/bandwidth.
118     *
119     * @return string[]
120     */
121    public function __sleep() {
122        // ensure alphadecimal is populated
123        $this->getAlphadecimal();
124        return [ 'alphadecimalValue' ];
125    }
126
127    public function __wakeup() {
128        // some B/C code
129        // if we have outdated data, correct it and purge all other properties
130        if ( $this->binaryValue && strlen( $this->binaryValue ) !== self::BIN_LEN ) {
131            $this->binaryValue = substr( $this->binaryValue, 0, self::BIN_LEN );
132            $this->hexValue = null;
133            $this->alphadecimalValue = null;
134        }
135        if ( $this->alphadecimalValue ) {
136            // Bug 71377 was writing invalid uuid's into cache with an upper cased first letter.  We
137            // added code in the constructor to prevent them from being created, but since this is
138            // coming from cache lets just fix them and move on with the request.
139            // We don't do a comparison first since we would have to lowercase the string to check
140            // anyways.
141            $this->alphadecimalValue = strtolower( $this->alphadecimalValue );
142        }
143    }
144
145    /**
146     * Returns a UUID objects based on given input. Will automatically try to
147     * determine the input format of the given $input or fail with an exception.
148     *
149     * @param mixed $input
150     * @return UUID|null
151     * @throws InvalidInputException
152     */
153    public static function create( $input = false ) {
154        // Most calls to UUID::create are binary strings, check string first
155        if ( is_string( $input ) || is_int( $input ) || $input === false ) {
156            if ( $input === false ) {
157                // new uuid in base 16 and pad to HEX_LEN with 0's
158                $hexValue = str_pad( \UIDGenerator::newTimestampedUID88( 16 ),
159                    self::HEX_LEN, '0', STR_PAD_LEFT );
160                return new static( $hexValue, static::INPUT_HEX );
161            } else {
162                $len = strlen( $input );
163                if ( $len === self::BIN_LEN ) {
164                    $value = $input;
165                    $type = static::INPUT_BIN;
166                } elseif ( $len >= self::MIN_ALNUM_LEN && $len <= self::ALNUM_LEN &&
167                    ctype_alnum( $input )
168                ) {
169                    $value = $input;
170                    $type = static::INPUT_ALNUM;
171                } elseif ( $len === self::HEX_LEN && ctype_xdigit( $input ) ) {
172                    $value = $input;
173                    $type = static::INPUT_HEX;
174                } elseif ( $len === self::OLD_BIN_LEN ) {
175                    $value = substr( $input, 0, self::BIN_LEN );
176                    $type = static::INPUT_BIN;
177                } elseif ( $len === self::OLD_HEX_LEN && ctype_xdigit( $input ) ) {
178                    $value = substr( $input, 0, self::HEX_LEN );
179                    $type = static::INPUT_HEX;
180                } elseif ( is_numeric( $input ) ) {
181                    // convert base 10 to base 16 and pad to HEX_LEN with 0's
182                    $value = \Wikimedia\base_convert( $input, 10, 16, self::HEX_LEN );
183                    $type = static::INPUT_HEX;
184                } else {
185                    throw new InvalidInputException( 'Unknown input to UUID class', 'invalid-input' );
186                }
187
188                return self::$instances[$type][$value] ?? new static( $value, $type );
189            }
190        } elseif ( is_array( $input ) ) {
191            // array syntax in the url (?foo[]=bar) will make $input an array
192            throw new InvalidInputException( 'Invalid input to UUID class', 'invalid-input' );
193        } elseif ( is_object( $input ) ) {
194            if ( $input instanceof UUID ) {
195                return $input;
196            } elseif ( $input instanceof Blob ) {
197                return self::create( $input->fetch() );
198            } else {
199                throw new InvalidParameterException( 'Unknown input of type ' . get_class( $input ) );
200            }
201        } elseif ( $input === null ) {
202            return null;
203        } else {
204            throw new InvalidParameterException( 'Unknown input type to UUID class: ' . gettype( $input ) );
205        }
206    }
207
208    /**
209     * @return string
210     */
211    public function __toString() {
212        wfWarn( __METHOD__ . ': UUID __toString auto-converted to alphaDecimal; please do manually.' );
213
214        return $this->getAlphadecimal();
215    }
216
217    /**
218     * @return string
219     */
220    public function serializeForApiResult() {
221        return $this->getAlphadecimal();
222    }
223
224    /**
225     * @return UUIDBlob UUID encoded in binary format for database storage. This value
226     *  is a Blob object and unusable as an array key
227     * @throws FlowException
228     */
229    public function getBinary() {
230        if ( $this->binaryValue !== null ) {
231            return new UUIDBlob( $this->binaryValue );
232        } elseif ( $this->hexValue !== null ) {
233            $this->binaryValue = static::hex2bin( $this->hexValue );
234        } elseif ( $this->alphadecimalValue !== null ) {
235            $this->hexValue = static::alnum2hex( $this->alphadecimalValue );
236            self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
237            $this->binaryValue = static::hex2bin( $this->hexValue );
238        } else {
239            throw new FlowException( 'No binary, hex or alphadecimal value available' );
240        }
241        self::$instances[self::INPUT_BIN][$this->binaryValue] = $this;
242        return new UUIDBlob( $this->binaryValue );
243    }
244
245    /**
246     * Gets the UUID in hexadecimal format.
247     * Should not be used in Flow itself, but is useful in the PHP debug shell
248     * in conjunction with LOWER(HEX('...')) in MySQL.
249     *
250     * @return string
251     * @throws FlowException
252     */
253    public function getHex() {
254        if ( $this->hexValue !== null ) {
255            return $this->hexValue;
256        } elseif ( $this->binaryValue !== null ) {
257            $this->hexValue = static::bin2hex( $this->binaryValue );
258        } elseif ( $this->alphadecimalValue !== null ) {
259            $this->hexValue = static::alnum2hex( $this->alphadecimalValue );
260        } else {
261            throw new FlowException( 'No binary, hex or alphadecimal value available' );
262        }
263        self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
264        return $this->hexValue;
265    }
266
267    /**
268     * @return string base 36 representation
269     * @throws FlowException
270     */
271    public function getAlphadecimal() {
272        if ( $this->alphadecimalValue !== null ) {
273            return $this->alphadecimalValue;
274        } elseif ( $this->hexValue !== null ) {
275            $alnum = static::hex2alnum( $this->hexValue );
276        } elseif ( $this->binaryValue !== null ) {
277            $this->hexValue = static::bin2hex( $this->binaryValue );
278            self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
279            $alnum = static::hex2alnum( $this->hexValue );
280        } else {
281            throw new FlowException( 'No binary, hex or alphadecimal value available' );
282        }
283
284        // pad some zeroes because (if initialized via ::getComparisonUUID) it
285        // could end up shorted than MIN_ALNUM_LEN, and whatever we output here
286        // should be able to feed into ::create again
287        $this->alphadecimalValue = str_pad( $alnum, static::MIN_ALNUM_LEN, '0', STR_PAD_LEFT );
288
289        self::$instances[self::INPUT_ALNUM][$this->alphadecimalValue] = $this;
290        return $this->alphadecimalValue;
291    }
292
293    /**
294     * @return MWTimestamp
295     * @throws TimestampException
296     */
297    public function getTimestampObj() {
298        if ( $this->timestamp === null ) {
299            try {
300                $this->timestamp = new MWTimestamp( self::hex2timestamp( $this->getHex() ) );
301            } catch ( TimestampException $e ) {
302                $alnum = $this->getAlphadecimal();
303                wfDebugLog( 'Flow', __METHOD__ . ": bogus time value: UUID=$alnum" );
304                throw $e;
305            }
306        }
307        return clone $this->timestamp;
308    }
309
310    /**
311     * Returns the timestamp in the desired format (defaults to TS_MW)
312     *
313     * @param int $format Desired format (TS_MW, TS_UNIX, etc.)
314     * @return string
315     */
316    public function getTimestamp( $format = TS_MW ) {
317        $ts = $this->getTimestampObj();
318        return $ts->getTimestamp( $format );
319    }
320
321    /**
322     * Takes an array of rows going to/from the database/cache.  Converts uuid and
323     * things that look like uuids into the requested format.
324     *
325     * @param array $array
326     * @param string $format
327     * @return string[]|UUIDBlob[] Typically an array of strings.  If required by the database when
328     *  $format === 'binary' uuid values will be represented as Blob objects.
329     */
330    public static function convertUUIDs( $array, $format = 'binary' ) {
331        $array = ObjectManager::makeArray( $array );
332        foreach ( $array as $key => $value ) {
333            if ( $value instanceof UUIDBlob ) {
334                // database encoded binary value
335                if ( $format === 'alphadecimal' ) {
336                    $array[$key] = self::create( $value->fetch() )->getAlphadecimal();
337                }
338            } elseif ( $value instanceof UUID ) {
339                if ( $format === 'binary' ) {
340                    $array[$key] = $value->getBinary();
341                } elseif ( $format === 'alphadecimal' ) {
342                    $array[$key] = $value->getAlphadecimal();
343                }
344            } elseif ( is_string( $value ) && substr( $key, -3 ) === '_id' ) {
345                // things that look like uuids
346                $len = strlen( $value );
347                if ( $format === 'alphadecimal' && $len === self::BIN_LEN ) {
348                    $array[$key] = self::create( $value )->getAlphadecimal();
349                } elseif ( $format === 'binary' && (
350                    ( $len >= self::MIN_ALNUM_LEN && $len <= self::ALNUM_LEN )
351                    ||
352                    $len === self::HEX_LEN
353                ) ) {
354                    $array[$key] = self::create( $value )->getBinary();
355                }
356            }
357        }
358
359        return $array;
360    }
361
362    /**
363     * @param UUID|null $other
364     * @return bool
365     */
366    public function equals( UUID $other = null ) {
367        return $other && $other->getAlphadecimal() === $this->getAlphadecimal();
368    }
369
370    /**
371     * Generates a fake UUID for a given timestamp that will have comparison
372     * results equivalent to a real UUID generated at that time
373     * @param mixed $ts Something accepted by wfTimestamp()
374     * @return UUID object.
375     */
376    public static function getComparisonUUID( $ts ) {
377        // It should be comparable with UUIDs in binary mode.
378        // Easiest way to do this is to take the 46 MSBs of the UNIX timestamp * 1000
379        // and pad the remaining characters with zeroes.
380        $millitime = (int)wfTimestamp( TS_UNIX, $ts ) * 1000;
381        // base 10 -> base 2, taking 46 bits
382        $timestampBinary = \Wikimedia\base_convert( (string)$millitime, 10, 2, 46 );
383        // pad out the 46 bits to binary len with 0's
384        $uuidBase2 = str_pad( $timestampBinary, self::BIN_LEN * 8, '0', STR_PAD_RIGHT );
385        // base 2 -> base 16
386        $uuidHex = \Wikimedia\base_convert( $uuidBase2, 2, 16, self::HEX_LEN );
387
388        return self::create( $uuidHex );
389    }
390
391    /**
392     * Converts binary UUID to HEX.
393     *
394     * @param string $binary Binary string (not a string of 1s & 0s)
395     * @return string
396     */
397    public static function bin2hex( $binary ) {
398        return str_pad( bin2hex( $binary ), self::HEX_LEN, '0', STR_PAD_LEFT );
399    }
400
401    /**
402     * Converts alphanumeric UUID to HEX.
403     *
404     * @param string $alnum
405     * @return string
406     */
407    public static function alnum2hex( $alnum ) {
408        return str_pad( \Wikimedia\base_convert( $alnum, 36, 16 ), self::HEX_LEN, '0', STR_PAD_LEFT );
409    }
410
411    /**
412     * Convert HEX UUID to binary string.
413     *
414     * @param string $hex
415     * @return string Binary string (not a string of 1s & 0s)
416     */
417    public static function hex2bin( $hex ) {
418        return pack( 'H*', $hex );
419    }
420
421    /**
422     * Converts HEX UUID to alphanumeric.
423     *
424     * @param string $hex
425     * @return string
426     */
427    public static function hex2alnum( $hex ) {
428        return \Wikimedia\base_convert( $hex, 16, 36 );
429    }
430
431    /**
432     * Converts a binary uuid into a MWTimestamp. This UUID must have
433     * been generated with \UIDGenerator::newTimestampedUID88.
434     *
435     * @param string $hex
436     * @return int Number of seconds since epoch
437     */
438    public static function hex2timestamp( $hex ) {
439        $msTimestamp = hexdec( substr( $hex, 0, 12 ) ) >> 2;
440        return intval( $msTimestamp / 1000 );
441    }
442}