5 * API for handling file uploads and server file management.
8 use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem;
9 use Drupal\Component\Utility\UrlHelper;
10 use Drupal\Component\PhpStorage\FileStorage;
11 use Drupal\Component\Utility\Bytes;
12 use Drupal\Core\File\FileSystem;
13 use Drupal\Core\Site\Settings;
14 use Drupal\Core\StreamWrapper\PublicStream;
15 use Drupal\Core\StreamWrapper\PrivateStream;
18 * Default mode for new directories. See drupal_chmod().
20 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
21 * Use \Drupal\Core\File\FileSystem::CHMOD_DIRECTORY.
23 * @see https://www.drupal.org/node/2418133
25 const FILE_CHMOD_DIRECTORY = FileSystem::CHMOD_DIRECTORY;
28 * Default mode for new files. See drupal_chmod().
30 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
31 * Use \Drupal\Core\File\FileSystem::CHMOD_FILE.
33 * @see https://www.drupal.org/node/2418133
35 const FILE_CHMOD_FILE = FileSystem::CHMOD_FILE;
38 * @defgroup file File interface
40 * Common file handling functions.
44 * Flag used by file_prepare_directory() -- create directory if not present.
46 const FILE_CREATE_DIRECTORY = 1;
49 * Flag used by file_prepare_directory() -- file permissions may be changed.
51 const FILE_MODIFY_PERMISSIONS = 2;
54 * Flag for dealing with existing files: Appends number until name is unique.
56 const FILE_EXISTS_RENAME = 0;
59 * Flag for dealing with existing files: Replace the existing file.
61 const FILE_EXISTS_REPLACE = 1;
64 * Flag for dealing with existing files: Do nothing and return FALSE.
66 const FILE_EXISTS_ERROR = 2;
69 * Indicates that the file is permanent and should not be deleted.
71 * Temporary files older than the system.file.temporary_maximum_age
72 * configuration value will be, if clean-up not disabled, removed during cron
73 * runs, but permanent files will not be removed during the file garbage
76 const FILE_STATUS_PERMANENT = 1;
79 * Returns the scheme of a URI (e.g. a stream).
81 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
82 * Use \Drupal\Core\File\FileSystem::uriScheme().
84 * @see https://www.drupal.org/node/2418133
86 function file_uri_scheme($uri) {
87 return \Drupal::service('file_system')->uriScheme($uri);
91 * Checks that the scheme of a stream URI is valid.
93 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
94 * Use \Drupal\Core\File\FileSystem::validScheme().
96 * @see https://www.drupal.org/node/2418133
98 function file_stream_wrapper_valid_scheme($scheme) {
99 return \Drupal::service('file_system')->validScheme($scheme);
103 * Returns the part of a URI after the schema.
106 * A stream, referenced as "scheme://target" or "data:target".
108 * @return string|bool
109 * A string containing the target (path), or FALSE if none.
110 * For example, the URI "public://sample/test.txt" would return
113 * @see file_uri_scheme()
115 function file_uri_target($uri) {
116 // Remove the scheme from the URI and remove erroneous leading or trailing,
117 // forward-slashes and backslashes.
118 $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
120 // If nothing was replaced, the URI doesn't have a valid scheme.
121 return $target !== $uri ? $target : FALSE;
125 * Gets the default file stream implementation.
128 * 'public', 'private' or any other file scheme defined as the default.
130 function file_default_scheme() {
131 return \Drupal::config('system.file')->get('default_scheme');
135 * Normalizes a URI by making it syntactically correct.
137 * A stream is referenced as "scheme://target".
139 * The following actions are taken:
140 * - Remove trailing slashes from target
141 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
144 * String reference containing the URI to normalize.
147 * The normalized URI.
149 function file_stream_wrapper_uri_normalize($uri) {
150 $scheme = \Drupal::service('file_system')->uriScheme($uri);
152 if (file_stream_wrapper_valid_scheme($scheme)) {
153 $target = file_uri_target($uri);
155 if ($target !== FALSE) {
156 $uri = $scheme . '://' . $target;
164 * Creates a web-accessible URL for a stream to an external or local file.
166 * Compatibility: normal paths and stream wrappers.
168 * There are two kinds of local files:
169 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
170 * These are files that have either been uploaded by users or were generated
171 * automatically (for example through CSS aggregation).
172 * - "shipped files", i.e. those outside of the files directory, which ship as
173 * part of Drupal core or contributed modules or themes.
176 * The URI to a file for which we need an external URL, or the path to a
180 * A string containing a URL that may be used to access the file.
181 * If the provided string already contains a preceding 'http', 'https', or
182 * '/', nothing is done and the same string is returned. If a stream wrapper
183 * could not be found to generate an external URL, then FALSE is returned.
185 * @see https://www.drupal.org/node/515192
186 * @see file_url_transform_relative()
188 function file_create_url($uri) {
189 // Allow the URI to be altered, e.g. to serve a file from a CDN or static
191 \Drupal::moduleHandler()->alter('file_url', $uri);
193 $scheme = \Drupal::service('file_system')->uriScheme($uri);
197 // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
198 // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
199 // http://example.com/bar.jpg by the browser when viewing a page over
200 // HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
201 // Both types of relative URIs are characterized by a leading slash, hence
202 // we can use a single check.
203 if (mb_substr($uri, 0, 1) == '/') {
207 // If this is not a properly formatted stream, then it is a shipped file.
208 // Therefore, return the urlencoded URI with the base URL prepended.
209 $options = UrlHelper::parse($uri);
210 $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
212 if ($options['query']) {
213 $path .= '?' . UrlHelper::buildQuery($options['query']);
217 if ($options['fragment']) {
218 $path .= '#' . $options['fragment'];
224 elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
225 // Check for HTTP and data URI-encoded URLs so that we don't have to
226 // implement getExternalUrl() for the HTTP and data schemes.
230 // Attempt to return an external URL using the appropriate wrapper.
231 if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
232 return $wrapper->getExternalUrl();
241 * Transforms an absolute URL of a local file to a relative URL.
243 * May be useful to prevent problems on multisite set-ups and prevent mixed
244 * content errors when using HTTPS + HTTP.
246 * @param string $file_url
247 * A file URL of a local file as generated by file_create_url().
250 * If the file URL indeed pointed to a local file and was indeed absolute,
251 * then the transformed, relative URL to the local file. Otherwise: the
252 * original value of $file_url.
254 * @see file_create_url()
256 function file_url_transform_relative($file_url) {
257 // Unfortunately, we pretty much have to duplicate Symfony's
258 // Request::getHttpHost() method because Request::getPort() may return NULL
259 // instead of a port number.
260 $request = \Drupal::request();
261 $host = $request->getHost();
262 $scheme = $request->getScheme();
263 $port = $request->getPort() ?: 80;
264 if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
268 $http_host = $host . ':' . $port;
271 return preg_replace('|^https?://' . preg_quote($http_host, '|') . '|', '', $file_url);
275 * Checks that the directory exists and is writable.
277 * Directories need to have execute permissions to be considered a directory by
281 * A string reference containing the name of a directory path or URI. A
282 * trailing slash will be trimmed from a path.
284 * A bitmask to indicate if the directory should be created if it does
285 * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
286 * (FILE_MODIFY_PERMISSIONS).
289 * TRUE if the directory exists (or was created) and is writable. FALSE
292 function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
293 if (!file_stream_wrapper_valid_scheme(\Drupal::service('file_system')->uriScheme($directory))) {
294 // Only trim if we're not dealing with a stream.
295 $directory = rtrim($directory, '/\\');
298 // Check if directory exists.
299 if (!is_dir($directory)) {
300 // Let mkdir() recursively create directories and use the default directory
302 if ($options & FILE_CREATE_DIRECTORY) {
303 return @drupal_mkdir($directory, NULL, TRUE);
307 // The directory exists, so check to see if it is writable.
308 $writable = is_writable($directory);
309 if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
310 return drupal_chmod($directory);
317 * Creates a .htaccess file in each Drupal files directory if it is missing.
319 function file_ensure_htaccess() {
320 file_save_htaccess('public://', FALSE);
321 $private_path = PrivateStream::basePath();
322 if (!empty($private_path)) {
323 file_save_htaccess('private://', TRUE);
325 file_save_htaccess('temporary://', TRUE);
327 // If a staging directory exists then it should contain a .htaccess file.
328 // @todo https://www.drupal.org/node/2696103 catch a more specific exception
329 // and simplify this code.
331 $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
333 catch (\Exception $e) {
337 // Note that we log an error here if we can't write the .htaccess file. This
338 // can occur if the staging directory is read-only. If it is then it is the
339 // user's responsibility to create the .htaccess file.
340 file_save_htaccess($staging, TRUE);
345 * Creates a .htaccess file in the given directory.
347 * @param string $directory
349 * @param bool $private
350 * (Optional) FALSE indicates that $directory should be a web-accessible
351 * directory. Defaults to TRUE which indicates a private directory.
352 * @param bool $force_overwrite
353 * (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
354 * if one is already present. Defaults to FALSE.
356 function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
357 if (\Drupal::service('file_system')->uriScheme($directory)) {
358 $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
361 $directory = rtrim($directory, '/\\');
362 $htaccess_path = $directory . '/.htaccess';
365 if (file_exists($htaccess_path) && !$force_overwrite) {
366 // Short circuit if the .htaccess file already exists.
369 $htaccess_lines = FileStorage::htaccessLines($private);
371 // Write the .htaccess file.
372 if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
373 return drupal_chmod($htaccess_path, 0444);
376 $variables = ['%directory' => $directory, '@htaccess' => $htaccess_lines];
377 \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: <pre><code>@htaccess</code></pre>", $variables);
383 * Returns the standard .htaccess lines that Drupal writes to file directories.
385 * @param bool $private
386 * (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
387 * public directory. The default is TRUE, which returns the .htaccess lines
388 * for a private directory that should not be web-accessible.
391 * The desired contents of the .htaccess file.
393 * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
394 * Use \Drupal\Component\PhpStorage\FileStorage::htaccessLines().
396 * @see https://www.drupal.org/node/2418133
398 function file_htaccess_lines($private = TRUE) {
399 return FileStorage::htaccessLines($private);
403 * Determines whether the URI has a valid scheme for file API operations.
405 * There must be a scheme and it must be a Drupal-provided scheme like
406 * 'public', 'private', 'temporary', or an extension provided with
407 * hook_stream_wrappers().
410 * The URI to be tested.
413 * TRUE if the URI is allowed.
415 function file_valid_uri($uri) {
416 // Assert that the URI has an allowed scheme. Bare paths are not allowed.
417 $uri_scheme = \Drupal::service('file_system')->uriScheme($uri);
418 if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
425 * Copies a file to a new location without database changes or hook invocation.
427 * This is a powerful function that in many ways performs like an advanced
429 * - Checks if $source and $destination are valid and readable/writable.
430 * - If file already exists in $destination either the call will error out,
431 * replace the file or rename the file based on the $replace parameter.
432 * - If the $source and $destination are equal, the behavior depends on the
433 * $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
434 * will rename the file until the $destination is unique.
435 * - Works around a PHP bug where copy() does not properly support streams if
436 * safe_mode or open_basedir are enabled.
437 * @see https://bugs.php.net/bug.php?id=60456
440 * A string specifying the filepath or URI of the source file.
441 * @param $destination
442 * A URI containing the destination that $source should be copied to. The
443 * URI may be a bare filepath (without a scheme). If this value is omitted,
444 * Drupal's default files scheme will be used, usually "public://".
446 * Replace behavior when the destination file already exists:
447 * - FILE_EXISTS_REPLACE - Replace the existing file.
448 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
450 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
453 * The path to the new file, or FALSE in the event of an error.
457 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
458 if (!file_unmanaged_prepare($source, $destination, $replace)) {
461 // Attempt to resolve the URIs. This is necessary in certain configurations
463 $file_system = \Drupal::service('file_system');
464 $real_source = $file_system->realpath($source) ?: $source;
465 $real_destination = $file_system->realpath($destination) ?: $destination;
466 // Perform the copy operation.
467 if (!@copy($real_source, $real_destination)) {
468 \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', ['%file' => $source, '%destination' => $destination]);
471 // Set the permissions on the new file.
472 drupal_chmod($destination);
477 * Internal function that prepares the destination for a file_unmanaged_copy or
478 * file_unmanaged_move operation.
480 * - Checks if $source and $destination are valid and readable/writable.
481 * - Checks that $source is not equal to $destination; if they are an error
483 * - If file already exists in $destination either the call will error out,
484 * replace the file or rename the file based on the $replace parameter.
487 * A string specifying the filepath or URI of the source file.
488 * @param $destination
489 * A URI containing the destination that $source should be moved/copied to.
490 * The URI may be a bare filepath (without a scheme) and in that case the
491 * default scheme (file://) will be used. If this value is omitted, Drupal's
492 * default files scheme will be used, usually "public://".
494 * Replace behavior when the destination file already exists:
495 * - FILE_EXISTS_REPLACE - Replace the existing file.
496 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
498 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
501 * TRUE, or FALSE in the event of an error.
503 * @see file_unmanaged_copy()
504 * @see file_unmanaged_move()
506 function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
507 $original_source = $source;
508 $logger = \Drupal::logger('file');
509 $file_system = \Drupal::service('file_system');
511 // Assert that the source file actually exists.
512 if (!file_exists($source)) {
513 // @todo Replace \Drupal::messenger()->addError() calls with exceptions
515 \Drupal::messenger()->addError(t('The specified file %file could not be moved/copied because no file by that name exists. Please check that you supplied the correct filename.', ['%file' => $original_source]));
516 if (($realpath = $file_system->realpath($original_source)) !== FALSE) {
517 $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', ['%file' => $original_source, '%realpath' => $realpath]);
520 $logger->notice('File %file could not be moved/copied because it does not exist.', ['%file' => $original_source]);
525 // Build a destination URI if necessary.
526 if (!isset($destination)) {
527 $destination = file_build_uri(drupal_basename($source));
530 // Prepare the destination directory.
531 if (file_prepare_directory($destination)) {
532 // The destination is already a directory, so append the source basename.
533 $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
536 // Perhaps $destination is a dir/file?
537 $dirname = drupal_dirname($destination);
538 if (!file_prepare_directory($dirname)) {
539 // The destination is not valid.
540 $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', ['%file' => $original_source, '%destination' => $dirname]);
541 \Drupal::messenger()->addError(t('The specified file %file could not be moved/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.', ['%file' => $original_source]));
546 // Determine whether we can perform this operation based on overwrite rules.
547 $destination = file_destination($destination, $replace);
548 if ($destination === FALSE) {
549 \Drupal::messenger()->addError(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', ['%file' => $original_source]));
550 $logger->notice('File %file could not be moved/copied because a file by that name already exists in the destination directory (%destination)', ['%file' => $original_source, '%destination' => $destination]);
554 // Assert that the source and destination filenames are not the same.
555 $real_source = $file_system->realpath($source);
556 $real_destination = $file_system->realpath($destination);
557 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
558 \Drupal::messenger()->addError(t('The specified file %file was not moved/copied because it would overwrite itself.', ['%file' => $source]));
559 $logger->notice('File %file could not be moved/copied because it would overwrite itself.', ['%file' => $source]);
562 // Make sure the .htaccess files are present.
563 file_ensure_htaccess();
568 * Constructs a URI to Drupal's default files location given a relative path.
570 function file_build_uri($path) {
571 $uri = file_default_scheme() . '://' . $path;
572 return file_stream_wrapper_uri_normalize($uri);
576 * Determines the destination path for a file.
578 * @param $destination
579 * A string specifying the desired final URI or filepath.
581 * Replace behavior when the destination file already exists.
582 * - FILE_EXISTS_REPLACE - Replace the existing file.
583 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
585 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
588 * The destination filepath, or FALSE if the file already exists
589 * and FILE_EXISTS_ERROR is specified.
591 function file_destination($destination, $replace) {
592 if (file_exists($destination)) {
594 case FILE_EXISTS_REPLACE:
595 // Do nothing here, we want to overwrite the existing file.
598 case FILE_EXISTS_RENAME:
599 $basename = drupal_basename($destination);
600 $directory = drupal_dirname($destination);
601 $destination = file_create_filename($basename, $directory);
604 case FILE_EXISTS_ERROR:
605 // Error reporting handled by calling function.
613 * Moves a file to a new location without database changes or hook invocation.
615 * This is a powerful function that in many ways performs like an advanced
616 * version of rename().
617 * - Checks if $source and $destination are valid and readable/writable.
618 * - Checks that $source is not equal to $destination; if they are an error
620 * - If file already exists in $destination either the call will error out,
621 * replace the file or rename the file based on the $replace parameter.
622 * - Works around a PHP bug where rename() does not properly support streams if
623 * safe_mode or open_basedir are enabled.
624 * @see https://bugs.php.net/bug.php?id=60456
627 * A string specifying the filepath or URI of the source file.
628 * @param $destination
629 * A URI containing the destination that $source should be moved to. The
630 * URI may be a bare filepath (without a scheme) and in that case the default
631 * scheme (file://) will be used. If this value is omitted, Drupal's default
632 * files scheme will be used, usually "public://".
634 * Replace behavior when the destination file already exists:
635 * - FILE_EXISTS_REPLACE - Replace the existing file.
636 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
638 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
641 * The path to the new file, or FALSE in the event of an error.
645 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
646 if (!file_unmanaged_prepare($source, $destination, $replace)) {
649 // Ensure compatibility with Windows.
650 // @see drupal_unlink()
651 if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
652 chmod($source, 0600);
654 // Attempt to resolve the URIs. This is necessary in certain configurations
655 // (see above) and can also permit fast moves across local schemes.
656 $file_system = \Drupal::service('file_system');
657 $real_source = $file_system->realpath($source) ?: $source;
658 $real_destination = $file_system->realpath($destination) ?: $destination;
659 // Perform the move operation.
660 if (!@rename($real_source, $real_destination)) {
661 // Fall back to slow copy and unlink procedure. This is necessary for
662 // renames across schemes that are not local, or where rename() has not been
663 // implemented. It's not necessary to use drupal_unlink() as the Windows
664 // issue has already been resolved above.
665 if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
666 \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', ['%file' => $source, '%destination' => $destination]);
670 // Set the permissions on the new file.
671 drupal_chmod($destination);
676 * Modifies a filename as needed for security purposes.
678 * Munging a file name prevents unknown file extensions from masking exploit
679 * files. When web servers such as Apache decide how to process a URL request,
680 * they use the file extension. If the extension is not recognized, Apache
681 * skips that extension and uses the previous file extension. For example, if
682 * the file being requested is exploit.php.pps, and Apache does not recognize
683 * the '.pps' extension, it treats the file as PHP and executes it. To make
684 * this file name safe for Apache and prevent it from executing as PHP, the
685 * .php extension is "munged" into .php_, making the safe file name
688 * Specifically, this function adds an underscore to all extensions that are
689 * between 2 and 5 characters in length, internal to the file name, and not
690 * included in $extensions.
692 * Function behavior is also controlled by the configuration
693 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
694 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
696 * File name to modify.
698 * A space-separated list of extensions that should not be altered.
700 * If TRUE, \Drupal::messenger()->addStatus() will be called to display
701 * a message if the file name was changed.
704 * The potentially modified $filename.
706 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
707 $original = $filename;
709 // Allow potentially insecure uploads for very savvy users and admin
710 if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
711 // Remove any null bytes. See
712 // http://php.net/manual/security.filesystem.nullbytes.php
713 $filename = str_replace(chr(0), '', $filename);
715 $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
717 // Split the filename up by periods. The first part becomes the basename
718 // the last part the final extension.
719 $filename_parts = explode('.', $filename);
720 // Remove file basename.
721 $new_filename = array_shift($filename_parts);
722 // Remove final extension.
723 $final_extension = array_pop($filename_parts);
725 // Loop through the middle parts of the name and add an underscore to the
726 // end of each section that could be a file extension but isn't in the list
727 // of allowed extensions.
728 foreach ($filename_parts as $filename_part) {
729 $new_filename .= '.' . $filename_part;
730 if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
731 $new_filename .= '_';
734 $filename = $new_filename . '.' . $final_extension;
736 if ($alerts && $original != $filename) {
737 \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $filename]));
745 * Undoes the effect of file_munge_filename().
748 * String with the filename to be unmunged.
751 * An unmunged filename string.
753 function file_unmunge_filename($filename) {
754 return str_replace('_.', '.', $filename);
758 * Creates a full file path from a directory and filename.
760 * If a file with the specified name already exists, an alternative will be
766 * String containing the directory or parent URI.
769 * File path consisting of $directory and a unique filename based off
772 function file_create_filename($basename, $directory) {
773 // Strip control characters (ASCII value < 32). Though these are allowed in
774 // some filesystems, not many applications handle them well.
775 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
776 if (substr(PHP_OS, 0, 3) == 'WIN') {
777 // These characters are not allowed in Windows filenames
778 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
781 // A URI or path may already have a trailing slash or look like "public://".
782 if (substr($directory, -1) == '/') {
789 $destination = $directory . $separator . $basename;
791 if (file_exists($destination)) {
792 // Destination file already exists, generate an alternative.
793 $pos = strrpos($basename, '.');
794 if ($pos !== FALSE) {
795 $name = substr($basename, 0, $pos);
796 $ext = substr($basename, $pos);
805 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
806 } while (file_exists($destination));
813 * Deletes a file and its database record.
815 * Instead of directly deleting a file, it is strongly recommended to delete
816 * file usages instead. That will automatically mark the file as temporary and
817 * remove it during cleanup.
822 * @see file_unmanaged_delete()
823 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
825 function file_delete($fid) {
826 return file_delete_multiple([$fid]);
832 * Instead of directly deleting a file, it is strongly recommended to delete
833 * file usages instead. That will automatically mark the file as temporary and
834 * remove it during cleanup.
839 * @see file_unmanaged_delete()
840 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
842 function file_delete_multiple(array $fids) {
843 entity_delete_multiple('file', $fids);
847 * Deletes a file without database changes or hook invocations.
849 * This function should be used when the file to be deleted does not have an
850 * entry recorded in the files table.
853 * A string containing a file path or (streamwrapper) URI.
856 * TRUE for success or path does not exist, or FALSE in the event of an
860 * @see file_unmanaged_delete_recursive()
862 function file_unmanaged_delete($path) {
863 if (is_file($path)) {
864 return drupal_unlink($path);
866 $logger = \Drupal::logger('file');
868 $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', ['%path' => $path]);
871 // Return TRUE for non-existent file, but log that nothing was actually
872 // deleted, as the current state is the intended result.
873 if (!file_exists($path)) {
874 $logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
877 // We cannot handle anything other than files and directories. Log an error
878 // for everything else (sockets, symbolic links, etc).
879 $logger->error('The file %path is not of a recognized type so it was not deleted.', ['%path' => $path]);
884 * Deletes all files and directories in the specified filepath recursively.
886 * If the specified path is a directory then the function will call itself
887 * recursively to process the contents. Once the contents have been removed the
888 * directory will also be removed.
890 * If the specified path is a file then it will be passed to
891 * file_unmanaged_delete().
893 * Note that this only deletes visible files with write permission.
896 * A string containing either an URI or a file or directory path.
897 * @param callable $callback
898 * (optional) Callback function to run on each file prior to deleting it and
899 * on each directory prior to traversing it. For example, can be used to
900 * modify permissions.
903 * TRUE for success or if path does not exist, FALSE in the event of an
906 * @see file_unmanaged_delete()
908 function file_unmanaged_delete_recursive($path, $callback = NULL) {
909 if (isset($callback)) {
910 call_user_func($callback, $path);
914 while (($entry = $dir->read()) !== FALSE) {
915 if ($entry == '.' || $entry == '..') {
918 $entry_path = $path . '/' . $entry;
919 file_unmanaged_delete_recursive($entry_path, $callback);
923 return drupal_rmdir($path);
925 return file_unmanaged_delete($path);
929 * Moves an uploaded file to a new location.
931 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
932 * Use \Drupal\Core\File\FileSystem::moveUploadedFile().
934 * @see https://www.drupal.org/node/2418133
936 function drupal_move_uploaded_file($filename, $uri) {
937 return \Drupal::service('file_system')->moveUploadedFile($filename, $uri);
941 * Saves a file to the specified destination without invoking file API.
943 * This function is identical to file_save_data() except the file will not be
944 * saved to the {file_managed} table and none of the file_* hooks will be
948 * A string containing the contents of the file.
949 * @param $destination
950 * A string containing the destination location. This must be a stream wrapper
951 * URI. If no value is provided, a randomized name will be generated and the
952 * file will be saved using Drupal's default files scheme, usually
955 * Replace behavior when the destination file already exists:
956 * - FILE_EXISTS_REPLACE - Replace the existing file.
957 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
959 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
962 * A string with the path of the resulting file, or FALSE on error.
964 * @see file_save_data()
966 function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
967 // Write the data to a temporary file.
968 $temp_name = drupal_tempnam('temporary://', 'file');
969 if (file_put_contents($temp_name, $data) === FALSE) {
970 \Drupal::messenger()->addError(t('The file could not be created.'));
974 // Move the file to its final destination.
975 return file_unmanaged_move($temp_name, $destination, $replace);
979 * Finds all files that match a given mask in a given directory.
981 * Directories and files beginning with a dot are excluded; this prevents
982 * hidden files and directories (such as SVN working directories) from being
983 * scanned. Use the umask option to skip configuration directories to
984 * eliminate the possibility of accidentally exposing configuration
985 * information. Also, you can use the base directory, recurse, and min_depth
986 * options to improve performance by limiting how much of the filesystem has
990 * The base directory or URI to scan, without trailing slash.
992 * The preg_match() regular expression for files to be included.
994 * An associative array of additional options, with the following elements:
995 * - 'nomask': The preg_match() regular expression for files to be excluded.
996 * Defaults to the 'file_scan_ignore_directories' setting.
997 * - 'callback': The callback function to call for each match. There is no
999 * - 'recurse': When TRUE, the directory scan will recurse the entire tree
1000 * starting at the provided directory. Defaults to TRUE.
1001 * - 'key': The key to be used for the returned associative array of files.
1002 * Possible values are 'uri', for the file's URI; 'filename', for the
1003 * basename of the file; and 'name' for the name of the file without the
1004 * extension. Defaults to 'uri'.
1005 * - 'min_depth': Minimum depth of directories to return files from. Defaults
1008 * The current depth of recursion. This parameter is only used internally and
1009 * should not be passed in.
1012 * An associative array (keyed on the chosen key) of objects with 'uri',
1013 * 'filename', and 'name' properties corresponding to the matched files.
1015 function file_scan_directory($dir, $mask, $options = [], $depth = 0) {
1016 // Merge in defaults.
1023 // Normalize $dir only once.
1025 $dir = file_stream_wrapper_uri_normalize($dir);
1026 $dir_has_slash = (substr($dir, -1) === '/');
1029 // Allow directories specified in settings.php to be ignored. You can use this
1030 // to not check for files in common special-purpose directories. For example,
1031 // node_modules and bower_components. Ignoring irrelevant directories is a
1032 // performance boost.
1033 if (!isset($options['nomask'])) {
1034 $ignore_directories = Settings::get('file_scan_ignore_directories', []);
1035 array_walk($ignore_directories, function (&$value) {
1036 $value = preg_quote($value, '/');
1038 $default_nomask = '/^' . implode('|', $ignore_directories) . '$/';
1041 $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
1043 // Avoid warnings when opendir does not have the permissions to open a
1046 if ($handle = @opendir($dir)) {
1047 while (FALSE !== ($filename = readdir($handle))) {
1048 // Skip this file if it matches the nomask or starts with a dot.
1049 if ($filename[0] != '.'
1050 && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
1051 && !(!empty($default_nomask) && preg_match($default_nomask, $filename))
1053 if ($depth == 0 && $dir_has_slash) {
1054 $uri = "$dir$filename";
1057 $uri = "$dir/$filename";
1059 if ($options['recurse'] && is_dir($uri)) {
1060 // Give priority to files in this folder by merging them in after
1061 // any subdirectory files.
1062 $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
1064 elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
1065 // Always use this match over anything already set in $files with
1066 // the same $options['key'].
1067 $file = new stdClass();
1069 $file->filename = $filename;
1070 $file->name = pathinfo($filename, PATHINFO_FILENAME);
1071 $key = $options['key'];
1072 $files[$file->$key] = $file;
1073 if ($options['callback']) {
1074 $options['callback']($uri);
1083 \Drupal::logger('file')->error('@dir can not be opened', ['@dir' => $dir]);
1091 * Determines the maximum file upload size by querying the PHP settings.
1094 * A file size limit in bytes based on the PHP upload_max_filesize and
1097 function file_upload_max_size() {
1098 static $max_size = -1;
1100 if ($max_size < 0) {
1101 // Start with post_max_size.
1102 $max_size = Bytes::toInt(ini_get('post_max_size'));
1104 // If upload_max_size is less, then reduce. Except if upload_max_size is
1105 // zero, which indicates no limit.
1106 $upload_max = Bytes::toInt(ini_get('upload_max_filesize'));
1107 if ($upload_max > 0 && $upload_max < $max_size) {
1108 $max_size = $upload_max;
1115 * Sets the permissions on a file or directory.
1117 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1118 * Use \Drupal\Core\File\FileSystem::chmod().
1120 * @see https://www.drupal.org/node/2418133
1122 function drupal_chmod($uri, $mode = NULL) {
1123 return \Drupal::service('file_system')->chmod($uri, $mode);
1129 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1130 * Use \Drupal\Core\File\FileSystem::unlink().
1132 * @see https://www.drupal.org/node/2418133
1134 function drupal_unlink($uri, $context = NULL) {
1135 return \Drupal::service('file_system')->unlink($uri, $context);
1139 * Resolves the absolute filepath of a local URI or filepath.
1141 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1142 * Use \Drupal\Core\File\FileSystem::realpath().
1144 * @see https://www.drupal.org/node/2418133
1146 function drupal_realpath($uri) {
1147 return \Drupal::service('file_system')->realpath($uri);
1151 * Gets the name of the directory from a given path.
1153 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1154 * Use \Drupal\Core\File\FileSystem::dirname().
1156 * @see https://www.drupal.org/node/2418133
1158 function drupal_dirname($uri) {
1159 return \Drupal::service('file_system')->dirname($uri);
1163 * Gets the filename from a given path.
1165 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1166 * Use \Drupal\Core\File\FileSystem::basename().
1168 * @see https://www.drupal.org/node/2418133
1170 function drupal_basename($uri, $suffix = NULL) {
1171 return \Drupal::service('file_system')->basename($uri, $suffix);
1175 * Creates a directory, optionally creating missing components in the path to
1178 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1179 * Use \Drupal\Core\File\FileSystem::mkdir().
1181 * @see https://www.drupal.org/node/2418133
1183 function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
1184 return \Drupal::service('file_system')->mkdir($uri, $mode, $recursive, $context);
1188 * Removes a directory.
1190 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1191 * Use \Drupal\Core\File\FileSystem::rmdir().
1193 * @see https://www.drupal.org/node/2418133
1195 function drupal_rmdir($uri, $context = NULL) {
1196 return \Drupal::service('file_system')->rmdir($uri, $context);
1200 * Creates a file with a unique filename in the specified directory.
1202 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1203 * Use \Drupal\Core\File\FileSystem::tempnam().
1205 * @see https://www.drupal.org/node/2418133
1207 function drupal_tempnam($directory, $prefix) {
1208 return \Drupal::service('file_system')->tempnam($directory, $prefix);
1212 * Gets and sets the path of the configured temporary directory.
1214 * @return mixed|null
1215 * A string containing the path to the temporary directory.
1217 function file_directory_temp() {
1218 $temporary_directory = \Drupal::config('system.file')->get('path.temporary');
1219 if (empty($temporary_directory)) {
1221 $config = \Drupal::configFactory()->getEditable('system.file');
1222 $temporary_directory = ComponentFileSystem::getOsTemporaryDirectory();
1224 if (empty($temporary_directory)) {
1225 // If no directory has been found default to 'files/tmp'.
1226 $temporary_directory = PublicStream::basePath() . '/tmp';
1228 // Windows accepts paths with either slash (/) or backslash (\), but will
1229 // not accept a path which contains both a slash and a backslash. Since
1230 // the 'file_public_path' variable may have either format, we sanitize
1231 // everything to use slash which is supported on all platforms.
1232 $temporary_directory = str_replace('\\', '/', $temporary_directory);
1234 // Save the path of the discovered directory. Do not check config schema on
1236 $config->set('path.temporary', (string) $temporary_directory)->save(TRUE);
1239 return $temporary_directory;
1243 * Discovers a writable system-appropriate temporary directory.
1246 * A string containing the path to the temporary directory.
1248 * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
1249 * Use \Drupal\Component\FileSystem\FileSystem::getOsTemporaryDirectory().
1251 * @see https://www.drupal.org/node/2418133
1253 function file_directory_os_temp() {
1254 return ComponentFileSystem::getOsTemporaryDirectory();
1258 * @} End of "defgroup file".