file.inc 86.5 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 10
 * Manually include stream wrapper code.
 *
11 12 13 14
 * Stream wrapper code is included here because there are cases where
 * File API is needed before a bootstrap, or in an alternate order (e.g.
 * maintenance theme).
 */
15
require_once DRUPAL_ROOT . '/core/includes/stream_wrappers.inc';
16

Kjartan's avatar
Kjartan committed
17
/**
Kjartan's avatar
Kjartan committed
18
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
19
 * @{
Dries's avatar
 
Dries committed
20
 * Common file handling functions.
21 22
 *
 * Fields on the file object:
23 24 25
 * - 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
26 27
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
28 29 30 31
 * - 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
32
 *   bits are reserved for Drupal core. The least significant bit indicates
33 34
 *   temporary (0) or permanent (1). Temporary files older than
 *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
35
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
 
Dries committed
36 37
 */

38
/**
39
 * Flag used by file_prepare_directory() -- create directory if not present.
40
 */
41
const FILE_CREATE_DIRECTORY = 1;
42 43

/**
44
 * Flag used by file_prepare_directory() -- file permissions may be changed.
45
 */
46
const FILE_MODIFY_PERMISSIONS = 2;
47 48

/**
49
 * Flag for dealing with existing files: Appends number until name is unique.
50
 */
51
const FILE_EXISTS_RENAME = 0;
52 53 54 55

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
56
const FILE_EXISTS_REPLACE = 1;
57 58 59 60

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

63
/**
64 65 66 67 68
 * 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.
69
 */
70
const FILE_STATUS_PERMANENT = 1;
71

72
/**
73
 * Provides Drupal stream wrapper registry.
74 75 76 77 78 79 80 81 82 83 84 85 86 87
 *
 * 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".
 *
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
 * 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
 *   $local_stream_wrappers = file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL);
 * @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
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL));
 * @endcode
 *
105
 * @param $filter
106 107 108 109 110 111
 *   (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.
112
 *
113
 * @return
114 115 116 117 118
 *   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.
119
 *
120 121 122
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
123 124
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__);
125

126
  if (!isset($wrappers_storage)) {
127
    $wrappers = module_invoke_all('stream_wrappers');
128 129 130 131
    foreach ($wrappers as $scheme => $info) {
      // Add defaults.
      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
    }
132 133 134 135 136 137 138 139 140 141 142 143 144
    drupal_alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
      if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
        // Record whether we are overriding an existing scheme.
        if (in_array($scheme, $existing, TRUE)) {
          $wrappers[$scheme]['override'] = TRUE;
          stream_wrapper_unregister($scheme);
        }
        else {
          $wrappers[$scheme]['override'] = FALSE;
        }
145 146
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
147 148
        }
        else {
149
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
150
        }
151
      }
152 153 154 155 156
      // 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];
      }
157 158
    }
  }
159 160 161 162 163

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
164
      if (($info['type'] & $filter) == $filter) {
165 166 167 168 169 170
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
171 172 173 174 175 176 177
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
178
 *
179 180 181 182 183 184 185 186 187 188 189 190 191
 * @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".
192
 *
193 194 195
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
196 197
 *
 * @see file_uri_target()
198 199
 */
function file_uri_scheme($uri) {
200 201
  $position = strpos($uri, '://');
  return $position ? substr($uri, 0, $position) : FALSE;
202 203 204
}

/**
205
 * Checks that the scheme of a stream URI is valid.
206 207 208 209 210 211 212
 *
 * 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".
213
 *
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
 * @return
 *   Returns TRUE if the string is the name of a validated stream,
 *   or FALSE if the scheme does not have a registered handler.
 */
