Merge "jquery.makeCollapsible: events for collapsing/expanding, tests"
[lhc/web/wiklou.git] / includes / UIDGenerator.php
index 05bb489..b042d8c 100644 (file)
 /**
  * Class for getting statistically unique IDs
  *
- * @since 1.20
+ * @since 1.21
  */
 class UIDGenerator {
        /** @var UIDGenerator */
        protected static $instance = null;
 
-       protected $nodeId24; // string; node ID in binary (24 bits)
        protected $nodeId32; // string; node ID in binary (32 bits)
        protected $nodeId48; // string; node ID in binary (48 bits)
-       protected $threadId; // integer; thread ID or random integer
 
-       protected $lockFile64; // string; local file path
        protected $lockFile88; // string; local file path
        protected $lockFile128; // string; local file path
 
-       const EPOCH_MS_40 = 1325376000; // "January 1, 2012"
-       const EPOCH_US_56 = 1325376000; // "January 1, 2012"
+       /** @var Array */
+       protected $fileHandles = array(); // cache file handles
+
+       const QUICK_RAND = 1; // get randomness from fast and insecure sources
 
        protected function __construct() {
-               # Try to get some ID that uniquely identifies this machine...
-               wfSuppressWarnings();
-               if ( wfIsWindows() ) {
-                       # http://technet.microsoft.com/en-us/library/bb490913.aspx
-                       $csv    = trim( `getmac /NH /FO CSV` );
-                       $line   = substr( $csv, 0, strcspn( $csv, "\n" ) );
-                       $info   = str_getcsv( $line );
-                       $hostId = count( $info ) ? str_replace( '-', '', $info[0] ) : '';
-               } else { // *nix
-                       # See http://linux.die.net/man/1/hostid
-                       $hostId = trim( `hostid` );
-               }
-               wfRestoreWarnings();
-               $hostId = strtolower( $hostId );
-               if ( preg_match( '/^[0-9a-f]{8}|[0-9a-f]{12}$/', $hostId ) ) {
-                       $this->nodeId24 = wfBaseConvert( substr( sha1( $hostId ), 0, 6 ), 16, 2, 24 );
-                       $this->nodeId32 = wfBaseConvert( substr( sha1( $hostId ), 0, 8 ), 16, 2, 32 );
-                       $this->nodeId48 = wfBaseConvert( $hostId, 16, 2, 48 );
-               } else { // fallback to host name
-                       $hash = sha1( wfHostname() . ':' . php_uname( 's' ) . ':' . php_uname( 'm' ) );
-                       $this->nodeId24 = wfBaseConvert( substr( $hash, 0, 6 ), 16, 2, 24 );
-                       $this->nodeId32 = wfBaseConvert( substr( $hash, 0, 8 ), 16, 2, 32 );
-                       $this->nodeId48 = wfBaseConvert( substr( $hash, 0, 12 ), 16, 2, 48 );
+               $idFile = wfTempDir() . '/mw-' . __CLASS__ . '-UID-nodeid';
+               $nodeId = is_file( $idFile ) ? file_get_contents( $idFile ) : '';
+               // Try to get some ID that uniquely identifies this machine (RFC 4122)...
+               if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
+                       wfSuppressWarnings();
+                       if ( wfIsWindows() ) {
+                               // http://technet.microsoft.com/en-us/library/bb490913.aspx
+                               $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
+                               $line = substr( $csv, 0, strcspn( $csv, "\n" ) );
+                               $info = str_getcsv( $line );
+                               $nodeId = isset( $info[0] ) ? str_replace( '-', '', $info[0] ) : '';
+                       } elseif ( is_executable( '/sbin/ifconfig' ) ) { // Linux/BSD/Solaris/OS X
+                               // See http://linux.die.net/man/8/ifconfig
+                               $m = array();
+                               preg_match( '/\s([0-9a-f]{2}(:[0-9a-f]{2}){5})\s/',
+                                       wfShellExec( '/sbin/ifconfig -a' ), $m );
+                               $nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : '';
+                       }
+                       wfRestoreWarnings();
+                       if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
+                               $nodeId = MWCryptRand::generateHex( 12, true );
+                               $nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit
+                       }
+                       file_put_contents( $idFile, $nodeId ); // cache
                }
-               # UID128 should be unique without having to check. The flock() calls may not be effective
-               # on operating systems when PHP is running in a threaded server model. In such cases, the
-               # Zend Thread Safety extension should be installed per php.net guidelines. This extension
-               # gives us zend_thread_id(), which we include in the UID to avoid collisions.
-               # See http://www.php.net/manual/en/install.unix.apache2.php.
-               $this->threadId    = function_exists( 'zend_thread_id' ) ? zend_thread_id() : 0;
-               $this->lockFile64  = wfTempDir() . '/mw-' . __CLASS__ . '-UID-64.lock';
-               $this->lockFile88  = wfTempDir() . '/mw-' . __CLASS__ . '-UID-88.lock';
-               $this->lockFile128 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-128.lock';
+               $this->nodeId32 = wfBaseConvert( substr( sha1( $nodeId ), 0, 8 ), 16, 2, 32 );
+               $this->nodeId48 = wfBaseConvert( $nodeId, 16, 2, 48 );
+               // If different processes run as different users, they may have different temp dirs.
+               // This is dealt with by initializing the clock sequence number and counters randomly.
+               $this->lockFile88 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-88';
+               $this->lockFile128 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-128';
        }
 
        /**
@@ -89,222 +86,265 @@ class UIDGenerator {
        }
 
        /**
-        * Get a statistically unique 64-bit unsigned integer ID string.
+        * Get a statistically unique 88-bit unsigned integer ID string.
         * The bits of the UID are prefixed with the time (down to the millisecond).
         *
-        * These IDs are suitable as values for the shard column of sharded DB tables.
+        * These IDs are suitable as values for the shard key of distributed data.
         * If a column uses these as values, it should be declared UNIQUE to handle collisions.
         * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
-        * They can also be stored efficiently as a "BIGINT UNSIGNED" in MySQL.
+        * They can also be stored "DECIMAL(27) UNSIGNED" or BINARY(11) in MySQL.
         *
         * UID generation is serialized on each server (as the node ID is for the whole machine).
         *
-        * @return string
+        * @param $base integer Specifies a base other than 10
+        * @return string Number
         * @throws MWException
         */
