PageRenderTime 47ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/core/includes/file.inc

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