file.inc 55.4 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
9
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
10
use Drupal\Component\Utility\String;
11
use Drupal\Core\StreamWrapper\PublicStream;
12

13
/**
webchick's avatar
webchick committed
14
 * Stream wrapper bit flags that are the basis for composite types.
15
 *
webchick's avatar
webchick committed
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
 * Note that 0x0002 is skipped, because it was the value of a constant that has
 * since been removed.
 */

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

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

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

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

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

/**
 * Composite stream wrapper bit flags that are usually used as the types.
 */

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

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

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

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

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

/**
 * Stream wrapper type flag -- visible, readable and writeable using local files.
81
 */
webchick's avatar
webchick committed
82
define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
83

Kjartan's avatar
Kjartan committed
84
/**
Kjartan's avatar
Kjartan committed
85
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
86
 * @{
Dries's avatar
   
Dries committed
87
 * Common file handling functions.
88
 *
89
 * Fields on the file entity:
90
91
92
 * - 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
93
94
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
95
96
97
98
 * - 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
99
 *   bits are reserved for Drupal core. The least significant bit indicates
100
101
 *   temporary (0) or permanent (1). Temporary files older than
 *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
102
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
 
Dries committed
103
104
 */

105
/**
106
 * Flag used by file_prepare_directory() -- create directory if not present.
107
 */
108
const FILE_CREATE_DIRECTORY = 1;
109
110

/**
111
 * Flag used by file_prepare_directory() -- file permissions may be changed.
112
 */
113
const FILE_MODIFY_PERMISSIONS = 2;
114
115

/**
116
 * Flag for dealing with existing files: Appends number until name is unique.
117
 */
118
const FILE_EXISTS_RENAME = 0;
119
120
121
122

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
123
const FILE_EXISTS_REPLACE = 1;
124
125
126
127

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

130
/**
131
132
133
134
135
 * Indicates that the file is permanent and should not be deleted.
 *
 * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed
 * during cron runs, but permanent files will not be removed during the file
 * garbage collection process.
136
 */
137
const FILE_STATUS_PERMANENT = 1;
138

139
/**
140
 * Provides Drupal stream wrapper registry.
141
142
143
144
145
146
147
148
149
150
151
152
153
154
 *
 * A stream wrapper is an abstraction of a file system that allows Drupal to
 * use the same set of methods to access both local files and remote resources.
 *
 * Provide a facility for managing and querying user-defined stream wrappers
 * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
 * registered to handle a stream, which we need to be able to find the handler
 * for class instantiation.
 *
 * If a module registers a scheme that is already registered with PHP, the
 * existing scheme will be unregistered and replaced with the specified class.
 *
 * A stream is referenced as "scheme://target".
 *
155
156
157
158
 * The optional $filter parameter can be used to retrieve only the stream
 * wrappers that are appropriate for particular usage. For example, this returns
 * only stream wrappers that use local file storage:
 * @code
159
 *   $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
160
161
162
163
164
165
166
167
168
 * @endcode
 *
 * The $filter parameter can only filter to types containing a particular flag.
 * In some cases, you may want to filter to types that do not contain a
 * particular flag. For example, you may want to retrieve all stream wrappers
 * that are not writable, or all stream wrappers that are not local. PHP's
 * array_diff_key() function can be used to help with this. For example, this
 * returns only stream wrappers that do not use local file storage:
 * @code
169
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
170
171
 * @endcode
 *
172
 * @param $filter
173
174
175
176
177
178
 *   (Optional) Filters out all types except those with an on bit for each on
 *   bit in $filter. For example, if $filter is STREAM_WRAPPERS_WRITE_VISIBLE,
 *   which is equal to (STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE |
 *   STREAM_WRAPPERS_VISIBLE), then only stream wrappers with all three of these
 *   bits set are returned. Defaults to STREAM_WRAPPERS_ALL, which returns all
 *   registered stream wrappers.
179
 *
180
 * @return
181
182
183
184
185
 *   An array keyed by scheme, with values containing an array of information
 *   about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
 *   is omitted or set to STREAM_WRAPPERS_ALL, the entire Drupal stream wrapper
 *   registry is returned. Otherwise only the stream wrappers whose 'type'
 *   bitmask has an on bit for each bit specified in $filter are returned.
186
 *
187
188
189
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
190
191
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__);
192

193
  if (!isset($wrappers_storage)) {
194
    $wrappers = \Drupal::moduleHandler()->invokeAll('stream_wrappers');
195
196
197
198
    foreach ($wrappers as $scheme => $info) {
      // Add defaults.
      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
    }
199
200
201
202
    drupal_alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
webchick's avatar
webchick committed
203
      if (in_array('Drupal\Core\StreamWrapper\StreamWrapperInterface', class_implements($info['class']), TRUE)) {
204
205
206
207
208
209
210
211
        // Record whether we are overriding an existing scheme.
        if (in_array($scheme, $existing, TRUE)) {
          $wrappers[$scheme]['override'] = TRUE;
          stream_wrapper_unregister($scheme);
        }
        else {
          $wrappers[$scheme]['override'] = FALSE;
        }
212
213
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
214
215
        }
        else {
216
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
217
        }
218
      }
219
220
221
222
223
      // Pre-populate the static cache with the filters most typically used.
      $wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme];
      if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
        $wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme];
      }
224
225
    }
  }
226
227
228
229
230

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
231
      if (($info['type'] & $filter) == $filter) {
232
233
234
235
236
237
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
238
239
240
241
242
243
244
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
245
 *
246
247
248
249
250
251
252
253
254
255
256
257
258
 * @return
 *   Return string if a scheme has a registered handler, or FALSE.
 */
