Chris@0: streamWrapperManager = $stream_wrapper_manager; Chris@0: $this->settings = $settings; Chris@0: $this->logger = $logger; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function moveUploadedFile($filename, $uri) { Chris@0: $result = @move_uploaded_file($filename, $uri); Chris@0: // PHP's move_uploaded_file() does not properly support streams if Chris@0: // open_basedir is enabled so if the move failed, try finding a real path Chris@0: // and retry the move operation. Chris@0: if (!$result) { Chris@0: if ($realpath = $this->realpath($uri)) { Chris@0: $result = move_uploaded_file($filename, $realpath); Chris@0: } Chris@0: else { Chris@0: $result = move_uploaded_file($filename, $uri); Chris@0: } Chris@0: } Chris@0: Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function chmod($uri, $mode = NULL) { Chris@0: if (!isset($mode)) { Chris@0: if (is_dir($uri)) { Chris@0: $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY); Chris@0: } Chris@0: else { Chris@0: $mode = $this->settings->get('file_chmod_file', static::CHMOD_FILE); Chris@0: } Chris@0: } Chris@0: Chris@0: if (@chmod($uri, $mode)) { Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: $this->logger->error('The file permissions could not be set on %uri.', ['%uri' => $uri]); Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function unlink($uri, $context = NULL) { Chris@0: $scheme = $this->uriScheme($uri); Chris@0: if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { Chris@0: chmod($uri, 0600); Chris@0: } Chris@0: if ($context) { Chris@0: return unlink($uri, $context); Chris@0: } Chris@0: else { Chris@0: return unlink($uri); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function realpath($uri) { Chris@0: // If this URI is a stream, pass it off to the appropriate stream wrapper. Chris@0: // Otherwise, attempt PHP's realpath. This allows use of this method even Chris@0: // for unmanaged files outside of the stream wrapper interface. Chris@0: if ($wrapper = $this->streamWrapperManager->getViaUri($uri)) { Chris@0: return $wrapper->realpath(); Chris@0: } Chris@0: Chris@0: return realpath($uri); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function dirname($uri) { Chris@0: $scheme = $this->uriScheme($uri); Chris@0: Chris@0: if ($this->validScheme($scheme)) { Chris@0: return $this->streamWrapperManager->getViaScheme($scheme)->dirname($uri); Chris@0: } Chris@0: else { Chris@0: return dirname($uri); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function basename($uri, $suffix = NULL) { Chris@0: $separators = '/'; Chris@0: if (DIRECTORY_SEPARATOR != '/') { Chris@0: // For Windows OS add special separator. Chris@0: $separators .= DIRECTORY_SEPARATOR; Chris@0: } Chris@0: // Remove right-most slashes when $uri points to directory. Chris@0: $uri = rtrim($uri, $separators); Chris@0: // Returns the trailing part of the $uri starting after one of the directory Chris@0: // separators. Chris@0: $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : ''; Chris@0: // Cuts off a suffix from the filename. Chris@0: if ($suffix) { Chris@0: $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename); Chris@0: } Chris@0: return $filename; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { Chris@0: if (!isset($mode)) { Chris@0: $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY); Chris@0: } Chris@0: Chris@0: // If the URI has a scheme, don't override the umask - schemes can handle Chris@0: // this issue in their own implementation. Chris@0: if ($this->uriScheme($uri)) { Chris@0: return $this->mkdirCall($uri, $mode, $recursive, $context); Chris@0: } Chris@0: Chris@0: // If recursive, create each missing component of the parent directory Chris@0: // individually and set the mode explicitly to override the umask. Chris@0: if ($recursive) { Chris@0: // Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing Chris@0: // slashes because they can throw off the loop when creating the parent Chris@0: // directories. Chris@0: $uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR); Chris@0: // Determine the components of the path. Chris@0: $components = explode(DIRECTORY_SEPARATOR, $uri); Chris@0: // If the filepath is absolute the first component will be empty as there Chris@0: // will be nothing before the first slash. Chris@0: if ($components[0] == '') { Chris@0: $recursive_path = DIRECTORY_SEPARATOR; Chris@0: // Get rid of the empty first component. Chris@0: array_shift($components); Chris@0: } Chris@0: else { Chris@0: $recursive_path = ''; Chris@0: } Chris@0: // Don't handle the top-level directory in this loop. Chris@0: array_pop($components); Chris@0: // Create each component if necessary. Chris@0: foreach ($components as $component) { Chris@0: $recursive_path .= $component; Chris@0: Chris@0: if (!file_exists($recursive_path)) { Chris@0: if (!$this->mkdirCall($recursive_path, $mode, FALSE, $context)) { Chris@0: return FALSE; Chris@0: } Chris@0: // Not necessary to use self::chmod() as there is no scheme. Chris@0: if (!chmod($recursive_path, $mode)) { Chris@0: return FALSE; Chris@0: } Chris@0: } Chris@0: Chris@0: $recursive_path .= DIRECTORY_SEPARATOR; Chris@0: } Chris@0: } Chris@0: Chris@0: // Do not check if the top-level directory already exists, as this condition Chris@0: // must cause this function to fail. Chris@0: if (!$this->mkdirCall($uri, $mode, FALSE, $context)) { Chris@0: return FALSE; Chris@0: } Chris@0: // Not necessary to use self::chmod() as there is no scheme. Chris@0: return chmod($uri, $mode); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Helper function. Ensures we don't pass a NULL as a context resource to Chris@0: * mkdir(). Chris@0: * Chris@0: * @see self::mkdir() Chris@0: */ Chris@0: protected function mkdirCall($uri, $mode, $recursive, $context) { Chris@0: if (is_null($context)) { Chris@0: return mkdir($uri, $mode, $recursive); Chris@0: } Chris@0: else { Chris@0: return mkdir($uri, $mode, $recursive, $context); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function rmdir($uri, $context = NULL) { Chris@0: $scheme = $this->uriScheme($uri); Chris@0: if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { Chris@0: chmod($uri, 0700); Chris@0: } Chris@0: if ($context) { Chris@0: return rmdir($uri, $context); Chris@0: } Chris@0: else { Chris@0: return rmdir($uri); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function tempnam($directory, $prefix) { Chris@0: $scheme = $this->uriScheme($directory); Chris@0: Chris@0: if ($this->validScheme($scheme)) { Chris@0: $wrapper = $this->streamWrapperManager->getViaScheme($scheme); Chris@0: Chris@0: if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) { Chris@0: return $scheme . '://' . static::basename($filename); Chris@0: } Chris@0: else { Chris@0: return FALSE; Chris@0: } Chris@0: } Chris@0: else { Chris@0: // Handle as a normal tempnam() call. Chris@0: return tempnam($directory, $prefix); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function uriScheme($uri) { Chris@0: if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) { Chris@0: // The scheme will always be the last element in the matches array. Chris@0: return array_pop($matches); Chris@0: } Chris@0: Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function validScheme($scheme) { Chris@0: if (!$scheme) { Chris@0: return FALSE; Chris@0: } Chris@0: return class_exists($this->streamWrapperManager->getClass($scheme)); Chris@0: } Chris@0: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function copy($source, $destination, $replace = self::EXISTS_RENAME) { Chris@18: $this->prepareDestination($source, $destination, $replace); Chris@18: Chris@18: // Perform the copy operation. Chris@18: if (!@copy($source, $destination)) { Chris@18: // If the copy failed and realpaths exist, retry the operation using them Chris@18: // instead. Chris@18: $real_source = $this->realpath($source) ?: $source; Chris@18: $real_destination = $this->realpath($destination) ?: $destination; Chris@18: if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) { Chris@18: $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [ Chris@18: '%source' => $source, Chris@18: '%destination' => $destination, Chris@18: ]); Chris@18: throw new FileWriteException("The specified file '$source' could not be copied to '$destination'."); Chris@18: } Chris@18: } Chris@18: Chris@18: // Set the permissions on the new file. Chris@18: $this->chmod($destination); Chris@18: Chris@18: return $destination; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function delete($path) { Chris@18: if (is_file($path)) { Chris@18: if (!$this->unlink($path)) { Chris@18: $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]); Chris@18: throw new FileException("Failed to unlink file '$path'."); Chris@18: } Chris@18: return TRUE; Chris@18: } Chris@18: Chris@18: if (is_dir($path)) { Chris@18: $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]); Chris@18: throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead."); Chris@18: } Chris@18: Chris@18: // Return TRUE for non-existent file, but log that nothing was actually Chris@18: // deleted, as the current state is the intended result. Chris@18: if (!file_exists($path)) { Chris@18: $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]); Chris@18: return TRUE; Chris@18: } Chris@18: Chris@18: // We cannot handle anything other than files and directories. Chris@18: // Throw an exception for everything else (sockets, symbolic links, etc). Chris@18: $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]); Chris@18: throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted."); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function deleteRecursive($path, callable $callback = NULL) { Chris@18: if ($callback) { Chris@18: call_user_func($callback, $path); Chris@18: } Chris@18: Chris@18: if (is_dir($path)) { Chris@18: $dir = dir($path); Chris@18: while (($entry = $dir->read()) !== FALSE) { Chris@18: if ($entry == '.' || $entry == '..') { Chris@18: continue; Chris@18: } Chris@18: $entry_path = $path . '/' . $entry; Chris@18: $this->deleteRecursive($entry_path, $callback); Chris@18: } Chris@18: $dir->close(); Chris@18: Chris@18: return $this->rmdir($path); Chris@18: } Chris@18: Chris@18: return $this->delete($path); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function move($source, $destination, $replace = self::EXISTS_RENAME) { Chris@18: $this->prepareDestination($source, $destination, $replace); Chris@18: Chris@18: // Ensure compatibility with Windows. Chris@18: // @see \Drupal\Core\File\FileSystemInterface::unlink(). Chris@18: $scheme = $this->uriScheme($source); Chris@18: if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { Chris@18: chmod($source, 0600); Chris@18: } Chris@18: // Attempt to resolve the URIs. This is necessary in certain Chris@18: // configurations (see above) and can also permit fast moves across local Chris@18: // schemes. Chris@18: $real_source = $this->realpath($source) ?: $source; Chris@18: $real_destination = $this->realpath($destination) ?: $destination; Chris@18: // Perform the move operation. Chris@18: if (!@rename($real_source, $real_destination)) { Chris@18: // Fall back to slow copy and unlink procedure. This is necessary for Chris@18: // renames across schemes that are not local, or where rename() has not Chris@18: // been implemented. It's not necessary to use drupal_unlink() as the Chris@18: // Windows issue has already been resolved above. Chris@18: if (!@copy($real_source, $real_destination)) { Chris@18: $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [ Chris@18: '%source' => $source, Chris@18: '%destination' => $destination, Chris@18: ]); Chris@18: throw new FileWriteException("The specified file '$source' could not be moved to '$destination'."); Chris@18: } Chris@18: if (!@unlink($real_source)) { Chris@18: $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [ Chris@18: '%source' => $source, Chris@18: '%destination' => $destination, Chris@18: ]); Chris@18: throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'."); Chris@18: } Chris@18: } Chris@18: Chris@18: // Set the permissions on the new file. Chris@18: $this->chmod($destination); Chris@18: Chris@18: return $destination; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Prepares the destination for a file copy or move operation. Chris@18: * Chris@18: * - Checks if $source and $destination are valid and readable/writable. Chris@18: * - Checks that $source is not equal to $destination; if they are an error Chris@18: * is reported. Chris@18: * - If file already exists in $destination either the call will error out, Chris@18: * replace the file or rename the file based on the $replace parameter. Chris@18: * Chris@18: * @param string $source Chris@18: * A string specifying the filepath or URI of the source file. Chris@18: * @param string|null $destination Chris@18: * A URI containing the destination that $source should be moved/copied to. Chris@18: * The URI may be a bare filepath (without a scheme) and in that case the Chris@18: * default scheme (file://) will be used. Chris@18: * @param int $replace Chris@18: * Replace behavior when the destination file already exists: Chris@18: * - FILE_EXISTS_REPLACE - Replace the existing file. Chris@18: * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename Chris@18: * is unique. Chris@18: * - FILE_EXISTS_ERROR - Do nothing and return FALSE. Chris@18: * Chris@18: * @see \Drupal\Core\File\FileSystemInterface::copy() Chris@18: * @see \Drupal\Core\File\FileSystemInterface::move() Chris@18: */ Chris@18: protected function prepareDestination($source, &$destination, $replace) { Chris@18: $original_source = $source; Chris@18: Chris@18: // Assert that the source file actually exists. Chris@18: if (!file_exists($source)) { Chris@18: if (($realpath = $this->realpath($original_source)) !== FALSE) { Chris@18: $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [ Chris@18: '%original_source' => $original_source, Chris@18: '%realpath' => $realpath, Chris@18: ]); Chris@18: throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist."); Chris@18: } Chris@18: else { Chris@18: $this->logger->error("File '%original_source' could not be copied because it does not exist.", [ Chris@18: '%original_source' => $original_source, Chris@18: ]); Chris@18: throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist."); Chris@18: } Chris@18: } Chris@18: Chris@18: // Prepare the destination directory. Chris@18: if ($this->prepareDirectory($destination)) { Chris@18: // The destination is already a directory, so append the source basename. Chris@18: $destination = file_stream_wrapper_uri_normalize($destination . '/' . $this->basename($source)); Chris@18: } Chris@18: else { Chris@18: // Perhaps $destination is a dir/file? Chris@18: $dirname = $this->dirname($destination); Chris@18: if (!$this->prepareDirectory($dirname)) { Chris@18: $this->logger->error("The specified file '%original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", [ Chris@18: '%original_source' => $original_source, Chris@18: ]); Chris@18: throw new DirectoryNotReadyException("The specified file '$original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions."); Chris@18: } Chris@18: } Chris@18: Chris@18: // Determine whether we can perform this operation based on overwrite rules. Chris@18: $destination = $this->getDestinationFilename($destination, $replace); Chris@18: if ($destination === FALSE) { Chris@18: $this->logger->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [ Chris@18: '%original_source' => $original_source, Chris@18: '%destination' => $destination, Chris@18: ]); Chris@18: throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination')."); Chris@18: } Chris@18: Chris@18: // Assert that the source and destination filenames are not the same. Chris@18: $real_source = $this->realpath($source); Chris@18: $real_destination = $this->realpath($destination); Chris@18: if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) { Chris@18: $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [ Chris@18: '%source' => $source, Chris@18: ]); Chris@18: throw new FileException("File '$source' could not be copied because it would overwrite itself."); Chris@18: } Chris@18: // Make sure the .htaccess files are present. Chris@18: // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304. Chris@18: file_ensure_htaccess(); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function saveData($data, $destination, $replace = self::EXISTS_RENAME) { Chris@18: // Write the data to a temporary file. Chris@18: $temp_name = $this->tempnam('temporary://', 'file'); Chris@18: if (file_put_contents($temp_name, $data) === FALSE) { Chris@18: $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]); Chris@18: throw new FileWriteException("Temporary file '$temp_name' could not be created."); Chris@18: } Chris@18: Chris@18: // Move the file to its final destination. Chris@18: return $this->move($temp_name, $destination, $replace); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) { Chris@18: if (!$this->validScheme($this->uriScheme($directory))) { Chris@18: // Only trim if we're not dealing with a stream. Chris@18: $directory = rtrim($directory, '/\\'); Chris@18: } Chris@18: Chris@18: // Check if directory exists. Chris@18: if (!is_dir($directory)) { Chris@18: // Let mkdir() recursively create directories and use the default Chris@18: // directory permissions. Chris@18: if ($options & static::CREATE_DIRECTORY) { Chris@18: return @$this->mkdir($directory, NULL, TRUE); Chris@18: } Chris@18: return FALSE; Chris@18: } Chris@18: // The directory exists, so check to see if it is writable. Chris@18: $writable = is_writable($directory); Chris@18: if (!$writable && ($options & static::MODIFY_PERMISSIONS)) { Chris@18: return $this->chmod($directory); Chris@18: } Chris@18: Chris@18: return $writable; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function getDestinationFilename($destination, $replace) { Chris@18: $basename = $this->basename($destination); Chris@18: if (!Unicode::validateUtf8($basename)) { Chris@18: throw new FileException(sprintf("Invalid filename '%s'", $basename)); Chris@18: } Chris@18: if (file_exists($destination)) { Chris@18: switch ($replace) { Chris@18: case FileSystemInterface::EXISTS_REPLACE: Chris@18: // Do nothing here, we want to overwrite the existing file. Chris@18: break; Chris@18: Chris@18: case FileSystemInterface::EXISTS_RENAME: Chris@18: $directory = $this->dirname($destination); Chris@18: $destination = $this->createFilename($basename, $directory); Chris@18: break; Chris@18: Chris@18: case FileSystemInterface::EXISTS_ERROR: Chris@18: // Error reporting handled by calling function. Chris@18: return FALSE; Chris@18: } Chris@18: } Chris@18: return $destination; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function createFilename($basename, $directory) { Chris@18: $original = $basename; Chris@18: // Strip control characters (ASCII value < 32). Though these are allowed in Chris@18: // some filesystems, not many applications handle them well. Chris@18: $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename); Chris@18: if (preg_last_error() !== PREG_NO_ERROR) { Chris@18: throw new FileException(sprintf("Invalid filename '%s'", $original)); Chris@18: } Chris@18: if (substr(PHP_OS, 0, 3) == 'WIN') { Chris@18: // These characters are not allowed in Windows filenames. Chris@18: $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename); Chris@18: } Chris@18: Chris@18: // A URI or path may already have a trailing slash or look like "public://". Chris@18: if (substr($directory, -1) == '/') { Chris@18: $separator = ''; Chris@18: } Chris@18: else { Chris@18: $separator = '/'; Chris@18: } Chris@18: Chris@18: $destination = $directory . $separator . $basename; Chris@18: Chris@18: if (file_exists($destination)) { Chris@18: // Destination file already exists, generate an alternative. Chris@18: $pos = strrpos($basename, '.'); Chris@18: if ($pos !== FALSE) { Chris@18: $name = substr($basename, 0, $pos); Chris@18: $ext = substr($basename, $pos); Chris@18: } Chris@18: else { Chris@18: $name = $basename; Chris@18: $ext = ''; Chris@18: } Chris@18: Chris@18: $counter = 0; Chris@18: do { Chris@18: $destination = $directory . $separator . $name . '_' . $counter++ . $ext; Chris@18: } while (file_exists($destination)); Chris@18: } Chris@18: Chris@18: return $destination; Chris@18: } Chris@18: Chris@0: }