function file_stream_wrapper_valid_scheme($scheme) {
  // Does the scheme have a registered handler that is callable?
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

229

230
/**
231
 * Returns the part of a URI after the schema.
232 233 234
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
235
 *
236 237 238 239
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
240 241
 *
 * @see file_uri_scheme()
242 243
 */
function file_uri_target($uri) {
244 245 246 247
  $data = explode('://', $uri, 2);

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

250
/**
251
 * Gets the default file stream implementation.
252 253 254 255 256 257 258 259
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
  return variable_get('file_default_scheme', 'public');
}

260 261 262 263 264 265 266 267 268 269 270
/**
 * 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.
271
 *
272 273
 * @return
 *   The normalized URI.
274 275 276 277 278 279 280
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

  if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
    $target = file_uri_target($uri);

281 282 283
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
284
  }
285 286 287 288
  else {
    // The default scheme is file://
    $url = 'file://' . $uri;
  }
289 290 291 292
  return $uri;
}

/**
293
 * Returns a reference to the stream wrapper class responsible for a given URI.
294 295 296 297 298 299
 *
 * 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".
300
 *
301 302 303 304 305 306 307 308 309 310
 * @return
 *   Returns a new stream wrapper object appropriate for the given URI or FALSE
 *   if no registered handler could be found. For example, a URI of
 *   "private://example.txt" would return a new private stream wrapper object
 *   (DrupalPrivateStreamWrapper).
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
  $scheme = file_uri_scheme($uri);
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
311
    $instance = new $class();
312 313 314 315 316 317 318 319 320
    $instance->setUri($uri);
    return $instance;
  }
  else {
    return FALSE;
  }
}

/**
321
 * Returns a reference to the stream wrapper class responsible for a scheme.
322 323 324 325 326 327 328 329 330 331 332
 *
 * 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.
333
 *
334 335 336 337 338 339 340 341 342
 * @return
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
 *   (DrupalPublicStreamWrapper).
 *   FALSE is returned if no registered handler could be found.
 */
function file_stream_wrapper_get_instance_by_scheme($scheme) {
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
343
    $instance = new $class();
344 345 346 347 348 349 350 351
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries's avatar
 
Dries committed
352
/**
353
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
 
Dries committed
354
 *
355
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
 
Dries committed
356
 *
357
 * There are two kinds of local files:
358 359 360
 * - "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).
361 362 363
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
364
 * @param $uri
365 366
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
367
 *
368
 * @return
369
 *   A string containing a URL that may be used to access the file.
370 371 372
 *   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.
373 374
 *
 * @see http://drupal.org/node/515192
Dries's avatar
 
Dries committed
375
 */
376
function file_create_url($uri) {
377 378 379
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
  drupal_alter('file_url', $uri);
380

381 382 383
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
384 385 386 387 388 389 390 391 392 393 394 395
    // 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.
396 397
      // Therefore, return the urlencoded URI with the base URL prepended.
      return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
398
    }
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
    // Check for http so that we don't have to implement getExternalUrl() for
    // the http wrapper.
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
    if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries's avatar
 
Dries committed
414 415 416
}

/**
417
 * Checks that the directory exists and is writable.
418 419 420 421
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
422
 * @param $directory
423 424 425
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
426 427 428
 *   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).
429
 *
430
 * @return
431 432
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
 
Dries committed
433
 */
434 435 436 437 438
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
  if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
    // Only trim if we're not dealing with a stream.
    $directory = rtrim($directory, '/\\');
  }
Dries's avatar
 
Dries committed
439 440 441

  // Check if directory exists.
  if (!is_dir($directory)) {
442 443
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
444 445
    if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
      return drupal_chmod($directory);
Dries's avatar
 
Dries committed
446
    }
447
    return FALSE;
Dries's avatar
 
Dries committed
448
  }
449 450 451 452
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
    return drupal_chmod($directory);
Dries's avatar
 
Dries committed
453 454
  }

455
  return $writable;
Dries's avatar
 
Dries committed
456 457 458
}

/**
459
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
 
Dries committed
460
 */
461
function file_ensure_htaccess() {
462
  file_save_htaccess('public://', FALSE);
463
  if (variable_get('file_private_path', FALSE)) {
464
    file_save_htaccess('private://', TRUE);
465
  }
466
  file_save_htaccess('temporary://', TRUE);
467
  file_save_htaccess(config_get_config_directory(), TRUE);
Dries's avatar
 
Dries committed
468 469 470
}

/**
471
 * Creates a .htaccess file in the given directory.
Dries's avatar
 
Dries committed
472
 *
473
 * @param $directory
474 475 476 477
 *   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
478
 */
479
function file_save_htaccess($directory, $private = TRUE) {
480 481
  if (file_uri_scheme($directory)) {
    $directory = file_stream_wrapper_uri_normalize($directory);
482 483
  }
  else {
484
    $directory = rtrim($directory, '/\\');
485
  }
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
  $htaccess_path =  $directory . '/.htaccess';

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

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

  // Write the .htaccess file.
  if (file_put_contents($htaccess_path, $htaccess_lines)) {
    drupal_chmod($htaccess_path, 0444);
  }
  else {
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
508
    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
509 510 511 512
  }
}