function file_stream_wrapper_get_class($scheme) {
  $wrappers = file_get_stream_wrappers();
  return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
}

/**
 * Returns the scheme of a URI (e.g. a stream).
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
259
 *
260
261
262
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
263
264
 *
 * @see file_uri_target()
265
266
 */
function file_uri_scheme($uri) {
267
268
  $position = strpos($uri, '://');
  return $position ? substr($uri, 0, $position) : FALSE;
269
270
271
}

/**
272
 * Checks that the scheme of a stream URI is valid.
273
274
275
276
277
278
279
 *
 * Confirms that there is a registered stream handler for the provided scheme
 * and that it is callable. This is useful if you want to confirm a valid
 * scheme without creating a new instance of the registered handler.
 *
 * @param $scheme
 *   A URI scheme, a stream is referenced as "scheme://target".
280
 *
281
282
283
284
285
 * @return
 *   Returns TRUE if the string is the name of a validated stream,
 *   or FALSE if the scheme does not have a registered handler.
 */
function file_stream_wrapper_valid_scheme($scheme) {
286
  return $scheme && class_exists(file_stream_wrapper_get_class($scheme));
287
288
}

289

290
/**
291
 * Returns the part of a URI after the schema.
292
293
294
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
295
 *
296
297
298
299
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
300
301
 *
 * @see file_uri_scheme()
302
303
 */
function file_uri_target($uri) {
304
305
306
307
  $data = explode('://', $uri, 2);

  // Remove erroneous leading or trailing, forward-slashes and backslashes.
  return count($data) == 2 ? trim($data[1], '\/') : FALSE;
308
309
}

