file.inc 52.7 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
Kjartan's avatar
Kjartan committed
2

Dries's avatar
   
Dries committed
3
4
5
6
7
/**
 * @file
 * API for handling file uploads and server file management.
 */

8
use Drupal\Component\Utility\Unicode;
9
use Drupal\Component\Utility\UrlHelper;
10
use Drupal\Component\PhpStorage\FileStorage;
11
use Drupal\Component\Utility\Bytes;
12
use Drupal\Component\Utility\String;
13
use Drupal\Core\Site\Settings;
14
use Drupal\Core\StreamWrapper\PublicStream;
15
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
16
use Drupal\Core\StreamWrapper\PrivateStream;
17
18
19
20
21
22
23
24
25
26
27

/**
 * Default mode for new directories. See drupal_chmod().
 */
const FILE_CHMOD_DIRECTORY = 0775;

/**
 * Default mode for new files. See drupal_chmod().
 */
const FILE_CHMOD_FILE = 0664;

Kjartan's avatar
Kjartan committed
28
/**
Kjartan's avatar
Kjartan committed
29
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
30
 * @{
Dries's avatar
   
Dries committed
31
 * Common file handling functions.
32
 *
33
 * Fields on the file entity:
34
35
36
 * - fid: File ID
 * - uid: The {users}.uid of the user who is associated with the file.
 * - filename: Name of the file with no path components. This may differ from
37
38
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
39
40
41
42
 * - uri: URI of the file.
 * - filemime: The file's MIME type.
 * - filesize: The size of the file in bytes.
 * - status: A bitmapped field indicating the status of the file. The first 8
43
 *   bits are reserved for Drupal core. The least significant bit indicates
44
45
46
47
 *   temporary (0) or permanent (1). Temporary files will be removed during
 *   cron runs if they are older than the configuration value
 *   "system.file.temporary_maximum_age", and if clean-up is enabled. Permanent
 *   files will not be removed.
48
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
   
Dries committed
49
50
 */

51
/**
52
 * Flag used by file_prepare_directory() -- create directory if not present.
53
 */
54
const FILE_CREATE_DIRECTORY = 1;
55
56

/**
57
 * Flag used by file_prepare_directory() -- file permissions may be changed.
58
 */
59
const FILE_MODIFY_PERMISSIONS = 2;
60
61

/**
62
 * Flag for dealing with existing files: Appends number until name is unique.
63
 */
64
const FILE_EXISTS_RENAME = 0;
65
66
67
68

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
69
const FILE_EXISTS_REPLACE = 1;
70
71
72
73

/**
 * Flag for dealing with existing files: Do nothing and return FALSE.
 */
74
const FILE_EXISTS_ERROR = 2;
Dries's avatar
   
Dries committed
75

76
/**
77
78
 * Indicates that the file is permanent and should not be deleted.
 *
79
80
81
82
 * Temporary files older than the system.file.temporary_maximum_age
 * configuration value will be, if clean-up not disabled, removed during cron
 * runs, but permanent files will not be removed during the file garbage
 * collection process.
83
 */
84
const FILE_STATUS_PERMANENT = 1;
85

86
/**
87
 * Provides Drupal stream wrapper registry.
88
 *
89
 * @param int $filter
90
 *   (Optional) Filters out all types except those with an on bit for each on
91
92
93
94
95
96
97
98
 *   bit in $filter. For example, if $filter is
 *   StreamWrapperInterface::WRITE_VISIBLE, which is equal to
 *   (StreamWrapperInterface::READ | StreamWrapperInterface::WRITE |
 *   StreamWrapperInterface::VISIBLE), then only stream wrappers with all three
 *   of these bits set are returned. Defaults to StreamWrapperInterface::ALL,
 *   which returns all registered stream wrappers.
 *
 * @return array
99
100
 *   An array keyed by scheme, with values containing an array of information
 *   about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
101
102
103
104
 *   is omitted or set to StreamWrapperInterface::ALL, the entire Drupal stream
 *   wrapper registry is returned. Otherwise only the stream wrappers whose
 *   'type' bitmask has an on bit for each bit specified in $filter are
 *   returned.
105
 *
106
107
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('stream_wrapper_manager')->getWrappers().
108
 */
