diff --git a/lib/classes/lock/db_record_lock_factory.php b/lib/classes/lock/db_record_lock_factory.php index dd2aa756774..83321d23d6d 100644 --- a/lib/classes/lock/db_record_lock_factory.php +++ b/lib/classes/lock/db_record_lock_factory.php @@ -103,7 +103,7 @@ class db_record_lock_factory implements lock_factory { * to duplicates in a clustered environment (especially on VMs due to poor time precision). */ protected function generate_unique_token() { - return generate_uuid(); + return \core\uuid::generate(); } /** diff --git a/lib/classes/uuid.php b/lib/classes/uuid.php new file mode 100644 index 00000000000..b0984fa540c --- /dev/null +++ b/lib/classes/uuid.php @@ -0,0 +1,144 @@ +. + +/** + * V4 UUID generator. + * + * @package core + * @copyright 2019 Matteo Scaramuccia + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core; + +use Exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * V4 UUID generator class. + * + * @package core + * @copyright 2019 Matteo Scaramuccia + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class uuid { + /** + * Generate a V4 UUID using PECL UUID extension. + * @see https://github.com/php/pecl-networking-uuid PECL uuid + * @see https://tools.ietf.org/html/rfc4122 + * + * @return string|bool The UUID when PECL UUID extension is available; + * otherwise, false. + */ + protected static function generate_uuid_via_pecl_uuid_extension() { + $uuid = false; + + // Check if PECL uuid extension has been actually installed. + if (function_exists('\uuid_time')) { + // Set V4 version. + $uuid = \uuid_create(UUID_TYPE_RANDOM); + } + + return $uuid; + } + + /** + * Generate a V4 UUID using PHP 7+ features. + * + * @see https://www.php.net/manual/en/function.random-bytes.php + * @see https://tools.ietf.org/html/rfc4122 + * + * @return string|bool The UUID when random_bytes() function is available; + * otherwise, false when missing the sources of randomness used by random_bytes(). + */ + protected static function generate_uuid_via_random_bytes() { + $uuid = false; + + // If none of the sources of randomness are available, + // then an Exception will be thrown. + try { + $data = random_bytes(16); + $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); // Set version to 0100. + $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); // Set bits 6-7 to 10. + $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } catch (Exception $e) { + // Could not generate a random string. Is this OS secure? + $uuid = false; + } + + return $uuid; + } + + /** + * Generate a V4 UUID. + * + * Unique is hard. Very hard. Attempt to use the PECL UUID function if available, and if not then revert to + * constructing the uuid using random_bytes or mt_rand. + * + * It is important that this token is not solely based on time as this could lead + * to duplicates in a clustered environment (especially on VMs due to poor time precision). + * + * UUIDs are just 128 bits long but with different supported versions (RFC 4122), mainly two: + * - V1: the goal is uniqueness, at the cost of anonymity since it is coupled to the host generating it, via its MAC address. + * - V4: the goal is randomness, at the cost of (rare) collisions. + * Here, the V4 type is the preferred choice. + * + * The format is: + * xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx + * where x is any hexadecimal digit and Y is a random choice from 8, 9, a, or b. + * + * @see https://tools.ietf.org/html/rfc4122 + * + * @return string The V4 UUID. + */ + public static function generate() { + // Try PHP UUID extensions first. + $uuid = self::generate_uuid_via_pecl_uuid_extension(); + + // Fall back to better random features, when possible. + if (empty($uuid)) { + $uuid = self::generate_uuid_via_random_bytes(); + } + + // Finally, create it with the available randomness. + if (empty($uuid)) { + // Fallback uuid generation based on: + // "http://www.php.net/manual/en/function.uniqid.php#94959". + $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + + // 32 bits for "time_low". + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + + // 16 bits for "time_mid". + mt_rand(0, 0xffff), + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4. + mt_rand(0, 0x0fff) | 0x4000, + + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1. + mt_rand(0, 0x3fff) | 0x8000, + + // 48 bits for "node". + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + } + + return trim($uuid); + } +} diff --git a/lib/setuplib.php b/lib/setuplib.php index c7b74efe243..5db32cec958 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -610,49 +610,24 @@ function get_exception_info($ex) { } /** - * Generate a uuid. + * Generate a V4 UUID. * - * Unique is hard. Very hard. Attempt to use the PECL UUID functions if available, and if not then revert to + * Unique is hard. Very hard. Attempt to use the PECL UUID function if available, and if not then revert to * constructing the uuid using mt_rand. * * It is important that this token is not solely based on time as this could lead * to duplicates in a clustered environment (especially on VMs due to poor time precision). * + * @see https://tools.ietf.org/html/rfc4122 + * + * @deprecated since Moodle 3.8 MDL-61038 - please do not use this function any more. + * @see \core\uuid::generate() + * * @return string The uuid. */ function generate_uuid() { - $uuid = ''; - - if (function_exists("uuid_create")) { - $context = null; - uuid_create($context); - - uuid_make($context, UUID_MAKE_V4); - uuid_export($context, UUID_FMT_STR, $uuid); - } else { - // Fallback uuid generation based on: - // "http://www.php.net/manual/en/function.uniqid.php#94959". - $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - - // 32 bits for "time_low". - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - - // 16 bits for "time_mid". - mt_rand(0, 0xffff), - - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4. - mt_rand(0, 0x0fff) | 0x4000, - - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1. - mt_rand(0, 0x3fff) | 0x8000, - - // 48 bits for "node". - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); - } - return trim($uuid); + debugging('generate_uuid() is deprecated. Please use \core\uuid::generate() instead.', DEBUG_DEVELOPER); + return \core\uuid::generate(); } /** @@ -1513,7 +1488,7 @@ function make_unique_writable_directory($basedir, $exceptiononerror = true) { do { // Generate a new (hopefully unique) directory name. - $uniquedir = $basedir . DIRECTORY_SEPARATOR . generate_uuid(); + $uniquedir = $basedir . DIRECTORY_SEPARATOR . \core\uuid::generate(); } while ( // Ensure that basedir is still writable - if we do not check, we could get stuck in a loop here. is_writable($basedir) && diff --git a/lib/tests/setuplib_test.php b/lib/tests/setuplib_test.php index e38124e6ad7..6e3ce5bd19e 100644 --- a/lib/tests/setuplib_test.php +++ b/lib/tests/setuplib_test.php @@ -25,7 +25,6 @@ defined('MOODLE_INTERNAL') || die(); - /** * Unit tests for setuplib.php * @@ -476,4 +475,65 @@ class core_setuplib_testcase extends advanced_testcase { public function test_get_real_size($input, $expectedbytes) { $this->assertEquals($expectedbytes, get_real_size($input)); } + + /** + * Validate the given V4 UUID. + * + * @param string $value The candidate V4 UUID + * @return bool True if valid; otherwise, false. + */ + protected static function is_valid_uuid_v4($value) { + // Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx + // where x is any hexadecimal digit and Y is one of 8, 9, aA, or bB. + // First, the size is 36 (32 + 4 dashes). + if (strlen($value) != 36) { + return false; + } + // Finally, check the format. + $uuidv4pattern = '/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i'; + return (preg_match($uuidv4pattern, $value) === 1); + } + + /** + * Test the \core\uuid::generate_uuid_via_pecl_uuid_extension() function. + */ + public function test_core_uuid_generate_uuid_via_pecl_uuid_extension() { + if (!extension_loaded('uuid')) { + $this->markTestSkipped("PHP 'uuid' extension not loaded."); + } + if (!function_exists('uuid_time')) { + $this->markTestSkipped("PHP PECL 'uuid' extension not loaded."); + } + + // The \core\uuid::generate_uuid_via_pecl_uuid_extension static method is protected. Use Reflection to call the method. + $method = new ReflectionMethod('\core\uuid', 'generate_uuid_via_pecl_uuid_extension'); + $method->setAccessible(true); + $uuid = $method->invoke(null); + $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 uuid: '$uuid'"); + } + + /** + * Test the \core\uuid::generate_uuid_via_random_bytes() function. + */ + public function test_core_uuid_generate_uuid_via_random_bytes() { + try { + random_bytes(1); + } catch (Exception $e) { + $this->markTestSkipped('No source of entropy for random_bytes. ' . $e->getMessage()); + } + + // The \core\uuid::generate_uuid_via_random_bytes static method is protected. Use Reflection to call the method. + $method = new ReflectionMethod('\core\uuid', 'generate_uuid_via_random_bytes'); + $method->setAccessible(true); + $uuid = $method->invoke(null); + $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 uuid: '$uuid'"); + } + + /** + * Test the \core\uuid::generate() function. + */ + public function test_core_uuid_generate() { + $uuid = \core\uuid::generate(); + $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 UUID: '$uuid'"); + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 631f3e6b893..e326b88abd1 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -3,6 +3,7 @@ information provided here is intended especially for developers. === 3.8 === * The yui checknet module is removed. Call \core\session\manager::keepalive instead. +* The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead. === 3.7 === * Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().