diff --git a/core/core.services.yml b/core/core.services.yml index 328acd8..a7e0291 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -172,6 +172,9 @@ services: factory_class: Drupal\Core\Database\Database factory_method: getConnection arguments: [default] + file_system: + class: Drupal\Core\File\FileSystem + arguments: ['@settings', '@logger.channel.file'] form_builder: class: Drupal\Core\Form\FormBuilder arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@theme.manager', '@?csrf_token'] @@ -219,6 +222,11 @@ services: logger.channel.cron: parent: logger.channel_base arguments: ['cron'] + logger.channel.file: + class: Drupal\Core\Logger\LoggerChannel + factory_method: get + factory_service: logger.factory + arguments: ['file'] logger.channel.form: parent: logger.channel_base arguments: ['form'] diff --git a/core/includes/file.inc b/core/includes/file.inc index 3472e24..d01beff 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -10,22 +10,11 @@ use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Bytes; use Drupal\Component\Utility\String; -use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Drupal\Core\StreamWrapper\PrivateStream; /** - * Default mode for new directories. See drupal_chmod(). - */ -const FILE_CHMOD_DIRECTORY = 0775; - -/** - * Default mode for new files. See drupal_chmod(). - */ -const FILE_CHMOD_FILE = 0664; - -/** * @defgroup file File interface * @{ * Common file handling functions. @@ -129,40 +118,21 @@ function file_stream_wrapper_get_class($scheme) { /** * Returns the scheme of a URI (e.g. a stream). * - * @param string $uri - * A stream, referenced as "scheme://target" or "data:target". - * - * @return string - * A string containing the name of the scheme, or FALSE if none. For example, - * the URI "public://example.txt" would return "public". - * - * @see file_uri_target() + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::uriScheme(). */ function file_uri_scheme($uri) { - if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) { - // The scheme will always be the last element in the matches array. - return array_pop($matches); - } - - return FALSE; + return \Drupal::service('file_system')->uriScheme($uri); } /** * Checks that the scheme of a stream URI is valid. * - * Confirms that there is a registered stream handler for the provided scheme - * and that it is callable. This is useful if you want to confirm a valid - * scheme without creating a new instance of the registered handler. - * - * @param string $scheme - * A URI scheme, a stream is referenced as "scheme://target". - * - * @return bool - * Returns TRUE if the string is the name of a validated stream, - * or FALSE if the scheme does not have a registered handler. + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::validScheme(). */ function file_stream_wrapper_valid_scheme($scheme) { - return $scheme && class_exists(file_stream_wrapper_get_class($scheme)); + return \Drupal::service('file_system')->validScheme($scheme); } @@ -957,38 +927,11 @@ function file_unmanaged_delete_recursive($path, $callback = NULL) { /** * Moves an uploaded file to a new location. * - * PHP's move_uploaded_file() does not properly support streams if open_basedir - * is enabled, so this function fills that gap. - * - * Compatibility: normal paths and stream wrappers. - * - * @param $filename - * The filename of the uploaded file. - * @param $uri - * A string containing the destination URI of the file. - * - * @return - * TRUE on success, or FALSE on failure. - * - * @see move_uploaded_file() - * @see http://drupal.org/node/515192 - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::moveUploadedFile(). */ function drupal_move_uploaded_file($filename, $uri) { - $result = @move_uploaded_file($filename, $uri); - // PHP's move_uploaded_file() does not properly support streams if - // open_basedir is enabled so if the move failed, try finding a real path and - // retry the move operation. - if (!$result) { - if ($realpath = drupal_realpath($uri)) { - $result = move_uploaded_file($filename, $realpath); - } - else { - $result = move_uploaded_file($filename, $uri); - } - } - - return $result; + return \Drupal::service('file_system')->moveUploadedFile($filename, $uri); } /** @@ -1179,338 +1122,82 @@ function file_get_mimetype($uri, $mapping = NULL) { /** * Sets the permissions on a file or directory. * - * This function will use the file_chmod_directory and - * file_chmod_file settings for the default modes for directories - * and uploaded/generated files. By default these will give everyone read access - * so that users accessing the files with a user account without the webserver - * group (e.g. via FTP) can read these files, and give group write permissions - * so webserver group members (e.g. a vhost account) can alter files uploaded - * and owned by the webserver. - * - * PHP's chmod does not support stream wrappers so we use our wrapper - * implementation which interfaces with chmod() by default. Contrib wrappers - * may override this behavior in their implementations as needed. - * - * @param $uri - * A string containing a URI file, or directory path. - * @param $mode - * Integer value for the permissions. Consult PHP chmod() documentation for - * more information. - * - * @return bool - * TRUE for success, FALSE in the event of an error. - * - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::chmod(). */ function drupal_chmod($uri, $mode = NULL) { - if (!isset($mode)) { - if (is_dir($uri)) { - $mode = Settings::get('file_chmod_directory', FILE_CHMOD_DIRECTORY); - } - else { - $mode = Settings::get('file_chmod_file', FILE_CHMOD_FILE); - } - } - - if (@chmod($uri, $mode)) { - return TRUE; - } - - \Drupal::logger('file')->error('The file permissions could not be set on %uri.', array('%uri' => $uri)); - return FALSE; + return \Drupal::service('file_system')->chmod($uri, $mode); } /** * Deletes a file. * - * PHP's unlink() is broken on Windows, as it can fail to remove a file - * when it has a read-only flag set. - * - * @param $uri - * A URI or pathname. - * @param $context - * Refer to http://php.net/manual/ref.stream.php - * - * @return - * Boolean TRUE on success, or FALSE on failure. - * - * @see unlink() - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::unlink(). */ function drupal_unlink($uri, $context = NULL) { - $scheme = file_uri_scheme($uri); - if (!file_stream_wrapper_valid_scheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { - chmod($uri, 0600); - } - if ($context) { - return unlink($uri, $context); - } - else { - return unlink($uri); - } + return \Drupal::service('file_system')->unlink($uri, $context); } /** * Resolves the absolute filepath of a local URI or filepath. * - * The use of drupal_realpath() is discouraged, because it does not work for - * remote URIs. Except in rare cases, URIs should not be manually resolved. - * - * Only use this function if you know that the stream wrapper in the URI uses - * the local file system, and you need to pass an absolute path to a function - * that is incompatible with stream URIs. - * - * @param string $uri - * A stream wrapper URI or a filepath, possibly including one or more symbolic - * links. - * - * @return string|false - * The absolute local filepath (with no symbolic links), or FALSE on failure. - * - * @see \Drupal\Core\StreamWrapper\StreamWrapperInterface::realpath() - * @see http://php.net/manual/function.realpath.php - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::realpath(). */ function drupal_realpath($uri) { - // If this URI is a stream, pass it off to the appropriate stream wrapper. - // Otherwise, attempt PHP's realpath. This allows use of drupal_realpath even - // for unmanaged files outside of the stream wrapper interface. - if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { - return $wrapper->realpath(); - } - - return realpath($uri); + return \Drupal::service('file_system')->realpath($uri); } /** * Gets the name of the directory from a given path. * - * PHP's dirname() does not properly pass streams, so this function fills - * that gap. It is backwards compatible with normal paths and will use - * PHP's dirname() as a fallback. - * - * Compatibility: normal paths and stream wrappers. - * - * @param $uri - * A URI or path. - * - * @return - * A string containing the directory name. - * - * @see dirname() - * @see http://drupal.org/node/515192 - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::dirname(). */ function drupal_dirname($uri) { - $scheme = file_uri_scheme($uri); - - if (file_stream_wrapper_valid_scheme($scheme)) { - return file_stream_wrapper_get_instance_by_scheme($scheme)->dirname($uri); - } - else { - return dirname($uri); - } + return \Drupal::service('file_system')->dirname($uri); } /** * Gets the filename from a given path. * - * PHP's basename() does not properly support streams or filenames beginning - * with a non-US-ASCII character. - * - * @see http://bugs.php.net/bug.php?id=37738 - * @see basename() - * - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::basename(). */ function drupal_basename($uri, $suffix = NULL) { - $separators = '/'; - if (DIRECTORY_SEPARATOR != '/') { - // For Windows OS add special separator. - $separators .= DIRECTORY_SEPARATOR; - } - // Remove right-most slashes when $uri points to directory. - $uri = rtrim($uri, $separators); - // Returns the trailing part of the $uri starting after one of the directory - // separators. - $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : ''; - // Cuts off a suffix from the filename. - if ($suffix) { - $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename); - } - return $filename; + return \Drupal::service('file_system')->basename($uri, $suffix); } /** * Creates a directory, optionally creating missing components in the path to * the directory. * - * When PHP's mkdir() creates a directory, the requested mode is affected by the - * process's umask. This function overrides the umask and sets the mode - * explicitly for all directory components created. - * - * @param $uri - * A URI or pathname. - * @param $mode - * Mode given to created directories. Defaults to the directory mode - * configured in the Drupal installation. It must have a leading zero. - * @param $recursive - * Create directories recursively, defaults to FALSE. Cannot work with a mode - * which denies writing or execution to the owner of the process. - * @param $context - * Refer to http://php.net/manual/ref.stream.php - * - * @return - * Boolean TRUE on success, or FALSE on failure. - * - * @see mkdir() - * @see http://drupal.org/node/515192 - * @ingroup php_wrappers - * - * @todo Update with open_basedir compatible recursion logic from - * \Drupal\Component\PhpStorage\FileStorage::ensureDirectory(). + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::mkdir(). */ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { - if (!isset($mode)) { - $mode = Settings::get('file_chmod_directory', FILE_CHMOD_DIRECTORY); - } - - // If the URI has a scheme, don't override the umask - schemes can handle this - // issue in their own implementation. - if (file_uri_scheme($uri)) { - return _drupal_mkdir_call($uri, $mode, $recursive, $context); - } - - // If recursive, create each missing component of the parent directory - // individually and set the mode explicitly to override the umask. - if ($recursive) { - // Ensure the path is using DIRECTORY_SEPARATOR. - $uri = str_replace('/', DIRECTORY_SEPARATOR, $uri); - // Determine the components of the path. - $components = explode(DIRECTORY_SEPARATOR, $uri); - // If the filepath is absolute the first component will be empty as there - // will be nothing before the first slash. - if ($components[0] == '') { - $recursive_path = DIRECTORY_SEPARATOR; - // Get rid of the empty first component. - array_shift($components); - } - else { - $recursive_path = ''; - } - // Don't handle the top-level directory in this loop. - array_pop($components); - // Create each component if necessary. - foreach ($components as $component) { - $recursive_path .= $component; - - if (!file_exists($recursive_path)) { - if (!_drupal_mkdir_call($recursive_path, $mode, FALSE, $context)) { - return FALSE; - } - // Not necessary to use drupal_chmod() as there is no scheme. - if (!chmod($recursive_path, $mode)) { - return FALSE; - } - } - - $recursive_path .= DIRECTORY_SEPARATOR; - } - } - - // Do not check if the top-level directory already exists, as this condition - // must cause this function to fail. - if (!_drupal_mkdir_call($uri, $mode, FALSE, $context)) { - return FALSE; - } - // Not necessary to use drupal_chmod() as there is no scheme. - return chmod($uri, $mode); -} - -/** - * Helper function. Ensures we don't pass a NULL as a context resource to - * mkdir(). - * - * @see drupal_mkdir() - */ -function _drupal_mkdir_call($uri, $mode, $recursive, $context) { - if (is_null($context)) { - return mkdir($uri, $mode, $recursive); - } - else { - return mkdir($uri, $mode, $recursive, $context); - } + return \Drupal::service('file_system')->mkdir($uri, $mode, $recursive, $context); } /** * Removes a directory. * - * PHP's rmdir() is broken on Windows, as it can fail to remove a directory - * when it has a read-only flag set. - * - * @param $uri - * A URI or pathname. - * @param $context - * Refer to http://php.net/manual/ref.stream.php - * - * @return - * Boolean TRUE on success, or FALSE on failure. - * - * @see rmdir() - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::rmdir(). */ function drupal_rmdir($uri, $context = NULL) { - $scheme = file_uri_scheme($uri); - if (!file_stream_wrapper_valid_scheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { - chmod($uri, 0700); - } - if ($context) { - return rmdir($uri, $context); - } - else { - return rmdir($uri); - } + return \Drupal::service('file_system')->rmdir($uri, $context); } /** * Creates a file with a unique filename in the specified directory. * - * PHP's tempnam() does not return a URI like we want. This function - * will return a URI if given a URI, or it will return a filepath if - * given a filepath. - * - * Compatibility: normal paths and stream wrappers. - * - * @param $directory - * The directory where the temporary filename will be created. - * @param $prefix - * The prefix of the generated temporary filename. - * Note: Windows uses only the first three characters of prefix. - * - * @return - * The new temporary filename, or FALSE on failure. - * - * @see tempnam() - * @see http://drupal.org/node/515192 - * @ingroup php_wrappers + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0. + * Use \Drupal\Core\File\FileSystem::tempnam(). */ function drupal_tempnam($directory, $prefix) { - $scheme = file_uri_scheme($directory); - - if (file_stream_wrapper_valid_scheme($scheme)) { - $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); - - if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) { - return $scheme . '://' . drupal_basename($filename); - } - else { - return FALSE; - } - } - else { - // Handle as a normal tempnam() call. - return tempnam($directory, $prefix); - } + return \Drupal::service('file_system')->tempnam($directory, $prefix); } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index d2c1312..c40c120 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -14,6 +14,7 @@ use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Logger\LoggerChannelFactory; use Drupal\Core\Site\Settings; use Drupal\Core\StringTranslation\Translator\FileTranslation; use Drupal\Core\Extension\ExtensionDiscovery; @@ -339,6 +340,15 @@ function install_begin_request($class_loader, &$install_state) { ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager') ->addArgument(new Reference('language_manager')); + // Register a minimalist logger factory without a reference to the container, + // so the factory will not try to access the current user and request stack + // services. + $container + ->register('logger.factory', 'Drupal\Core\Logger\LoggerChannelFactory'); + $container + ->register('file_system', 'Drupal\Core\File\FileSystem') + ->addArgument(Settings::getInstance()) + ->addArgument($container->get('logger.factory')->get('file')); // Register the stream wrapper manager. $container ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager') diff --git a/core/lib/Drupal/Core/File/FileSystem.php b/core/lib/Drupal/Core/File/FileSystem.php new file mode 100644 index 0000000..75e9b57 --- /dev/null +++ b/core/lib/Drupal/Core/File/FileSystem.php @@ -0,0 +1,507 @@ +settings = $settings; + $this->logger = $logger; + } + + /** + * Moves an uploaded file to a new location. + * + * PHP's move_uploaded_file() does not properly support streams if + * open_basedir is enabled, so this function fills that gap. + * + * Compatibility: normal paths and stream wrappers. + * + * @param string $filename + * The filename of the uploaded file. + * @param string $uri + * A string containing the destination URI of the file. + * + * @return bool + * TRUE on success, or FALSE on failure. + * + * @see move_uploaded_file() + * @see http://drupal.org/node/515192 + * @ingroup php_wrappers + */ + public function moveUploadedFile($filename, $uri) { + $result = @move_uploaded_file($filename, $uri); + // PHP's move_uploaded_file() does not properly support streams if + // open_basedir is enabled so if the move failed, try finding a real path + // and retry the move operation. + if (!$result) { + if ($realpath = $this->realpath($uri)) { + $result = move_uploaded_file($filename, $realpath); + } + else { + $result = move_uploaded_file($filename, $uri); + } + } + + return $result; + } + + /** + * Sets the permissions on a file or directory. + * + * This function will use the file_chmod_directory and + * file_chmod_file settings for the default modes for directories + * and uploaded/generated files. By default these will give everyone read + * access so that users accessing the files with a user account without the + * webserver group (e.g. via FTP) can read these files, and give group write + * permissions so webserver group members (e.g. a vhost account) can alter + * files uploaded and owned by the webserver. + * + * PHP's chmod does not support stream wrappers so we use our wrapper + * implementation which interfaces with chmod() by default. Contrib wrappers + * may override this behavior in their implementations as needed. + * + * @param $uri + * A string containing a URI file, or directory path. + * @param $mode + * Integer value for the permissions. Consult PHP chmod() documentation for + * more information. + * + * @return bool + * TRUE for success, FALSE in the event of an error. + * + * @ingroup php_wrappers + */ + public function chmod($uri, $mode = NULL) { + if (!isset($mode)) { + if (is_dir($uri)) { + $mode = $this->getSetting('file_chmod_directory', static::CHMOD_DIRECTORY); + } + else { + $mode = $this->getSetting('file_chmod_file', static::CHMOD_FILE); + } + } + + if (@chmod($uri, $mode)) { + return TRUE; + } + + $this->logger->error('The file permissions could not be set on %uri.', array('%uri' => $uri)); + return FALSE; + } + + /** + * Deletes a file. + * + * PHP's unlink() is broken on Windows, as it can fail to remove a file when + * it has a read-only flag set. + * + * @param string $uri + * A URI or pathname. + * @param $context + * Refer to http://php.net/manual/ref.stream.php + * + * @return bool + * Boolean TRUE on success, or FALSE on failure. + * + * @see unlink() + * @ingroup php_wrappers + */ + public function unlink($uri, $context = NULL) { + $scheme = $this->uriScheme($uri); + if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { + chmod($uri, 0600); + } + if ($context) { + return unlink($uri, $context); + } + else { + return unlink($uri); + } + } + + /** + * Resolves the absolute filepath of a local URI or filepath. + * + * The use of this method is discouraged, because it does not work for + * remote URIs. Except in rare cases, URIs should not be manually resolved. + * + * Only use this function if you know that the stream wrapper in the URI uses + * the local file system, and you need to pass an absolute path to a function + * that is incompatible with stream URIs. + * + * @param string $uri + * A stream wrapper URI or a filepath, possibly including one or more + * symbolic links. + * + * @return string|false + * The absolute local filepath (with no symbolic links) or FALSE on failure. + * + * @see \Drupal\Core\StreamWrapper\StreamWrapperInterface::realpath() + * @see http://php.net/manual/function.realpath.php + * @ingroup php_wrappers + */ + public function realpath($uri) { + // If this URI is a stream, pass it off to the appropriate stream wrapper. + // Otherwise, attempt PHP's realpath. This allows use of this method even + // for unmanaged files outside of the stream wrapper interface. + if ($wrapper = $this->getStreamWrapperByUri($uri)) { + return $wrapper->realpath(); + } + + return realpath($uri); + } + + /** + * Gets the name of the directory from a given path. + * + * PHP's dirname() does not properly pass streams, so this function fills that + * gap. It is backwards compatible with normal paths and will use PHP's + * dirname() as a fallback. + * + * Compatibility: normal paths and stream wrappers. + * + * @param string $uri + * A URI or path. + * + * @return string + * A string containing the directory name. + * + * @see dirname() + * @see http://drupal.org/node/515192 + * @ingroup php_wrappers + */ + public function dirname($uri) { + $scheme = $this->uriScheme($uri); + + if ($this->validScheme($scheme)) { + return $this->getStreamWrapperByScheme($scheme)->dirname($uri); + } + else { + return dirname($uri); + } + } + + /** + * Gets the filename from a given path. + * + * PHP's basename() does not properly support streams or filenames beginning + * with a non-US-ASCII character. + * + * @see http://bugs.php.net/bug.php?id=37738 + * @see basename() + * + * @ingroup php_wrappers + */ + public function basename($uri, $suffix = NULL) { + $separators = '/'; + if (DIRECTORY_SEPARATOR != '/') { + // For Windows OS add special separator. + $separators .= DIRECTORY_SEPARATOR; + } + // Remove right-most slashes when $uri points to directory. + $uri = rtrim($uri, $separators); + // Returns the trailing part of the $uri starting after one of the directory + // separators. + $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : ''; + // Cuts off a suffix from the filename. + if ($suffix) { + $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename); + } + return $filename; + } + + /** + * Creates a directory, optionally creating missing components in the path to + * the directory. + * + * When PHP's mkdir() creates a directory, the requested mode is affected by + * the process's umask. This function overrides the umask and sets the mode + * explicitly for all directory components created. + * + * @param $uri + * A URI or pathname. + * @param $mode + * Mode given to created directories. Defaults to the directory mode + * configured in the Drupal installation. It must have a leading zero. + * @param $recursive + * Create directories recursively, defaults to FALSE. Cannot work with a + * mode which denies writing or execution to the owner of the process. + * @param $context + * Refer to http://php.net/manual/ref.stream.php + * + * @return bool + * Boolean TRUE on success, or FALSE on failure. + * + * @see mkdir() + * @see http://drupal.org/node/515192 + * @ingroup php_wrappers + * + * @todo Update with open_basedir compatible recursion logic from + * \Drupal\Component\PhpStorage\FileStorage::ensureDirectory(). + */ + public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { + if (!isset($mode)) { + $mode = $this->getSetting('file_chmod_directory', static::CHMOD_DIRECTORY); + } + + // If the URI has a scheme, don't override the umask - schemes can handle + // this issue in their own implementation. + if ($this->uriScheme($uri)) { + return $this->mkdirCall($uri, $mode, $recursive, $context); + } + + // If recursive, create each missing component of the parent directory + // individually and set the mode explicitly to override the umask. + if ($recursive) { + // Ensure the path is using DIRECTORY_SEPARATOR. + $uri = str_replace('/', DIRECTORY_SEPARATOR, $uri); + // Determine the components of the path. + $components = explode(DIRECTORY_SEPARATOR, $uri); + // If the filepath is absolute the first component will be empty as there + // will be nothing before the first slash. + if ($components[0] == '') { + $recursive_path = DIRECTORY_SEPARATOR; + // Get rid of the empty first component. + array_shift($components); + } + else { + $recursive_path = ''; + } + // Don't handle the top-level directory in this loop. + array_pop($components); + // Create each component if necessary. + foreach ($components as $component) { + $recursive_path .= $component; + + if (!file_exists($recursive_path)) { + if (!$this->mkdirCall($recursive_path, $mode, FALSE, $context)) { + return FALSE; + } + // Not necessary to use self::chmod() as there is no scheme. + if (!chmod($recursive_path, $mode)) { + return FALSE; + } + } + + $recursive_path .= DIRECTORY_SEPARATOR; + } + } + + // Do not check if the top-level directory already exists, as this condition + // must cause this function to fail. + if (!$this->mkdirCall($uri, $mode, FALSE, $context)) { + return FALSE; + } + // Not necessary to use self::chmod() as there is no scheme. + return chmod($uri, $mode); + } + + /** + * Helper function. Ensures we don't pass a NULL as a context resource to + * mkdir(). + * + * @see self::mkdir() + */ + protected function mkdirCall($uri, $mode, $recursive, $context) { + if (is_null($context)) { + return mkdir($uri, $mode, $recursive); + } + else { + return mkdir($uri, $mode, $recursive, $context); + } + } + + /** + * Removes a directory. + * + * PHP's rmdir() is broken on Windows, as it can fail to remove a directory + * when it has a read-only flag set. + * + * @param $uri + * A URI or pathname. + * @param $context + * Refer to http://php.net/manual/ref.stream.php + * + * @return bool + * Boolean TRUE on success, or FALSE on failure. + * + * @see rmdir() + * @ingroup php_wrappers + */ + public function rmdir($uri, $context = NULL) { + $scheme = $this->uriScheme($uri); + if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { + chmod($uri, 0700); + } + if ($context) { + return rmdir($uri, $context); + } + else { + return rmdir($uri); + } + } + + /** + * Creates a file with a unique filename in the specified directory. + * + * PHP's tempnam() does not return a URI like we want. This function will + * return a URI if given a URI, or it will return a filepath if given a + * filepath. + * + * Compatibility: normal paths and stream wrappers. + * + * @param $directory + * The directory where the temporary filename will be created. + * @param $prefix + * The prefix of the generated temporary filename. + * Note: Windows uses only the first three characters of prefix. + * + * @return string|bool + * The new temporary filename, or FALSE on failure. + * + * @see tempnam() + * @see http://drupal.org/node/515192 + * @ingroup php_wrappers + */ + public function tempnam($directory, $prefix) { + $scheme = $this->uriScheme($directory); + + if ($this->validScheme($scheme)) { + $wrapper = $this->getStreamWrapperByScheme($scheme); + + if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) { + return $scheme . '://' . static::basename($filename); + } + else { + return FALSE; + } + } + else { + // Handle as a normal tempnam() call. + return tempnam($directory, $prefix); + } + } + + /** + * Returns the scheme of a URI (e.g. a stream). + * + * @param string $uri + * A stream, referenced as "scheme://target" or "data:target". + * + * @return string|bool + * A string containing the name of the scheme, or FALSE if none. For + * example, the URI "public://example.txt" would return "public". + * + * @see file_uri_target() + */ + public function uriScheme($uri) { + if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) { + // The scheme will always be the last element in the matches array. + return array_pop($matches); + } + + return FALSE; + } + + /** + * Checks that the scheme of a stream URI is valid. + * + * Confirms that there is a registered stream handler for the provided scheme + * and that it is callable. This is useful if you want to confirm a valid + * scheme without creating a new instance of the registered handler. + * + * @param $scheme + * A URI scheme, a stream is referenced as "scheme://target". + * + * @return bool + * Returns TRUE if the string is the name of a validated stream, or FALSE if + * the scheme does not have a registered handler. + */ + public function validScheme($scheme) { + if (!$scheme) { + return FALSE; + } + return class_exists($this->getStreamWrapperClass($scheme)); + } + + /** + * Wraps file_stream_wrapper_get_class(). + * + * @codeCoverageIgnore + */ + protected function getStreamWrapperClass($scheme) { + return file_stream_wrapper_get_class($scheme); + } + + /** + * Wraps file_stream_wrapper_get_instance_by_scheme(). + * + * @codeCoverageIgnore + */ + protected function getStreamWrapperByScheme($scheme) { + return file_stream_wrapper_get_instance_by_scheme($scheme); + } + + /** + * Wraps file_stream_wrapper_get_instance_by_uri(). + * + * @codeCoverageIgnore + */ + protected function getStreamWrapperByUri($uri) { + return file_stream_wrapper_get_instance_by_uri($uri); + } + + /** + * Wraps the global Settings singleton. + * + * @codeCoverageIgnore + */ + protected function getSetting($name, $default = NULL) { + return $this->settings->get($name, $default); + } + +} diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 6991181..20cfc88 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -142,9 +142,6 @@ protected function setUp() { copy($settings_services_file, DRUPAL_ROOT . '/' . $this->siteDirectory . '/services.yml'); } - // Create and set new configuration directories. - $this->prepareConfigDirectories(); - // Add this test class as a service provider. // @todo Remove the indirection; implement ServiceProviderInterface instead. $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider'; @@ -163,6 +160,9 @@ protected function setUp() { // method sets additional settings. new Settings($settings + Settings::getAll()); + // Create and set new configuration directories. + $this->prepareConfigDirectories(); + // Set the request scope. $this->container = $this->kernel->getContainer(); $this->container->get('request_stack')->push($request); diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 4109d83..7416152 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -984,11 +984,6 @@ protected function installParameters() { ), ); - // If we only have one db driver available, we cannot set the driver. - include_once DRUPAL_ROOT . '/core/includes/install.inc'; - if (count(drupal_get_database_types()) == 1) { - unset($parameters['forms']['install_settings_form']['driver']); - } return $parameters; } diff --git a/core/modules/system/src/Tests/File/UnmanagedCopyTest.php b/core/modules/system/src/Tests/File/UnmanagedCopyTest.php index 0169702..f525c1d 100644 --- a/core/modules/system/src/Tests/File/UnmanagedCopyTest.php +++ b/core/modules/system/src/Tests/File/UnmanagedCopyTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\File; use Drupal\Core\Site\Settings; +use Drupal\Core\File\FileSystem; /** * Tests the unmanaged file copy function. @@ -29,7 +30,7 @@ function testNormal() { $this->assertEqual($new_filepath, $desired_filepath, 'Returned expected filepath.'); $this->assertTrue(file_exists($uri), 'Original file remains.'); $this->assertTrue(file_exists($new_filepath), 'New file exists.'); - $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); // Copying with rename. $desired_filepath = 'public://' . $this->randomMachineName(); @@ -39,7 +40,7 @@ function testNormal() { $this->assertNotEqual($newer_filepath, $desired_filepath, 'Returned expected filepath.'); $this->assertTrue(file_exists($uri), 'Original file remains.'); $this->assertTrue(file_exists($newer_filepath), 'New file exists.'); - $this->assertFilePermissions($newer_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($newer_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); // TODO: test copying to a directory (rather than full directory/file path) // TODO: test copying normal files using normal paths (rather than only streams) @@ -69,7 +70,7 @@ function testOverwriteSelf() { $this->assertNotEqual($new_filepath, $uri, 'Copied file has a new name.'); $this->assertTrue(file_exists($uri), 'Original file exists after copying onto itself.'); $this->assertTrue(file_exists($new_filepath), 'Copied file exists after copying onto itself.'); - $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); // Copy the file onto itself without renaming fails. $new_filepath = file_unmanaged_copy($uri, $uri, FILE_EXISTS_ERROR); @@ -87,6 +88,6 @@ function testOverwriteSelf() { $this->assertNotEqual($new_filepath, $uri, 'Copied file has a new name.'); $this->assertTrue(file_exists($uri), 'Original file exists after copying onto itself.'); $this->assertTrue(file_exists($new_filepath), 'Copied file exists after copying onto itself.'); - $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); } } diff --git a/core/modules/system/src/Tests/File/UnmanagedMoveTest.php b/core/modules/system/src/Tests/File/UnmanagedMoveTest.php index e2bece8..ea39c54 100644 --- a/core/modules/system/src/Tests/File/UnmanagedMoveTest.php +++ b/core/modules/system/src/Tests/File/UnmanagedMoveTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\File; use Drupal\Core\Site\Settings; +use Drupal\Core\File\FileSystem; /** * Tests the unmanaged file move function. @@ -29,7 +30,7 @@ function testNormal() { $this->assertEqual($new_filepath, $desired_filepath, 'Returned expected filepath.'); $this->assertTrue(file_exists($new_filepath), 'File exists at the new location.'); $this->assertFalse(file_exists($uri), 'No file remains at the old location.'); - $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($new_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); // Moving with rename. $desired_filepath = 'public://' . $this->randomMachineName(); @@ -40,7 +41,7 @@ function testNormal() { $this->assertNotEqual($newer_filepath, $desired_filepath, 'Returned expected filepath.'); $this->assertTrue(file_exists($newer_filepath), 'File exists at the new location.'); $this->assertFalse(file_exists($new_filepath), 'No file remains at the old location.'); - $this->assertFilePermissions($newer_filepath, Settings::get('file_chmod_file', FILE_CHMOD_FILE)); + $this->assertFilePermissions($newer_filepath, Settings::get('file_chmod_file', FileSystem::CHMOD_FILE)); // TODO: test moving to a directory (rather than full directory/file path) // TODO: test creating and moving normal files (rather than streams) diff --git a/core/tests/Drupal/Tests/Core/File/FileSystemTest.php b/core/tests/Drupal/Tests/Core/File/FileSystemTest.php new file mode 100644 index 0000000..da854f7 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/File/FileSystemTest.php @@ -0,0 +1,182 @@ +logger = $this->getMock('Psr\Log\LoggerInterface'); + $this->fileSystem = new FileSystem($settings, $this->logger); + } + + /** + * @covers ::chmod + */ + public function testChmodFile() { + vfsStream::setup('dir'); + vfsStream::create(['test.txt' => 'asdf']); + $uri = 'vfs://dir/test.txt'; + + $this->assertTrue($this->fileSystem->chmod($uri)); + $this->assertFilePermissions(FileSystem::CHMOD_FILE, $uri); + $this->assertTrue($this->fileSystem->chmod($uri, 0444)); + $this->assertFilePermissions(0444, $uri); + } + + /** + * @covers ::chmod + */ + public function testChmodDir() { + vfsStream::setup('dir'); + vfsStream::create(['nested_dir' => []]); + $uri = 'vfs://dir/nested_dir'; + + $this->assertTrue($this->fileSystem->chmod($uri)); + $this->assertFilePermissions(FileSystem::CHMOD_DIRECTORY, $uri); + $this->assertTrue($this->fileSystem->chmod($uri, 0444)); + $this->assertFilePermissions(0444, $uri); + } + + /** + * @covers ::chmod + */ + public function testChmodUnsuccessful() { + vfsStream::setup('dir'); + $this->logger->expects($this->once()) + ->method('error'); + $this->assertFalse($this->fileSystem->chmod('vfs://dir/test.txt')); + } + + /** + * @covers ::unlink + */ + public function testUnlink() { + vfsStream::setup('dir'); + vfsStream::create(['test.txt' => 'asdf']); + $uri = 'vfs://dir/test.txt'; + + $this->fileSystem = $this->getMockBuilder('Drupal\Core\File\FileSystem') + ->disableOriginalConstructor() + ->setMethods(['validScheme']) + ->getMock(); + $this->fileSystem->expects($this->once()) + ->method('validScheme') + ->willReturn(TRUE); + + $this->assertFileExists($uri); + $this->fileSystem->unlink($uri); + $this->assertFileNotExists($uri); + } + + /** + * @covers ::basename + * + * @dataProvider providerTestBasename + */ + public function testBasename($uri, $expected, $suffix = NULL) { + $this->assertSame($expected, $this->fileSystem->basename($uri, $suffix)); + } + + public function providerTestBasename() { + $data = []; + $data[] = [ + 'public://nested/dir', + 'dir', + ]; + $data[] = [ + 'public://dir/test.txt', + 'test.txt', + ]; + $data[] = [ + 'public://dir/test.txt', + 'test', + '.txt' + ]; + return $data; + } + + /** + * @covers ::uriScheme + * + * @dataProvider providerTestUriScheme + */ + public function testUriScheme($uri, $expected) { + $this->assertSame($expected, $this->fileSystem->uriScheme($uri)); + } + + public function providerTestUriScheme() { + $data = []; + $data[] = [ + 'public://filename', + 'public', + ]; + $data[] = [ + 'public://extra://', + 'public', + ]; + $data[] = [ + 'invalid', + FALSE, + ]; + return $data; + } + + /** + * Asserts that the file permissions of a given URI matches. + * + * @param int $expected_mode + * @param string $uri + * @param string $message + */ + protected function assertFilePermissions($expected_mode, $uri, $message = '') { + // Mask out all but the last three octets. + $actual_mode = fileperms($uri) & 0777; + + // PHP on Windows has limited support for file permissions. Usually each of + // "user", "group" and "other" use one octal digit (3 bits) to represent the + // read/write/execute bits. On Windows, chmod() ignores the "group" and + // "other" bits, and fileperms() returns the "user" bits in all three + // positions. $expected_mode is updated to reflect this. + if (substr(PHP_OS, 0, 3) == 'WIN') { + // Reset the "group" and "other" bits. + $expected_mode = $expected_mode & 0700; + // Shift the "user" bits to the "group" and "other" positions also. + $expected_mode = $expected_mode | $expected_mode >> 3 | $expected_mode >> 6; + } + $this->assertSame($expected_mode, $actual_mode, $message); + } + +}