Skip to content
file.inc 66.7 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.
 */

 * Stream wrapper code is included here because there are cases where
 * File API is needed before a bootstrap, or in an alternate order (e.g.
 * maintenance theme).
 */
require_once DRUPAL_ROOT . '/includes/stream_wrappers.inc';

 * @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 older than
 *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
 * - 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.
Dries Buytaert's avatar
 
Dries Buytaert committed
define('FILE_CREATE_DIRECTORY', 1);
 * Flag used by file_prepare_directory() -- file permissions may be changed.
Dries Buytaert's avatar
 
Dries Buytaert committed
define('FILE_MODIFY_PERMISSIONS', 2);
 * Flag for dealing with existing files: Appends number until name is unique.
Dries Buytaert's avatar
 
Dries Buytaert committed
define('FILE_EXISTS_RENAME', 0);

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
define('FILE_EXISTS_REPLACE', 1);

/**
 * Flag for dealing with existing files: Do nothing and return FALSE.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
define('FILE_EXISTS_ERROR', 2);
Dries Buytaert's avatar
Dries Buytaert committed

 * Indicates that the file is permanent and should not be deleted.
 *
 * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed
 * during cron runs, but permanent files will not be removed during the file
 * garbage collection process.
/**
 * Methods to manage a registry of stream wrappers.
 */

/**
 * Drupal stream wrapper registry.
 *
 * 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".
 *
 * @param $filter
 *  Optionally filter out all types except these.  Defaults to
 *  STREAM_WRAPPERS_ALL, which returns all registered stream wrappers.
 *
 * @return
 *   Returns the entire Drupal stream wrapper registry.
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__);
    $wrappers = module_invoke_all('stream_wrappers');
    foreach ($wrappers as $scheme => $info) {
      // Add defaults.
      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
    }
    drupal_alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
      if (in_array('DrupalStreamWrapperInterface', 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;
        }
        stream_wrapper_register($scheme, $info['class']);
      }
      // 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.
      if ($info['type'] & $filter == $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 $uri
 *   A stream, referenced as "scheme://target".
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
 */
function file_uri_scheme($uri) {
  $data = explode('://', $uri, 2);

  return count($data) == 2 ? $data[0] : FALSE;
}

/**
 * Check 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) {
  // Does the scheme have a registered handler that is callable?
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Returns the target of a URI (e.g. a stream).
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
 */
function file_uri_target($uri) {
  $data = explode('://', $uri, 2);

  if (count($data) != 2) {
    return FALSE;
  }

  // Remove erroneous beginning forward slash.
  $data[1] = ltrim($data[1], '\/');

  return $data[1];
}

/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove all occurrences of the wrapper's directory path
 * - 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 ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
    $target = file_uri_target($uri);

    // Remove all occurrences of the wrapper's directory path.
    $directory_path = file_stream_wrapper_get_instance_by_scheme($scheme)->getDirectoryPath();
    $target = str_replace($directory_path, '', $target);

    // Trim trailing slashes from target.
    $target = rtrim($target, '/');

    // Trim erroneous leading slashes from target.
    $uri = $scheme . '://' . ltrim($target, '/');
  }
  return $uri;
}

/**
 * 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
 *   (DrupalPrivateStreamWrapper).
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
  $scheme = file_uri_scheme($uri);
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
    $instance = new $class;
    $instance->setUri($uri);
    return $instance;
  }
  else {
    return FALSE;
  }
}

/**
 * Returns a reference to the stream wrapper class responsible for a given 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
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
 *   (DrupalPublicStreamWrapper).
 *   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 = new $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.
 * @see http://drupal.org/node/515192
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * There are two kinds of local files:
 * - "created files", i.e. those in the files directory (which is stored in
 *   the file_directory_path variable and can be retrieved using
 *   file_directory_path()). 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', nothing is done
 *   and the same string is returned. If a valid stream wrapper could not be
 *   found to generate an external URL, then FALSE will be returned.
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_alter('file_url', $uri);
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
    // If this is not a properly formatted stream, then it is a shipped file.
    // Therefor, return the URI with the base URL prepended.
    return $GLOBALS['base_url'] . '/' . $uri;
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
    // Check for http so that we don't have to implement getExternalUrl() for
    // the http wrapper.
    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
}

/**
 * Check that the directory exists and is writable.
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
 * @param &$directory
 *   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) && @drupal_mkdir($directory, NULL, TRUE)) {
      return drupal_chmod($directory);
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)) {
    return drupal_chmod($directory);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

Dries Buytaert's avatar
Dries Buytaert committed
}

/**
 * If missing, create a .htaccess file in each Drupal files directory.
Dries Buytaert's avatar
Dries Buytaert committed
 */
