secret = $configuration['secret']; } /** * {@inheritdoc} */ public function save($name, $data) { $this->ensureDirectory($this->directory); // Write the file out to a temporary location. Prepend with a '.' to keep it // hidden from listings and web servers. $temporary_path = $this->tempnam($this->directory, '.'); if (!$temporary_path || !@file_put_contents($temporary_path, $data)) { return FALSE; } // The file will not be chmod() in the future so this is the final // permission. chmod($temporary_path, 0444); // Determine the exact modification time of the file. $mtime = $this->getUncachedMTime($temporary_path); // Move the temporary file into the proper directory. Note that POSIX // compliant systems as well as modern Windows perform the rename operation // atomically, i.e. there is no point at which another process attempting to // access the new path will find it missing. $directory = $this->getContainingDirectoryFullPath($name); $this->ensureDirectory($directory); $full_path = $this->getFullPath($name, $directory, $mtime); $result = rename($temporary_path, $full_path); // Finally reset the modification time of the directory to match the one of // the newly created file. In order to prevent the creation of a file if the // directory does not exist, ensure that the path terminates with a // directory separator. // // Recall that when subsequently loading the file, the hash is calculated // based on the file name, the containing mtime, and a the secret string. // Hence updating the mtime here is comparable to pointing a symbolic link // at a new target, i.e., the newly created file. if ($result) { $result &= touch($directory . '/', $mtime); } return (bool) $result; } /** * Gets the full path where the file is or should be stored. * * This function creates a file path that includes a unique containing * directory for the file and a file name that is a hash of the virtual file * name, a cryptographic secret, and the containing directory mtime. If the * file is overridden by an insecure upload script, the directory mtime gets * modified, invalidating the file, thus protecting against untrusted code * getting executed. * * @param string $name * The virtual file name. Can be a relative path. * @param string $directory * (optional) The directory containing the file. If not passed, this is * retrieved by calling getContainingDirectoryFullPath(). * @param int $directory_mtime * (optional) The mtime of $directory. Can be passed to avoid an extra * filesystem call when the mtime of the directory is already known. * * @return string * The full path where the file is or should be stored. */ public function getFullPath($name, &$directory = NULL, &$directory_mtime = NULL) { if (!isset($directory)) { $directory = $this->getContainingDirectoryFullPath($name); } if (!isset($directory_mtime)) { $directory_mtime = file_exists($directory) ? filemtime($directory) : 0; } return $directory . '/' . Crypt::hmacBase64($name, $this->secret . $directory_mtime) . '.php'; } /** * {@inheritdoc} */ public function delete($name) { $path = $this->getContainingDirectoryFullPath($name); if (file_exists($path)) { return $this->unlink($path); } return FALSE; } /** * {@inheritdoc} */ public function garbageCollection() { $flags = \FilesystemIterator::CURRENT_AS_FILEINFO; $flags += \FilesystemIterator::SKIP_DOTS; foreach ($this->listAll() as $name) { $directory = $this->getContainingDirectoryFullPath($name); try { $dir_iterator = new \FilesystemIterator($directory, $flags); } catch (\UnexpectedValueException $e) { // FilesystemIterator throws an UnexpectedValueException if the // specified path is not a directory, or if it is not accessible. continue; } $directory_unlink = TRUE; $directory_mtime = filemtime($directory); foreach ($dir_iterator as $fileinfo) { if ($directory_mtime > $fileinfo->getMTime()) { // Ensure the folder is writable. @chmod($directory, 0777); @unlink($fileinfo->getPathName()); } else { // The directory still contains valid files. $directory_unlink = FALSE; } } if ($directory_unlink) { $this->unlink($name); } } } /** * Gets the full path of the containing directory where the file is or should * be stored. * * @param string $name * The virtual file name. Can be a relative path. * * @return string * The full path of the containing directory where the file is or should be * stored. */ protected function getContainingDirectoryFullPath($name) { // Remove the .php file extension from the directory name. // Within a single directory, a subdirectory cannot have the same name as a // file. Thus, when switching between MTimeProtectedFastFileStorage and // FileStorage, the subdirectory or the file cannot be created in case the // other file type exists already. if (substr($name, -4) === '.php') { $name = substr($name, 0, -4); } return $this->directory . '/' . str_replace('/', '#', $name); } /** * Clears PHP's stat cache and returns the directory's mtime. */ protected function getUncachedMTime($directory) { clearstatcache(TRUE, $directory); return filemtime($directory); } /** * A brute force tempnam implementation supporting streams. * * @param $directory * The directory where the temporary filename will be created. * @param $prefix * The prefix of the generated temporary filename. * @return string * Returns the new temporary filename (with path), or FALSE on failure. */ protected function tempnam($directory, $prefix) { do { $path = $directory . '/' . $prefix . Crypt::randomBytesBase64(20); } while (file_exists($path)); return $path; } }