/**
513
 * Loads file objects from the database.
514
 *
515 516 517
 * @param $fids
 *   An array of file IDs.
 * @param $conditions
518 519 520 521 522
 *   (deprecated) An associative array of conditions on the {file_managed}
 *   table, where the keys are the database fields and the values are the
 *   values those fields must have. Instead, it is preferable to use
 *   EntityFieldQuery to retrieve a list of entity IDs loadable by
 *   this function.
523
 *
524
 * @return
525
 *   An array of file objects, indexed by fid.
526
 *
527 528
 * @todo Remove $conditions in Drupal 8.
 *
529
 * @see hook_file_load()
530
 * @see file_load()
531 532
 * @see entity_load()
 * @see EntityFieldQuery
533
 */
534
function file_load_multiple($fids = array(), $conditions = array()) {
535
  return entity_load('file', $fids, $conditions);
536
}
537

538
/**
539
 * Loads a single file object from the database.
540 541
 *
 * @param $fid
542
 *   A file ID.
543
 *
544 545 546 547 548 549 550 551 552
 * @return
 *   A file object.
 *
 * @see hook_file_load()
 * @see file_load_multiple()
 */
function file_load($fid) {
  $files = file_load_multiple(array($fid), array());
  return reset($files);
553 554 555
}

/**
556
 * Saves a file object to the database.
557
 *
558
 * If the $file->fid is not set a new record will be added.
559 560 561
 *
 * @param $file
 *   A file object returned by file_load().
562
 *
563 564
 * @return
 *   The updated file object.
565
 *
566 567 568
 * @see hook_file_insert()
 * @see hook_file_update()
 */
569
function file_save(stdClass $file) {
570
  $file->timestamp = REQUEST_TIME;
571
  $file->filesize = filesize($file->uri);
572 573 574 575 576 577 578
  if (!isset($file->langcode)) {
    // Default the file's language code to none, because files are language
    // neutral more often than language dependent. Until we have better flexible
    // settings.
    // @todo See http://drupal.org/node/258785 and followups.
    $file->langcode = LANGUAGE_NONE;
  }
579

580 581 582 583 584
  // Load the stored entity, if any.
  if (!empty($file->fid) && !isset($file->original)) {
    $file->original = entity_load_unchanged('file', $file->fid);
  }

585 586 587
  module_invoke_all('file_presave', $file);
  module_invoke_all('entity_presave', $file, 'file');

588
  if (empty($file->fid)) {
589
    drupal_write_record('file_managed', $file);
590 591
    // Inform modules about the newly added file.
    module_invoke_all('file_insert', $file);
592
    module_invoke_all('entity_insert', $file, 'file');
593 594
  }
  else {
595
    drupal_write_record('file_managed', $file, 'fid');
596 597
    // Inform modules that the file has been updated.
    module_invoke_all('file_update', $file);
598
    module_invoke_all('entity_update', $file, 'file');
599 600
  }

601
  unset($file->original);
602 603 604 605
  return $file;
}

/**
606 607 608 609 610 611 612
 * Determines where a file is used.
 *
 * @param $file
 *   A file object.
 *
 * @return
 *   A nested array with usage data. The first level is keyed by module name,
613 614
 *   the second by object type and the third by the object id. The value
 *   of the third level contains the usage count.
615 616 617 618 619 620 621 622 623 624 625 626
 *
 * @see file_usage_add()
 * @see file_usage_delete()
 */
function file_usage_list(stdClass $file) {
  $result = db_select('file_usage', 'f')
    ->fields('f', array('module', 'type', 'id', 'count'))
    ->condition('fid', $file->fid)
    ->condition('count', 0, '>')
    ->execute();
  $references = array();
  foreach ($result as $usage) {
627
    $references[$usage->module][$usage->type][$usage->id] = $usage->count;
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
  }
  return $references;
}

/**
 * Records that a module is using a file.
 *
 * This usage information will be queried during file_delete() to ensure that
 * a file is not in use before it is physically removed from disk.
 *
 * Examples:
 * - A module that associates files with nodes, so $type would be
 *   'node' and $id would be the node's nid. Files for all revisions are stored
 *   within a single nid.
 * - The User module associates an image with a user, so $type would be 'user'
 *   and the $id would be the user's uid.
 *
 * @param $file
 *   A file object.
 * @param $module
 *   The name of the module using the file.
 * @param $type
650
 *   The type of the object that contains the referenced file.
651
 * @param $id
652
 *   The unique, numeric ID of the object containing the referenced file.
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
 * @param $count
 *   (optional) The number of references to add to the object. Defaults to 1.
 *
 * @see file_usage_list()
 * @see file_usage_delete()
 */