310
/**
311
 * Gets the default file stream implementation.
312
313
314
315
316
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
317
  return \Drupal::config('system.file')->get('default_scheme');
318
319
}

320
321
322
323
324
325
326
327
328
329
330
/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove trailing slashes from target
 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
 *
 * @param $uri
 *   String reference containing the URI to normalize.
331
 *
332
333
 * @return
 *   The normalized URI.
334
335
336
337
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

338
  if (file_stream_wrapper_valid_scheme($scheme)) {
339
340
    $target = file_uri_target($uri);

341
342
343
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
344
  }
345

346
347
348
349
  return $uri;
}

/**
350
 * Returns a reference to the stream wrapper class responsible for a given URI.
351
352
353
354
355
356
 *
 * The scheme determines the stream wrapper class that should be
 * used by consulting the stream wrapper registry.
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
357
 *
358
359
360
361
 * @return
 *   Returns a new stream wrapper object appropriate for the given URI or FALSE
 *   if no registered handler could be found. For example, a URI of
 *   "private://example.txt" would return a new private stream wrapper object
webchick's avatar
webchick committed
362
 *   (Drupal\Core\StreamWrapper\PrivateStream).
363
364
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
365
366
367
368
369
370
371
  if ($scheme = file_uri_scheme($uri)) {
    $class = file_stream_wrapper_get_class($scheme);
    if (class_exists($class)) {
      $instance = new $class();
      $instance->setUri($uri);
      return $instance;
    }
372
  }
373
  return FALSE;
374
375
376
}

/**
377
 * Returns a reference to the stream wrapper class responsible for a scheme.
378
379
380
381
382
383
384
385
386
387
388
 *
 * This helper method returns a stream instance using a scheme. That is, the
 * passed string does not contain a "://". For example, "public" is a scheme
 * but "public://" is a URI (stream). This is because the later contains both
 * a scheme and target despite target being empty.
 *
 * Note: the instance URI will be initialized to "scheme://" so that you can
 * make the customary method calls as if you had retrieved an instance by URI.
 *
 * @param $scheme
 *   If the stream was "public://target", "public" would be the scheme.
389
 *
390
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface
391
392
 *   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
393
 *   (Drupal\Core\StreamWrapper\PublicStream).
394
395
396
397
398
 *   FALSE is returned if no registered handler could be found.
 */
function file_stream_wrapper_get_instance_by_scheme($scheme) {
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
399
    $instance = new $class();
400
401
402
403
404
405
406
407
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries's avatar
 
Dries committed
408
/**
409
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
   
Dries committed
410
 *
411
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
   
Dries committed
412
 *
413
 * There are two kinds of local files:
414
415
416
 * - "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).
417
418
419
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
420
 * @param $uri
421
422
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
423
 *
424
 * @return
425
 *   A string containing a URL that may be used to access the file.
426
427
428
 *   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.
429
430
 *
 * @see http://drupal.org/node/515192
Dries's avatar
 
Dries committed
431
 */
432
function file_create_url($uri) {
433
434
435
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
  drupal_alter('file_url', $uri);
436

437
438
439
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
440
441
442
443
444
445
446
447
448
449
450
451
    // Allow for:
    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
    //   http://example.com/bar.jpg by the browser when viewing a page over
    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
    // Both types of relative URIs are characterized by a leading slash, hence
    // we can use a single check.
    if (drupal_substr($uri, 0, 1) == '/') {
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
452
453
      // Therefore, return the urlencoded URI with the base URL prepended.
      return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
454
    }
455
456
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
457
458
    // Check for HTTP so that we don't have to implement getExternalUrl() for
    // the HTTP wrapper.
459
460
461
462
463
464
465
466
467
468
469
    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
470
471
472
}

/**
473
 * Checks that the directory exists and is writable.
474
475
476
477
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
478
 * @param $directory
479
480
481
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
482
483
484
 *   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).
485
486
487
 * @param int $mode
 *   Octal value for the permissions. Consult PHP chmod() documentation for
 *   more information.
488
 *
489
 * @return
490
491
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
 
Dries committed
492
 */
493
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS, $mode = NULL) {
494
495
496
497
  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
498
499
500

  // Check if directory exists.
  if (!is_dir($directory)) {
501
502
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
503
    if ($options & FILE_CREATE_DIRECTORY) {
504
      return @drupal_mkdir($directory, $mode, TRUE);
Dries's avatar
   
Dries committed
505
    }
506
    return FALSE;
Dries's avatar
   
Dries committed
507
  }
508
509
510
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
511
    return drupal_chmod($directory, $mode);
Dries's avatar
   
Dries committed
512
513
  }

514
  return $writable;
Dries's avatar
 
Dries committed
515
516
517
}

/**
518
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
 
Dries committed
519
 */