109
110
function file_get_stream_wrappers($filter = StreamWrapperInterface::ALL) {
  return \Drupal::service('stream_wrapper_manager')->getWrappers($filter);
111
112
113
114
115
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
116
 * @param string $scheme
117
 *   Stream scheme.
118
 *
119
 * @return string|bool
120
 *   Return string if a scheme has a registered handler, or FALSE.
121
122
123
 *
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('stream_wrapper_manager')->getClass().
124
125
 */
function file_stream_wrapper_get_class($scheme) {
126
  return \Drupal::service('stream_wrapper_manager')->getClass($scheme);
127
128
129
130
131
}

/**
 * Returns the scheme of a URI (e.g. a stream).
 *
132
133
 * @param string $uri
 *   A stream, referenced as "scheme://target"  or "data:target".
134
 *
135
 * @return string
136
137
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
138
139
 *
 * @see file_uri_target()
140
141
 */
function file_uri_scheme($uri) {
142
143
144
145
146
147
  if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) {
    // The scheme will always be the last element in the matches array.
    return array_pop($matches);
  }

  return FALSE;
148
149
150
}

/**
151
 * Checks that the scheme of a stream URI is valid.
152
153
154
155
156
 *
 * Confirms that there is a registered stream handler for the provided scheme
 * and that it is callable. This is useful if you want to confirm a valid
 * scheme without creating a new instance of the registered handler.
 *
157
 * @param string $scheme
158
 *   A URI scheme, a stream is referenced as "scheme://target".
159
 *
160
 * @return bool
161
162
163
164
 *   Returns TRUE if the string is the name of a validated stream,
 *   or FALSE if the scheme does not have a registered handler.
 */
function file_stream_wrapper_valid_scheme($scheme) {
165
  return $scheme && class_exists(file_stream_wrapper_get_class($scheme));
166
167
}

168

169
/**
170
 * Returns the part of a URI after the schema.
171
 *
172
173
 * @param string $uri
 *   A stream, referenced as "scheme://target" or "data:target".
174
 *
175
 * @return string|bool
176
177
178
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
179
180
 *
 * @see file_uri_scheme()
181
182
 */
function file_uri_target($uri) {
183
184
185
  // Remove the scheme from the URI and remove erroneous leading or trailing,
  // forward-slashes and backslashes.
  $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
186

187
188
  // If nothing was replaced, the URI doesn't have a valid scheme.
  return $target !== $uri ? $target : FALSE;
189
190
}

