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