Skip to content
file.inc 57.6 KiB
Newer Older
Dries Buytaert's avatar
Dries Buytaert committed
<?php
Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * @file
 * API for handling file uploads and server file management.
 */

use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\StreamWrapper\PublicStream;
Angie Byron's avatar
Angie Byron committed
 * Stream wrapper bit flags that are the basis for composite types.
Angie Byron's avatar
Angie Byron committed
 * Note that 0x0002 is skipped, because it was the value of a constant that has
 * since been removed.
 */

/**
 * Stream wrapper bit flag -- a filter that matches all wrappers.
 */
const STREAM_WRAPPERS_ALL = 0x0000;

/**
 * Stream wrapper bit flag -- refers to a local file system location.
 */
const STREAM_WRAPPERS_LOCAL = 0x0001;

/**
 * Stream wrapper bit flag -- wrapper is readable (almost always true).
 */
const STREAM_WRAPPERS_READ = 0x0004;

/**
 * Stream wrapper bit flag -- wrapper is writeable.
 */
const STREAM_WRAPPERS_WRITE = 0x0008;

/**
 * Stream wrapper bit flag -- exposed in the UI and potentially web accessible.
 */
const STREAM_WRAPPERS_VISIBLE = 0x0010;


/**
 * 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;

Angie Byron's avatar
Angie Byron committed
/**
 * Composite stream wrapper bit flags that are usually used as the types.
 */

/**
 * Stream wrapper type flag -- not visible in the UI or accessible via web,
 * but readable and writable. E.g. the temporary directory for uploads.
 */
define('STREAM_WRAPPERS_HIDDEN', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE);

/**
 * Stream wrapper type flag -- hidden, readable and writeable using local files.
 */
define('STREAM_WRAPPERS_LOCAL_HIDDEN', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_HIDDEN);

/**
 * Stream wrapper type flag -- visible, readable and writeable.
 */