191
/**
192
 * Gets the default file stream implementation.
193
 *
194
 * @return string
195
196
197
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
198
  return \Drupal::config('system.file')->get('default_scheme');
199
200
}

201
202
203
204
205
206
207
208
209
/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove trailing slashes from target
 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
 *
210
 * @param string $uri
211
 *   String reference containing the URI to normalize.
212
 *
213
 * @return string
214
 *   The normalized URI.
215
216
217
218
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

219
  if (file_stream_wrapper_valid_scheme($scheme)) {
220
221
    $target = file_uri_target($uri);

222
223
224
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
225
  }
226

227
228
229
230
  return $uri;
}

/**
231
 * Returns a reference to the stream wrapper class responsible for a given URI.
232
233
234
235
 *
 * The scheme determines the stream wrapper class that should be
 * used by consulting the stream wrapper registry.
 *
236
 * @param string $uri
237
 *   A stream, referenced as "scheme://target".
238
 *
239
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface|bool
240
241
242
 *   Returns a new stream wrapper object appropriate for the given URI or FALSE
 *   if no registered handler could be found. For example, a URI of
 *   "private://example.txt" would return a new private stream wrapper object
webchick's avatar
webchick committed
243
 *   (Drupal\Core\StreamWrapper\PrivateStream).
244
245
246
 *
 * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('stream_wrapper_manager')->getViaUri().
247
248
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
249
  return \Drupal::service('stream_wrapper_manager')->getViaUri($uri);
250
251
252
}

/**
253
 * Returns a reference to the stream wrapper class responsible for a scheme.
254
255
256
257
258
259
260
261
262
 *
 * This helper method returns a stream instance using a scheme. That is, the
 * passed string does not contain a "://". For example, "public" is a scheme
 * but "public://" is a URI (stream). This is because the later contains both
 * a scheme and target despite target being empty.
 *
 * Note: the instance URI will be initialized to "scheme://" so that you can
 * make the customary method calls as if you had retrieved an instance by URI.
 *
263
 * @param string $scheme
264
 *   If the stream was "public://target", "public" would be the scheme.
265
 *
266
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface|bool
267
268
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
webchick's avatar
webchick committed
269
 *   (Drupal\Core\StreamWrapper\PublicStream).
270
 *   FALSE is returned if no registered handler could be found.
271
272
273
 *
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('stream_wrapper_manager')->getViaScheme().
274
275
 */
function file_stream_wrapper_get_instance_by_scheme($scheme) {
276
  return \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme);
277
278
}

Dries's avatar
   
Dries committed
279
/**
280
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
   
Dries committed
281
 *
282
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
   
Dries committed
283
 *
284
 * There are two kinds of local files:
285
286
287
 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
 *   These are files that have either been uploaded by users or were generated
 *   automatically (for example through CSS aggregation).
288
289
290
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
291
 * @param string $uri
292
293
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
294
 *
295
 * @return string
296
 *   A string containing a URL that may be used to access the file.
297
298
299
 *   If the provided string already contains a preceding 'http', 'https', or
 *   '/', nothing is done and the same string is returned. If a stream wrapper
 *   could not be found to generate an external URL, then FALSE is returned.
300
301
 *
 * @see http://drupal.org/node/515192
302
 * @see file_url_transform_relative()
Dries's avatar
   
Dries committed
303
 */
304
function file_create_url($uri) {
305
306
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
307
  \Drupal::moduleHandler()->alter('file_url', $uri);
308

309
310
311
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
312
313
314
315
316
317
318
    // Allow for:
    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
    //   http://example.com/bar.jpg by the browser when viewing a page over
    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
    // Both types of relative URIs are characterized by a leading slash, hence
    // we can use a single check.
319
    if (Unicode::substr($uri, 0, 1) == '/') {
320
321
322
323
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
324
      // Therefore, return the urlencoded URI with the base URL prepended.
325
326
327
328
329
330
331
332
333
334
335
336
337
      $options = UrlHelper::parse($uri);
      $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
      // Append the query.
      if ($options['query']) {
        $path .= '?' . UrlHelper::buildQuery($options['query']);
      }

      // Append fragment.
      if ($options['fragment']) {
        $path .= '#' . $options['fragment'];
      }

      return $path;
338
    }
339
  }
340
341
342
  elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
    // Check for HTTP and data URI-encoded URLs so that we don't have to
    // implement getExternalUrl() for the HTTP and data schemes.
343
344
345
346
347
348
349
350
351
352
353
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
    if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries's avatar
   
Dries committed
354
355
}

356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/**
 * Transforms an absolute URL of a local file to a relative URL.
 *
 * May be useful to prevent problems on multisite set-ups and prevent mixed
 * content errors when using HTTPS + HTTP.
 *
 * @param string $file_url
 *   A file URL of a local file as generated by file_create_url().
 *
 * @return string
 *   If the file URL indeed pointed to a local file and was indeed absolute,
 *   then the transformed, relative URL to the local file. Otherwise: the
 *   original value of $file_url.
 *
 * @see file_create_url()
 */
