| Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Model/UUID.php |
<?php
namespace Flow\Model;
use ApiSerializable;
use Blob;
use Flow\Data\ObjectManager;
use Flow\Exception\FlowException;
use Flow\Exception\InvalidParameterException;
use Flow\Exception\InvalidInputException;
use Language;
use MWTimestamp;
use TimestampException;
use User;
/**
* Immutable class modeling timestamped UUID's from
* the core UIDGenerator.
*
* @todo probably should be UID since these dont match the UUID standard
*/
class UUID implements ApiSerializable {
/**
* @var UUID[][][]
*/
private static $instances;
/**
* binary UUID string
*
* @var string
*/
protected $binaryValue;
/**
* base16 representation
*
* @var string
*/
protected $hexValue;
/**
* base36 representation
*
* @var string
*/
protected $alphadecimalValue;
/**
* Timestamp uuid was created
*
* @var MWTimestamp|null
*/
protected $timestamp;
/**
* Acceptable input values for constructor.
* Values are the property names the input data will be saved to.
*
* @var string
*/
const INPUT_BIN = 'binaryValue',
INPUT_HEX = 'hexValue',
INPUT_ALNUM = 'alphadecimalValue';
// UUID length in hex, always padded
const HEX_LEN = 22;
// UUID length in binary, always padded
const BIN_LEN = 11;
// UUID length in base36, with padding
const ALNUM_LEN = 19;
// unpadded base36 input string
const MIN_ALNUM_LEN = 16;
// 126 bit binary length
const OLD_BIN_LEN = 16;
// 128 bit hex length
const OLD_HEX_LEN = 32;
/**
* Constructs a UUID object based on either the binary, hex or alphanumeric
* representation.
*
* @param string $value UUID value
* @param string $format UUID format (static::INPUT_BIN, static::input_HEX
* or static::input_ALNUM)
* @throws InvalidParameterException On logic error, or for an invalid UUID string
* in a format not used directly by end-users
* @throws InvalidInputException
*/
protected function __construct( $value, $format ) {
if ( !in_array( $format, array( static::INPUT_BIN, static::INPUT_HEX, static::INPUT_ALNUM ) ) ) {
throw new InvalidParameterException( 'Invalid UUID input format: ' . $format );
}
// doublecheck validity of inputs, based on pre-determined lengths
$len = strlen( $value );
if ( $format === static::INPUT_BIN && $len !== self::BIN_LEN ) {
throw new InvalidInputException( 'Expected ' . self::BIN_LEN . ' char binary string, got: ' . $value, 'invalid-input' );
} elseif ( $format === static::INPUT_HEX && $len !== self::HEX_LEN ) {
throw new InvalidInputException( 'Expected ' . self::HEX_LEN . ' char hex string, got: ' . $value, 'invalid-input' );
} elseif ( $format === static::INPUT_ALNUM && ( $len < self::MIN_ALNUM_LEN || $len > self::ALNUM_LEN || !ctype_alnum( $value ) ) ) {
throw new InvalidInputException( 'Expected ' . self::MIN_ALNUM_LEN . ' to ' . self::ALNUM_LEN . ' char alphanumeric string, got: ' . $value, 'invalid-input' );
}
// If this is not a binary UUID, reject any string containing upper case characters.
if ( $format !== self::INPUT_BIN && $value !== strtolower( $value ) ) {
throw new InvalidInputException( 'Input UUID strings must be lowercase', 'invalid-input' );
}
self::$instances[$format][$value] = $this;
$this->{$format} = $value;
}
/**
* Alphanumeric value is all we need to construct a UUID object; saving
* anything more is just wasted storage/bandwidth.
*
* @return string[]
*/
public function __sleep() {
// ensure alphadecimal is populated
$this->getAlphadecimal();
return array( 'alphadecimalValue' );
}
public function __wakeup() {
// some B/C code
// if we have outdated data, correct it and purge all other properties
if ( $this->binaryValue && strlen( $this->binaryValue ) !== self::BIN_LEN ) {
$this->binaryValue = substr( $this->binaryValue, 0, self::BIN_LEN );
$this->hexValue = null;
$this->alphadecimalValue = null;
}
if ( $this->alphadecimalValue ) {
// Bug 71377 was writing invalid uuid's into cache with an upper cased first letter. We
// added code in the constructor to prevent them from being created, but since this is
// coming from cache lets just fix them and move on with the request.
// We don't do a comparison first since we would have to lowercase the string to check
// anyways.
$this->alphadecimalValue = strtolower( $this->alphadecimalValue );
}
}
/**
* Returns a UUID objects based on given input. Will automatically try to
* determine the input format of the given $input or fail with an exception.
*
* @param mixed $input
* @return UUID|null
* @throws InvalidInputException
*/
static public function create( $input = false ) {
// Most calls to UUID::create are binary strings, check string first
if ( is_string( $input ) || is_int( $input ) || $input === false ) {
if ( $input === false ) {
// new uuid in base 16 and pad to HEX_LEN with 0's
$hexValue = str_pad( \UIDGenerator::newTimestampedUID88( 16 ), self::HEX_LEN, '0', STR_PAD_LEFT );
return new static( $hexValue, static::INPUT_HEX );
} else {
$len = strlen( $input );
if ( $len === self::BIN_LEN ) {
$value = $input;
$type = static::INPUT_BIN;
} elseif ( $len >= self::MIN_ALNUM_LEN && $len <= self::ALNUM_LEN && ctype_alnum( $input ) ) {
$value = $input;
$type = static::INPUT_ALNUM;
} elseif ( $len === self::HEX_LEN && ctype_xdigit( $input ) ) {
$value = $input;
$type = static::INPUT_HEX;
} elseif ( $len === self::OLD_BIN_LEN ) {
$value = substr( $input, 0, self::BIN_LEN );
$type = static::INPUT_BIN;
} elseif ( $len === self::OLD_HEX_LEN && ctype_xdigit( $input ) ) {
$value = substr( $input, 0, self::HEX_LEN );
$type = static::INPUT_HEX;
} elseif ( is_numeric( $input ) ) {
// convert base 10 to base 16 and pad to HEX_LEN with 0's
$value = \Wikimedia\base_convert( $input, 10, 16, self::HEX_LEN );
$type = static::INPUT_HEX;
} else {
throw new InvalidInputException( 'Unknown input to UUID class', 'invalid-input' );
}
if ( isset( self::$instances[$type][$value] ) ) {
return self::$instances[$type][$value];
} else {
return new static( $value, $type );
}
}
} else if ( is_object( $input ) ) {
if ( $input instanceof UUID ) {
return $input;
} elseif ( $input instanceof Blob ) {
return self::create( $input->fetch() );
} else {
throw new InvalidParameterException( 'Unknown input of type ' . get_class( $input ) );
}
} elseif ( $input === null ) {
return null;
} else {
throw new InvalidParameterException( 'Unknown input type to UUID class: ' . gettype( $input ) );
}
}
/**
* @return string
*/
public function __toString() {
wfWarn( __METHOD__ . ': UUID __toString auto-converted to alphaDecimal; please do manually.' );
return $this->getAlphadecimal();
}
/**
* @return mixed
*/
public function serializeForApiResult() {
return $this->getAlphadecimal();
}
/**
* @return UUIDBlob UUID encoded in binary format for database storage. This value
* is a Blob object and unusable as an array key
* @throws FlowException
*/
public function getBinary() {
if ( $this->binaryValue !== null ) {
return new UUIDBlob( $this->binaryValue );
} elseif ( $this->hexValue !== null ) {
$this->binaryValue = static::hex2bin( $this->hexValue );
} elseif ( $this->alphadecimalValue !== null ) {
$this->hexValue = static::alnum2hex( $this->alphadecimalValue );
self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
$this->binaryValue = static::hex2bin( $this->hexValue );
} else {
throw new FlowException( 'No binary, hex or alphadecimal value available' );
}
self::$instances[self::INPUT_BIN][$this->binaryValue] = $this;
return new UUIDBlob( $this->binaryValue );
}
/**
* Gets the UUID in hexadecimal format.
* Should not be used in Flow itself, but is useful in the PHP debug shell
* in conjunction with LOWER(HEX('...')) in MySQL.
*
* @return string
* @throws FlowException
*/
public function getHex() {
if ( $this->hexValue !== null ) {
return $this->hexValue;
} elseif ( $this->binaryValue !== null ) {
$this->hexValue = static::bin2hex( $this->binaryValue );
} elseif ( $this->alphadecimalValue !== null ) {
$this->hexValue = static::alnum2hex( $this->alphadecimalValue );
} else {
throw new FlowException( 'No binary, hex or alphadecimal value available' );
}
self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
return $this->hexValue;
}
/**
* @return string base 36 representation
* @throws FlowException
*/
public function getAlphadecimal() {
if ( $this->alphadecimalValue !== null ) {
return $this->alphadecimalValue;
} elseif ( $this->hexValue !== null ) {
$alnum = static::hex2alnum( $this->hexValue );
} elseif ( $this->binaryValue !== null ) {
$this->hexValue = static::bin2hex( $this->binaryValue );
self::$instances[self::INPUT_HEX][$this->hexValue] = $this;
$alnum = static::hex2alnum( $this->hexValue );
} else {
throw new FlowException( 'No binary, hex or alphadecimal value available' );
}
// pad some zeroes because (if initialized via ::getComparisonUUID) it
// could end up shorted than MIN_ALNUM_LEN, and whatever we output here
// should be able to feed into ::create again
$this->alphadecimalValue = str_pad( $alnum, static::MIN_ALNUM_LEN, '0', STR_PAD_LEFT );
self::$instances[self::INPUT_ALNUM][$this->alphadecimalValue] = $this;
return $this->alphadecimalValue;
}
/**
* @return MWTimestamp
* @throws TimestampException
*/
public function getTimestampObj() {
if ( $this->timestamp === null ) {
try {
$this->timestamp = new MWTimestamp( self::hex2timestamp( $this->getHex() ) );
} catch ( TimestampException $e ) {
$alnum = $this->getAlphadecimal();
wfDebugLog( 'Flow', __METHOD__ . ": bogus time value: UUID=$alnum" );
throw $e;
}
}
return clone $this->timestamp;
}
/**
* Returns the timestamp in the desired format (defaults to TS_MW)
*
* @param int $format Desired format (TS_MW, TS_UNIX, etc.)
* @return string
*/
public function getTimestamp( $format = TS_MW ) {
$ts = $this->getTimestampObj();
return $ts->getTimestamp( $format );
}
/**
* @param UUID|MWTimestamp|null $relativeTo
* @param User|null $user
* @param Language|null $lang
* @return string|false
* @throws InvalidParameterException
*/
public function getHumanTimestamp( $relativeTo = null, User $user = null, Language $lang = null ) {
if ( $relativeTo instanceof UUID ) {
$rel = $relativeTo->getTimestampObj();
} elseif ( $relativeTo instanceof MWTimestamp ) {
$rel = $relativeTo;
} else {
throw new InvalidParameterException( 'Expected MWTimestamp or UUID, got ' . get_class( $relativeTo ) );
}
$ts = $this->getTimestampObj();
return $ts ? $ts->getHumanTimestamp( $rel, $user, $lang ) : false;
}
/**
* Takes an array of rows going to/from the database/cache. Converts uuid and
* things that look like uuids into the requested format.
*
* @param array $array
* @param string $format
* @return string[]|UUIDBlob[] Typically an array of strings. If required by the database when
* $format === 'binary' uuid values will be represented as Blob objects.
*/
public static function convertUUIDs( $array, $format = 'binary' ) {
$array = ObjectManager::makeArray( $array );
foreach( $array as $key => $value ) {
if ( $value instanceof UUIDBlob ) {
// database encoded binary value
if ( $format === 'alphadecimal' ) {
$array[$key] = UUID::create( $value->fetch() )->getAlphadecimal();
}
} elseif ( $value instanceof UUID ) {
if ( $format === 'binary' ) {
$array[$key] = $value->getBinary();
} elseif ( $format === 'alphadecimal' ) {
$array[$key] = $value->getAlphadecimal();
}
} elseif ( is_string( $value ) && substr( $key, -3 ) === '_id' ) {
// things that look like uuids
$len = strlen( $value );
if ( $format === 'alphadecimal' && $len === self::BIN_LEN ) {
$array[$key] = UUID::create( $value )->getAlphadecimal();
} elseif ( $format === 'binary' && (
( $len >= self::MIN_ALNUM_LEN && $len <= self::ALNUM_LEN )
||
$len === self::HEX_LEN
) ) {
$array[$key] = UUID::create( $value )->getBinary();
}
}
}
return $array;
}
/**
* @param UUID|null $other
* @return boolean
*/
public function equals( UUID $other = null ) {
return $other && $other->getAlphadecimal() === $this->getAlphadecimal();
}
/**
* Generates a fake UUID for a given timestamp that will have comparison
* results equivalent to a real UUID generated at that time
* @param mixed $ts Something accepted by wfTimestamp()
* @return UUID object.
*/
public static function getComparisonUUID( $ts ) {
// It should be comparable with UUIDs in binary mode.
// Easiest way to do this is to take the 46 MSBs of the UNIX timestamp * 1000
// and pad the remaining characters with zeroes.
$millitime = wfTimestamp( TS_UNIX, $ts ) * 1000;
// base 10 -> base 2, taking 46 bits
$timestampBinary = \Wikimedia\base_convert( $millitime, 10, 2, 46 );
// pad out the 46 bits to binary len with 0's
$uuidBase2 = str_pad( $timestampBinary, self::BIN_LEN * 8, '0', STR_PAD_RIGHT );
// base 2 -> base 16
$uuidHex = \Wikimedia\base_convert( $uuidBase2, 2, 16, self::HEX_LEN );
return self::create( $uuidHex );
}
/**
* Converts binary UUID to HEX.
*
* @param string $binary Binary string (not a string of 1s & 0s)
* @return string
*/
public static function bin2hex( $binary ) {
return str_pad( bin2hex( $binary ), self::HEX_LEN, '0', STR_PAD_LEFT );
}
/**
* Converts alphanumeric UUID to HEX.
*
* @param string $alnum
* @return string
*/
public static function alnum2hex( $alnum ) {
return str_pad( \Wikimedia\base_convert( $alnum, 36, 16 ), self::HEX_LEN, '0', STR_PAD_LEFT );
}
/**
* Convert HEX UUID to binary string.
*
* @param string $hex
* @return string Binary string (not a string of 1s & 0s)
*/
public static function hex2bin( $hex ) {
return pack( 'H*', $hex );
}
/**
* Converts HEX UUID to alphanumeric.
*
* @param string $hex
* @return string
*/
public static function hex2alnum( $hex ) {
return \Wikimedia\base_convert( $hex, 16, 36 );
}
/**
* Converts a binary uuid into a MWTimestamp. This UUID must have
* been generated with \UIDGenerator::newTimestampedUID88.
*
* @param string $hex
* @return integer Number of seconds since epoch
*/
public static function hex2timestamp( $hex ) {
$msTimestamp = hexdec( substr( $hex, 0, 12 ) ) >> 2;
return intval( $msTimestamp / 1000 );
}
}
/**
* Extend Blob so we can identify UUID specific blobs
*/
class UUIDBlob extends \Blob {
/**
* We'll want to be able to compare the (string) value of 2 blobs.
*
* @return string
*/
public function __toString() {
return $this->fetch();
}
}