function file_ensure_htaccess() {
  file_create_htaccess('public://', FALSE);
  file_create_htaccess('private://', TRUE);
  file_create_htaccess('temporary://', TRUE);
Dries Buytaert's avatar
Dries Buytaert committed
}

/**
 * Creates an .htaccess file in the given directory.
Dries Buytaert's avatar
Dries Buytaert committed
 *
 *   The directory.
 * @param $private
 *   FALSE indicates that $directory should be an open and public directory.
 *   The default is TRUE which indicates a private and protected directory.
Dries Buytaert's avatar
Dries Buytaert committed
 */
function file_create_htaccess($directory, $private = TRUE) {
  if (file_uri_scheme($directory)) {
    $directory = file_stream_wrapper_uri_normalize($directory);
    $directory = rtrim($directory, '/\\');
  $htaccess_path =  $directory . '/.htaccess';

  if (file_exists($htaccess_path)) {
    // Short circuit if the .htaccess file already exists.
    return;
  }

  if ($private) {
    // Private .htaccess file.
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks";
  }
  else {
    // Public .htaccess file.
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
  }

  // Write the .htaccess file.
  if (file_put_contents($htaccess_path, $htaccess_lines)) {
    drupal_chmod($htaccess_path, 0444);
  }
  else {
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
    watchdog('security', "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, WATCHDOG_ERROR);
Dries Buytaert's avatar
Dries Buytaert committed
  }
}

/**
 * @param $fids
 *   An array of file IDs.
 * @param $conditions
 *   An array of conditions to match against the {files} table. These
 *   should be supplied in the form array('field_name' => 'field_value').
 *   An array of file objects, indexed by fid.
function file_load_multiple($fids = array(), $conditions = array()) {
  return entity_load('file', $fids, $conditions);
/**
 * Load a file object from the database.
 *
 * @param $fid
 * @return
 *   A file object.
 *
 * @see hook_file_load()
 * @see file_load_multiple()
 */
function file_load($fid) {
  $files = file_load_multiple(array($fid), array());
  return reset($files);
}

/**
 * Save a file object to the database.
 *
 * If the $file->fid is not set a new record will be added. Re-saving an
 * existing file will not change its status.
 *
 * @param $file
 *   A file object returned by file_load().
 * @return
 *   The updated file object.
 * @see hook_file_insert()
 * @see hook_file_update()
 */
function file_save(stdClass $file) {
  $file->filesize = filesize($file->uri);
    drupal_write_record('file', $file);
    // Inform modules about the newly added file.
    module_invoke_all('file_insert', $file);
    entity_invoke('insert', 'file', $file);
    drupal_write_record('file', $file, 'fid');
    // Inform modules that the file has been updated.
    module_invoke_all('file_update', $file);
    entity_invoke('update', 'file', $file);
  }

  return $file;
}

/**
 * Copy a file to a new location and adds a file record to the database.
 *
 * This function should be used when manipulating files that have records
 * stored in the database. 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.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database. If the source file is a
 *   temporary file, the resulting file will also be a temporary file. See
 *   file_save_upload() for details on temporary files.
 *
 * @param $source
 *   A file object.
 * @param $destination
 *   A string containing the destination that $source should be copied to.
 *   This should be a stream wrapper URI. If this value is omitted, Drupal's
 *   public files scheme will be used, "public://".
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated. If
 *       no database entry is found then a new one will be created.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 * @return
 *   File object if the copy is successful, or FALSE in the event of an error.
 * @see file_unmanaged_copy()
 * @see hook_file_copy()
 */
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
    $file->uri = $uri;
    $file->filename = basename($uri);
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $file->fid = $existing->fid;
        $file->filename = $existing->filename;
      }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
      $file->filename = basename($destination);
    }

    $file = file_save($file);

    // Inform modules that the file has been copied.
    module_invoke_all('file_copy', $file, $source);

    return $file;
 * Copy 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.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 *
 * @param $source
 *   A string specifying the filepath or URI of the original file.
 *   A URI containing the destination that $source should be copied to. If
 *   NULL the default scheme will be used as the destination.
 * @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) {
  $original_source = $source;
  $original_destination = $destination;

  // Assert that the source file actually exists.
  $source = drupal_realpath($source);
    // @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');
Dries Buytaert's avatar
Dries Buytaert committed

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

  // Assert that the destination contains a valid stream.
  $destination_scheme = file_uri_scheme($destination);
  if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
    drupal_set_message(t('The specified file %file could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_unmanaged_copy() or a missing stream wrapper.', array('%file' => $original_source, '%destination' => $destination)), 'error');
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 . '/' . basename($source));
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
      drupal_set_message(t('The specified file %file could not be copied, because the destination %directory is not properly configured. This is often caused by a problem with file or directory permissions.', array('%file' => $original_source, '%directory' => $destination)), 'error');
      return FALSE;
    }
  }
  // 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 (%directory)', array('%file' => $source, '%directory' => $destination)), 'error');
