diff --git a/core/lib/Drupal/Component/PhpStorage/FileStorage.php b/core/lib/Drupal/Component/PhpStorage/FileStorage.php index e4e7fde..a9f0759 100644 --- a/core/lib/Drupal/Component/PhpStorage/FileStorage.php +++ b/core/lib/Drupal/Component/PhpStorage/FileStorage.php @@ -20,6 +20,21 @@ class FileStorage implements PhpStorageInterface { protected $directory; /** + * Set to TRUE if this is a Windows server. + * + * @var bool + */ + protected $is_windows; + + /** + * Stores the Unicode-compatible verison of $directory for use on Windows + * servers with a 260 MAX_PATH limitation. + * + * @var string + */ + protected $windows_path; + + /** * Constructs this FileStorage object. * * @param array $configuration @@ -29,10 +44,56 @@ class FileStorage implements PhpStorageInterface { * the same configuration, but for different bins.. */ public function __construct(array $configuration) { - $this->directory = $configuration['directory'] . '/' . $configuration['bin']; + // We must use the appropriate directory separator since we're using the + // Unicode path on Windows. + $this->directory = $configuration['directory'] . DIRECTORY_SEPARATOR . $configuration['bin']; + $this->is_windows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + if ($this->is_windows) { + $this->windows_path = $this->convertToUnicodePath($this->directory); + } } /** + * Converts a standard path to a Windows-compatible Unicode path on the + * Windows platform, allowing Windows to use paths greater than 260 + * characters. Does nothing to an already-converted path or any path on a + * non-Windows platform. + * + * @param string $path + * The path to convert. Some part of $path must be an existing directory . + * + * @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath + */ + protected function convertToUnicodePath($path) { + if (!$this->is_windows || strpos($path, '\\\\?\\') !== FALSE) { + // If the path has already been converted, no need to do it again. + return $path; + } + + // Relative paths are not allowed with the '\\?\' notation, so we use + // realpath() to resolve them. Since realpath() returns false if the file or + // directory does not exist, we walk up the specified directory until we + // find an existing directory and use realpath() to determine the full path, + // prepend the '\\?\' notation to it, and remove that part of the path from + // $directory. + $previous = ''; + $parent = dirname($this->directory); + while (!($realpath = realpath($parent)) && $parent != $previous) { + $previous = $parent; + $parent = dirname($parent); + } + assert(!empty($realpath), 'Unable to find a Windows Unicode path for ' . $path); + + // Replace the existing part of $directory with the realpath version + // prefixed by the '\\?\' notation. + $return = '\\\\?\\' . $realpath . str_replace($parent, '', $path); + + // Cannot use forward-slashes in paths when using the '\\?\' syntax on + // Windows. + $return = str_replace('/', '\\', $return); + return $return; + } + /** * {@inheritdoc} */ public function exists($name) { @@ -55,12 +116,20 @@ public function save($name, $code) { $path = $this->getFullPath($name); $directory = dirname($path); if ($this->ensureDirectory($directory)) { - $htaccess_path = $directory . '/.htaccess'; - if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) { - @chmod($htaccess_path, 0444); + if ($this->is_windows) { + // Cannot use file_put_contents with a Windows Unicode (prefixed + // with '\\?\') path. Instead we write to a temp file and move it to + // the final location. + $temp = \Drupal::service('file_system')->tempnam(file_directory_temp(), 'filestorage_'); + if (file_put_contents($temp, $code)) { + return rename($temp, $path); + } + } + else { + return (bool) file_put_contents($path, $code); } } - return (bool) file_put_contents($path, $code); + return FALSE; } /** @@ -114,7 +183,7 @@ public static function htaccessLines($private = TRUE) { } /** - * Ensures the directory exists, has the right permissions, and a .htaccess. + * Ensures the directory exists, has the right permissions and a .htaccess. * * For compatibility with open_basedir, the requested directory is created * using a recursion logic that is based on the relative directory path/tree: @@ -131,12 +200,32 @@ public static function htaccessLines($private = TRUE) { * TRUE if the directory exists or has been created, FALSE otherwise. */ protected function ensureDirectory($directory, $mode = 0777) { + if ($this->is_windows) { + $directory = $this->convertToUnicodePath($directory); + } if ($this->createDirectory($directory, $mode)) { - $htaccess_path = $directory . '/.htaccess'; - if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) { - @chmod($htaccess_path, 0444); + // We must use the appropriate directory separator since we're using the + // Unicode path on Windows. + $htaccess_path = $directory . DIRECTORY_SEPARATOR . '.htaccess'; + if (!file_exists($htaccess_path)) { + if ($this->is_windows) { + // Cannot use file_put_contents with a Windows Unicode (prefixed + // with '\\?\') path. Instead we write to a temp file and move it to + // the final location. + $temp = \Drupal::service('file_system')->tempnam(file_directory_temp(), 'filestorage_'); + if (file_put_contents($temp, static::htaccessLines())) { + rename($temp, $htaccess_path); + return @chmod($htaccess_path, 0444); + } + } + else { + if (file_put_contents($htaccess_path, static::htaccessLines())) { + return @chmod($htaccess_path, 0444); + } + } } } + return FALSE; } /** @@ -159,10 +248,17 @@ protected function ensureDirectory($directory, $mode = 0777) { * TRUE if the directory exists or has been created, FALSE otherwise. */ protected function createDirectory($directory, $mode = 0777, $is_backwards_recursive = FALSE) { + // createDirectory() can be called recursively. Only convert to the Windows + // path on the initial call, if needed. + if ($this->is_windows && !$is_backwards_recursive) { + $directory = $this->convertToUnicodePath($directory); + } + // If the directory exists already, there's nothing to do. if (is_dir($directory)) { return TRUE; } + // Otherwise, try to create the directory and ensure to set its permissions, // because mkdir() obeys the umask of the current process. if (is_dir($parent = dirname($directory))) { @@ -197,7 +293,8 @@ public function delete($name) { * {@inheritdoc} */ public function getFullPath($name) { - return $this->directory . '/' . $name; + $directory = $this->is_windows ? $this->windows_path : $this->directory; + return $directory . DIRECTORY_SEPARATOR . $name; } /** @@ -211,7 +308,8 @@ public function writeable() { * {@inheritdoc} */ public function deleteAll() { - return $this->unlink($this->directory); + $directory = $this->is_windows ? $this->windows_path : $this->directory; + return $this->unlink($directory); } /** @@ -253,8 +351,9 @@ protected function unlink($path) { */ public function listAll() { $names = array(); - if (file_exists($this->directory)) { - foreach (new \DirectoryIterator($this->directory) as $fileinfo) { + $directory = $this->is_windows ? $this->windows_path : $this->directory; + if (file_exists($directory)) { + foreach (new \DirectoryIterator($directory) as $fileinfo) { if (!$fileinfo->isDot()) { $name = $fileinfo->getFilename(); if ($name != '.htaccess') {