-       public static function timestampedUID64() {
-               $generator = self::singleton();
-
-               // Acquire the UID lock file
-               $handle = fopen( $generator->lockFile64, 'a' );
-               if ( $handle === false ) {
-                       throw new MWException( "Could not open '{$this->lockFile64}'." );
-               } elseif ( !flock( $handle, LOCK_EX ) ) {
-                       fclose( $handle ); // bail out
-                       throw new MWException( 'Could not acquire local UID64 lock.' );
+       public static function newTimestampedUID88( $base = 10 ) {
+               if ( !is_integer( $base ) || $base > 36 || $base < 2 ) {
+                       throw new MWException( "Base must an integer be between 2 and 36" );
                }
-               // Get the current UNIX timestamp on this server
-               $time = self::millitime(); // millisecond precision
-               // Delay so the next process on this server will generate higher UIDs
-               while ( self::lessThanOrEqualTime( self::millitime(), $time ) );
-               // Release the UID lock file
-               flock( $handle, LOCK_UN );
-               fclose( $handle );
-
-               // Generate a UID that is higher than the last one from this server
-               return $generator->newTimestampedID64( $time );
+               $gen = self::singleton();
+               $time = $gen->getTimestampAndDelay( 'lockFile88', 1, 1024 );
+               return wfBaseConvert( $gen->getTimestampedID88( $time ), 2, $base );
        }
 
        /**
-        * @param $time array Result of UIDGenerator::millitime()
-        * @return string
+        * @param array $time (UIDGenerator::millitime(), clock sequence)
+        * @return string 88 bits
         */
-       protected function newTimestampedID64( array $time ) {
-               list( $sec, $msec ) = $time;
-               $sec = $sec - self::EPOCH_MS_40; // start from epoch
-               # Take the 40 MSBs of "milliseconds since epoch" (rolls over in 2046)
-               if ( PHP_INT_SIZE === 8 ) { // 64 bit integers
-                       $ts = ( 1e3 * $sec + $msec );
-                       $id_bin = str_pad( decbin( $ts % pow( 2, 40 ) ), 40, '0', STR_PAD_LEFT );
-               } elseif ( extension_loaded( 'bcmath' ) ) { // 32 bit integers
-                       $ts = bcadd( bcmul( $sec, 1e3 ), $msec );
-                       $id_bin = wfBaseConvert( bcmod( $ts, bcpow( 2, 40 ) ), 10, 2, 40 );
-               } else {
-                       throw new MWException( 'bcmath extension required for 32 bit machines.' );
-               }
-               # Add the 24 bit node ID resulting in 64 bits total
-               $id_bin .= $this->nodeId24;
-               # Convert to a 1-20 digit integer string
-               if ( strlen( $id_bin ) !== 64 ) {
+       protected function getTimestampedID88( array $info ) {
+               list( $time, $counter ) = $info;
+               // Take the 46 MSBs of "milliseconds since epoch"
+               $id_bin = $this->millisecondsSinceEpochBinary( $time );
+               // Add a 10 bit counter resulting in 56 bits total
+               $id_bin .= str_pad( decbin( $counter ), 10, '0', STR_PAD_LEFT );
+               // Add the 32 bit node ID resulting in 88 bits total
+               $id_bin .= $this->nodeId32;
+               // Convert to a 1-27 digit integer string
+               if ( strlen( $id_bin ) !== 88 ) {
                        throw new MWException( "Detected overflow for millisecond timestamp." );
                }
-               return wfBaseConvert( $id_bin, 2, 10 );
+               return $id_bin;
        }
 
        /**
-        * Get a statistically unique 88-bit unsigned integer ID string.
-        * The bits of the UID are prefixed with the time (down to the microsecond).
+        * Get a statistically unique 128-bit unsigned integer ID string.
+        * The bits of the UID are prefixed with the time (down to the millisecond).
         *
-        * These IDs are suitable as values for the shard column of sharded DB tables.
-        * If a column uses these as values, it should be declared UNIQUE to handle collisions.
+        * These IDs are suitable as globally unique IDs, without any enforced uniqueness.
         * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
-        * They can also be stored reasonably as a "DECIMAL(27) UNSIGNED" in MySQL.
+        * They can also be stored as "DECIMAL(39) UNSIGNED" or BINARY(16) in MySQL.
         *
         * UID generation is serialized on each server (as the node ID is for the whole machine).
         *
-        * @return string
+        * @param $base integer Specifies a base other than 10
+        * @return string Number
         * @throws MWException
         */
-       public static function timestampedUID88() {
-               $generator = self::singleton();
-
-               // Acquire the UID lock file
-               $handle = fopen( $generator->lockFile88, 'a' );
-               if ( $handle === false ) {
-                       throw new MWException( "Could not open '{$this->lockFile88}'." );
-               } elseif ( !flock( $handle, LOCK_EX ) ) {
-                       fclose( $handle ); // bail out
-                       throw new MWException( 'Could not acquire local UID88 lock.' );
+       public static function newTimestampedUID128( $base = 10 ) {
+               if ( !is_integer( $base ) || $base > 36 || $base < 2 ) {
+                       throw new MWException( "Base must be an integer between 2 and 36" );
                }
-               // Get the current UNIX timestamp on this server
-               $time = self::microtime(); // microsecond precision
-               // Delay so the next process on this server will generate higher UIDs
-               while ( self::lessThanOrEqualTime( self::microtime(), $time ) );
-               // Release the UID lock file
-               flock( $handle, LOCK_UN );
-               fclose( $handle );
-
-               // Generate a UID that is higher than the last one from this server
-               return $generator->newTimestampedID88( $time );
+               $gen = self::singleton();
+               $time = $gen->getTimestampAndDelay( 'lockFile128', 16384, 1048576 );
+               return wfBaseConvert( $gen->getTimestampedID128( $time ), 2, $base );
        }
 
        /**
-        * @param $time array Result of UIDGenerator::microtime()
-        * @return string
+        * @param array $info (UIDGenerator::millitime(), counter, clock sequence)
+        * @return string 128 bits
         */
-       protected function newTimestampedID88( array $time ) {
-               list( $sec, $usec ) = $time;
-               $sec = $sec - self::EPOCH_US_56; // start from epoch
-               # Take the 56 MSBs of "microseconds since epoch" (rolls over in 4354)
-               if ( PHP_INT_SIZE === 8 ) { // 64 bit integers
-                       $ts = ( 1e6 * $sec + $usec );
-                       $id_bin = str_pad( decbin( $ts % pow( 2, 56 ) ), 56, '0', STR_PAD_LEFT );
-               } elseif ( extension_loaded( 'bcmath' ) ) { // 32 bit integers
-                       $ts = bcadd( bcmul( $sec, 1e6 ), $usec );
-                       $id_bin = wfBaseConvert( bcmod( $ts, bcpow( 2, 56 ) ), 10, 2, 56 );
-               } else {
-                       throw new MWException( 'bcmath extension required for 32 bit machines.' );
-               }
-               # Add the 32 bit node ID resulting in 88 bits total
-               $id_bin .= $this->nodeId32;
-               # Convert to a 1-27 digit integer string
-               if ( strlen( $id_bin ) !== 88 ) {
-                       throw new MWException( "Detected overflow for microsecond timestamp." );
+       protected function getTimestampedID128( array $info ) {
+               list( $time, $counter, $clkSeq ) = $info;
+               // Take the 46 MSBs of "milliseconds since epoch"
+               $id_bin = $this->millisecondsSinceEpochBinary( $time );
+               // Add a 20 bit counter resulting in 66 bits total
+               $id_bin .= str_pad( decbin( $counter ), 20, '0', STR_PAD_LEFT );
+               // Add a 14 bit clock sequence number resulting in 80 bits total
+               $id_bin .= str_pad( decbin( $clkSeq ), 14, '0', STR_PAD_LEFT );
+               // Add the 48 bit node ID resulting in 128 bits total
+               $id_bin .= $this->nodeId48;
+               // Convert to a 1-39 digit integer string
+               if ( strlen( $id_bin ) !== 128 ) {
+                       throw new MWException( "Detected overflow for millisecond timestamp." );
                }
-               return wfBaseConvert( $id_bin, 2, 10 );
+               return $id_bin;
        }
 
        /**
-        * Get a statistically unique 128-bit unsigned integer ID string.
-        * The bits of the UID are prefixed with the time (down to the microsecond).
-        *
-        * These IDs are suitable as globally unique IDs, without any enforced uniqueness.
-        * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
-        * They can also be stored reasonably as a "DECIMAL(39) UNSIGNED" in MySQL.
-        *
-        * UID generation is serialized on each server (as the node ID is for the whole machine).
+        * Return an RFC4122 compliant v4 UUID
         *
+        * @param $flags integer Bitfield (supports UIDGenerator::QUICK_RAND)
         * @return string
         * @throws MWException
         */
-       public static function timestampedUID128() {
-               $generator = self::singleton();
+       public static function newUUIDv4( $flags = 0 ) {
+               $hex = ( $flags & self::QUICK_RAND )
+                       ? wfRandomString( 31 )
+                       : MWCryptRand::generateHex( 31 );
 
+               return sprintf( '%s-%s-%s-%s-%s',
+                       // "time_low" (32 bits)
+                       substr( $hex, 0, 8 ),
+                       // "time_mid" (16 bits)
+                       substr( $hex, 8, 4 ),
+                       // "time_hi_and_version" (16 bits)
+                       '4' . substr( $hex, 12, 3 ),
+                       // "clk_seq_hi_res (8 bits, variant is binary 10x) and "clk_seq_low" (8 bits)
+                       dechex( 0x8 | ( hexdec( $hex[15] ) & 0x3 ) ) . $hex[16] . substr( $hex, 17, 2 ),
+                       // "node" (48 bits)
+                       substr( $hex, 19, 12 )
+               );
+       }
+
+       /**
+        * Return an RFC4122 compliant v4 UUID
+        *
+        * @param $flags integer Bitfield (supports UIDGenerator::QUICK_RAND)
+        * @return string 32 hex characters with no hyphens
+        * @throws MWException
+        */
+       public static function newRawUUIDv4( $flags = 0 ) {
+               return str_replace( '-', '', self::newUUIDv4( $flags ) );
+       }
+
+       /**
+        * Get a (time,counter,clock sequence) where (time,counter) is higher
+        * than any previous (time,counter) value for the given clock sequence.
+        * This is useful for making UIDs sequential on a per-node bases.
+        *
+        * @param string $lockFile Name of a local lock file
+        * @param $clockSeqSize integer The number of possible clock sequence values
+        * @param $counterSize integer The number of possible counter values
+        * @return Array (result of UIDGenerator::millitime(), counter, clock sequence)
+        * @throws MWException
+        */
+       protected function getTimestampAndDelay( $lockFile, $clockSeqSize, $counterSize ) {
+               // Get the UID lock file handle
+               if ( isset( $this->fileHandles[$lockFile] ) ) {
+                       $handle = $this->fileHandles[$lockFile];
+               } else {
+                       $handle = fopen( $this->$lockFile, 'cb+' );
+                       $this->fileHandles[$lockFile] = $handle ?: null; // cache
+               }
                // Acquire the UID lock file
-               $handle = fopen( $generator->lockFile128, 'a' );
                if ( $handle === false ) {
-                       throw new MWException( "Could not open '{$this->lockFile128}'." );
+                       throw new MWException( "Could not open '{$this->$lockFile}'." );
                } elseif ( !flock( $handle, LOCK_EX ) ) {
-                       fclose( $handle ); // bail out
-                       throw new MWException( 'Could not acquire local UID128 lock.' );
+                       throw new MWException( "Could not acquire '{$this->$lockFile}'." );
                }
-               // Get the current UNIX timestamp on this server
-               $time = self::microtime(); // microsecond precision
-               // Delay so the next process on this server will generate higher UIDs
-               while ( self::lessThanOrEqualTime( self::microtime(), $time ) );
+               // Get the current timestamp, clock sequence number, last time, and counter
+               rewind( $handle );
+               $data = explode( ' ', fgets( $handle ) ); // "<clk seq> <sec> <msec> <counter> <offset>"
+               $clockChanged = false; // clock set back significantly?
+               if ( count( $data ) == 5 ) { // last UID info already initialized
+                       $clkSeq = (int) $data[0] % $clockSeqSize;
+                       $prevTime = array( (int) $data[1], (int) $data[2] );
+                       $offset = (int) $data[4] % $counterSize; // random counter offset
+                       $counter = 0; // counter for UIDs with the same timestamp
+                       // Delay until the clock reaches the time of the last ID.
+                       // This detects any microtime() drift among processes.
+                       $time = $this->timeWaitUntil( $prevTime );
+                       if ( !$time ) { // too long to delay?
+                               $clockChanged = true; // bump clock sequence number
+                               $time = self::millitime();
+                       } elseif ( $time == $prevTime ) {
+                               // Bump the counter if there are timestamp collisions
+                               $counter = (int) $data[3] % $counterSize;
+                               if ( ++$counter >= $counterSize ) { // sanity (starts at 0)
+                                       flock( $handle, LOCK_UN ); // abort
+                                       throw new MWException( "Counter overflow for timestamp value." );
+                               }
+                       }
+               } else { // last UID info not initialized
+                       $clkSeq = mt_rand( 0, $clockSeqSize - 1 );
+                       $counter = 0;
+                       $offset = mt_rand( 0, $counterSize - 1 );
+                       $time = self::millitime();
+               }
+               // microtime() and gettimeofday() can drift from time() at least on Windows.
+               // The drift is immediate for processes running while the system clock changes.
+               // time() does not have this problem. See https://bugs.php.net/bug.php?id=42659.
+               if ( abs( time() - $time[0] ) >= 2 ) {
+                       // We don't want processes using too high or low timestamps to avoid duplicate
+                       // UIDs and clock sequence number churn. This process should just be restarted.
+                       flock( $handle, LOCK_UN ); // abort
+                       throw new MWException( "Process clock is outdated or drifted." );
+               }
+               // If microtime() is synced and a clock change was detected, then the clock went back
+               if ( $clockChanged ) {
+                       // Bump the clock sequence number and also randomize the counter offset,
+                       // which is useful for UIDs that do not include the clock sequence number.
+                       $clkSeq = ( $clkSeq + 1 ) % $clockSeqSize;
+                       $offset = mt_rand( 0, $counterSize - 1 );
+                       trigger_error( "Clock was set back; sequence number incremented." );
+               }
+               // Update the (clock sequence number, timestamp, counter)
+               ftruncate( $handle, 0 );
+               rewind( $handle );
+               fwrite( $handle, "{$clkSeq} {$time[0]} {$time[1]} {$counter} {$offset}" );
+               fflush( $handle );
                // Release the UID lock file
                flock( $handle, LOCK_UN );
-               fclose( $handle );
 
-               // Generate a UID that is higher than the last one from this server
-               return $generator->newTimestampedID128( $time );
+               return array( $time, ( $counter + $offset ) % $counterSize, $clkSeq );
        }
 
        /**
-        * @param $time array Result of UIDGenerator::microtime()
-        * @return string
+        * Wait till the current timestamp reaches $time and return the current
+        * timestamp. This returns false if it would have to wait more than 10ms.
+        *
+        * @param array $time Result of UIDGenerator::millitime()
+        * @return Array|bool UIDGenerator::millitime() result or false
         */
-       protected function newTimestampedID128( array $time ) {
-               list( $sec, $usec ) = $time;
-               $sec  = $sec - self::EPOCH_US_56; // start from epoch
-               $rand = mt_rand( 0, 255 );
-               $tid  = $this->threadId ? $this->threadId : mt_rand( 0, 65535 );
-               # Take the 56 MSBs of "microseconds since epoch" (rolls over in 4354)
-               if ( PHP_INT_SIZE === 8 ) { // 64 bit integers
-                       $ts = ( 1e6 * $sec + $usec );
-                       $id_bin = str_pad( decbin( $ts % pow( 2, 56 ) ), 56, '0', STR_PAD_LEFT );
-               } elseif ( extension_loaded( 'bcmath' ) ) { // 32 bit integers
-                       $ts = bcadd( bcmul( $sec, 1e6 ), $usec );
-                       $id_bin = wfBaseConvert( bcmod( $ts, bcpow( 2, 56 ) ), 10, 2, 56 );
-               } else {
-                       throw new MWException( 'bcmath extension required for 32 bit machines.' );
-               }
-               # Add on 8 bits of randomness to make 64 bits total (2^8 = 256)
-               $id_bin .= str_pad( decbin( $rand ), 8, '0', STR_PAD_LEFT );
-               # Add on 16 bits of thread ID or randomness to make 80 bits total (2^16 = 65536)
-               $id_bin .= str_pad( decbin( $tid % 65536 ), 16, '0', STR_PAD_LEFT );
-               # Add the 48 bit node ID resulting in 128 bits total
-               $id_bin .= $this->nodeId48;
-               # Convert to a 1-39 digit integer string
-               if ( strlen( $id_bin ) !== 128 ) {
-                       throw new MWException( "Detected overflow for microsecond timestamp." );
-               }
-               return wfBaseConvert( $id_bin, 2, 10 );
+       protected function timeWaitUntil( array $time ) {
+               do {
+                       $ct = self::millitime();
+                       if ( $ct >= $time ) { // http://php.net/manual/en/language.operators.comparison.php
+                               return $ct; // current timestamp is higher than $time
+                       }
+               } while ( ( ( $time[0] - $ct[0] )*1000 + ( $time[1] - $ct[1] ) ) <= 10 );
+
+               return false;
        }
 
        /**
-        * @return array (current time in seconds, milliseconds since then)
+        * @param array $time Result of UIDGenerator::millitime()
+        * @return string 46 MSBs of "milliseconds since epoch" in binary (rolls over in 4201)
         */
-       protected static function millitime() {
-               list( $usec, $sec ) = explode( ' ', microtime() );
-               return array( (int)$sec, (int)( $usec * 1e3 ) );
+       protected function millisecondsSinceEpochBinary( array $time ) {
+               list( $sec, $msec ) = $time;
+               if ( PHP_INT_SIZE >= 8 ) { // 64 bit integers
+                       $ts = ( 1000 * $sec + $msec );
+                       $id_bin = str_pad( decbin( $ts % pow( 2, 46 ) ), 46, '0', STR_PAD_LEFT );
+               } elseif ( extension_loaded( 'gmp' ) ) {
+                       $ts = gmp_mod( // wrap around
+                               gmp_add( gmp_mul( (string) $sec, (string) 1000 ), (string) $msec ),
+                               gmp_pow( '2', '46' )
+                       );
+                       $id_bin = str_pad( gmp_strval( $ts, 2 ), 46, '0', STR_PAD_LEFT );
+               } elseif ( extension_loaded( 'bcmath' ) ) {
+                       $ts = bcmod( // wrap around
+                               bcadd( bcmul( $sec, 1000 ), $msec ),
+                               bcpow( 2, 46 )
+                       );
+                       $id_bin = wfBaseConvert( $ts, 10, 2, 46 );
+               } else {
+                       throw new MWException( 'bcmath or gmp extension required for 32 bit machines.' );
+               }
+               return $id_bin;
        }
 
        /**
-        * @return array (current time in seconds, microseconds since then)
+        * @return Array (current time in seconds, milliseconds since then)
         */
-       protected static function microtime() {
-               list( $usec, $sec ) = explode( ' ', microtime() );
-               return array( (int)$sec, (int)( $usec * 1e6 ) );
+       protected static function millitime() {
+               list( $msec, $sec ) = explode( ' ', microtime() );
+               return array( (int) $sec, (int) ( $msec * 1000 ) );
        }
 
-       /**
-        * Returns true if time $t1 is <= time $t2
-        *
-        * @param $t1 array Result of UIDGenerator::microtime() or UIDGenerator::millitime()
-        * @param $t2 array Result of UIDGenerator::microtime() or UIDGenerator::millitime()
-        */
-       protected static function lessThanOrEqualTime( array $t1, array $t2 ) {
-               return ( $t1[0] < $t2[0] || ( $t1[0] === $t2[0] && $t1[1] <= $t2[1] ) );
+       function __destruct() {
+               array_map( 'fclose', $this->fileHandles );
        }
 }