define('STREAM_WRAPPERS_WRITE_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE | STREAM_WRAPPERS_VISIBLE);

/**
 * Stream wrapper type flag -- visible and read-only.
 */
define('STREAM_WRAPPERS_READ_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_VISIBLE);

/**
 * Stream wrapper type flag -- the default when 'type' is omitted from
 * hook_stream_wrappers(). This does not include STREAM_WRAPPERS_LOCAL,
 * because PHP grants a greater trust level to local files (for example, they
 * can be used in an "include" statement, regardless of the "allow_url_include"
 * setting), so stream wrappers need to explicitly opt-in to this.
 */
define('STREAM_WRAPPERS_NORMAL', STREAM_WRAPPERS_WRITE_VISIBLE);

/**
 * Stream wrapper type flag -- visible, readable and writeable using local files.
Angie Byron's avatar
Angie Byron committed
define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
 * @defgroup file File interface
Dries Buytaert's avatar
 
Dries Buytaert committed
 * Common file handling functions.
 * - fid: File ID
 * - uid: The {users}.uid of the user who is associated with the file.
 * - filename: Name of the file with no path components. This may differ from
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
 * - uri: URI of the file.
 * - filemime: The file's MIME type.
 * - filesize: The size of the file in bytes.
 * - status: A bitmapped field indicating the status of the file. The first 8
 *   bits are reserved for Drupal core. The least significant bit indicates
 *   temporary (0) or permanent (1). Temporary files will be removed during
 *   cron runs if they are older than the configuration value
 *   "system.file.temporary_maximum_age", and if clean-up is enabled. Permanent
 *   files will not be removed.
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries Buytaert's avatar
Dries Buytaert committed
 */

 * Flag used by file_prepare_directory() -- create directory if not present.
 * Flag used by file_prepare_directory() -- file permissions may be changed.
const FILE_MODIFY_PERMISSIONS = 2;
 * Flag for dealing with existing files: Appends number until name is unique.

/**
 * Flag for dealing with existing files: Replace the existing file.
 */

/**
 * Flag for dealing with existing files: Do nothing and return FALSE.
 */
Dries Buytaert's avatar
Dries Buytaert committed

 * Indicates that the file is permanent and should not be deleted.
 *
 * Temporary files older than the system.file.temporary_maximum_age
 * configuration value will be, if clean-up not disabled, removed during cron
 * runs, but permanent files will not be removed during the file garbage
 * collection process.
 *
 * A stream wrapper is an abstraction of a file system that allows Drupal to
 * use the same set of methods to access both local files and remote resources.
 *
 * Provide a facility for managing and querying user-defined stream wrappers
 * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
 * registered to handle a stream, which we need to be able to find the handler
 * for class instantiation.
 *
 * If a module registers a scheme that is already registered with PHP, the
 * existing scheme will be unregistered and replaced with the specified class.
 *
 * A stream is referenced as "scheme://target".
 *
 * The optional $filter parameter can be used to retrieve only the stream
 * wrappers that are appropriate for particular usage. For example, this returns
 * only stream wrappers that use local file storage:
 * @code
 *   $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
 * @endcode
 *
 * The $filter parameter can only filter to types containing a particular flag.
 * In some cases, you may want to filter to types that do not contain a
 * particular flag. For example, you may want to retrieve all stream wrappers
 * that are not writable, or all stream wrappers that are not local. PHP's
 * array_diff_key() function can be used to help with this. For example, this
 * returns only stream wrappers that do not use local file storage:
 * @code
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
 *   (Optional) Filters out all types except those with an on bit for each on
 *   bit in $filter. For example, if $filter is STREAM_WRAPPERS_WRITE_VISIBLE,
 *   which is equal to (STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE |
 *   STREAM_WRAPPERS_VISIBLE), then only stream wrappers with all three of these
 *   bits set are returned. Defaults to STREAM_WRAPPERS_ALL, which returns all
 *   registered stream wrappers.
 *   An array keyed by scheme, with values containing an array of information
 *   about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
 *   is omitted or set to STREAM_WRAPPERS_ALL, the entire Drupal stream wrapper
 *   registry is returned. Otherwise only the stream wrappers whose 'type'
 *   bitmask has an on bit for each bit specified in $filter are returned.
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__, array());
  if (empty($wrappers_storage)) {
    // Initialize $wrappers_storage, so that we are not calling this method
    // repeatedly if no stream wrappers exist.
    $wrappers_storage[STREAM_WRAPPERS_ALL] = array();
    if (\Drupal::hasService('module_handler')) {
      $wrappers = \Drupal::moduleHandler()->invokeAll('stream_wrappers');
      foreach ($wrappers as $scheme => $info) {
        // Add defaults.
        $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
      }
      \Drupal::moduleHandler()->alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
Angie Byron's avatar
Angie Byron committed
      if (in_array('Drupal\Core\StreamWrapper\StreamWrapperInterface', class_implements($info['class']), TRUE)) {
        // Record whether we are overriding an existing scheme.
        if (in_array($scheme, $existing, TRUE)) {
          $wrappers[$scheme]['override'] = TRUE;
          stream_wrapper_unregister($scheme);
        }
        else {
          $wrappers[$scheme]['override'] = FALSE;
        }
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
      // Pre-populate the static cache with the filters most typically used.
      $wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme];
      if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
        $wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme];
      }

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
 * @return
 *   Return string if a scheme has a registered handler, or FALSE.
 */
function file_stream_wrapper_get_class($scheme) {
  $wrappers = file_get_stream_wrappers();
  return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
}

/**
 * Returns the scheme of a URI (e.g. a stream).
 *
 * @param string $uri
 *   A stream, referenced as "scheme://target"  or "data:target".
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
  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
 *   Returns TRUE if the string is the name of a validated stream,
 *   or FALSE if the scheme does not have a registered handler.
 */
function file_stream_wrapper_valid_scheme($scheme) {
  return $scheme && class_exists(file_stream_wrapper_get_class($scheme));
 * @param string $uri
 *   A stream, referenced as "scheme://target" or "data:target".
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
  // Remove the scheme from the URI and remove erroneous leading or trailing,
  // forward-slashes and backslashes.
  $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
  // If nothing was replaced, the URI doesn't have a valid scheme.
  return $target !== $uri ? $target : FALSE;
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
  return \Drupal::config('system.file')->get('default_scheme');
/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove trailing slashes from target
 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
 *
 * @param $uri
 *   String reference containing the URI to normalize.
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

  if (file_stream_wrapper_valid_scheme($scheme)) {
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
 * Returns a reference to the stream wrapper class responsible for a given URI.
 *
 * The scheme determines the stream wrapper class that should be
 * used by consulting the stream wrapper registry.
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
 * @return
 *   Returns a new stream wrapper object appropriate for the given URI or FALSE
 *   if no registered handler could be found. For example, a URI of
 *   "private://example.txt" would return a new private stream wrapper object
Angie Byron's avatar
Angie Byron committed
 *   (Drupal\Core\StreamWrapper\PrivateStream).
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
  if ($scheme = file_uri_scheme($uri)) {
    $class = file_stream_wrapper_get_class($scheme);
    if (class_exists($class)) {
      $instance = new $class();
      $instance->setUri($uri);
      return $instance;
    }
 * Returns a reference to the stream wrapper class responsible for a scheme.
 *
 * This helper method returns a stream instance using a scheme. That is, the
 * passed string does not contain a "://". For example, "public" is a scheme
 * but "public://" is a URI (stream). This is because the later contains both
 * a scheme and target despite target being empty.
 *
 * Note: the instance URI will be initialized to "scheme://" so that you can
 * make the customary method calls as if you had retrieved an instance by URI.
 *
 * @param $scheme
 *   If the stream was "public://target", "public" would be the scheme.
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
Angie Byron's avatar
Angie Byron committed
 *   (Drupal\Core\StreamWrapper\PublicStream).
 *   FALSE is returned if no registered handler could be found.
 */
function file_stream_wrapper_get_instance_by_scheme($scheme) {
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries Buytaert's avatar
Dries Buytaert committed
/**
 * Creates a web-accessible URL for a stream to an external or local file.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * Compatibility: normal paths and stream wrappers.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
 *   These are files that have either been uploaded by users or were generated
 *   automatically (for example through CSS aggregation).
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
 *   A string containing a URL that may be used to access the file.
 *   If the provided string already contains a preceding 'http', 'https', or
 *   '/', nothing is done and the same string is returned. If a stream wrapper
 *   could not be found to generate an external URL, then FALSE is returned.
 * @see file_url_transform_relative()
Dries Buytaert's avatar
Dries Buytaert committed
 */
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
  \Drupal::moduleHandler()->alter('file_url', $uri);
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
    // Allow for:
    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
    //   http://example.com/bar.jpg by the browser when viewing a page over
    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
    // Both types of relative URIs are characterized by a leading slash, hence
    // we can use a single check.
    if (drupal_substr($uri, 0, 1) == '/') {
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
      // Therefore, return the urlencoded URI with the base URL prepended.
      return $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($uri);
  elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
    // Check for HTTP and data URI-encoded URLs so that we don't have to
    // implement getExternalUrl() for the HTTP and data schemes.
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
    if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries Buytaert's avatar
Dries Buytaert committed
}

/**
 * Transforms an absolute URL of a local file to a relative URL.
 *
 * May be useful to prevent problems on multisite set-ups and prevent mixed
 * content errors when using HTTPS + HTTP.
 *
 * @param string $file_url
 *   A file URL of a local file as generated by file_create_url().
 *
 * @return string
 *   If the file URL indeed pointed to a local file and was indeed absolute,
 *   then the transformed, relative URL to the local file. Otherwise: the
 *   original value of $file_url.
 *
 * @see file_create_url()
 */
function file_url_transform_relative($file_url) {
  // Unfortunately, we pretty much have to duplicate Symfony's
  // Request::getHttpHost() method because Request::getPort() may return NULL
  // instead of a port number.
  $http_host = '';
  $request = \Drupal::request();
  $host = $request->getHost();
  $scheme = $request->getScheme();
  $port = $request->getPort() ?: 80;
  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
    $http_host = $host;
  }
  else {
    $http_host = $host . ':' . $port;
  }

  return preg_replace('|^https?://' . $http_host . '|', '', $file_url);
}

Dries Buytaert's avatar
Dries Buytaert committed
/**
 * Checks that the directory exists and is writable.
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
 *   A bitmask to indicate if the directory should be created if it does
 *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
 *   (FILE_MODIFY_PERMISSIONS).
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries Buytaert's avatar
Dries Buytaert committed
 */
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
  if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
    // Only trim if we're not dealing with a stream.
    $directory = rtrim($directory, '/\\');
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

  // Check if directory exists.
  if (!is_dir($directory)) {
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
    if ($options & FILE_CREATE_DIRECTORY) {
      return @drupal_mkdir($directory, NULL, TRUE);
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

Dries Buytaert's avatar
Dries Buytaert committed
}

/**
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries Buytaert's avatar
Dries Buytaert committed
 */
  file_save_htaccess('public://', FALSE);
  $private_path = \Drupal::config('system.file')->get('path.private');
    file_save_htaccess('private://', TRUE);
  file_save_htaccess('temporary://', TRUE);
  file_save_htaccess(config_get_config_directory(), TRUE);
  file_save_htaccess(config_get_config_directory(CONFIG_STAGING_DIRECTORY), TRUE);
Dries Buytaert's avatar
Dries Buytaert committed
}

/**
 * Creates a .htaccess file in the given directory.
Dries Buytaert's avatar
Dries Buytaert committed
 *
 * @param bool $private
 *   (Optional) FALSE indicates that $directory should be a web-accessible
 *   directory. Defaults to TRUE which indicates a private directory.
 * @param bool $force_overwrite
 *   (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
 *   if one is already present. Defaults to FALSE.
 */
function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
    $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
    $directory = rtrim($directory, '/\\');
    $htaccess_path = $directory . '/.htaccess';
  if (file_exists($htaccess_path) && !$force_overwrite) {
    // Short circuit if the .htaccess file already exists.
  $htaccess_lines = file_htaccess_lines($private);
  if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
    return drupal_chmod($htaccess_path, 0444);
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(String::checkPlain($htaccess_lines)));
    \Drupal::logger('security')->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables);
Dries Buytaert's avatar
Dries Buytaert committed
  }
}

/**
 * Returns the standard .htaccess lines that Drupal writes to file directories.
 *
 * @param bool $private
 *   (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
 *   public directory. The default is TRUE, which returns the .htaccess lines
 *   for a private directory that should not be web-accessible.
 *
 * @return string
 *   The desired contents of the .htaccess file.
 *
 * @see file_create_htaccess()
 */
function file_htaccess_lines($private = TRUE) {
  return FileStorage::htaccessLines($private);
}

 * Determines whether the URI has a valid scheme for file API operations.
 *
 * There must be a scheme and it must be a Drupal-provided scheme like
 * 'public', 'private', 'temporary', or an extension provided with
 * hook_stream_wrappers().
 *
 * @param $uri
 *   The URI to be tested.
 *
 * @return
 *   TRUE if the URI is allowed.
 */
function file_valid_uri($uri) {
  // Assert that the URI has an allowed scheme. Bare paths are not allowed.
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
 * Copies a file to a new location without invoking the file API.
Dries Buytaert's avatar
Dries Buytaert committed
 *
 * This is a powerful function that in many ways performs like an advanced
 * version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - If the $source and $destination are equal, the behavior depends on the
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
 *   will rename the file until the $destination is unique.
 * - Provides a fallback using realpaths if the move fails using stream
 *   wrappers. This can occur because PHP's copy() function does not properly
 *   support streams if open_basedir is enabled. See
 *   https://bugs.php.net/bug.php?id=60456
 *   A string specifying the filepath or URI of the source file.
 *   A URI containing the destination that $source should be copied to. The
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 * @return
 *   The path to the new file, or FALSE in the event of an error.
Dries Buytaert's avatar
Dries Buytaert committed
 */
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  // Assert that the source file actually exists.
    // @todo Replace drupal_set_message() calls with exceptions instead.
    drupal_set_message(t('The specified file %file could not be copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
      $logger->notice('File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
      $logger->notice('File %file could not be copied because it does not exist.', array('%file' => $original_source));
Dries Buytaert's avatar
Dries Buytaert committed

  // Build a destination URI if necessary.
  if (!isset($destination)) {
    $destination = file_build_uri(drupal_basename($source));
Dries Buytaert's avatar
Dries Buytaert committed


  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
      $logger->notice('File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
      drupal_set_message(t('The specified file %file could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
    drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
    $logger->notice('File %file could not be copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
Dries Buytaert's avatar
Dries Buytaert committed
  }

  // Assert that the source and destination filenames are not the same.
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
    $logger->notice('File %file could not be copied because it would overwrite itself.', array('%file' => $source));
Dries Buytaert's avatar
Dries Buytaert committed
  }
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
    // If the copy failed and realpaths exist, retry the operation using them
    // instead.
    if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
      $logger->error('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
Dries Buytaert's avatar
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

  // Set the permissions on the new file.
  drupal_chmod($destination);
Dries Buytaert's avatar
Dries Buytaert committed
}

 * Constructs a URI to Drupal's default files location given a relative path.
  $uri = file_default_scheme() . '://' . $path;
  return file_stream_wrapper_uri_normalize($uri);
}

 *   A string specifying the desired final URI or filepath.
 * @param $replace
 *   Replace behavior when the destination file already exists.
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

        $basename = drupal_basename($destination);
        $directory = drupal_dirname($destination);
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
        // Error reporting handled by calling function.
 * Moves a file to a new location without database changes or hook invocation.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 *   A string specifying the filepath or URI of the original file.
 *   A string containing the destination that $source should be moved to.
 *   This must be a stream wrapper URI. If this value is omitted, Drupal's
 *   default files scheme will be used, usually "public://".
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 *   The URI of the moved file, or FALSE in the event of an error.
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  $filepath = file_unmanaged_copy($source, $destination, $replace);
  if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
Dries Buytaert's avatar
Dries Buytaert committed
  }
Dries Buytaert's avatar
Dries Buytaert committed
}

 * Modifies a filename as needed for security purposes.
 * Munging a file name prevents unknown file extensions from masking exploit
 * files. When web servers such as Apache decide how to process a URL request,
 * they use the file extension. If the extension is not recognized, Apache
 * skips that extension and uses the previous file extension. For example, if
 * the file being requested is exploit.php.pps, and Apache does not recognize
 * the '.pps' extension, it treats the file as PHP and executes it. To make
 * this file name safe for Apache and prevent it from executing as PHP, the
 * .php extension is "munged" into .php_, making the safe file name
 * exploit.php_.pps.
 *
 * Specifically, this function adds an underscore to all extensions that are
 * between 2 and 5 characters in length, internal to the file name, and not
 * Function behavior is also controlled by the configuration
 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
 *   A space-separated list of extensions that should not be altered.
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

  // Allow potentially insecure uploads for very savvy users and admin
  if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
    // Remove any null bytes. See http://php.net/manual/en/security.filesystem.nullbytes.php
    $filename = str_replace(chr(0), '', $filename);

    $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));

    // Split the filename up by periods. The first part becomes the basename
    // the last part the final extension.
    $filename_parts = explode('.', $filename);
    $new_filename = array_shift($filename_parts); // Remove file basename.
    $final_extension = array_pop($filename_parts); // Remove final extension.

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
      $new_filename .= '.' . $filename_part;
      if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
    $filename = $new_filename . '.' . $final_extension;

    if ($alerts && $original != $filename) {
      drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
    }
  }

  return $filename;
}

/**
 * @param $filename
 *   String with the filename to be unmunged.
 * @return
 *   An unmunged filename string.
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

 * Creates a full file path from a directory and filename.
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
 * @param $basename
 *   String filename
 * @param $directory
 *   String containing the directory or parent URI.
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
Dries Buytaert's avatar
 
Dries Buytaert committed
function file_create_filename($basename, $directory) {
  // Strip control characters (ASCII value < 32). Though these are allowed in
  // some filesystems, not many applications handle them well.
  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
  // A URI or path may already have a trailing slash or look like "public://".
  if (substr($directory, -1) == '/') {
    $separator = '';
  }
  else {
    $separator = '/';
  }

  $destination = $directory . $separator . $basename;
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
    // Destination file already exists, generate an alternative.
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries Buytaert's avatar
 
Dries Buytaert committed
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
Dries Buytaert's avatar
 
Dries Buytaert committed
    }

    $counter = 0;
    do {
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

Dries Buytaert's avatar
 
Dries Buytaert committed
}

 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}