520
function file_ensure_htaccess() {
521
  file_save_htaccess('public://', FALSE);
522
  $private_path = \Drupal::config('system.file')->get('path.private');
523
  if (!empty($private_path)) {
524
    file_save_htaccess('private://', TRUE);
525
  }
526
  file_save_htaccess('temporary://', TRUE);
527
  file_save_htaccess(config_get_config_directory(), TRUE);
528
  file_save_htaccess(config_get_config_directory(CONFIG_STAGING_DIRECTORY), TRUE);
Dries's avatar
 
Dries committed
529
530
531
}

/**
532
 * Creates a .htaccess file in the given directory.
Dries's avatar
 
Dries committed
533
 *
534
 * @param $directory
535
536
537
538
 *   The directory.
 * @param $private
 *   FALSE indicates that $directory should be an open and public directory.
 *   The default is TRUE which indicates a private and protected directory.
Dries's avatar
 
Dries committed
539
 */
540
function file_save_htaccess($directory, $private = TRUE) {
541
542
  if (file_uri_scheme($directory)) {
    $directory = file_stream_wrapper_uri_normalize($directory);
543
544
  }
  else {
545
    $directory = rtrim($directory, '/\\');
546
  }
547
548
549
550
551
552
553
554
555
  $htaccess_path =  $directory . '/.htaccess';

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

  if ($private) {
    // Private .htaccess file.
556
    $htaccess_lines = MTimeProtectedFastFileStorage::HTACCESS;
557
558
559
560
561
562
563
564
565
566
567
  }
  else {
    // Public .htaccess file.
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
  }

  // Write the .htaccess file.
  if (file_put_contents($htaccess_path, $htaccess_lines)) {
    drupal_chmod($htaccess_path, 0444);
  }
  else {
568
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(String::checkPlain($htaccess_lines)));
569
    watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
Dries's avatar
 
Dries committed
570
571
572
  }
}

573
/**
574
 * Determines whether the URI has a valid scheme for file API operations.
575
576
577
578
579
580
581
582
583
584
585
586
587
588
 *
 * There must be a scheme and it must be a Drupal-provided scheme like
 * 'public', 'private', 'temporary', or an extension provided with
 * hook_stream_wrappers().
 *
 * @param $uri
 *   The URI to be tested.
 *
 * @return
 *   TRUE if the URI is allowed.
 */
function file_valid_uri($uri) {
  // Assert that the URI has an allowed scheme. Barepaths are not allowed.
  $uri_scheme = file_uri_scheme($uri);
589
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
590
591
592
593
594
    return FALSE;
  }
  return TRUE;
}

595
/**
596
 * Copies a file to a new location without invoking the file API.
Dries's avatar
 
Dries committed
597
 *
598
599
600
601
602
 * 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.
603
604
605
 * - If the $source and $destination are equal, the behavior depends on the
 *   $replace parameter. FILE_EXIST_REPLACE will error out. FILE_EXIST_RENAME
 *   will rename the file until the $destination is unique.
606
607
608
609
 * - Provides a fallback using realpaths if the move fails using stream
 *   wrappers. This can occur because PHP's copy() function does not properly
 *   support streams if safe_mode or open_basedir are enabled. See
 *   https://bugs.php.net/bug.php?id=60456
610
611
 *
 * @param $source
612
 *   A string specifying the filepath or URI of the source file.
613
 * @param $destination
614
 *   A URI containing the destination that $source should be copied to. The
615
616
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
617
618
619
620
 * @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
621
 *       unique.
622
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
623
 *
624
625
 * @return
 *   The path to the new file, or FALSE in the event of an error.
626
 *
627
 * @see file_copy()
Dries's avatar
 
Dries committed
628
 */
629
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
630
631
  $original_source = $source;

632
  // Assert that the source file actually exists.
633
  if (!file_exists($source)) {
634
    // @todo Replace drupal_set_message() calls with exceptions instead.
635
    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');
636
637
638
639
640
641
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
      watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
    }
    else {
      watchdog('file', 'File %file could not be copied because it does not exist.', array('%file' => $original_source));
    }
642
643
    return FALSE;
  }
Dries's avatar
 
Dries committed
644

645
646
  // Build a destination URI if necessary.
  if (!isset($destination)) {
647
    $destination = file_build_uri(drupal_basename($source));
648
  }