Dries Buytaert's avatar
Dries Buytaert committed
  }

  // Assert that the source and destination filenames are not the same.
  if (drupal_realpath($source) == drupal_realpath($destination)) {
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
    return FALSE;
Dries Buytaert's avatar
Dries Buytaert committed
  }
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
  if (!@copy($source, $destination)) {
    drupal_set_message(t('The specified file %file could not be copied.', array('%file' => $source)), 'error');
    return FALSE;
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
}

/**
 * Given a relative path, construct a URI into Drupal's default files location.
 */
function file_build_uri($path) {
  $uri = variable_get('file_default_scheme', 'public') . '://' . $path;
  return file_stream_wrapper_uri_normalize($uri);
}

/**
 * Determines the destination path for a file depending on how replacement of
 * existing files should be handled.
 *
 *   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;

      case FILE_EXISTS_RENAME:
        $basename = basename($destination);
        $directory = drupal_dirname($destination);
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
        // Error reporting handled by calling function.
Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Move a file to a new location and update the file's database entry.
 *
 * Moving a file is performed by copying the file to the new location and then
 * deleting the original.
 * - Checks if $source and $destination are valid and readable/writable.
 * - Performs a file move if $source is not equal to $destination.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database.
 *
 * @param $source
 *   A file object.
 * @param $destination
 *   A string containing the destination that $source should be moved to. This
 *   must be a URI matching a Drupal stream wrapper. If this value is omitted,
 *   Drupal's 'files' directory will be used.
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated and
 *       file_delete() called on the source file after hook_file_move is called.
 *       If no database entry is found then the source files record will be
 *       updated.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 * @return
 *   Resulting file object for success, or FALSE in the event of an error.
 * @see file_unmanaged_move()
 * @see hook_file_move()
 */
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $delete_source = TRUE;
        $file->fid = $existing->fid;
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
      $file->filename = basename($destination);

    $file = file_save($file);

    // Inform modules that the file has been moved.
    module_invoke_all('file_move', $file, $source);

    if ($delete_source) {
      // Try a soft delete to remove original if it's not in use elsewhere.
      file_delete($source);
    }

    return $file;
  }
  return FALSE;
}

/**
 * Move a file to a new location without calling any hooks or making any
 * changes to the database.
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 URI matching a Drupal stream wrapper. If this value is omitted,
 *   Drupal's 'files' directory will be used.
 * @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
}

 * Modify a filename as needed for security purposes.
 * Dangerous file names will be altered; for instance, the file name
 * "exploit.php.pps" will become "exploit.php_.pps". All extensions that are
 * between 2 and 5 characters in length, internal to the file name, and not
 * included in $extensions will be altered by adding an underscore. If variable
 * 'allow_insecure_uploads' evaluates to TRUE, no alterations will be made.
 *   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 (!variable_get('allow_insecure_uploads', 0)) {
    $whitelist = array_unique(explode(' ', 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($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
        $new_filename .= '_';
      }
    }
    $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;
}

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

 * Create 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);

  // 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
}

 * Delete a file and its database record.
 *
 * If the $force parameter is not TRUE hook_file_references() will be called
 * to determine if the file is being used by any modules. If the file is being
 * used is the delete will be canceled.
 *
 * @param $file
 *   A file object.
 * @param $force
 *   Boolean indicating that the file should be deleted even if
 *   hook_file_references() reports that the file is in use.
 * @return mixed
 *   TRUE for success, FALSE in the event of an error, or an array if the file
 *   is being used by another module. The array keys are the module's name and
 *   the values are the number of references.
 * @see file_unmanaged_delete()
 * @see hook_file_references()
 * @see hook_file_delete()
 */
function file_delete(stdClass $file, $force = FALSE) {
  // If any module returns a value from the reference hook, the file will not
  // be deleted from Drupal, but file_delete will return a populated array that
  // tests as TRUE.
  if (!$force && ($references = module_invoke_all('file_references', $file))) {
    return $references;
  }

  // Let other modules clean up any references to the deleted file.
  module_invoke_all('file_delete', $file);

  // Make sure the file is deleted before removing its row from the
  // database, so UIs can still find the file in the database.
  if (file_unmanaged_delete($file->uri)) {
    db_delete('file')->condition('fid', $file->fid)->execute();
    return TRUE;
  }
  return FALSE;
}

/**
 * Delete a file without calling any hooks or making any changes to the
 * database.
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
 *   A string containing a file path or (streamwrapper) URI.
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
function file_unmanaged_delete($path) {