diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 7daf311..7580167 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3541,3 +3541,80 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) { // the operation. return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required))); } + +/** + * Instantiates and statically caches the correct class for a PHP loader. + * + * By default, this returns an instance of the Drupal\Component\PhpLoader\MTimeProtectedLoader + * class. + * + * Classes implementing Drupal\Component\PhpLoader\PhpLoaderInterface can + * register themselves both as a default implementation and for specific bins. + * + * @param $bin + * The bin for which the loader object should be returned. Defaults to + * 'default'. + * + * @return Drupal\Component\PhpLoader\PhpLoaderInterface + * The loader object associated with the specified bin. + * + * @see Drupal\Component\PhpLoader\PhpLoaderInterface + */ +function _drupal_php_get_loader($bin) { + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['loader_objects'] = &drupal_static(__FUNCTION__); + } + $loader_objects = &$drupal_static_fast['loader_objects']; + if (!isset($loader_objects[$bin])) { + $loader_backends = variable_get('loader_classes', array('default' => + array( + 'class' => 'Drupal\Component\PhpLoader\MTimeProtectedLoader', + 'prefix' => variable_get('file_public_path', conf_path() . '/files') . '/codegen', + 'secret' => $GLOBALS['drupal_hash_salt'], + ), + )); + $loader = isset($loader_backends[$bin]) ? $loader_backends[$bin] : $loader_backends['default']; + $class = $loader['class']; + $loader_objects[$bin] = new $class($loader); + } + return $loader_objects[$bin]; +} + +/** + * include a PHP file. + * + * @param string $filename + * The filename. Can be a relative path. + * @param string $bin + * An optional bin. Separate bins can use a different loader class. + */ +function drupal_php_include($filename, $bin = 'default') { + return _drupal_php_get_loader($bin)->phpInclude($filename); +} + +/** + * Write a PHP file. + * + * @param string $filename + * The filename. Can be a relative path. + * @param string $data + * The PHP code to be written. + * @param string $bin + * An optional bin. Separate bins can use a different loader class. + */ +function drupal_php_write($filename, $data, $bin = 'default') { + return _drupal_php_get_loader($bin)->write($filename, $data); +} + +/** + * Delete a PHP file. + * + * @param string $filename + * The filename to be deleted. Can be a relative path. + * @param string $bin + * An optional bin. Separate bins can use a different loader class. + */ +function drupal_php_delete($filename, $bin = 'default') { + return _drupal_php_get_loader($bin)->delete($filename); +} diff --git a/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php b/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php new file mode 100644 index 0000000..1bce4d6 --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php @@ -0,0 +1,130 @@ +prefix = $config['prefix']; + $this->secret = $config['secret']; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::phpInclude() + */ + public function phpInclude($filename) { + $filename = str_replace('/', '#', $filename); + $dir = $this->prefix . '/' . $filename; + $filename = $this->getPath($dir, $filename, filemtime($dir)); + if (file_exists($filename)) { + include_once $filename; + return TRUE; + } + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::write() + */ + public function write($filename, $data) { + $filename = str_replace('/', '#', $filename); + $original_path = "$this->prefix/.$filename"; + if (!file_exists($this->prefix)) { + mkdir($this->prefix, 0700, TRUE); + chmod($this->prefix, 0700); + } + if (!@file_put_contents($original_path, $data)) { + return FALSE; + } + chmod($original_path, 0400); + $dir = $this->prefix . '/' . $filename; + if (file_exists($dir)) { + $this->cleanDir($dir); + touch($dir); + } + else { + mkdir($dir); + } + $previous_mtime = 0; + $loop = 0; + // Now move the file to its final place. The mtime of a directory is the + // time of the last file create or delete in the directory. So the moving + // will update the directory mtime. This update will very likely not show + // up in filemtime, however, because it has a coarse, one second + // granularity and typical moves takes significantly less than that. In + // the unlucky case it does, we need to redo the rename to a new filename + // because read() expects the filemtime to be less or equal to the + // directory mtime. So renaming needs to happen in a loop. Also note that + // clearstatcache() returns NULL so it does not affect the loop condition. + while (clearstatcache() || (($mtime = filemtime($dir)) && $previous_mtime != $mtime)) { + $previous_mtime = $mtime; + chmod($dir, 0300); + // Reset the file back in the original place if this is not the first + // iteration. + if ($loop) { + rename($full_path, $original_path); + // Make sure to not to have an infinite loop on a hopelessly slow + // filesystem. + if ($loop > 10) { + unlink($original_path); + return FALSE; + } + } + $full_path = $this->getPath($dir, $filename, $mtime); + rename($original_path, $full_path); + chmod($dir, 0100); + $loop++; + } + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::delete() + */ + public function delete($filename) { + $dir = $this->prefix . '/' . str_replace('/', '#', $filename); + $this->cleanDir($dir); + rmdir($dir); + } + + /** + * Removes everything in a directory, leaving it empty. + */ + protected function cleanDir($dir) { + chmod($dir, 0700); + foreach (new DirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) as $file) { + unlink($file->getPathName()); + } + } + + /** + * Constructs the secret path based on the filename and the mtime. + * + * @param string $dir + * @param string $filename + * @param int $dir_mtime + */ + protected function getPath($dir, $filename, $dir_mtime) { + return $dir . '/' . hash_hmac('sha256', $filename, $this->secret . $dir_mtime) . '.php'; + } +} diff --git a/core/lib/Drupal/Component/PhpLoader/NativeLoader.php b/core/lib/Drupal/Component/PhpLoader/NativeLoader.php new file mode 100644 index 0000000..2a2227c --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/NativeLoader.php @@ -0,0 +1,48 @@ +prefix = $config['prefix']; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::include() + */ + public function phpInclude($filename) { + include_once $this->prefix . '/' . $filename; + return TRUE; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::write() + */ + public function write($filename, $data) { + return file_put_contents($this->prefix . '/' . $filename, $data); + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::delete() + */ + public function delete($filename) { + unlink($filename); + } +} diff --git a/core/lib/Drupal/Component/PhpLoader/PhpLoaderInterface.php b/core/lib/Drupal/Component/PhpLoader/PhpLoaderInterface.php new file mode 100644 index 0000000..52bbb08 --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/PhpLoaderInterface.php @@ -0,0 +1,39 @@ +originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); + _simpletest_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); // Restore original database connection. Database::removeConnection('default'); diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 4138916..3518977 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -507,7 +507,7 @@ function simpletest_clean_temporary_directories() { foreach ($files as $file) { $path = 'public://simpletest/' . $file; if (is_dir($path) && (is_numeric($file) || strpos($file, 'config_simpletest') !== FALSE)) { - file_unmanaged_delete_recursive($path); + _simpletest_delete_recursive($path); $count++; } } @@ -522,6 +522,35 @@ function simpletest_clean_temporary_directories() { } /** + * Recursively delete a directory regardless of permissions (if possible). + * + * @param $path + * The path that will be obliterated. + */ +function _simpletest_delete_recursive($path) { + // We can not use file_unmanaged_delete_recursive because it + // deliberately only removes visible files with write permission. + chmod($path, 0700); + // We can't use a RecursiveDirectoryIterator either because the directories + // might need chmod first. + if (is_dir($path)) { + $dir = dir($path); + while (($entry = $dir->read()) !== FALSE) { + if ($entry == '.' || $entry == '..') { + continue; + } + $entry_path = $path . '/' . $entry; + _simpletest_delete_recursive($entry_path); + } + $dir->close(); + rmdir($path); + } + else { + unlink($path); + } +} + +/** * Clear the test result tables. * * @param $test_id diff --git a/core/modules/system/lib/Drupal/system/Tests/PhpLoader/MTimeProtectedLoaderTest.php b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/MTimeProtectedLoaderTest.php new file mode 100644 index 0000000..436d59f --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/MTimeProtectedLoaderTest.php @@ -0,0 +1,41 @@ + 'Loader test', + 'description' => 'Tests the protected loader.', + 'group' => 'System', + ); + } + + public function testMTimeProtectedLoader() { + variable_set('loader_classes', array('default' => + array( + 'class' => 'Drupal\Component\PhpLoader\MTimeProtectedLoader', + 'prefix' => variable_get('file_public_path', conf_path() . '/files') . '/codegen', + 'secret' => $GLOBALS['drupal_hash_salt'], + ), + )); + drupal_static_reset('_drupal_php_get_loader'); + $filename = $this->randomName() . '/' . $this->randomName() . '.php'; + do { + $random = mt_rand(10000, 100000); + $function = 'test' . $random; + } while (function_exists($function)); + $contents = "assertIdentical($function(), $random); + } +}