function file_url_transform_relative($file_url) {
  // Unfortunately, we pretty much have to duplicate Symfony's
  // Request::getHttpHost() method because Request::getPort() may return NULL
  // instead of a port number.
  $http_host = '';
  $request = \Drupal::request();
  $host = $request->getHost();
  $scheme = $request->getScheme();
  $port = $request->getPort() ?: 80;
  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
    $http_host = $host;
  }
  else {
    $http_host = $host . ':' . $port;
  }

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

Dries's avatar
   
Dries committed
391
/**
392
 * Checks that the directory exists and is writable.
393
394
395
396
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
397
 * @param $directory
398
399
400
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
401
402
403
 *   A bitmask to indicate if the directory should be created if it does
 *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
 *   (FILE_MODIFY_PERMISSIONS).
404
 *
405
 * @return
406
407
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
   
Dries committed
408
 */
409
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
410
411
412
413
  if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
    // Only trim if we're not dealing with a stream.
    $directory = rtrim($directory, '/\\');
  }
Dries's avatar
   
Dries committed
414
415
416

  // Check if directory exists.
  if (!is_dir($directory)) {
417
418
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
419
    if ($options & FILE_CREATE_DIRECTORY) {
420
      return @drupal_mkdir($directory, NULL, TRUE);
Dries's avatar
   
Dries committed
421
    }
422
    return FALSE;
Dries's avatar
   
Dries committed
423
  }
424
425
426
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
427
    return drupal_chmod($directory);
Dries's avatar
   
Dries committed
428
429
  }

430
  return $writable;
Dries's avatar
   
Dries committed
431
432
433
}

/**
434
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
   
Dries committed
435
 */
436
function file_ensure_htaccess() {
437
  file_save_htaccess('public://', FALSE);
438
  $private_path = PrivateStream::basePath();
439
  if (!empty($private_path)) {
440
    file_save_htaccess('private://', TRUE);
441
  }
442
  file_save_htaccess('temporary://', TRUE);
443
  file_save_htaccess(config_get_config_directory(), TRUE);
444
  file_save_htaccess(config_get_config_directory(CONFIG_STAGING_DIRECTORY), TRUE);
Dries's avatar
   
Dries committed
445
446
447
}

/**
448
 * Creates a .htaccess file in the given directory.
Dries's avatar
   
Dries committed
449
 *
450
 * @param string $directory
451
 *   The directory.
452
453
454
455
456
457
458
459
 * @param bool $private
 *   (Optional) FALSE indicates that $directory should be a web-accessible
 *   directory. Defaults to TRUE which indicates a private directory.
 * @param bool $force_overwrite
 *   (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
 *   if one is already present. Defaults to FALSE.
 */
function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
460
  if (file_uri_scheme($directory)) {
461
    $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
462
463
  }
  else {
464
    $directory = rtrim($directory, '/\\');
465
    $htaccess_path = $directory . '/.htaccess';
466
  }
467

468
  if (file_exists($htaccess_path) && !$force_overwrite) {
469
    // Short circuit if the .htaccess file already exists.
470
    return TRUE;
471
  }
472
  $htaccess_lines = file_htaccess_lines($private);
473
474

  // Write the .htaccess file.
475
476
  if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
    return drupal_chmod($htaccess_path, 0444);
477
478
  }
  else {
479
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(String::checkPlain($htaccess_lines)));
480
    \Drupal::logger('security')->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables);
481
    return FALSE;
Dries's avatar
   
Dries committed
482
483
484
  }
}

485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
/**
 * Returns the standard .htaccess lines that Drupal writes to file directories.
 *
 * @param bool $private
 *   (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
 *   public directory. The default is TRUE, which returns the .htaccess lines
 *   for a private directory that should not be web-accessible.
 *
 * @return string
 *   The desired contents of the .htaccess file.
 *
 * @see file_create_htaccess()
 */
