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

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

8
use Drupal\Component\Utility\UrlHelper;
9
use Drupal\Core\StreamWrapper\StreamWrapperManager;
10

Kjartan's avatar
Kjartan committed
11
/**
Kjartan's avatar
Kjartan committed
12
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
13
 * @{
Dries's avatar
 
Dries committed
14
 * Common file handling functions.
Dries's avatar
 
Dries committed
15 16
 */

17
/**
18 19
 * Indicates that the file is permanent and should not be deleted.
 *
20 21 22 23
 * Temporary files older than the system.file.temporary_maximum_age
 * configuration value will be, if clean-up not disabled, removed during cron
 * runs, but permanent files will not be removed during the file garbage
 * collection process.
24
 */
25
const FILE_STATUS_PERMANENT = 1;
26

Dries's avatar
 
Dries committed
27
/**
28
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
 
Dries committed
29
 *
30
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
 
Dries committed
31
 *
32
 * There are two kinds of local files:
33 34 35
 * - "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).
36 37 38
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
39
 * @param string $uri
40 41
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
42
 *
43
 * @return string
44
 *   A string containing a URL that may be used to access the file.
45 46 47
 *   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.
48
 *
49
 * @see https://www.drupal.org/node/515192
50
 * @see file_url_transform_relative()
Dries's avatar
 
Dries committed
51
 */
52
function file_create_url($uri) {
53 54
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
55
  \Drupal::moduleHandler()->alter('file_url', $uri);
56

57
  $scheme = StreamWrapperManager::getScheme($uri);
58 59

  if (!$scheme) {
60 61 62 63 64 65 66
    // 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.
67
    if (mb_substr($uri, 0, 1) == '/') {
68 69 70 71
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
72
      // Therefore, return the urlencoded URI with the base URL prepended.
73 74 75 76 77 78 79 80 81 82 83 84 85
      $options = UrlHelper::parse($uri);
      $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
      // Append the query.
      if ($options['query']) {
        $path .= '?' . UrlHelper::buildQuery($options['query']);
      }

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

      return $path;
86
    }
87
  }
88 89 90
  elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
    // Check for HTTP and data URI-encoded URLs so that we don't have to
    // implement getExternalUrl() for the HTTP and data schemes.
91 92 93 94
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
95
    if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
96 97 98 99 100 101
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries's avatar
 
Dries committed
102 103
}

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
/**
 * Transforms an absolute URL of a local file to a relative URL.
 *
 * May be useful to prevent problems on multisite set-ups and prevent mixed
 * content errors when using HTTPS + HTTP.
 *
 * @param string $file_url
 *   A file URL of a local file as generated by file_create_url().
 *
 * @return string
 *   If the file URL indeed pointed to a local file and was indeed absolute,
 *   then the transformed, relative URL to the local file. Otherwise: the
 *   original value of $file_url.
 *
 * @see file_create_url()
 */
function file_url_transform_relative($file_url) {
  // Unfortunately, we pretty much have to duplicate Symfony's
  // Request::getHttpHost() method because Request::getPort() may return NULL
  // instead of a port number.
  $request = \Drupal::request();
  $host = $request->getHost();
  $scheme = $request->getScheme();
  $port = $request->getPort() ?: 80;
  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
    $http_host = $host;
  }
  else {
    $http_host = $host . ':' . $port;
  }

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

138
/**
139
 * Constructs a URI to Drupal's default files location given a relative path.
140 141
 */
function file_build_uri($path) {
142
  $uri = \Drupal::config('system.file')->get('default_scheme') . '://' . $path;
143 144 145
  /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  return $stream_wrapper_manager->normalizeUri($uri);
146 147
}

148
/**
149
 * Modifies a filename as needed for security purposes.
150
 *
151 152 153 154 155 156 157 158 159 160 161
 * Munging a file name prevents unknown file extensions from masking exploit
 * files. When web servers such as Apache decide how to process a URL request,
 * they use the file extension. If the extension is not recognized, Apache
 * skips that extension and uses the previous file extension. For example, if
 * the file being requested is exploit.php.pps, and Apache does not recognize
 * the '.pps' extension, it treats the file as PHP and executes it. To make
 * this file name safe for Apache and prevent it from executing as PHP, the
 * .php extension is "munged" into .php_, making the safe file name
 * exploit.php_.pps.
 *
 * Specifically, this function adds an underscore to all extensions that are
162 163
 * between 2 and 5 characters in length, internal to the file name, and either
 * included in the list of unsafe extensions, or not included in $extensions.
164
 *
165 166 167
 * Function behavior is also controlled by the configuration
 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
168
 * @param $filename
169
 *   File name to modify.
170
 * @param $extensions
171 172
 *   A space-separated list of extensions that should not be altered. Note that
 *   extensions that are unsafe will be altered regardless of this parameter.
173
 * @param $alerts
174 175
 *   If TRUE, \Drupal::messenger()->addStatus() will be called to display
 *   a message if the file name was changed.
176
 *
177
 * @return string
178
 *   The potentially modified $filename.
179 180 181 182 183
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

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

189
    $allowed_extensions = array_unique(explode(' ', strtolower(trim($extensions))));
190

191 192 193 194 195 196
    // Remove unsafe extensions from the allowed list of extensions.
    // @todo https://www.drupal.org/project/drupal/issues/3032390 Make the list
    //   of unsafe extensions a constant. The list is copied from
    //   FILE_INSECURE_EXTENSION_REGEX.
    $allowed_extensions = array_diff($allowed_extensions, explode('|', 'phar|php|pl|py|cgi|asp|js'));

197 198 199
    // Split the filename up by periods. The first part becomes the basename
    // the last part the final extension.
    $filename_parts = explode('.', $filename);
200 201 202 203
    // Remove file basename.
    $new_filename = array_shift($filename_parts);
    // Remove final extension.
    $final_extension = array_pop($filename_parts);
204 205 206 207 208

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
209
      $new_filename .= '.' . $filename_part;
210
      if (!in_array(strtolower($filename_part), $allowed_extensions) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
211 212 213
        $new_filename .= '_';
      }
    }
214
    $filename = $new_filename . '.' . $final_extension;
215 216

    if ($alerts && $original != $filename) {
217
      \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $filename]));
218 219 220 221 222 223 224
    }
  }

  return $filename;
}

/**
225
 * Undoes the effect of file_munge_filename().
226
 *
227 228
 * @param $filename
 *   String with the filename to be unmunged.
229
 *
230 231
 * @return
 *   An unmunged filename string.
232 233 234 235 236
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

237 238
/**
 * @} End of "defgroup file".
239
 */