diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 7daf311..5e79ea8 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3541,3 +3541,42 @@ 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_loader($bin = 'default') { + 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]; +} diff --git a/core/includes/file.inc b/core/includes/file.inc index 133d64f..5f270e0 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -1325,6 +1325,8 @@ function file_unmanaged_delete($path) { * * @param $path * A string containing either an URI or a file or directory path. + * @param $function + * The function to recurse with. _simpletest_delete_recursive() uses this. * * @return * TRUE for success or if path does not exist, FALSE in the event of an @@ -1332,7 +1334,7 @@ function file_unmanaged_delete($path) { * * @see file_unmanaged_delete() */ -function file_unmanaged_delete_recursive($path) { +function file_unmanaged_delete_recursive($path, $function = 'file_unmanaged_delete_recursive') { if (is_dir($path)) { $dir = dir($path); while (($entry = $dir->read()) !== FALSE) { @@ -1340,7 +1342,7 @@ function file_unmanaged_delete_recursive($path) { continue; } $entry_path = $path . '/' . $entry; - file_unmanaged_delete_recursive($entry_path); + $function($entry_path, $function); } $dir->close(); diff --git a/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php b/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php new file mode 100644 index 0000000..3fa96f1 --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/MTimeProtectedLoader.php @@ -0,0 +1,155 @@ +prefix = $config['prefix']; + $this->secret = $config['secret']; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::includePhp() + */ + public function includePhp($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; + } + return FALSE; + } + + /** + * 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++; + } + return TRUE; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::delete() + */ + public function delete($filename) { + $dir = $this->prefix . '/' . str_replace('/', '#', $filename); + $this->cleanDir($dir); + return rmdir($dir); + } + + /** + * Removes everything in a directory, leaving it empty. + * + * @param $dir + * The dir to be emptied out. + */ + 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. + * + * This function returns a full path starting with $dir and ending in .php, + * the filename itself will be a secure hash based on $filename, the secret + * based to the constructor and $dir_mtime. + * + * @param string $dir + * The directory containing the file. + * @param string $filename + * The filename. + * @param int $dir_mtime + * The mtime of $dir. + */ + 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..1963139 --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/NativeLoader.php @@ -0,0 +1,59 @@ +prefix = $config['prefix']; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::include() + */ + public function includePhp($filename) { + $path = $this->prefix . '/' . $filename; + if (file_exists($path)) { + include_once $path; + return TRUE; + } + return FALSE; + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::write() + */ + public function write($filename, $data) { + $path = $this->prefix . '/' . $filename; + @mkdir(dirname($path), 0700, TRUE); + return (bool) file_put_contents($path, $data); + } + + /** + * Implements Drupal\Component\PhpLoader\PhpLoaderInterface::delete() + */ + public function delete($filename) { + return 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..ad3acab --- /dev/null +++ b/core/lib/Drupal/Component/PhpLoader/PhpLoaderInterface.php @@ -0,0 +1,51 @@ +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..88ac724 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,19 @@ function simpletest_clean_temporary_directories() { } /** + * Deletes a directory recursively regardless of permissions (if possible). + * + * @param $path + * The path that will be obliterated. + */ +function _simpletest_delete_recursive($path) { + // file_unmanaged_delete_recursive() deliberately only removes visible + // files with write permission. + chmod($path, 0700); + file_unmanaged_delete_recursive($path, '_simpletest_delete_recursive'); +} + +/** * 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..c8c6063 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/MTimeProtectedLoaderTest.php @@ -0,0 +1,51 @@ + 'Loader test', + 'description' => 'Tests the protected loader.', + 'group' => 'System', + ); + } + + /** + * Tests the directory mtime based PHP loader implementation. + * + * This test first writes a file containing the definition of a non-existing + * function returning a random. Then loads it back, calls the function and + * asserts the return is correct. + */ + public function testMTimeProtectedLoader() { + $conf['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 = "write($filename, $contents); + drupal_php_loader()->includePhp($filename); + $this->assertIdentical($function(), $random); + } +}