function file_htaccess_lines($private = TRUE) {
  return FileStorage::htaccessLines($private);
}

502
/**
503
 * Determines whether the URI has a valid scheme for file API operations.
504
505
506
507
508
509
510
511
512
513
514
515
 *
 * There must be a scheme and it must be a Drupal-provided scheme like
 * 'public', 'private', 'temporary', or an extension provided with
 * hook_stream_wrappers().
 *
 * @param $uri
 *   The URI to be tested.
 *
 * @return
 *   TRUE if the URI is allowed.
 */
function file_valid_uri($uri) {
516
  // Assert that the URI has an allowed scheme. Bare paths are not allowed.
517
  $uri_scheme = file_uri_scheme($uri);
518
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
519
520
521
522
523
    return FALSE;
  }
  return TRUE;
}

524
/**
525
 * Copies a file to a new location without invoking the file API.
Dries's avatar
   
Dries committed
526
 *
527
528
529
530
531
 * This is a powerful function that in many ways performs like an advanced
 * version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
532
 * - If the $source and $destination are equal, the behavior depends on the
533
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
534
 *   will rename the file until the $destination is unique.
535
536
 * - Provides a fallback using realpaths if the move fails using stream
 *   wrappers. This can occur because PHP's copy() function does not properly
537
 *   support streams if open_basedir is enabled. See
538
 *   https://bugs.php.net/bug.php?id=60456
539
540
 *
 * @param $source
541
 *   A string specifying the filepath or URI of the source file.
542
 * @param $destination
543
 *   A URI containing the destination that $source should be copied to. The
544
545
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
546
547
548
549
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
550
 *       unique.
551
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
552
 *
553
554
 * @return
 *   The path to the new file, or FALSE in the event of an error.
555
 *
556
 * @see file_copy()
Dries's avatar
   
Dries committed
557
 */
558
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
559
  $original_source = $source;
560
  $logger = \Drupal::logger('file');
561

562
  // Assert that the source file actually exists.
563
  if (!file_exists($source)) {
564
    // @todo Replace drupal_set_message() calls with exceptions instead.
565
    drupal_set_message(t('The specified file %file could not be copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
566
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
567
      $logger->notice('File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
568
569
    }
    else {
570
      $logger->notice('File %file could not be copied because it does not exist.', array('%file' => $original_source));
571
    }
572
573
    return FALSE;
  }
Dries's avatar
   
Dries committed
574

575
576
  // Build a destination URI if necessary.
  if (!isset($destination)) {
577
    $destination = file_build_uri(drupal_basename($source));
578
  }
Dries's avatar
   
Dries committed
579
580


581
582
583
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
584
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
585
586
587
588
589
590
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
591
      $logger->notice('File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
592
      drupal_set_message(t('The specified file %file could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
593
594
595
      return FALSE;
    }
  }
596

597
598
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
599
  if ($destination === FALSE) {
600
    drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
601
    $logger->notice('File %file could not be copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
602
    return FALSE;
Dries's avatar
   
Dries committed
603
  }
604
605

  // Assert that the source and destination filenames are not the same.
606
607
608
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
609
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
610
    $logger->notice('File %file could not be copied because it would overwrite itself.', array('%file' => $source));
611
    return FALSE;
Dries's avatar
   
Dries committed
612
  }
613
614
615
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
616
  if (!@copy($source, $destination)) {
617
618
619
    // If the copy failed and realpaths exist, retry the operation using them
    // instead.
    if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
620
      $logger->error('The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
621
622
      return FALSE;
    }
Dries's avatar
   
Dries committed
623
  }
Dries's avatar
   
Dries committed
624

625
626
  // Set the permissions on the new file.
  drupal_chmod($destination);
627
628

  return $destination;
Dries's avatar
   
Dries committed
629
630
}

631
/**
632
 * Constructs a URI to Drupal's default files location given a relative path.
633
634
 */
function file_build_uri($path) {
635
  $uri = file_default_scheme() . '://' . $path;
636
637
638
  return file_stream_wrapper_uri_normalize($uri);
}

639
/**
640
 * Determines the destination path for a file.
641
 *
642
 * @param $destination
643
 *   A string specifying the desired final URI or filepath.
644
645
 * @param $replace
 *   Replace behavior when the destination file already exists.
646
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
647
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
648
 *       unique.
649
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
650
 *
651
 * @return
652
653
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
654
655
656
657
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
658
659
660
661
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

662
      case FILE_EXISTS_RENAME:
663
        $basename = drupal_basename($destination);
664
        $directory = drupal_dirname($destination);
665
666
667
668
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
669
        // Error reporting handled by calling function.
670
671
672
673
674
675
        return FALSE;
    }
  }
  return $destination;
}

676
/**
677
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
678
 *
679
 * @param $source
680
 *   A string specifying the filepath or URI of the original file.
681
 * @param $destination
682
683
684
 *   A string containing the destination that $source should be moved to.
 *   This must be a stream wrapper URI. If this value is omitted, Drupal's
 *   default files scheme will be used, usually "public://".
685
686
687
688
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
689
 *       unique.
690
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
691
 *
692
 * @return
693
 *   The URI of the moved file, or FALSE in the event of an error.
694
 *
695
 * @see file_move()
Dries's avatar
   
Dries committed
696
 */
697
698
699
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  $filepath = file_unmanaged_copy($source, $destination, $replace);
  if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
700
    return FALSE;
Dries's avatar
   
Dries committed
701
  }
702
  return $filepath;
Dries's avatar
   
Dries committed
703
704
}