Dries's avatar
 
Dries committed
649
650


651
652
653
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
654
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
655
656
657
658
659
660
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
661
662
      watchdog('file', 'File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
      drupal_set_message(t('The specified file %file could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
663
664
665
      return FALSE;
    }
  }
666

667
668
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
669
  if ($destination === FALSE) {
670
    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');
671
    watchdog('file', '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));
672
    return FALSE;
Dries's avatar
 
Dries committed
673
  }
674
675

  // Assert that the source and destination filenames are not the same.
676
677
678
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
679
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
680
    watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
681
    return FALSE;
Dries's avatar
 
Dries committed
682
  }
683
684
685
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
686
  if (!@copy($source, $destination)) {
687
688
689
690
691
692
    // 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)) {
      watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR);
      return FALSE;
    }
Dries's avatar
 
Dries committed
693
  }
Dries's avatar
   
Dries committed
694

695
696
  // Set the permissions on the new file.
  drupal_chmod($destination);
697
698

  return $destination;
Dries's avatar
 
Dries committed
699
700
}

701
/**
702
 * Constructs a URI to Drupal's default files location given a relative path.
703
704
 */
function file_build_uri($path) {
705
  $uri = file_default_scheme() . '://' . $path;
706
707
708
  return file_stream_wrapper_uri_normalize($uri);
}

709
/**
710
 * Determines the destination path for a file.
711
 *
712
 * @param $destination
713
 *   A string specifying the desired final URI or filepath.
714
715
 * @param $replace
 *   Replace behavior when the destination file already exists.
716
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
717
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
718
 *       unique.
719
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
720
 *
721
 * @return
722
723
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
724
725
726
727
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
728
729
730
731
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

732
      case FILE_EXISTS_RENAME:
733
        $basename = drupal_basename($destination);
734
        $directory = drupal_dirname($destination);
735
736
737
738
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
739
        // Error reporting handled by calling function.
740
741
742
743
744
745
        return FALSE;
    }
  }
  return $destination;
}

746
/**
747
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
748
 *
749
 * @param $source
750
 *   A string specifying the filepath or URI of the original file.
751
 * @param $destination
752
753
754
 *   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://".
755
756
757
758
 * @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
759
 *       unique.
760
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
761
 *
762
 * @return
763
 *   The URI of the moved file, or FALSE in the event of an error.
764
 *
765
 * @see file_move()
Dries's avatar
   
Dries committed
766
 */
767
768
769
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) {
770
    return FALSE;
Dries's avatar
 
Dries committed
771
  }
772
  return $filepath;
Dries's avatar
 
Dries committed
773
774
}

775
/**
776
 * Modifies a filename as needed for security purposes.
777
 *
778
779
780
781
782
783
784
785
786
787
788
 * 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
789
 * between 2 and 5 characters in length, internal to the file name, and not
790
791
 * included in $extensions.
 *
792
793
794
 * 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'. *
795
 * @param $filename
796
 *   File name to modify.
797
 * @param $extensions
798
 *   A space-separated list of extensions that should not be altered.
799
 * @param $alerts
800
801
802
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
803
 * @return string
804
 *   The potentially modified $filename.
805
806
807
808
809
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

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

814
815
816
817
818
819
820
821
822
823
824
825
    $whitelist = array_unique(explode(' ', trim($extensions)));

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

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
826
      $new_filename .= '.' . $filename_part;
827
828
829
830
      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
        $new_filename .= '_';
      }
    }
831
    $filename = $new_filename . '.' . $final_extension;
832
833
834
835
836
837
838
839
840
841

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

  return $filename;
}

/**
842
 * Undoes the effect of file_munge_filename().
843
 *
844
845
 * @param $filename
 *   String with the filename to be unmunged.
846
 *
847
848
 * @return
 *   An unmunged filename string.
849
850
851
852
853
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

854
/**
855
 * Creates a full file path from a directory and filename.
856
857
858
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
859
 *
860
861
862
 * @param $basename
 *   String filename
 * @param $directory
863
 *   String containing the directory or parent URI.
864
 *
865
 * @return
866
867
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
868
 */