function file_usage_add(stdClass $file, $module, $type, $id, $count = 1) {
  db_merge('file_usage')
    ->key(array(
      'fid' => $file->fid,
      'module' => $module,
      'type' => $type,
      'id' => $id,
    ))
    ->fields(array('count' => $count))
    ->expression('count', 'count + :count', array(':count' => $count))
    ->execute();
}

/**
 * Removes a record to indicate that a module is no longer using a file.
 *
 * The file_delete() function is typically called after removing a file usage
 * to remove the record from the file_managed table and delete the file itself.
 *
 * @param $file
 *   A file object.
 * @param $module
 *   The name of the module using the file.
 * @param $type
 *   (optional) The type of the object that contains the referenced file. May
 *   be omitted if all module references to a file are being deleted.
 * @param $id
 *   (optional) The unique, numeric ID of the object containing the referenced
 *   file. May be omitted if all module references to a file are being deleted.
 * @param $count
 *   (optional) The number of references to delete from the object. Defaults to
 *   1. 0 may be specified to delete all references to the file within a
 *   specific object.
 *
 * @see file_usage_add()
 * @see file_usage_list()
 * @see file_delete()
 */
function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $count = 1) {
  // Delete rows that have a exact or less value to prevent empty rows.
  $query = db_delete('file_usage')
    ->condition('module', $module)
    ->condition('fid', $file->fid);
  if ($type && $id) {
    $query
      ->condition('type', $type)
      ->condition('id', $id);
  }
  if ($count) {
    $query->condition('count', $count, '<=');
  }
  $result = $query->execute();

  // If the row has more than the specified count decrement it by that number.
713
  if (!$result && $count > 0) {
714 715 716 717 718 719 720 721
    $query = db_update('file_usage')
      ->condition('module', $module)
      ->condition('fid', $file->fid);
    if ($type && $id) {
      $query
        ->condition('type', $type)
        ->condition('id', $id);
    }
722
    $query->expression('count', 'count - :count', array(':count' => $count));
723 724 725 726 727 728
    $query->execute();
  }
}

/**
 * Copies a file to a new location and adds a file record to the database.
729 730 731 732 733 734 735 736 737 738
 *
 * This function should be used when manipulating files that have records
 * stored in the database. This is a powerful function that in many ways
 * performs like an advanced version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database. If the source file is a
739 740
 *   temporary file, the resulting file will also be a temporary file. See
 *   file_save_upload() for details on temporary files.
741 742 743 744
 *
 * @param $source
 *   A file object.
 * @param $destination
745
 *   A string containing the destination that $source should be copied to.
746
 *   This must be a stream wrapper URI.
747 748
 * @param $replace
 *   Replace behavior when the destination file already exists:
749 750 751
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated. If
 *       no database entry is found then a new one will be created.
752
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
753
 *       unique.
754
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
755
 *
756 757
 * @return
 *   File object if the copy is successful, or FALSE in the event of an error.
758
 *
759 760 761
 * @see file_unmanaged_copy()
 * @see hook_file_copy()
 */
762
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
763
  if (!file_valid_uri($destination)) {
764
    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
765
      watchdog('file', 'File %file (%realpath) could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
766 767
    }
    else {
768
      watchdog('file', 'File %file could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
769
    }
770
    drupal_set_message(t('The specified file %file could not be copied because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
771 772 773
    return FALSE;
  }

774
  if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
775
    $file = clone $source;
776
    $file->fid = NULL;
777
    $file->uri = $uri;
778
    $file->filename = drupal_basename($uri);
779 780
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
781
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
782 783 784 785 786
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $file->fid = $existing->fid;
        $file->filename = $existing->filename;
      }
787
    }
788 789
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
790
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
791
      $file->filename = drupal_basename($destination);
792 793 794 795 796 797 798 799
    }

    $file = file_save($file);

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

    return $file;
800 801 802 803
  }
  return FALSE;
}

804
/**
805
 * Determines whether the URI has a valid scheme for file API operations.
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
 *
 * 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);
  if (empty($uri_scheme) || !file_stream_wrapper_valid_scheme($uri_scheme)) {
    return FALSE;
  }
  return TRUE;
}

826
/**
827
 * Copies a file to a new location without invoking the file API.
Dries's avatar
 
Dries committed
828
 *
829 830 831 832 833 834 835
 * This is a powerful function that in many ways performs like an advanced
 * version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
836 837 838 839
 * - 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
840 841
 *
 * @param $source
842
 *   A string specifying the filepath or URI of the source file.
843
 * @param $destination
844 845 846 847
 *   A URI containing the destination that $source should be copied to. The
 *   URI may be a bare filepath (without a scheme) and in that case the default
 *   scheme (file://) will be used. If this value is omitted, Drupal's default
 *   files scheme will be used, usually "public://".
848 849 850 851
 * @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
852
 *       unique.
853
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
854
 *
855 856
 * @return
 *   The path to the new file, or FALSE in the event of an error.
857
 *
858
 * @see file_copy()
Dries's avatar
 
Dries committed
859
 */