705
/**
706
 * Modifies a filename as needed for security purposes.
707
 *
708
709
710
711
712
713
714
715
716
717
718
 * Munging a file name prevents unknown file extensions from masking exploit
 * files. When web servers such as Apache decide how to process a URL request,
 * they use the file extension. If the extension is not recognized, Apache
 * skips that extension and uses the previous file extension. For example, if
 * the file being requested is exploit.php.pps, and Apache does not recognize
 * the '.pps' extension, it treats the file as PHP and executes it. To make
 * this file name safe for Apache and prevent it from executing as PHP, the
 * .php extension is "munged" into .php_, making the safe file name
 * exploit.php_.pps.
 *
 * Specifically, this function adds an underscore to all extensions that are
719
 * between 2 and 5 characters in length, internal to the file name, and not
720
721
 * included in $extensions.
 *
722
723
724
 * Function behavior is also controlled by the configuration
 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
725
 * @param $filename
726
 *   File name to modify.
727
 * @param $extensions
728
 *   A space-separated list of extensions that should not be altered.
729
 * @param $alerts
730
731
732
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
733
 * @return string
734
 *   The potentially modified $filename.
735
736
737
738
739
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

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

744
    $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
745
746
747
748
749
750
751
752
753
754
755

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

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
756
      $new_filename .= '.' . $filename_part;
757
      if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
758
759
760
        $new_filename .= '_';
      }
    }
761
    $filename = $new_filename . '.' . $final_extension;
762
763
764
765
766
767
768
769
770
771

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

  return $filename;
}

/**
772
 * Undoes the effect of file_munge_filename().
773
 *
774
775
 * @param $filename
 *   String with the filename to be unmunged.
776
 *
777
778
 * @return
 *   An unmunged filename string.
779
780
781
782
783
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

784
/**
785
 * Creates a full file path from a directory and filename.
786
787
788
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
789
 *
790
791
792
 * @param $basename
 *   String filename
 * @param $directory
793
 *   String containing the directory or parent URI.
794
 *
795
 * @return
796
797
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
798
 */
Dries's avatar
   