Dries's avatar
   
Dries committed
869
function file_create_filename($basename, $directory) {
870
871
872
  // 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);
873
874
875
876
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
877

878
879
880
881
882
883
884
885
886
  // 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
887

888
  if (file_exists($destination)) {
Dries's avatar
   
Dries committed
889
    // Destination file already exists, generate an alternative.
890
891
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries's avatar
   
Dries committed
892
893
894
895
896
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
897
      $ext = '';
Dries's avatar
   
Dries committed
898
899
900
901
    }

    $counter = 0;
    do {
902
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
903
    } while (file_exists($destination));
Dries's avatar
   
Dries committed
904
905
  }

906
  return $destination;
Dries's avatar
   
Dries committed
907
908
}

909
/**
910
 * Deletes a file and its database record.
911
 *
912
913
914
 * 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.
915
 *
916
917
 * @param $fid
 *   The file id.
918
 *
919
 * @see file_unmanaged_delete()
920
 * @see file_usage()->listUsage()
921
 */
922
923
924
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}
925

926
927
928
929
930
931
932
933
934
935
936
/**
 * 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()
937
 * @see file_usage()->listUsage()
938
939
940
 */
function file_delete_multiple(array $fids) {
  entity_delete_multiple('file', $fids);
941
942
943
}

/**
944
 * Deletes a file without database changes or hook invocations.
945
946
947
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
948
 *
949
 * @param $path
950
 *   A string containing a file path or (streamwrapper) URI.
951
 *
952
953
954
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
955
 *
956
 * @see file_delete()
957
 * @see file_unmanaged_delete_recursive()
958
 */
959
function file_unmanaged_delete($path) {
960
  if (is_dir($path)) {
961
    watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
962
963
    return FALSE;
  }
964
  if (is_file($path)) {
965
    return drupal_unlink($path);
966
  }
967
  // Return TRUE for non-existent file, but log that nothing was actually
968
  // deleted, as the current state is the intended result.
969
  if (!file_exists($path)) {
970
    watchdog('file', 'The file %path was not deleted because it does not exist.', array('%path' => $path), WATCHDOG_NOTICE);
971
972
    return TRUE;
  }
973
974
  // We cannot handle anything other than files and directories. Log an error
  // for everything else (sockets, symbolic links, etc).
975
  watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), WATCHDOG_ERROR);
976
  return FALSE;
Dries's avatar
 
Dries committed
977
978
}

979
/**
980
 * Deletes all files and directories in the specified filepath recursively.
981
982
983
984
985
986
987
988
989
990
991
 *
 * 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
992
 *   A string containing either an URI or a file or directory path.
993
994
995
996
 * @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.
997
 *
998
 * @return
999
 *   TRUE for success or if path does not exist, FALSE in the event of an
1000
1001
1002
1003
 *   error.
 *
 * @see file_unmanaged_delete()
 */
1004
1005
1006
1007
function file_unmanaged_delete_recursive($path, $callback = NULL) {
  if (isset($callback)) {
    call_user_func($callback, $path);
  }
1008
1009
1010
1011
1012
1013
1014
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
1015
      file_unmanaged_delete_recursive($entry_path, $callback);
1016
    }
1017
    $dir->close();
1018
1019

    return drupal_rmdir($path);
1020
1021
1022
1023
  }
  return file_unmanaged_delete($path);
}

1024

Dries's avatar
 
Dries committed
1025

1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
/**
 * Moves an uploaded file to a new location.
 *
 * PHP's move_uploaded_file() does not properly support streams if safe_mode
 * or open_basedir are enabled, so this function fills that gap.
 *
 * Compatibility: normal paths and stream wrappers.
 *
 * @param $filename
 *   The filename of the uploaded file.
 * @param $uri
 *   A string containing the destination URI of the file.
 *
 * @return
 *   TRUE on success, or FALSE on failure.
 *
 * @see move_uploaded_file()
1043
 * @see http://drupal.org/node/515192
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
 * @ingroup php_wrappers
 */