annotate core/includes/file.inc @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * API for handling file uploads and server file management.
Chris@0 6 */
Chris@0 7
Chris@0 8 use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem;
Chris@0 9 use Drupal\Component\Utility\Unicode;
Chris@0 10 use Drupal\Component\Utility\UrlHelper;
Chris@0 11 use Drupal\Component\PhpStorage\FileStorage;
Chris@0 12 use Drupal\Component\Utility\Bytes;
Chris@0 13 use Drupal\Core\File\FileSystem;
Chris@0 14 use Drupal\Core\Site\Settings;
Chris@0 15 use Drupal\Core\StreamWrapper\PublicStream;
Chris@0 16 use Drupal\Core\StreamWrapper\PrivateStream;
Chris@0 17
Chris@0 18 /**
Chris@0 19 * Default mode for new directories. See drupal_chmod().
Chris@0 20 *
Chris@0 21 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 22 * Use \Drupal\Core\File\FileSystem::CHMOD_DIRECTORY.
Chris@0 23 *
Chris@0 24 * @see https://www.drupal.org/node/2418133
Chris@0 25 */
Chris@0 26 const FILE_CHMOD_DIRECTORY = FileSystem::CHMOD_DIRECTORY;
Chris@0 27
Chris@0 28 /**
Chris@0 29 * Default mode for new files. See drupal_chmod().
Chris@0 30 *
Chris@0 31 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 32 * Use \Drupal\Core\File\FileSystem::CHMOD_FILE.
Chris@0 33 *
Chris@0 34 * @see https://www.drupal.org/node/2418133
Chris@0 35 */
Chris@0 36 const FILE_CHMOD_FILE = FileSystem::CHMOD_FILE;
Chris@0 37
Chris@0 38 /**
Chris@0 39 * @defgroup file File interface
Chris@0 40 * @{
Chris@0 41 * Common file handling functions.
Chris@0 42 */
Chris@0 43
Chris@0 44 /**
Chris@0 45 * Flag used by file_prepare_directory() -- create directory if not present.
Chris@0 46 */
Chris@0 47 const FILE_CREATE_DIRECTORY = 1;
Chris@0 48
Chris@0 49 /**
Chris@0 50 * Flag used by file_prepare_directory() -- file permissions may be changed.
Chris@0 51 */
Chris@0 52 const FILE_MODIFY_PERMISSIONS = 2;
Chris@0 53
Chris@0 54 /**
Chris@0 55 * Flag for dealing with existing files: Appends number until name is unique.
Chris@0 56 */
Chris@0 57 const FILE_EXISTS_RENAME = 0;
Chris@0 58
Chris@0 59 /**
Chris@0 60 * Flag for dealing with existing files: Replace the existing file.
Chris@0 61 */
Chris@0 62 const FILE_EXISTS_REPLACE = 1;
Chris@0 63
Chris@0 64 /**
Chris@0 65 * Flag for dealing with existing files: Do nothing and return FALSE.
Chris@0 66 */
Chris@0 67 const FILE_EXISTS_ERROR = 2;
Chris@0 68
Chris@0 69 /**
Chris@0 70 * Indicates that the file is permanent and should not be deleted.
Chris@0 71 *
Chris@0 72 * Temporary files older than the system.file.temporary_maximum_age
Chris@0 73 * configuration value will be, if clean-up not disabled, removed during cron
Chris@0 74 * runs, but permanent files will not be removed during the file garbage
Chris@0 75 * collection process.
Chris@0 76 */
Chris@0 77 const FILE_STATUS_PERMANENT = 1;
Chris@0 78
Chris@0 79 /**
Chris@0 80 * Returns the scheme of a URI (e.g. a stream).
Chris@0 81 *
Chris@0 82 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 83 * Use \Drupal\Core\File\FileSystem::uriScheme().
Chris@0 84 *
Chris@0 85 * @see https://www.drupal.org/node/2418133
Chris@0 86 */
Chris@0 87 function file_uri_scheme($uri) {
Chris@0 88 return \Drupal::service('file_system')->uriScheme($uri);
Chris@0 89 }
Chris@0 90
Chris@0 91 /**
Chris@0 92 * Checks that the scheme of a stream URI is valid.
Chris@0 93 *
Chris@0 94 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 95 * Use \Drupal\Core\File\FileSystem::validScheme().
Chris@0 96 *
Chris@0 97 * @see https://www.drupal.org/node/2418133
Chris@0 98 */
Chris@0 99 function file_stream_wrapper_valid_scheme($scheme) {
Chris@0 100 return \Drupal::service('file_system')->validScheme($scheme);
Chris@0 101 }
Chris@0 102
Chris@0 103
Chris@0 104 /**
Chris@0 105 * Returns the part of a URI after the schema.
Chris@0 106 *
Chris@0 107 * @param string $uri
Chris@0 108 * A stream, referenced as "scheme://target" or "data:target".
Chris@0 109 *
Chris@0 110 * @return string|bool
Chris@0 111 * A string containing the target (path), or FALSE if none.
Chris@0 112 * For example, the URI "public://sample/test.txt" would return
Chris@0 113 * "sample/test.txt".
Chris@0 114 *
Chris@0 115 * @see file_uri_scheme()
Chris@0 116 */
Chris@0 117 function file_uri_target($uri) {
Chris@0 118 // Remove the scheme from the URI and remove erroneous leading or trailing,
Chris@0 119 // forward-slashes and backslashes.
Chris@0 120 $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
Chris@0 121
Chris@0 122 // If nothing was replaced, the URI doesn't have a valid scheme.
Chris@0 123 return $target !== $uri ? $target : FALSE;
Chris@0 124 }
Chris@0 125
Chris@0 126 /**
Chris@0 127 * Gets the default file stream implementation.
Chris@0 128 *
Chris@0 129 * @return string
Chris@0 130 * 'public', 'private' or any other file scheme defined as the default.
Chris@0 131 */
Chris@0 132 function file_default_scheme() {
Chris@0 133 return \Drupal::config('system.file')->get('default_scheme');
Chris@0 134 }
Chris@0 135
Chris@0 136 /**
Chris@0 137 * Normalizes a URI by making it syntactically correct.
Chris@0 138 *
Chris@0 139 * A stream is referenced as "scheme://target".
Chris@0 140 *
Chris@0 141 * The following actions are taken:
Chris@0 142 * - Remove trailing slashes from target
Chris@0 143 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
Chris@0 144 *
Chris@0 145 * @param string $uri
Chris@0 146 * String reference containing the URI to normalize.
Chris@0 147 *
Chris@0 148 * @return string
Chris@0 149 * The normalized URI.
Chris@0 150 */
Chris@0 151 function file_stream_wrapper_uri_normalize($uri) {
Chris@0 152 $scheme = \Drupal::service('file_system')->uriScheme($uri);
Chris@0 153
Chris@0 154 if (file_stream_wrapper_valid_scheme($scheme)) {
Chris@0 155 $target = file_uri_target($uri);
Chris@0 156
Chris@0 157 if ($target !== FALSE) {
Chris@0 158 $uri = $scheme . '://' . $target;
Chris@0 159 }
Chris@0 160 }
Chris@0 161
Chris@0 162 return $uri;
Chris@0 163 }
Chris@0 164
Chris@0 165 /**
Chris@0 166 * Creates a web-accessible URL for a stream to an external or local file.
Chris@0 167 *
Chris@0 168 * Compatibility: normal paths and stream wrappers.
Chris@0 169 *
Chris@0 170 * There are two kinds of local files:
Chris@0 171 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
Chris@0 172 * These are files that have either been uploaded by users or were generated
Chris@0 173 * automatically (for example through CSS aggregation).
Chris@0 174 * - "shipped files", i.e. those outside of the files directory, which ship as
Chris@0 175 * part of Drupal core or contributed modules or themes.
Chris@0 176 *
Chris@0 177 * @param string $uri
Chris@0 178 * The URI to a file for which we need an external URL, or the path to a
Chris@0 179 * shipped file.
Chris@0 180 *
Chris@0 181 * @return string
Chris@0 182 * A string containing a URL that may be used to access the file.
Chris@0 183 * If the provided string already contains a preceding 'http', 'https', or
Chris@0 184 * '/', nothing is done and the same string is returned. If a stream wrapper
Chris@0 185 * could not be found to generate an external URL, then FALSE is returned.
Chris@0 186 *
Chris@0 187 * @see https://www.drupal.org/node/515192
Chris@0 188 * @see file_url_transform_relative()
Chris@0 189 */
Chris@0 190 function file_create_url($uri) {
Chris@0 191 // Allow the URI to be altered, e.g. to serve a file from a CDN or static
Chris@0 192 // file server.
Chris@0 193 \Drupal::moduleHandler()->alter('file_url', $uri);
Chris@0 194
Chris@0 195 $scheme = \Drupal::service('file_system')->uriScheme($uri);
Chris@0 196
Chris@0 197 if (!$scheme) {
Chris@0 198 // Allow for:
Chris@0 199 // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
Chris@0 200 // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
Chris@0 201 // http://example.com/bar.jpg by the browser when viewing a page over
Chris@0 202 // HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
Chris@0 203 // Both types of relative URIs are characterized by a leading slash, hence
Chris@0 204 // we can use a single check.
Chris@0 205 if (Unicode::substr($uri, 0, 1) == '/') {
Chris@0 206 return $uri;
Chris@0 207 }
Chris@0 208 else {
Chris@0 209 // If this is not a properly formatted stream, then it is a shipped file.
Chris@0 210 // Therefore, return the urlencoded URI with the base URL prepended.
Chris@0 211 $options = UrlHelper::parse($uri);
Chris@0 212 $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
Chris@0 213 // Append the query.
Chris@0 214 if ($options['query']) {
Chris@0 215 $path .= '?' . UrlHelper::buildQuery($options['query']);
Chris@0 216 }
Chris@0 217
Chris@0 218 // Append fragment.
Chris@0 219 if ($options['fragment']) {
Chris@0 220 $path .= '#' . $options['fragment'];
Chris@0 221 }
Chris@0 222
Chris@0 223 return $path;
Chris@0 224 }
Chris@0 225 }
Chris@0 226 elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
Chris@0 227 // Check for HTTP and data URI-encoded URLs so that we don't have to
Chris@0 228 // implement getExternalUrl() for the HTTP and data schemes.
Chris@0 229 return $uri;
Chris@0 230 }
Chris@0 231 else {
Chris@0 232 // Attempt to return an external URL using the appropriate wrapper.
Chris@0 233 if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
Chris@0 234 return $wrapper->getExternalUrl();
Chris@0 235 }
Chris@0 236 else {
Chris@0 237 return FALSE;
Chris@0 238 }
Chris@0 239 }
Chris@0 240 }
Chris@0 241
Chris@0 242 /**
Chris@0 243 * Transforms an absolute URL of a local file to a relative URL.
Chris@0 244 *
Chris@0 245 * May be useful to prevent problems on multisite set-ups and prevent mixed
Chris@0 246 * content errors when using HTTPS + HTTP.
Chris@0 247 *
Chris@0 248 * @param string $file_url
Chris@0 249 * A file URL of a local file as generated by file_create_url().
Chris@0 250 *
Chris@0 251 * @return string
Chris@0 252 * If the file URL indeed pointed to a local file and was indeed absolute,
Chris@0 253 * then the transformed, relative URL to the local file. Otherwise: the
Chris@0 254 * original value of $file_url.
Chris@0 255 *
Chris@0 256 * @see file_create_url()
Chris@0 257 */
Chris@0 258 function file_url_transform_relative($file_url) {
Chris@0 259 // Unfortunately, we pretty much have to duplicate Symfony's
Chris@0 260 // Request::getHttpHost() method because Request::getPort() may return NULL
Chris@0 261 // instead of a port number.
Chris@0 262 $request = \Drupal::request();
Chris@0 263 $host = $request->getHost();
Chris@0 264 $scheme = $request->getScheme();
Chris@0 265 $port = $request->getPort() ?: 80;
Chris@0 266 if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
Chris@0 267 $http_host = $host;
Chris@0 268 }
Chris@0 269 else {
Chris@0 270 $http_host = $host . ':' . $port;
Chris@0 271 }
Chris@0 272
Chris@0 273 return preg_replace('|^https?://' . preg_quote($http_host, '|') . '|', '', $file_url);
Chris@0 274 }
Chris@0 275
Chris@0 276 /**
Chris@0 277 * Checks that the directory exists and is writable.
Chris@0 278 *
Chris@0 279 * Directories need to have execute permissions to be considered a directory by
Chris@0 280 * FTP servers, etc.
Chris@0 281 *
Chris@0 282 * @param $directory
Chris@0 283 * A string reference containing the name of a directory path or URI. A
Chris@0 284 * trailing slash will be trimmed from a path.
Chris@0 285 * @param $options
Chris@0 286 * A bitmask to indicate if the directory should be created if it does
Chris@0 287 * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
Chris@0 288 * (FILE_MODIFY_PERMISSIONS).
Chris@0 289 *
Chris@0 290 * @return
Chris@0 291 * TRUE if the directory exists (or was created) and is writable. FALSE
Chris@0 292 * otherwise.
Chris@0 293 */
Chris@0 294 function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
Chris@0 295 if (!file_stream_wrapper_valid_scheme(\Drupal::service('file_system')->uriScheme($directory))) {
Chris@0 296 // Only trim if we're not dealing with a stream.
Chris@0 297 $directory = rtrim($directory, '/\\');
Chris@0 298 }
Chris@0 299
Chris@0 300 // Check if directory exists.
Chris@0 301 if (!is_dir($directory)) {
Chris@0 302 // Let mkdir() recursively create directories and use the default directory
Chris@0 303 // permissions.
Chris@0 304 if ($options & FILE_CREATE_DIRECTORY) {
Chris@0 305 return @drupal_mkdir($directory, NULL, TRUE);
Chris@0 306 }
Chris@0 307 return FALSE;
Chris@0 308 }
Chris@0 309 // The directory exists, so check to see if it is writable.
Chris@0 310 $writable = is_writable($directory);
Chris@0 311 if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
Chris@0 312 return drupal_chmod($directory);
Chris@0 313 }
Chris@0 314
Chris@0 315 return $writable;
Chris@0 316 }
Chris@0 317
Chris@0 318 /**
Chris@0 319 * Creates a .htaccess file in each Drupal files directory if it is missing.
Chris@0 320 */
Chris@0 321 function file_ensure_htaccess() {
Chris@0 322 file_save_htaccess('public://', FALSE);
Chris@0 323 $private_path = PrivateStream::basePath();
Chris@0 324 if (!empty($private_path)) {
Chris@0 325 file_save_htaccess('private://', TRUE);
Chris@0 326 }
Chris@0 327 file_save_htaccess('temporary://', TRUE);
Chris@0 328
Chris@0 329 // If a staging directory exists then it should contain a .htaccess file.
Chris@0 330 // @todo https://www.drupal.org/node/2696103 catch a more specific exception
Chris@0 331 // and simplify this code.
Chris@0 332 try {
Chris@0 333 $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
Chris@0 334 }
Chris@0 335 catch (\Exception $e) {
Chris@0 336 $staging = FALSE;
Chris@0 337 }
Chris@0 338 if ($staging) {
Chris@0 339 // Note that we log an error here if we can't write the .htaccess file. This
Chris@0 340 // can occur if the staging directory is read-only. If it is then it is the
Chris@0 341 // user's responsibility to create the .htaccess file.
Chris@0 342 file_save_htaccess($staging, TRUE);
Chris@0 343 }
Chris@0 344 }
Chris@0 345
Chris@0 346 /**
Chris@0 347 * Creates a .htaccess file in the given directory.
Chris@0 348 *
Chris@0 349 * @param string $directory
Chris@0 350 * The directory.
Chris@0 351 * @param bool $private
Chris@0 352 * (Optional) FALSE indicates that $directory should be a web-accessible
Chris@0 353 * directory. Defaults to TRUE which indicates a private directory.
Chris@0 354 * @param bool $force_overwrite
Chris@0 355 * (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
Chris@0 356 * if one is already present. Defaults to FALSE.
Chris@0 357 */
Chris@0 358 function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
Chris@0 359 if (\Drupal::service('file_system')->uriScheme($directory)) {
Chris@0 360 $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
Chris@0 361 }
Chris@0 362 else {
Chris@0 363 $directory = rtrim($directory, '/\\');
Chris@0 364 $htaccess_path = $directory . '/.htaccess';
Chris@0 365 }
Chris@0 366
Chris@0 367 if (file_exists($htaccess_path) && !$force_overwrite) {
Chris@0 368 // Short circuit if the .htaccess file already exists.
Chris@0 369 return TRUE;
Chris@0 370 }
Chris@0 371 $htaccess_lines = FileStorage::htaccessLines($private);
Chris@0 372
Chris@0 373 // Write the .htaccess file.
Chris@0 374 if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
Chris@0 375 return drupal_chmod($htaccess_path, 0444);
Chris@0 376 }
Chris@0 377 else {
Chris@0 378 $variables = ['%directory' => $directory, '@htaccess' => $htaccess_lines];
Chris@0 379 \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);
Chris@0 380 return FALSE;
Chris@0 381 }
Chris@0 382 }
Chris@0 383
Chris@0 384 /**
Chris@0 385 * Returns the standard .htaccess lines that Drupal writes to file directories.
Chris@0 386 *
Chris@0 387 * @param bool $private
Chris@0 388 * (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
Chris@0 389 * public directory. The default is TRUE, which returns the .htaccess lines
Chris@0 390 * for a private directory that should not be web-accessible.
Chris@0 391 *
Chris@0 392 * @return string
Chris@0 393 * The desired contents of the .htaccess file.
Chris@0 394 *
Chris@0 395 * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
Chris@0 396 * Use \Drupal\Component\PhpStorage\FileStorage::htaccessLines().
Chris@0 397 *
Chris@0 398 * @see https://www.drupal.org/node/2418133
Chris@0 399 */
Chris@0 400 function file_htaccess_lines($private = TRUE) {
Chris@0 401 return FileStorage::htaccessLines($private);
Chris@0 402 }
Chris@0 403
Chris@0 404 /**
Chris@0 405 * Determines whether the URI has a valid scheme for file API operations.
Chris@0 406 *
Chris@0 407 * There must be a scheme and it must be a Drupal-provided scheme like
Chris@0 408 * 'public', 'private', 'temporary', or an extension provided with
Chris@0 409 * hook_stream_wrappers().
Chris@0 410 *
Chris@0 411 * @param $uri
Chris@0 412 * The URI to be tested.
Chris@0 413 *
Chris@0 414 * @return
Chris@0 415 * TRUE if the URI is allowed.
Chris@0 416 */
Chris@0 417 function file_valid_uri($uri) {
Chris@0 418 // Assert that the URI has an allowed scheme. Bare paths are not allowed.
Chris@0 419 $uri_scheme = \Drupal::service('file_system')->uriScheme($uri);
Chris@0 420 if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
Chris@0 421 return FALSE;
Chris@0 422 }
Chris@0 423 return TRUE;
Chris@0 424 }
Chris@0 425
Chris@0 426 /**
Chris@0 427 * Copies a file to a new location without database changes or hook invocation.
Chris@0 428 *
Chris@0 429 * This is a powerful function that in many ways performs like an advanced
Chris@0 430 * version of copy().
Chris@0 431 * - Checks if $source and $destination are valid and readable/writable.
Chris@0 432 * - If file already exists in $destination either the call will error out,
Chris@0 433 * replace the file or rename the file based on the $replace parameter.
Chris@0 434 * - If the $source and $destination are equal, the behavior depends on the
Chris@0 435 * $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
Chris@0 436 * will rename the file until the $destination is unique.
Chris@0 437 * - Works around a PHP bug where copy() does not properly support streams if
Chris@0 438 * safe_mode or open_basedir are enabled.
Chris@0 439 * @see https://bugs.php.net/bug.php?id=60456
Chris@0 440 *
Chris@0 441 * @param $source
Chris@0 442 * A string specifying the filepath or URI of the source file.
Chris@0 443 * @param $destination
Chris@0 444 * A URI containing the destination that $source should be copied to. The
Chris@0 445 * URI may be a bare filepath (without a scheme). If this value is omitted,
Chris@0 446 * Drupal's default files scheme will be used, usually "public://".
Chris@0 447 * @param $replace
Chris@0 448 * Replace behavior when the destination file already exists:
Chris@0 449 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@0 450 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
Chris@0 451 * unique.
Chris@0 452 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@0 453 *
Chris@0 454 * @return
Chris@0 455 * The path to the new file, or FALSE in the event of an error.
Chris@0 456 *
Chris@0 457 * @see file_copy()
Chris@0 458 */
Chris@0 459 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
Chris@0 460 if (!file_unmanaged_prepare($source, $destination, $replace)) {
Chris@0 461 return FALSE;
Chris@0 462 }
Chris@0 463 // Attempt to resolve the URIs. This is necessary in certain configurations
Chris@0 464 // (see above).
Chris@0 465 $file_system = \Drupal::service('file_system');
Chris@0 466 $real_source = $file_system->realpath($source) ?: $source;
Chris@0 467 $real_destination = $file_system->realpath($destination) ?: $destination;
Chris@0 468 // Perform the copy operation.
Chris@0 469 if (!@copy($real_source, $real_destination)) {
Chris@0 470 \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', ['%file' => $source, '%destination' => $destination]);
Chris@0 471 return FALSE;
Chris@0 472 }
Chris@0 473 // Set the permissions on the new file.
Chris@0 474 drupal_chmod($destination);
Chris@0 475 return $destination;
Chris@0 476 }
Chris@0 477
Chris@0 478 /**
Chris@0 479 * Internal function that prepares the destination for a file_unmanaged_copy or
Chris@0 480 * file_unmanaged_move operation.
Chris@0 481 *
Chris@0 482 * - Checks if $source and $destination are valid and readable/writable.
Chris@0 483 * - Checks that $source is not equal to $destination; if they are an error
Chris@0 484 * is reported.
Chris@0 485 * - If file already exists in $destination either the call will error out,
Chris@0 486 * replace the file or rename the file based on the $replace parameter.
Chris@0 487 *
Chris@0 488 * @param $source
Chris@0 489 * A string specifying the filepath or URI of the source file.
Chris@0 490 * @param $destination
Chris@0 491 * A URI containing the destination that $source should be moved/copied to.
Chris@0 492 * The URI may be a bare filepath (without a scheme) and in that case the
Chris@0 493 * default scheme (file://) will be used. If this value is omitted, Drupal's
Chris@0 494 * default files scheme will be used, usually "public://".
Chris@0 495 * @param $replace
Chris@0 496 * Replace behavior when the destination file already exists:
Chris@0 497 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@0 498 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
Chris@0 499 * unique.
Chris@0 500 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@0 501 *
Chris@0 502 * @return
Chris@0 503 * TRUE, or FALSE in the event of an error.
Chris@0 504 *
Chris@0 505 * @see file_unmanaged_copy()
Chris@0 506 * @see file_unmanaged_move()
Chris@0 507 */
Chris@0 508 function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
Chris@0 509 $original_source = $source;
Chris@0 510 $logger = \Drupal::logger('file');
Chris@0 511 $file_system = \Drupal::service('file_system');
Chris@0 512
Chris@0 513 // Assert that the source file actually exists.
Chris@0 514 if (!file_exists($source)) {
Chris@0 515 // @todo Replace drupal_set_message() calls with exceptions instead.
Chris@0 516 drupal_set_message(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]), 'error');
Chris@0 517 if (($realpath = $file_system->realpath($original_source)) !== FALSE) {
Chris@0 518 $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', ['%file' => $original_source, '%realpath' => $realpath]);
Chris@0 519 }
Chris@0 520 else {
Chris@0 521 $logger->notice('File %file could not be moved/copied because it does not exist.', ['%file' => $original_source]);
Chris@0 522 }
Chris@0 523 return FALSE;
Chris@0 524 }
Chris@0 525
Chris@0 526 // Build a destination URI if necessary.
Chris@0 527 if (!isset($destination)) {
Chris@0 528 $destination = file_build_uri(drupal_basename($source));
Chris@0 529 }
Chris@0 530
Chris@0 531 // Prepare the destination directory.
Chris@0 532 if (file_prepare_directory($destination)) {
Chris@0 533 // The destination is already a directory, so append the source basename.
Chris@0 534 $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
Chris@0 535 }
Chris@0 536 else {
Chris@0 537 // Perhaps $destination is a dir/file?
Chris@0 538 $dirname = drupal_dirname($destination);
Chris@0 539 if (!file_prepare_directory($dirname)) {
Chris@0 540 // The destination is not valid.
Chris@0 541 $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', ['%file' => $original_source, '%destination' => $dirname]);
Chris@0 542 drupal_set_message(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]), 'error');
Chris@0 543 return FALSE;
Chris@0 544 }
Chris@0 545 }
Chris@0 546
Chris@0 547 // Determine whether we can perform this operation based on overwrite rules.
Chris@0 548 $destination = file_destination($destination, $replace);
Chris@0 549 if ($destination === FALSE) {
Chris@0 550 drupal_set_message(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', ['%file' => $original_source]), 'error');
Chris@0 551 $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]);
Chris@0 552 return FALSE;
Chris@0 553 }
Chris@0 554
Chris@0 555 // Assert that the source and destination filenames are not the same.
Chris@0 556 $real_source = $file_system->realpath($source);
Chris@0 557 $real_destination = $file_system->realpath($destination);
Chris@0 558 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
Chris@0 559 drupal_set_message(t('The specified file %file was not moved/copied because it would overwrite itself.', ['%file' => $source]), 'error');
Chris@0 560 $logger->notice('File %file could not be moved/copied because it would overwrite itself.', ['%file' => $source]);
Chris@0 561 return FALSE;
Chris@0 562 }
Chris@0 563 // Make sure the .htaccess files are present.
Chris@0 564 file_ensure_htaccess();
Chris@0 565 return TRUE;
Chris@0 566 }
Chris@0 567
Chris@0 568 /**
Chris@0 569 * Constructs a URI to Drupal's default files location given a relative path.
Chris@0 570 */
Chris@0 571 function file_build_uri($path) {
Chris@0 572 $uri = file_default_scheme() . '://' . $path;
Chris@0 573 return file_stream_wrapper_uri_normalize($uri);
Chris@0 574 }
Chris@0 575
Chris@0 576 /**
Chris@0 577 * Determines the destination path for a file.
Chris@0 578 *
Chris@0 579 * @param $destination
Chris@0 580 * A string specifying the desired final URI or filepath.
Chris@0 581 * @param $replace
Chris@0 582 * Replace behavior when the destination file already exists.
Chris@0 583 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@0 584 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
Chris@0 585 * unique.
Chris@0 586 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@0 587 *
Chris@0 588 * @return
Chris@0 589 * The destination filepath, or FALSE if the file already exists
Chris@0 590 * and FILE_EXISTS_ERROR is specified.
Chris@0 591 */
Chris@0 592 function file_destination($destination, $replace) {
Chris@0 593 if (file_exists($destination)) {
Chris@0 594 switch ($replace) {
Chris@0 595 case FILE_EXISTS_REPLACE:
Chris@0 596 // Do nothing here, we want to overwrite the existing file.
Chris@0 597 break;
Chris@0 598
Chris@0 599 case FILE_EXISTS_RENAME:
Chris@0 600 $basename = drupal_basename($destination);
Chris@0 601 $directory = drupal_dirname($destination);
Chris@0 602 $destination = file_create_filename($basename, $directory);
Chris@0 603 break;
Chris@0 604
Chris@0 605 case FILE_EXISTS_ERROR:
Chris@0 606 // Error reporting handled by calling function.
Chris@0 607 return FALSE;
Chris@0 608 }
Chris@0 609 }
Chris@0 610 return $destination;
Chris@0 611 }
Chris@0 612
Chris@0 613 /**
Chris@0 614 * Moves a file to a new location without database changes or hook invocation.
Chris@0 615 *
Chris@0 616 * This is a powerful function that in many ways performs like an advanced
Chris@0 617 * version of rename().
Chris@0 618 * - Checks if $source and $destination are valid and readable/writable.
Chris@0 619 * - Checks that $source is not equal to $destination; if they are an error
Chris@0 620 * is reported.
Chris@0 621 * - If file already exists in $destination either the call will error out,
Chris@0 622 * replace the file or rename the file based on the $replace parameter.
Chris@0 623 * - Works around a PHP bug where rename() does not properly support streams if
Chris@0 624 * safe_mode or open_basedir are enabled.
Chris@0 625 * @see https://bugs.php.net/bug.php?id=60456
Chris@0 626 *
Chris@0 627 * @param $source
Chris@0 628 * A string specifying the filepath or URI of the source file.
Chris@0 629 * @param $destination
Chris@0 630 * A URI containing the destination that $source should be moved to. The
Chris@0 631 * URI may be a bare filepath (without a scheme) and in that case the default
Chris@0 632 * scheme (file://) will be used. If this value is omitted, Drupal's default
Chris@0 633 * files scheme will be used, usually "public://".
Chris@0 634 * @param $replace
Chris@0 635 * Replace behavior when the destination file already exists:
Chris@0 636 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@0 637 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
Chris@0 638 * unique.
Chris@0 639 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@0 640 *
Chris@0 641 * @return
Chris@0 642 * The path to the new file, or FALSE in the event of an error.
Chris@0 643 *
Chris@0 644 * @see file_move()
Chris@0 645 */
Chris@0 646 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
Chris@0 647 if (!file_unmanaged_prepare($source, $destination, $replace)) {
Chris@0 648 return FALSE;
Chris@0 649 }
Chris@0 650 // Ensure compatibility with Windows.
Chris@0 651 // @see drupal_unlink()
Chris@0 652 if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
Chris@0 653 chmod($source, 0600);
Chris@0 654 }
Chris@0 655 // Attempt to resolve the URIs. This is necessary in certain configurations
Chris@0 656 // (see above) and can also permit fast moves across local schemes.
Chris@0 657 $file_system = \Drupal::service('file_system');
Chris@0 658 $real_source = $file_system->realpath($source) ?: $source;
Chris@0 659 $real_destination = $file_system->realpath($destination) ?: $destination;
Chris@0 660 // Perform the move operation.
Chris@0 661 if (!@rename($real_source, $real_destination)) {
Chris@0 662 // Fall back to slow copy and unlink procedure. This is necessary for
Chris@0 663 // renames across schemes that are not local, or where rename() has not been
Chris@0 664 // implemented. It's not necessary to use drupal_unlink() as the Windows
Chris@0 665 // issue has already been resolved above.
Chris@0 666 if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
Chris@0 667 \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', ['%file' => $source, '%destination' => $destination]);
Chris@0 668 return FALSE;
Chris@0 669 }
Chris@0 670 }
Chris@0 671 // Set the permissions on the new file.
Chris@0 672 drupal_chmod($destination);
Chris@0 673 return $destination;
Chris@0 674 }
Chris@0 675
Chris@0 676 /**
Chris@0 677 * Modifies a filename as needed for security purposes.
Chris@0 678 *
Chris@0 679 * Munging a file name prevents unknown file extensions from masking exploit
Chris@0 680 * files. When web servers such as Apache decide how to process a URL request,
Chris@0 681 * they use the file extension. If the extension is not recognized, Apache
Chris@0 682 * skips that extension and uses the previous file extension. For example, if
Chris@0 683 * the file being requested is exploit.php.pps, and Apache does not recognize
Chris@0 684 * the '.pps' extension, it treats the file as PHP and executes it. To make
Chris@0 685 * this file name safe for Apache and prevent it from executing as PHP, the
Chris@0 686 * .php extension is "munged" into .php_, making the safe file name
Chris@0 687 * exploit.php_.pps.
Chris@0 688 *
Chris@0 689 * Specifically, this function adds an underscore to all extensions that are
Chris@0 690 * between 2 and 5 characters in length, internal to the file name, and not
Chris@0 691 * included in $extensions.
Chris@0 692 *
Chris@0 693 * Function behavior is also controlled by the configuration
Chris@0 694 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
Chris@0 695 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
Chris@0 696 * @param $filename
Chris@0 697 * File name to modify.
Chris@0 698 * @param $extensions
Chris@0 699 * A space-separated list of extensions that should not be altered.
Chris@0 700 * @param $alerts
Chris@0 701 * If TRUE, drupal_set_message() will be called to display a message if the
Chris@0 702 * file name was changed.
Chris@0 703 *
Chris@0 704 * @return string
Chris@0 705 * The potentially modified $filename.
Chris@0 706 */
Chris@0 707 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
Chris@0 708 $original = $filename;
Chris@0 709
Chris@0 710 // Allow potentially insecure uploads for very savvy users and admin
Chris@0 711 if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
Chris@0 712 // Remove any null bytes. See
Chris@0 713 // http://php.net/manual/security.filesystem.nullbytes.php
Chris@0 714 $filename = str_replace(chr(0), '', $filename);
Chris@0 715
Chris@0 716 $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
Chris@0 717
Chris@0 718 // Split the filename up by periods. The first part becomes the basename
Chris@0 719 // the last part the final extension.
Chris@0 720 $filename_parts = explode('.', $filename);
Chris@0 721 // Remove file basename.
Chris@0 722 $new_filename = array_shift($filename_parts);
Chris@0 723 // Remove final extension.
Chris@0 724 $final_extension = array_pop($filename_parts);
Chris@0 725
Chris@0 726 // Loop through the middle parts of the name and add an underscore to the
Chris@0 727 // end of each section that could be a file extension but isn't in the list
Chris@0 728 // of allowed extensions.
Chris@0 729 foreach ($filename_parts as $filename_part) {
Chris@0 730 $new_filename .= '.' . $filename_part;
Chris@0 731 if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
Chris@0 732 $new_filename .= '_';
Chris@0 733 }
Chris@0 734 }
Chris@0 735 $filename = $new_filename . '.' . $final_extension;
Chris@0 736
Chris@0 737 if ($alerts && $original != $filename) {
Chris@0 738 drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $filename]));
Chris@0 739 }
Chris@0 740 }
Chris@0 741
Chris@0 742 return $filename;
Chris@0 743 }
Chris@0 744
Chris@0 745 /**
Chris@0 746 * Undoes the effect of file_munge_filename().
Chris@0 747 *
Chris@0 748 * @param $filename
Chris@0 749 * String with the filename to be unmunged.
Chris@0 750 *
Chris@0 751 * @return
Chris@0 752 * An unmunged filename string.
Chris@0 753 */
Chris@0 754 function file_unmunge_filename($filename) {
Chris@0 755 return str_replace('_.', '.', $filename);
Chris@0 756 }
Chris@0 757
Chris@0 758 /**
Chris@0 759 * Creates a full file path from a directory and filename.
Chris@0 760 *
Chris@0 761 * If a file with the specified name already exists, an alternative will be
Chris@0 762 * used.
Chris@0 763 *
Chris@0 764 * @param $basename
Chris@0 765 * String filename
Chris@0 766 * @param $directory
Chris@0 767 * String containing the directory or parent URI.
Chris@0 768 *
Chris@0 769 * @return
Chris@0 770 * File path consisting of $directory and a unique filename based off
Chris@0 771 * of $basename.
Chris@0 772 */
Chris@0 773 function file_create_filename($basename, $directory) {
Chris@0 774 // Strip control characters (ASCII value < 32). Though these are allowed in
Chris@0 775 // some filesystems, not many applications handle them well.
Chris@0 776 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
Chris@0 777 if (substr(PHP_OS, 0, 3) == 'WIN') {
Chris@0 778 // These characters are not allowed in Windows filenames
Chris@0 779 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
Chris@0 780 }
Chris@0 781
Chris@0 782 // A URI or path may already have a trailing slash or look like "public://".
Chris@0 783 if (substr($directory, -1) == '/') {
Chris@0 784 $separator = '';
Chris@0 785 }
Chris@0 786 else {
Chris@0 787 $separator = '/';
Chris@0 788 }
Chris@0 789
Chris@0 790 $destination = $directory . $separator . $basename;
Chris@0 791
Chris@0 792 if (file_exists($destination)) {
Chris@0 793 // Destination file already exists, generate an alternative.
Chris@0 794 $pos = strrpos($basename, '.');
Chris@0 795 if ($pos !== FALSE) {
Chris@0 796 $name = substr($basename, 0, $pos);
Chris@0 797 $ext = substr($basename, $pos);
Chris@0 798 }
Chris@0 799 else {
Chris@0 800 $name = $basename;
Chris@0 801 $ext = '';
Chris@0 802 }
Chris@0 803
Chris@0 804 $counter = 0;
Chris@0 805 do {
Chris@0 806 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
Chris@0 807 } while (file_exists($destination));
Chris@0 808 }
Chris@0 809
Chris@0 810 return $destination;
Chris@0 811 }
Chris@0 812
Chris@0 813 /**
Chris@0 814 * Deletes a file and its database record.
Chris@0 815 *
Chris@0 816 * Instead of directly deleting a file, it is strongly recommended to delete
Chris@0 817 * file usages instead. That will automatically mark the file as temporary and
Chris@0 818 * remove it during cleanup.
Chris@0 819 *
Chris@0 820 * @param $fid
Chris@0 821 * The file id.
Chris@0 822 *
Chris@0 823 * @see file_unmanaged_delete()
Chris@0 824 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
Chris@0 825 */
Chris@0 826 function file_delete($fid) {
Chris@0 827 return file_delete_multiple([$fid]);
Chris@0 828 }
Chris@0 829
Chris@0 830 /**
Chris@0 831 * Deletes files.
Chris@0 832 *
Chris@0 833 * Instead of directly deleting a file, it is strongly recommended to delete
Chris@0 834 * file usages instead. That will automatically mark the file as temporary and
Chris@0 835 * remove it during cleanup.
Chris@0 836 *
Chris@0 837 * @param $fid
Chris@0 838 * The file id.
Chris@0 839 *
Chris@0 840 * @see file_unmanaged_delete()
Chris@0 841 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
Chris@0 842 */
Chris@0 843 function file_delete_multiple(array $fids) {
Chris@0 844 entity_delete_multiple('file', $fids);
Chris@0 845 }
Chris@0 846
Chris@0 847 /**
Chris@0 848 * Deletes a file without database changes or hook invocations.
Chris@0 849 *
Chris@0 850 * This function should be used when the file to be deleted does not have an
Chris@0 851 * entry recorded in the files table.
Chris@0 852 *
Chris@0 853 * @param $path
Chris@0 854 * A string containing a file path or (streamwrapper) URI.
Chris@0 855 *
Chris@0 856 * @return
Chris@0 857 * TRUE for success or path does not exist, or FALSE in the event of an
Chris@0 858 * error.
Chris@0 859 *
Chris@0 860 * @see file_delete()
Chris@0 861 * @see file_unmanaged_delete_recursive()
Chris@0 862 */
Chris@0 863 function file_unmanaged_delete($path) {
Chris@0 864 if (is_file($path)) {
Chris@0 865 return drupal_unlink($path);
Chris@0 866 }
Chris@0 867 $logger = \Drupal::logger('file');
Chris@0 868 if (is_dir($path)) {
Chris@0 869 $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', ['%path' => $path]);
Chris@0 870 return FALSE;
Chris@0 871 }
Chris@0 872 // Return TRUE for non-existent file, but log that nothing was actually
Chris@0 873 // deleted, as the current state is the intended result.
Chris@0 874 if (!file_exists($path)) {
Chris@0 875 $logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
Chris@0 876 return TRUE;
Chris@0 877 }
Chris@0 878 // We cannot handle anything other than files and directories. Log an error
Chris@0 879 // for everything else (sockets, symbolic links, etc).
Chris@0 880 $logger->error('The file %path is not of a recognized type so it was not deleted.', ['%path' => $path]);
Chris@0 881 return FALSE;
Chris@0 882 }
Chris@0 883
Chris@0 884 /**
Chris@0 885 * Deletes all files and directories in the specified filepath recursively.
Chris@0 886 *
Chris@0 887 * If the specified path is a directory then the function will call itself
Chris@0 888 * recursively to process the contents. Once the contents have been removed the
Chris@0 889 * directory will also be removed.
Chris@0 890 *
Chris@0 891 * If the specified path is a file then it will be passed to
Chris@0 892 * file_unmanaged_delete().
Chris@0 893 *
Chris@0 894 * Note that this only deletes visible files with write permission.
Chris@0 895 *
Chris@0 896 * @param $path
Chris@0 897 * A string containing either an URI or a file or directory path.
Chris@0 898 * @param callable $callback
Chris@0 899 * (optional) Callback function to run on each file prior to deleting it and
Chris@0 900 * on each directory prior to traversing it. For example, can be used to
Chris@0 901 * modify permissions.
Chris@0 902 *
Chris@0 903 * @return
Chris@0 904 * TRUE for success or if path does not exist, FALSE in the event of an
Chris@0 905 * error.
Chris@0 906 *
Chris@0 907 * @see file_unmanaged_delete()
Chris@0 908 */
Chris@0 909 function file_unmanaged_delete_recursive($path, $callback = NULL) {
Chris@0 910 if (isset($callback)) {
Chris@0 911 call_user_func($callback, $path);
Chris@0 912 }
Chris@0 913 if (is_dir($path)) {
Chris@0 914 $dir = dir($path);
Chris@0 915 while (($entry = $dir->read()) !== FALSE) {
Chris@0 916 if ($entry == '.' || $entry == '..') {
Chris@0 917 continue;
Chris@0 918 }
Chris@0 919 $entry_path = $path . '/' . $entry;
Chris@0 920 file_unmanaged_delete_recursive($entry_path, $callback);
Chris@0 921 }
Chris@0 922 $dir->close();
Chris@0 923
Chris@0 924 return drupal_rmdir($path);
Chris@0 925 }
Chris@0 926 return file_unmanaged_delete($path);
Chris@0 927 }
Chris@0 928
Chris@0 929
Chris@0 930 /**
Chris@0 931 * Moves an uploaded file to a new location.
Chris@0 932 *
Chris@0 933 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 934 * Use \Drupal\Core\File\FileSystem::moveUploadedFile().
Chris@0 935 *
Chris@0 936 * @see https://www.drupal.org/node/2418133
Chris@0 937 */
Chris@0 938 function drupal_move_uploaded_file($filename, $uri) {
Chris@0 939 return \Drupal::service('file_system')->moveUploadedFile($filename, $uri);
Chris@0 940 }
Chris@0 941
Chris@0 942 /**
Chris@0 943 * Saves a file to the specified destination without invoking file API.
Chris@0 944 *
Chris@0 945 * This function is identical to file_save_data() except the file will not be
Chris@0 946 * saved to the {file_managed} table and none of the file_* hooks will be
Chris@0 947 * called.
Chris@0 948 *
Chris@0 949 * @param $data
Chris@0 950 * A string containing the contents of the file.
Chris@0 951 * @param $destination
Chris@0 952 * A string containing the destination location. This must be a stream wrapper
Chris@0 953 * URI. If no value is provided, a randomized name will be generated and the
Chris@0 954 * file will be saved using Drupal's default files scheme, usually
Chris@0 955 * "public://".
Chris@0 956 * @param $replace
Chris@0 957 * Replace behavior when the destination file already exists:
Chris@0 958 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@0 959 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
Chris@0 960 * unique.
Chris@0 961 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@0 962 *
Chris@0 963 * @return
Chris@0 964 * A string with the path of the resulting file, or FALSE on error.
Chris@0 965 *
Chris@0 966 * @see file_save_data()
Chris@0 967 */
Chris@0 968 function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
Chris@0 969 // Write the data to a temporary file.
Chris@0 970 $temp_name = drupal_tempnam('temporary://', 'file');
Chris@0 971 if (file_put_contents($temp_name, $data) === FALSE) {
Chris@0 972 drupal_set_message(t('The file could not be created.'), 'error');
Chris@0 973 return FALSE;
Chris@0 974 }
Chris@0 975
Chris@0 976 // Move the file to its final destination.
Chris@0 977 return file_unmanaged_move($temp_name, $destination, $replace);
Chris@0 978 }
Chris@0 979
Chris@0 980 /**
Chris@0 981 * Finds all files that match a given mask in a given directory.
Chris@0 982 *
Chris@0 983 * Directories and files beginning with a dot are excluded; this prevents
Chris@0 984 * hidden files and directories (such as SVN working directories) from being
Chris@0 985 * scanned. Use the umask option to skip configuration directories to
Chris@0 986 * eliminate the possibility of accidentally exposing configuration
Chris@0 987 * information. Also, you can use the base directory, recurse, and min_depth
Chris@0 988 * options to improve performance by limiting how much of the filesystem has
Chris@0 989 * to be traversed.
Chris@0 990 *
Chris@0 991 * @param $dir
Chris@0 992 * The base directory or URI to scan, without trailing slash.
Chris@0 993 * @param $mask
Chris@0 994 * The preg_match() regular expression for files to be included.
Chris@0 995 * @param $options
Chris@0 996 * An associative array of additional options, with the following elements:
Chris@0 997 * - 'nomask': The preg_match() regular expression for files to be excluded.
Chris@0 998 * Defaults to the 'file_scan_ignore_directories' setting.
Chris@0 999 * - 'callback': The callback function to call for each match. There is no
Chris@0 1000 * default callback.
Chris@0 1001 * - 'recurse': When TRUE, the directory scan will recurse the entire tree
Chris@0 1002 * starting at the provided directory. Defaults to TRUE.
Chris@0 1003 * - 'key': The key to be used for the returned associative array of files.
Chris@0 1004 * Possible values are 'uri', for the file's URI; 'filename', for the
Chris@0 1005 * basename of the file; and 'name' for the name of the file without the
Chris@0 1006 * extension. Defaults to 'uri'.
Chris@0 1007 * - 'min_depth': Minimum depth of directories to return files from. Defaults
Chris@0 1008 * to 0.
Chris@0 1009 * @param $depth
Chris@0 1010 * The current depth of recursion. This parameter is only used internally and
Chris@0 1011 * should not be passed in.
Chris@0 1012 *
Chris@0 1013 * @return
Chris@0 1014 * An associative array (keyed on the chosen key) of objects with 'uri',
Chris@0 1015 * 'filename', and 'name' properties corresponding to the matched files.
Chris@0 1016 */
Chris@0 1017 function file_scan_directory($dir, $mask, $options = [], $depth = 0) {
Chris@0 1018 // Merge in defaults.
Chris@0 1019 $options += [
Chris@0 1020 'callback' => 0,
Chris@0 1021 'recurse' => TRUE,
Chris@0 1022 'key' => 'uri',
Chris@0 1023 'min_depth' => 0,
Chris@0 1024 ];
Chris@0 1025 // Normalize $dir only once.
Chris@0 1026 if ($depth == 0) {
Chris@0 1027 $dir = file_stream_wrapper_uri_normalize($dir);
Chris@0 1028 $dir_has_slash = (substr($dir, -1) === '/');
Chris@0 1029 }
Chris@0 1030
Chris@0 1031 // Allow directories specified in settings.php to be ignored. You can use this
Chris@0 1032 // to not check for files in common special-purpose directories. For example,
Chris@0 1033 // node_modules and bower_components. Ignoring irrelevant directories is a
Chris@0 1034 // performance boost.
Chris@0 1035 if (!isset($options['nomask'])) {
Chris@0 1036 $ignore_directories = Settings::get('file_scan_ignore_directories', []);
Chris@0 1037 array_walk($ignore_directories, function (&$value) {
Chris@0 1038 $value = preg_quote($value, '/');
Chris@0 1039 });
Chris@0 1040 $default_nomask = '/^' . implode('|', $ignore_directories) . '$/';
Chris@0 1041 }
Chris@0 1042
Chris@0 1043 $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
Chris@0 1044 $files = [];
Chris@0 1045 // Avoid warnings when opendir does not have the permissions to open a
Chris@0 1046 // directory.
Chris@0 1047 if (is_dir($dir)) {
Chris@0 1048 if ($handle = @opendir($dir)) {
Chris@0 1049 while (FALSE !== ($filename = readdir($handle))) {
Chris@0 1050 // Skip this file if it matches the nomask or starts with a dot.
Chris@0 1051 if ($filename[0] != '.'
Chris@0 1052 && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
Chris@0 1053 && !(!empty($default_nomask) && preg_match($default_nomask, $filename))
Chris@0 1054 ) {
Chris@0 1055 if ($depth == 0 && $dir_has_slash) {
Chris@0 1056 $uri = "$dir$filename";
Chris@0 1057 }
Chris@0 1058 else {
Chris@0 1059 $uri = "$dir/$filename";
Chris@0 1060 }
Chris@0 1061 if ($options['recurse'] && is_dir($uri)) {
Chris@0 1062 // Give priority to files in this folder by merging them in after
Chris@0 1063 // any subdirectory files.
Chris@0 1064 $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
Chris@0 1065 }
Chris@0 1066 elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
Chris@0 1067 // Always use this match over anything already set in $files with
Chris@0 1068 // the same $options['key'].
Chris@0 1069 $file = new stdClass();
Chris@0 1070 $file->uri = $uri;
Chris@0 1071 $file->filename = $filename;
Chris@0 1072 $file->name = pathinfo($filename, PATHINFO_FILENAME);
Chris@0 1073 $key = $options['key'];
Chris@0 1074 $files[$file->$key] = $file;
Chris@0 1075 if ($options['callback']) {
Chris@0 1076 $options['callback']($uri);
Chris@0 1077 }
Chris@0 1078 }
Chris@0 1079 }
Chris@0 1080 }
Chris@0 1081
Chris@0 1082 closedir($handle);
Chris@0 1083 }
Chris@0 1084 else {
Chris@0 1085 \Drupal::logger('file')->error('@dir can not be opened', ['@dir' => $dir]);
Chris@0 1086 }
Chris@0 1087 }
Chris@0 1088
Chris@0 1089 return $files;
Chris@0 1090 }
Chris@0 1091
Chris@0 1092 /**
Chris@0 1093 * Determines the maximum file upload size by querying the PHP settings.
Chris@0 1094 *
Chris@0 1095 * @return
Chris@0 1096 * A file size limit in bytes based on the PHP upload_max_filesize and
Chris@0 1097 * post_max_size
Chris@0 1098 */
Chris@0 1099 function file_upload_max_size() {
Chris@0 1100 static $max_size = -1;
Chris@0 1101
Chris@0 1102 if ($max_size < 0) {
Chris@0 1103 // Start with post_max_size.
Chris@0 1104 $max_size = Bytes::toInt(ini_get('post_max_size'));
Chris@0 1105
Chris@0 1106 // If upload_max_size is less, then reduce. Except if upload_max_size is
Chris@0 1107 // zero, which indicates no limit.
Chris@0 1108 $upload_max = Bytes::toInt(ini_get('upload_max_filesize'));
Chris@0 1109 if ($upload_max > 0 && $upload_max < $max_size) {
Chris@0 1110 $max_size = $upload_max;
Chris@0 1111 }
Chris@0 1112 }
Chris@0 1113 return $max_size;
Chris@0 1114 }
Chris@0 1115
Chris@0 1116 /**
Chris@0 1117 * Sets the permissions on a file or directory.
Chris@0 1118 *
Chris@0 1119 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1120 * Use \Drupal\Core\File\FileSystem::chmod().
Chris@0 1121 *
Chris@0 1122 * @see https://www.drupal.org/node/2418133
Chris@0 1123 */
Chris@0 1124 function drupal_chmod($uri, $mode = NULL) {
Chris@0 1125 return \Drupal::service('file_system')->chmod($uri, $mode);
Chris@0 1126 }
Chris@0 1127
Chris@0 1128 /**
Chris@0 1129 * Deletes a file.
Chris@0 1130 *
Chris@0 1131 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1132 * Use \Drupal\Core\File\FileSystem::unlink().
Chris@0 1133 *
Chris@0 1134 * @see https://www.drupal.org/node/2418133
Chris@0 1135 */
Chris@0 1136 function drupal_unlink($uri, $context = NULL) {
Chris@0 1137 return \Drupal::service('file_system')->unlink($uri, $context);
Chris@0 1138 }
Chris@0 1139
Chris@0 1140 /**
Chris@0 1141 * Resolves the absolute filepath of a local URI or filepath.
Chris@0 1142 *
Chris@0 1143 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1144 * Use \Drupal\Core\File\FileSystem::realpath().
Chris@0 1145 *
Chris@0 1146 * @see https://www.drupal.org/node/2418133
Chris@0 1147 */
Chris@0 1148 function drupal_realpath($uri) {
Chris@0 1149 return \Drupal::service('file_system')->realpath($uri);
Chris@0 1150 }
Chris@0 1151
Chris@0 1152 /**
Chris@0 1153 * Gets the name of the directory from a given path.
Chris@0 1154 *
Chris@0 1155 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1156 * Use \Drupal\Core\File\FileSystem::dirname().
Chris@0 1157 *
Chris@0 1158 * @see https://www.drupal.org/node/2418133
Chris@0 1159 */
Chris@0 1160 function drupal_dirname($uri) {
Chris@0 1161 return \Drupal::service('file_system')->dirname($uri);
Chris@0 1162 }
Chris@0 1163
Chris@0 1164 /**
Chris@0 1165 * Gets the filename from a given path.
Chris@0 1166 *
Chris@0 1167 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1168 * Use \Drupal\Core\File\FileSystem::basename().
Chris@0 1169 *
Chris@0 1170 * @see https://www.drupal.org/node/2418133
Chris@0 1171 */
Chris@0 1172 function drupal_basename($uri, $suffix = NULL) {
Chris@0 1173 return \Drupal::service('file_system')->basename($uri, $suffix);
Chris@0 1174 }
Chris@0 1175
Chris@0 1176 /**
Chris@0 1177 * Creates a directory, optionally creating missing components in the path to
Chris@0 1178 * the directory.
Chris@0 1179 *
Chris@0 1180 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1181 * Use \Drupal\Core\File\FileSystem::mkdir().
Chris@0 1182 *
Chris@0 1183 * @see https://www.drupal.org/node/2418133
Chris@0 1184 */
Chris@0 1185 function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
Chris@0 1186 return \Drupal::service('file_system')->mkdir($uri, $mode, $recursive, $context);
Chris@0 1187 }
Chris@0 1188
Chris@0 1189 /**
Chris@0 1190 * Removes a directory.
Chris@0 1191 *
Chris@0 1192 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1193 * Use \Drupal\Core\File\FileSystem::rmdir().
Chris@0 1194 *
Chris@0 1195 * @see https://www.drupal.org/node/2418133
Chris@0 1196 */
Chris@0 1197 function drupal_rmdir($uri, $context = NULL) {
Chris@0 1198 return \Drupal::service('file_system')->rmdir($uri, $context);
Chris@0 1199 }
Chris@0 1200
Chris@0 1201 /**
Chris@0 1202 * Creates a file with a unique filename in the specified directory.
Chris@0 1203 *
Chris@0 1204 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1205 * Use \Drupal\Core\File\FileSystem::tempnam().
Chris@0 1206 *
Chris@0 1207 * @see https://www.drupal.org/node/2418133
Chris@0 1208 */
Chris@0 1209 function drupal_tempnam($directory, $prefix) {
Chris@0 1210 return \Drupal::service('file_system')->tempnam($directory, $prefix);
Chris@0 1211 }
Chris@0 1212
Chris@0 1213 /**
Chris@0 1214 * Gets and sets the path of the configured temporary directory.
Chris@0 1215 *
Chris@0 1216 * @return mixed|null
Chris@0 1217 * A string containing the path to the temporary directory.
Chris@0 1218 */
Chris@0 1219 function file_directory_temp() {
Chris@0 1220 $temporary_directory = \Drupal::config('system.file')->get('path.temporary');
Chris@0 1221 if (empty($temporary_directory)) {
Chris@0 1222 // Needs set up.
Chris@0 1223 $config = \Drupal::configFactory()->getEditable('system.file');
Chris@0 1224 $temporary_directory = ComponentFileSystem::getOsTemporaryDirectory();
Chris@0 1225
Chris@0 1226 if (empty($temporary_directory)) {
Chris@0 1227 // If no directory has been found default to 'files/tmp'.
Chris@0 1228 $temporary_directory = PublicStream::basePath() . '/tmp';
Chris@0 1229
Chris@0 1230 // Windows accepts paths with either slash (/) or backslash (\), but will
Chris@0 1231 // not accept a path which contains both a slash and a backslash. Since
Chris@0 1232 // the 'file_public_path' variable may have either format, we sanitize
Chris@0 1233 // everything to use slash which is supported on all platforms.
Chris@0 1234 $temporary_directory = str_replace('\\', '/', $temporary_directory);
Chris@0 1235 }
Chris@0 1236 // Save the path of the discovered directory. Do not check config schema on
Chris@0 1237 // save.
Chris@0 1238 $config->set('path.temporary', (string) $temporary_directory)->save(TRUE);
Chris@0 1239 }
Chris@0 1240
Chris@0 1241 return $temporary_directory;
Chris@0 1242 }
Chris@0 1243
Chris@0 1244 /**
Chris@0 1245 * Discovers a writable system-appropriate temporary directory.
Chris@0 1246 *
Chris@0 1247 * @return mixed
Chris@0 1248 * A string containing the path to the temporary directory.
Chris@0 1249 *
Chris@0 1250 * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
Chris@0 1251 * Use \Drupal\Component\FileSystem\FileSystem::getOsTemporaryDirectory().
Chris@0 1252 *
Chris@0 1253 * @see https://www.drupal.org/node/2418133
Chris@0 1254 */
Chris@0 1255 function file_directory_os_temp() {
Chris@0 1256 return ComponentFileSystem::getOsTemporaryDirectory();
Chris@0 1257 }
Chris@0 1258
Chris@0 1259 /**
Chris@0 1260 * @} End of "defgroup file".
Chris@0 1261 */