Dries committed
799
function file_create_filename($basename, $directory) {
800
801
802
  // Strip control characters (ASCII value < 32). Though these are allowed in
  // some filesystems, not many applications handle them well.
  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
803
804
805
806
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
807

808
809
810
811
812
813
814
815
816
  // A URI or path may already have a trailing slash or look like "public://".
  if (substr($directory, -1) == '/') {
    $separator = '';
  }
  else {
    $separator = '/';
  }

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

818
  if (file_exists($destination)) {
Dries's avatar
   
Dries committed
819
    // Destination file already exists, generate an alternative.
820
821
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries's avatar
   
Dries committed
822
823
824
825
826
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
827
      $ext = '';
Dries's avatar
   
Dries committed
828
829
830
831
    }

    $counter = 0;
    do {
832
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
833
    } while (file_exists($destination));
Dries's avatar
   
Dries committed
834
835
  }

836
  return $destination;
Dries's avatar
   
Dries committed
837
838
}

839
/**
840
 * Deletes a file and its database record.
841
 *
842
843
844
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
845
 *
846
847
 * @param $fid
 *   The file id.
848
 *
849
 * @see file_unmanaged_delete()
850
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
851
 */
852
853
854
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}
855

856
857
858
859
860
861
862
863
864
865
866
/**
 * Deletes files.
 *
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
 *
 * @param $fid
 *   The file id.
 *
 * @see file_unmanaged_delete()
867
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
868
869
870
 */
function file_delete_multiple(array $fids) {
  entity_delete_multiple('file', $fids);
871
872
873
}

/**
874
 * Deletes a file without database changes or hook invocations.
875
876
877
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
878
 *
879
 * @param $path
880
 *   A string containing a file path or (streamwrapper) URI.
881
 *
882
883
884
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
885
 *
886
 * @see file_delete()
887
 * @see file_unmanaged_delete_recursive()
888
 */
889
function file_unmanaged_delete($path) {
890
891
892
  if (is_file($path)) {
    return drupal_unlink($path);
  }
893
  $logger = \Drupal::logger('file');
894
  if (is_dir($path)) {
895
    $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path));
896
897
    return FALSE;
  }
898
  // Return TRUE for non-existent file, but log that nothing was actually
899
  // deleted, as the current state is the intended result.
900
  if (!file_exists($path)) {
901
    $logger->notice('The file %path was not deleted because it does not exist.', array('%path' => $path));
902
903
    return TRUE;
  }
904
905
  // We cannot handle anything other than files and directories. Log an error
  // for everything else (sockets, symbolic links, etc).
906
  $logger->error('The file %path is not of a recognized type so it was not deleted.', array('%path' => $path));
907
  return FALSE;
Dries's avatar
   
Dries committed
908
909
}

910
/**
911
 * Deletes all files and directories in the specified filepath recursively.
912
913
914
915
916
917
918
919
920
921
922
 *
 * If the specified path is a directory then the function will call itself
 * recursively to process the contents. Once the contents have been removed the
 * directory will also be removed.
 *
 * If the specified path is a file then it will be passed to
 * file_unmanaged_delete().
 *
 * Note that this only deletes visible files with write permission.
 *
 * @param $path
923
 *   A string containing either an URI or a file or directory path.
924
925
926
927
 * @param $callback
 *   (optional) Callback function to run on each file prior to deleting it and
 *   on each directory prior to traversing it. For example, can be used to
 *   modify permissions.
928
 *
929
 * @return
930
 *   TRUE for success or if path does not exist, FALSE in the event of an
931
932
933
934
 *   error.
 *
 * @see file_unmanaged_delete()
 */
935
936
937
938
function file_unmanaged_delete_recursive($path, $callback = NULL) {
  if (isset($callback)) {
    call_user_func($callback, $path);
  }
939
940
941
942
943
944
945
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
946
      file_unmanaged_delete_recursive($entry_path, $callback);
947
    }
948
    $dir->close();
949
950

    return drupal_rmdir($path);
951
952
953
954
  }
  return file_unmanaged_delete($path);
}

955

Dries's avatar
   
Dries committed