860
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
861 862 863
  $original_source = $source;
  $original_destination = $destination;

864
  // Assert that the source file actually exists.
865
  if (!file_exists($source)) {
866
    // @todo Replace drupal_set_message() calls with exceptions instead.
867
    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');
868 869 870 871 872 873
    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));
    }
874 875
    return FALSE;
  }
Dries's avatar
 
Dries committed
876

877 878
  // Build a destination URI if necessary.
  if (!isset($destination)) {
879
    $destination = file_build_uri(drupal_basename($source));
880
  }
Dries's avatar
 
Dries committed
881 882


883 884 885
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
886
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
887 888 889 890 891 892
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
893 894
      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');
895 896 897
      return FALSE;
    }
  }
898

899 900
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
901
  if ($destination === FALSE) {
902
    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');
903
    watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%destination' => $destination));
904
    return FALSE;
Dries's avatar
 
Dries committed
905
  }
906 907

  // Assert that the source and destination filenames are not the same.
908 909 910
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
911
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
912
    watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
913
    return FALSE;
Dries's avatar
 
Dries committed
914
  }
915 916 917
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
918
  if (!@copy($source, $destination)) {
919 920 921 922 923 924
    // 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
925
  }
Dries's avatar
 
Dries committed
926

927 928
  // Set the permissions on the new file.
  drupal_chmod($destination);
929 930

  return $destination;
Dries's avatar
 
Dries committed
931 932
}

933
/**
934
 * Constructs a URI to Drupal's default files location given a relative path.
935 936
 */
function file_build_uri($path) {
937
  $uri = file_default_scheme() . '://' . $path;
938 939 940
  return file_stream_wrapper_uri_normalize($uri);
}

941
/**
942
 * Determines the destination path for a file.
943
 *
944
 * @param $destination
945
 *   A string specifying the desired final URI or filepath.
946 947
 * @param $replace
 *   Replace behavior when the destination file already exists.
948
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
949
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
950
 *       unique.
951
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
952
 *
953
 * @return
954 955
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
956 957 958 959
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
960 961 962 963
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

964
      case FILE_EXISTS_RENAME:
965
        $basename = drupal_basename($destination);
966
        $directory = drupal_dirname($destination);
967 968 969 970
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
971
        // Error reporting handled by calling function.
972 973 974 975 976 977
        return FALSE;
    }
  }
  return $destination;
}

Dries's avatar
 
Dries committed
978
/**
979
 * Moves a file to a new location and update the file's database entry.
980 981 982 983 984 985 986 987 988 989 990 991
 *
 * Moving a file is performed by copying the file to the new location and then
 * deleting the original.
 * - Checks if $source and $destination are valid and readable/writable.
 * - Performs a file move if $source is not equal to $destination.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database.
 *
 * @param $source
 *   A file object.
 * @param $destination
992
 *   A string containing the destination that $source should be moved to.
993
 *   This must be a stream wrapper URI.
994 995
 * @param $replace
 *   Replace behavior when the destination file already exists:
996 997 998 999 1000
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated and
 *       file_delete() called on the source file after hook_file_move is called.
 *       If no database entry is found then the source files record will be
 *       updated.
1001
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
1002
 *       unique.
1003
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
1004
 *
1005 1006
 * @return
 *   Resulting file object for success, or FALSE in the event of an error.
1007
 *
1008 1009 1010
 * @see file_unmanaged_move()
 * @see hook_file_move()
 */
1011
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
1012
  if (!file_valid_uri($destination)) {
1013
    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
1014
      watchdog('file', 'File %file (%realpath) could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
1015 1016
    }
    else {
1017
      watchdog('file', 'File %file could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
1018
    }
1019
    drupal_set_message(t('The specified file %file could not be moved because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
1020 1021 1022
    return FALSE;
  }

1023
  if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
1024 1025
    $delete_source = FALSE;

1026
    $file = clone $source;
1027
    $file->uri = $uri;
1028 1029
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
1030
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
1031 1032 1033 1034 1035 1036 1037 1038
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $delete_source = TRUE;
        $file->fid = $existing->fid;
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
1039
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
1040
      $file->filename = drupal_basename($destination);
1041
    }
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053

    $file = file_save($file);

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

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

    return $file;