Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\File;
|
Chris@0
|
4
|
Chris@18
|
5 use Drupal\Component\Utility\Unicode;
|
Chris@18
|
6 use Drupal\Core\File\Exception\DirectoryNotReadyException;
|
Chris@18
|
7 use Drupal\Core\File\Exception\FileException;
|
Chris@18
|
8 use Drupal\Core\File\Exception\FileExistsException;
|
Chris@18
|
9 use Drupal\Core\File\Exception\FileNotExistsException;
|
Chris@18
|
10 use Drupal\Core\File\Exception\FileWriteException;
|
Chris@18
|
11 use Drupal\Core\File\Exception\NotRegularFileException;
|
Chris@0
|
12 use Drupal\Core\Site\Settings;
|
Chris@0
|
13 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
|
Chris@0
|
14 use Psr\Log\LoggerInterface;
|
Chris@0
|
15
|
Chris@0
|
16 /**
|
Chris@0
|
17 * Provides helpers to operate on files and stream wrappers.
|
Chris@0
|
18 */
|
Chris@0
|
19 class FileSystem implements FileSystemInterface {
|
Chris@0
|
20
|
Chris@0
|
21 /**
|
Chris@0
|
22 * Default mode for new directories. See self::chmod().
|
Chris@0
|
23 */
|
Chris@0
|
24 const CHMOD_DIRECTORY = 0775;
|
Chris@0
|
25
|
Chris@0
|
26 /**
|
Chris@0
|
27 * Default mode for new files. See self::chmod().
|
Chris@0
|
28 */
|
Chris@0
|
29 const CHMOD_FILE = 0664;
|
Chris@0
|
30
|
Chris@0
|
31 /**
|
Chris@0
|
32 * The site settings.
|
Chris@0
|
33 *
|
Chris@0
|
34 * @var \Drupal\Core\Site\Settings
|
Chris@0
|
35 */
|
Chris@0
|
36 protected $settings;
|
Chris@0
|
37
|
Chris@0
|
38 /**
|
Chris@0
|
39 * The file logger channel.
|
Chris@0
|
40 *
|
Chris@0
|
41 * @var \Psr\Log\LoggerInterface
|
Chris@0
|
42 */
|
Chris@0
|
43 protected $logger;
|
Chris@0
|
44
|
Chris@0
|
45 /**
|
Chris@0
|
46 * The stream wrapper manager.
|
Chris@0
|
47 *
|
Chris@0
|
48 * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
|
Chris@0
|
49 */
|
Chris@0
|
50 protected $streamWrapperManager;
|
Chris@0
|
51
|
Chris@0
|
52 /**
|
Chris@0
|
53 * Constructs a new FileSystem.
|
Chris@0
|
54 *
|
Chris@0
|
55 * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
|
Chris@0
|
56 * The stream wrapper manager.
|
Chris@0
|
57 * @param \Drupal\Core\Site\Settings $settings
|
Chris@0
|
58 * The site settings.
|
Chris@0
|
59 * @param \Psr\Log\LoggerInterface $logger
|
Chris@0
|
60 * The file logger channel.
|
Chris@0
|
61 */
|
Chris@0
|
62 public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, Settings $settings, LoggerInterface $logger) {
|
Chris@0
|
63 $this->streamWrapperManager = $stream_wrapper_manager;
|
Chris@0
|
64 $this->settings = $settings;
|
Chris@0
|
65 $this->logger = $logger;
|
Chris@0
|
66 }
|
Chris@0
|
67
|
Chris@0
|
68 /**
|
Chris@0
|
69 * {@inheritdoc}
|
Chris@0
|
70 */
|
Chris@0
|
71 public function moveUploadedFile($filename, $uri) {
|
Chris@0
|
72 $result = @move_uploaded_file($filename, $uri);
|
Chris@0
|
73 // PHP's move_uploaded_file() does not properly support streams if
|
Chris@0
|
74 // open_basedir is enabled so if the move failed, try finding a real path
|
Chris@0
|
75 // and retry the move operation.
|
Chris@0
|
76 if (!$result) {
|
Chris@0
|
77 if ($realpath = $this->realpath($uri)) {
|
Chris@0
|
78 $result = move_uploaded_file($filename, $realpath);
|
Chris@0
|
79 }
|
Chris@0
|
80 else {
|
Chris@0
|
81 $result = move_uploaded_file($filename, $uri);
|
Chris@0
|
82 }
|
Chris@0
|
83 }
|
Chris@0
|
84
|
Chris@0
|
85 return $result;
|
Chris@0
|
86 }
|
Chris@0
|
87
|
Chris@0
|
88 /**
|
Chris@0
|
89 * {@inheritdoc}
|
Chris@0
|
90 */
|
Chris@0
|
91 public function chmod($uri, $mode = NULL) {
|
Chris@0
|
92 if (!isset($mode)) {
|
Chris@0
|
93 if (is_dir($uri)) {
|
Chris@0
|
94 $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
|
Chris@0
|
95 }
|
Chris@0
|
96 else {
|
Chris@0
|
97 $mode = $this->settings->get('file_chmod_file', static::CHMOD_FILE);
|
Chris@0
|
98 }
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 if (@chmod($uri, $mode)) {
|
Chris@0
|
102 return TRUE;
|
Chris@0
|
103 }
|
Chris@0
|
104
|
Chris@0
|
105 $this->logger->error('The file permissions could not be set on %uri.', ['%uri' => $uri]);
|
Chris@0
|
106 return FALSE;
|
Chris@0
|
107 }
|
Chris@0
|
108
|
Chris@0
|
109 /**
|
Chris@0
|
110 * {@inheritdoc}
|
Chris@0
|
111 */
|
Chris@0
|
112 public function unlink($uri, $context = NULL) {
|
Chris@0
|
113 $scheme = $this->uriScheme($uri);
|
Chris@0
|
114 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
|
Chris@0
|
115 chmod($uri, 0600);
|
Chris@0
|
116 }
|
Chris@0
|
117 if ($context) {
|
Chris@0
|
118 return unlink($uri, $context);
|
Chris@0
|
119 }
|
Chris@0
|
120 else {
|
Chris@0
|
121 return unlink($uri);
|
Chris@0
|
122 }
|
Chris@0
|
123 }
|
Chris@0
|
124
|
Chris@0
|
125 /**
|
Chris@0
|
126 * {@inheritdoc}
|
Chris@0
|
127 */
|
Chris@0
|
128 public function realpath($uri) {
|
Chris@0
|
129 // If this URI is a stream, pass it off to the appropriate stream wrapper.
|
Chris@0
|
130 // Otherwise, attempt PHP's realpath. This allows use of this method even
|
Chris@0
|
131 // for unmanaged files outside of the stream wrapper interface.
|
Chris@0
|
132 if ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
|
Chris@0
|
133 return $wrapper->realpath();
|
Chris@0
|
134 }
|
Chris@0
|
135
|
Chris@0
|
136 return realpath($uri);
|
Chris@0
|
137 }
|
Chris@0
|
138
|
Chris@0
|
139 /**
|
Chris@0
|
140 * {@inheritdoc}
|
Chris@0
|
141 */
|
Chris@0
|
142 public function dirname($uri) {
|
Chris@0
|
143 $scheme = $this->uriScheme($uri);
|
Chris@0
|
144
|
Chris@0
|
145 if ($this->validScheme($scheme)) {
|
Chris@0
|
146 return $this->streamWrapperManager->getViaScheme($scheme)->dirname($uri);
|
Chris@0
|
147 }
|
Chris@0
|
148 else {
|
Chris@0
|
149 return dirname($uri);
|
Chris@0
|
150 }
|
Chris@0
|
151 }
|
Chris@0
|
152
|
Chris@0
|
153 /**
|
Chris@0
|
154 * {@inheritdoc}
|
Chris@0
|
155 */
|
Chris@0
|
156 public function basename($uri, $suffix = NULL) {
|
Chris@0
|
157 $separators = '/';
|
Chris@0
|
158 if (DIRECTORY_SEPARATOR != '/') {
|
Chris@0
|
159 // For Windows OS add special separator.
|
Chris@0
|
160 $separators .= DIRECTORY_SEPARATOR;
|
Chris@0
|
161 }
|
Chris@0
|
162 // Remove right-most slashes when $uri points to directory.
|
Chris@0
|
163 $uri = rtrim($uri, $separators);
|
Chris@0
|
164 // Returns the trailing part of the $uri starting after one of the directory
|
Chris@0
|
165 // separators.
|
Chris@0
|
166 $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
|
Chris@0
|
167 // Cuts off a suffix from the filename.
|
Chris@0
|
168 if ($suffix) {
|
Chris@0
|
169 $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
|
Chris@0
|
170 }
|
Chris@0
|
171 return $filename;
|
Chris@0
|
172 }
|
Chris@0
|
173
|
Chris@0
|
174 /**
|
Chris@0
|
175 * {@inheritdoc}
|
Chris@0
|
176 */
|
Chris@0
|
177 public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
|
Chris@0
|
178 if (!isset($mode)) {
|
Chris@0
|
179 $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
|
Chris@0
|
180 }
|
Chris@0
|
181
|
Chris@0
|
182 // If the URI has a scheme, don't override the umask - schemes can handle
|
Chris@0
|
183 // this issue in their own implementation.
|
Chris@0
|
184 if ($this->uriScheme($uri)) {
|
Chris@0
|
185 return $this->mkdirCall($uri, $mode, $recursive, $context);
|
Chris@0
|
186 }
|
Chris@0
|
187
|
Chris@0
|
188 // If recursive, create each missing component of the parent directory
|
Chris@0
|
189 // individually and set the mode explicitly to override the umask.
|
Chris@0
|
190 if ($recursive) {
|
Chris@0
|
191 // Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing
|
Chris@0
|
192 // slashes because they can throw off the loop when creating the parent
|
Chris@0
|
193 // directories.
|
Chris@0
|
194 $uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR);
|
Chris@0
|
195 // Determine the components of the path.
|
Chris@0
|
196 $components = explode(DIRECTORY_SEPARATOR, $uri);
|
Chris@0
|
197 // If the filepath is absolute the first component will be empty as there
|
Chris@0
|
198 // will be nothing before the first slash.
|
Chris@0
|
199 if ($components[0] == '') {
|
Chris@0
|
200 $recursive_path = DIRECTORY_SEPARATOR;
|
Chris@0
|
201 // Get rid of the empty first component.
|
Chris@0
|
202 array_shift($components);
|
Chris@0
|
203 }
|
Chris@0
|
204 else {
|
Chris@0
|
205 $recursive_path = '';
|
Chris@0
|
206 }
|
Chris@0
|
207 // Don't handle the top-level directory in this loop.
|
Chris@0
|
208 array_pop($components);
|
Chris@0
|
209 // Create each component if necessary.
|
Chris@0
|
210 foreach ($components as $component) {
|
Chris@0
|
211 $recursive_path .= $component;
|
Chris@0
|
212
|
Chris@0
|
213 if (!file_exists($recursive_path)) {
|
Chris@0
|
214 if (!$this->mkdirCall($recursive_path, $mode, FALSE, $context)) {
|
Chris@0
|
215 return FALSE;
|
Chris@0
|
216 }
|
Chris@0
|
217 // Not necessary to use self::chmod() as there is no scheme.
|
Chris@0
|
218 if (!chmod($recursive_path, $mode)) {
|
Chris@0
|
219 return FALSE;
|
Chris@0
|
220 }
|
Chris@0
|
221 }
|
Chris@0
|
222
|
Chris@0
|
223 $recursive_path .= DIRECTORY_SEPARATOR;
|
Chris@0
|
224 }
|
Chris@0
|
225 }
|
Chris@0
|
226
|
Chris@0
|
227 // Do not check if the top-level directory already exists, as this condition
|
Chris@0
|
228 // must cause this function to fail.
|
Chris@0
|
229 if (!$this->mkdirCall($uri, $mode, FALSE, $context)) {
|
Chris@0
|
230 return FALSE;
|
Chris@0
|
231 }
|
Chris@0
|
232 // Not necessary to use self::chmod() as there is no scheme.
|
Chris@0
|
233 return chmod($uri, $mode);
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 /**
|
Chris@0
|
237 * Helper function. Ensures we don't pass a NULL as a context resource to
|
Chris@0
|
238 * mkdir().
|
Chris@0
|
239 *
|
Chris@0
|
240 * @see self::mkdir()
|
Chris@0
|
241 */
|
Chris@0
|
242 protected function mkdirCall($uri, $mode, $recursive, $context) {
|
Chris@0
|
243 if (is_null($context)) {
|
Chris@0
|
244 return mkdir($uri, $mode, $recursive);
|
Chris@0
|
245 }
|
Chris@0
|
246 else {
|
Chris@0
|
247 return mkdir($uri, $mode, $recursive, $context);
|
Chris@0
|
248 }
|
Chris@0
|
249 }
|
Chris@0
|
250
|
Chris@0
|
251 /**
|
Chris@0
|
252 * {@inheritdoc}
|
Chris@0
|
253 */
|
Chris@0
|
254 public function rmdir($uri, $context = NULL) {
|
Chris@0
|
255 $scheme = $this->uriScheme($uri);
|
Chris@0
|
256 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
|
Chris@0
|
257 chmod($uri, 0700);
|
Chris@0
|
258 }
|
Chris@0
|
259 if ($context) {
|
Chris@0
|
260 return rmdir($uri, $context);
|
Chris@0
|
261 }
|
Chris@0
|
262 else {
|
Chris@0
|
263 return rmdir($uri);
|
Chris@0
|
264 }
|
Chris@0
|
265 }
|
Chris@0
|
266
|
Chris@0
|
267 /**
|
Chris@0
|
268 * {@inheritdoc}
|
Chris@0
|
269 */
|
Chris@0
|
270 public function tempnam($directory, $prefix) {
|
Chris@0
|
271 $scheme = $this->uriScheme($directory);
|
Chris@0
|
272
|
Chris@0
|
273 if ($this->validScheme($scheme)) {
|
Chris@0
|
274 $wrapper = $this->streamWrapperManager->getViaScheme($scheme);
|
Chris@0
|
275
|
Chris@0
|
276 if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
|
Chris@0
|
277 return $scheme . '://' . static::basename($filename);
|
Chris@0
|
278 }
|
Chris@0
|
279 else {
|
Chris@0
|
280 return FALSE;
|
Chris@0
|
281 }
|
Chris@0
|
282 }
|
Chris@0
|
283 else {
|
Chris@0
|
284 // Handle as a normal tempnam() call.
|
Chris@0
|
285 return tempnam($directory, $prefix);
|
Chris@0
|
286 }
|
Chris@0
|
287 }
|
Chris@0
|
288
|
Chris@0
|
289 /**
|
Chris@0
|
290 * {@inheritdoc}
|
Chris@0
|
291 */
|
Chris@0
|
292 public function uriScheme($uri) {
|
Chris@0
|
293 if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) {
|
Chris@0
|
294 // The scheme will always be the last element in the matches array.
|
Chris@0
|
295 return array_pop($matches);
|
Chris@0
|
296 }
|
Chris@0
|
297
|
Chris@0
|
298 return FALSE;
|
Chris@0
|
299 }
|
Chris@0
|
300
|
Chris@0
|
301 /**
|
Chris@0
|
302 * {@inheritdoc}
|
Chris@0
|
303 */
|
Chris@0
|
304 public function validScheme($scheme) {
|
Chris@0
|
305 if (!$scheme) {
|
Chris@0
|
306 return FALSE;
|
Chris@0
|
307 }
|
Chris@0
|
308 return class_exists($this->streamWrapperManager->getClass($scheme));
|
Chris@0
|
309 }
|
Chris@0
|
310
|
Chris@18
|
311 /**
|
Chris@18
|
312 * {@inheritdoc}
|
Chris@18
|
313 */
|
Chris@18
|
314 public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
|
Chris@18
|
315 $this->prepareDestination($source, $destination, $replace);
|
Chris@18
|
316
|
Chris@18
|
317 // Perform the copy operation.
|
Chris@18
|
318 if (!@copy($source, $destination)) {
|
Chris@18
|
319 // If the copy failed and realpaths exist, retry the operation using them
|
Chris@18
|
320 // instead.
|
Chris@18
|
321 $real_source = $this->realpath($source) ?: $source;
|
Chris@18
|
322 $real_destination = $this->realpath($destination) ?: $destination;
|
Chris@18
|
323 if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
|
Chris@18
|
324 $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [
|
Chris@18
|
325 '%source' => $source,
|
Chris@18
|
326 '%destination' => $destination,
|
Chris@18
|
327 ]);
|
Chris@18
|
328 throw new FileWriteException("The specified file '$source' could not be copied to '$destination'.");
|
Chris@18
|
329 }
|
Chris@18
|
330 }
|
Chris@18
|
331
|
Chris@18
|
332 // Set the permissions on the new file.
|
Chris@18
|
333 $this->chmod($destination);
|
Chris@18
|
334
|
Chris@18
|
335 return $destination;
|
Chris@18
|
336 }
|
Chris@18
|
337
|
Chris@18
|
338 /**
|
Chris@18
|
339 * {@inheritdoc}
|
Chris@18
|
340 */
|
Chris@18
|
341 public function delete($path) {
|
Chris@18
|
342 if (is_file($path)) {
|
Chris@18
|
343 if (!$this->unlink($path)) {
|
Chris@18
|
344 $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]);
|
Chris@18
|
345 throw new FileException("Failed to unlink file '$path'.");
|
Chris@18
|
346 }
|
Chris@18
|
347 return TRUE;
|
Chris@18
|
348 }
|
Chris@18
|
349
|
Chris@18
|
350 if (is_dir($path)) {
|
Chris@18
|
351 $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]);
|
Chris@18
|
352 throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead.");
|
Chris@18
|
353 }
|
Chris@18
|
354
|
Chris@18
|
355 // Return TRUE for non-existent file, but log that nothing was actually
|
Chris@18
|
356 // deleted, as the current state is the intended result.
|
Chris@18
|
357 if (!file_exists($path)) {
|
Chris@18
|
358 $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
|
Chris@18
|
359 return TRUE;
|
Chris@18
|
360 }
|
Chris@18
|
361
|
Chris@18
|
362 // We cannot handle anything other than files and directories.
|
Chris@18
|
363 // Throw an exception for everything else (sockets, symbolic links, etc).
|
Chris@18
|
364 $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]);
|
Chris@18
|
365 throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted.");
|
Chris@18
|
366 }
|
Chris@18
|
367
|
Chris@18
|
368 /**
|
Chris@18
|
369 * {@inheritdoc}
|
Chris@18
|
370 */
|
Chris@18
|
371 public function deleteRecursive($path, callable $callback = NULL) {
|
Chris@18
|
372 if ($callback) {
|
Chris@18
|
373 call_user_func($callback, $path);
|
Chris@18
|
374 }
|
Chris@18
|
375
|
Chris@18
|
376 if (is_dir($path)) {
|
Chris@18
|
377 $dir = dir($path);
|
Chris@18
|
378 while (($entry = $dir->read()) !== FALSE) {
|
Chris@18
|
379 if ($entry == '.' || $entry == '..') {
|
Chris@18
|
380 continue;
|
Chris@18
|
381 }
|
Chris@18
|
382 $entry_path = $path . '/' . $entry;
|
Chris@18
|
383 $this->deleteRecursive($entry_path, $callback);
|
Chris@18
|
384 }
|
Chris@18
|
385 $dir->close();
|
Chris@18
|
386
|
Chris@18
|
387 return $this->rmdir($path);
|
Chris@18
|
388 }
|
Chris@18
|
389
|
Chris@18
|
390 return $this->delete($path);
|
Chris@18
|
391 }
|
Chris@18
|
392
|
Chris@18
|
393 /**
|
Chris@18
|
394 * {@inheritdoc}
|
Chris@18
|
395 */
|
Chris@18
|
396 public function move($source, $destination, $replace = self::EXISTS_RENAME) {
|
Chris@18
|
397 $this->prepareDestination($source, $destination, $replace);
|
Chris@18
|
398
|
Chris@18
|
399 // Ensure compatibility with Windows.
|
Chris@18
|
400 // @see \Drupal\Core\File\FileSystemInterface::unlink().
|
Chris@18
|
401 $scheme = $this->uriScheme($source);
|
Chris@18
|
402 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
|
Chris@18
|
403 chmod($source, 0600);
|
Chris@18
|
404 }
|
Chris@18
|
405 // Attempt to resolve the URIs. This is necessary in certain
|
Chris@18
|
406 // configurations (see above) and can also permit fast moves across local
|
Chris@18
|
407 // schemes.
|
Chris@18
|
408 $real_source = $this->realpath($source) ?: $source;
|
Chris@18
|
409 $real_destination = $this->realpath($destination) ?: $destination;
|
Chris@18
|
410 // Perform the move operation.
|
Chris@18
|
411 if (!@rename($real_source, $real_destination)) {
|
Chris@18
|
412 // Fall back to slow copy and unlink procedure. This is necessary for
|
Chris@18
|
413 // renames across schemes that are not local, or where rename() has not
|
Chris@18
|
414 // been implemented. It's not necessary to use drupal_unlink() as the
|
Chris@18
|
415 // Windows issue has already been resolved above.
|
Chris@18
|
416 if (!@copy($real_source, $real_destination)) {
|
Chris@18
|
417 $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [
|
Chris@18
|
418 '%source' => $source,
|
Chris@18
|
419 '%destination' => $destination,
|
Chris@18
|
420 ]);
|
Chris@18
|
421 throw new FileWriteException("The specified file '$source' could not be moved to '$destination'.");
|
Chris@18
|
422 }
|
Chris@18
|
423 if (!@unlink($real_source)) {
|
Chris@18
|
424 $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
|
Chris@18
|
425 '%source' => $source,
|
Chris@18
|
426 '%destination' => $destination,
|
Chris@18
|
427 ]);
|
Chris@18
|
428 throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'.");
|
Chris@18
|
429 }
|
Chris@18
|
430 }
|
Chris@18
|
431
|
Chris@18
|
432 // Set the permissions on the new file.
|
Chris@18
|
433 $this->chmod($destination);
|
Chris@18
|
434
|
Chris@18
|
435 return $destination;
|
Chris@18
|
436 }
|
Chris@18
|
437
|
Chris@18
|
438 /**
|
Chris@18
|
439 * Prepares the destination for a file copy or move operation.
|
Chris@18
|
440 *
|
Chris@18
|
441 * - Checks if $source and $destination are valid and readable/writable.
|
Chris@18
|
442 * - Checks that $source is not equal to $destination; if they are an error
|
Chris@18
|
443 * is reported.
|
Chris@18
|
444 * - If file already exists in $destination either the call will error out,
|
Chris@18
|
445 * replace the file or rename the file based on the $replace parameter.
|
Chris@18
|
446 *
|
Chris@18
|
447 * @param string $source
|
Chris@18
|
448 * A string specifying the filepath or URI of the source file.
|
Chris@18
|
449 * @param string|null $destination
|
Chris@18
|
450 * A URI containing the destination that $source should be moved/copied to.
|
Chris@18
|
451 * The URI may be a bare filepath (without a scheme) and in that case the
|
Chris@18
|
452 * default scheme (file://) will be used.
|
Chris@18
|
453 * @param int $replace
|
Chris@18
|
454 * Replace behavior when the destination file already exists:
|
Chris@18
|
455 * - FILE_EXISTS_REPLACE - Replace the existing file.
|
Chris@18
|
456 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename
|
Chris@18
|
457 * is unique.
|
Chris@18
|
458 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
Chris@18
|
459 *
|
Chris@18
|
460 * @see \Drupal\Core\File\FileSystemInterface::copy()
|
Chris@18
|
461 * @see \Drupal\Core\File\FileSystemInterface::move()
|
Chris@18
|
462 */
|
Chris@18
|
463 protected function prepareDestination($source, &$destination, $replace) {
|
Chris@18
|
464 $original_source = $source;
|
Chris@18
|
465
|
Chris@18
|
466 // Assert that the source file actually exists.
|
Chris@18
|
467 if (!file_exists($source)) {
|
Chris@18
|
468 if (($realpath = $this->realpath($original_source)) !== FALSE) {
|
Chris@18
|
469 $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
|
Chris@18
|
470 '%original_source' => $original_source,
|
Chris@18
|
471 '%realpath' => $realpath,
|
Chris@18
|
472 ]);
|
Chris@18
|
473 throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist.");
|
Chris@18
|
474 }
|
Chris@18
|
475 else {
|
Chris@18
|
476 $this->logger->error("File '%original_source' could not be copied because it does not exist.", [
|
Chris@18
|
477 '%original_source' => $original_source,
|
Chris@18
|
478 ]);
|
Chris@18
|
479 throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist.");
|
Chris@18
|
480 }
|
Chris@18
|
481 }
|
Chris@18
|
482
|
Chris@18
|
483 // Prepare the destination directory.
|
Chris@18
|
484 if ($this->prepareDirectory($destination)) {
|
Chris@18
|
485 // The destination is already a directory, so append the source basename.
|
Chris@18
|
486 $destination = file_stream_wrapper_uri_normalize($destination . '/' . $this->basename($source));
|
Chris@18
|
487 }
|
Chris@18
|
488 else {
|
Chris@18
|
489 // Perhaps $destination is a dir/file?
|
Chris@18
|
490 $dirname = $this->dirname($destination);
|
Chris@18
|
491 if (!$this->prepareDirectory($dirname)) {
|
Chris@18
|
492 $this->logger->error("The specified file '%original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", [
|
Chris@18
|
493 '%original_source' => $original_source,
|
Chris@18
|
494 ]);
|
Chris@18
|
495 throw new DirectoryNotReadyException("The specified file '$original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.");
|
Chris@18
|
496 }
|
Chris@18
|
497 }
|
Chris@18
|
498
|
Chris@18
|
499 // Determine whether we can perform this operation based on overwrite rules.
|
Chris@18
|
500 $destination = $this->getDestinationFilename($destination, $replace);
|
Chris@18
|
501 if ($destination === FALSE) {
|
Chris@18
|
502 $this->logger->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [
|
Chris@18
|
503 '%original_source' => $original_source,
|
Chris@18
|
504 '%destination' => $destination,
|
Chris@18
|
505 ]);
|
Chris@18
|
506 throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination').");
|
Chris@18
|
507 }
|
Chris@18
|
508
|
Chris@18
|
509 // Assert that the source and destination filenames are not the same.
|
Chris@18
|
510 $real_source = $this->realpath($source);
|
Chris@18
|
511 $real_destination = $this->realpath($destination);
|
Chris@18
|
512 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
|
Chris@18
|
513 $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [
|
Chris@18
|
514 '%source' => $source,
|
Chris@18
|
515 ]);
|
Chris@18
|
516 throw new FileException("File '$source' could not be copied because it would overwrite itself.");
|
Chris@18
|
517 }
|
Chris@18
|
518 // Make sure the .htaccess files are present.
|
Chris@18
|
519 // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
|
Chris@18
|
520 file_ensure_htaccess();
|
Chris@18
|
521 }
|
Chris@18
|
522
|
Chris@18
|
523 /**
|
Chris@18
|
524 * {@inheritdoc}
|
Chris@18
|
525 */
|
Chris@18
|
526 public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
|
Chris@18
|
527 // Write the data to a temporary file.
|
Chris@18
|
528 $temp_name = $this->tempnam('temporary://', 'file');
|
Chris@18
|
529 if (file_put_contents($temp_name, $data) === FALSE) {
|
Chris@18
|
530 $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]);
|
Chris@18
|
531 throw new FileWriteException("Temporary file '$temp_name' could not be created.");
|
Chris@18
|
532 }
|
Chris@18
|
533
|
Chris@18
|
534 // Move the file to its final destination.
|
Chris@18
|
535 return $this->move($temp_name, $destination, $replace);
|
Chris@18
|
536 }
|
Chris@18
|
537
|
Chris@18
|
538 /**
|
Chris@18
|
539 * {@inheritdoc}
|
Chris@18
|
540 */
|
Chris@18
|
541 public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
|
Chris@18
|
542 if (!$this->validScheme($this->uriScheme($directory))) {
|
Chris@18
|
543 // Only trim if we're not dealing with a stream.
|
Chris@18
|
544 $directory = rtrim($directory, '/\\');
|
Chris@18
|
545 }
|
Chris@18
|
546
|
Chris@18
|
547 // Check if directory exists.
|
Chris@18
|
548 if (!is_dir($directory)) {
|
Chris@18
|
549 // Let mkdir() recursively create directories and use the default
|
Chris@18
|
550 // directory permissions.
|
Chris@18
|
551 if ($options & static::CREATE_DIRECTORY) {
|
Chris@18
|
552 return @$this->mkdir($directory, NULL, TRUE);
|
Chris@18
|
553 }
|
Chris@18
|
554 return FALSE;
|
Chris@18
|
555 }
|
Chris@18
|
556 // The directory exists, so check to see if it is writable.
|
Chris@18
|
557 $writable = is_writable($directory);
|
Chris@18
|
558 if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
|
Chris@18
|
559 return $this->chmod($directory);
|
Chris@18
|
560 }
|
Chris@18
|
561
|
Chris@18
|
562 return $writable;
|
Chris@18
|
563 }
|
Chris@18
|
564
|
Chris@18
|
565 /**
|
Chris@18
|
566 * {@inheritdoc}
|
Chris@18
|
567 */
|
Chris@18
|
568 public function getDestinationFilename($destination, $replace) {
|
Chris@18
|
569 $basename = $this->basename($destination);
|
Chris@18
|
570 if (!Unicode::validateUtf8($basename)) {
|
Chris@18
|
571 throw new FileException(sprintf("Invalid filename '%s'", $basename));
|
Chris@18
|
572 }
|
Chris@18
|
573 if (file_exists($destination)) {
|
Chris@18
|
574 switch ($replace) {
|
Chris@18
|
575 case FileSystemInterface::EXISTS_REPLACE:
|
Chris@18
|
576 // Do nothing here, we want to overwrite the existing file.
|
Chris@18
|
577 break;
|
Chris@18
|
578
|
Chris@18
|
579 case FileSystemInterface::EXISTS_RENAME:
|
Chris@18
|
580 $directory = $this->dirname($destination);
|
Chris@18
|
581 $destination = $this->createFilename($basename, $directory);
|
Chris@18
|
582 break;
|
Chris@18
|
583
|
Chris@18
|
584 case FileSystemInterface::EXISTS_ERROR:
|
Chris@18
|
585 // Error reporting handled by calling function.
|
Chris@18
|
586 return FALSE;
|
Chris@18
|
587 }
|
Chris@18
|
588 }
|
Chris@18
|
589 return $destination;
|
Chris@18
|
590 }
|
Chris@18
|
591
|
Chris@18
|
592 /**
|
Chris@18
|
593 * {@inheritdoc}
|
Chris@18
|
594 */
|
Chris@18
|
595 public function createFilename($basename, $directory) {
|
Chris@18
|
596 $original = $basename;
|
Chris@18
|
597 // Strip control characters (ASCII value < 32). Though these are allowed in
|
Chris@18
|
598 // some filesystems, not many applications handle them well.
|
Chris@18
|
599 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
|
Chris@18
|
600 if (preg_last_error() !== PREG_NO_ERROR) {
|
Chris@18
|
601 throw new FileException(sprintf("Invalid filename '%s'", $original));
|
Chris@18
|
602 }
|
Chris@18
|
603 if (substr(PHP_OS, 0, 3) == 'WIN') {
|
Chris@18
|
604 // These characters are not allowed in Windows filenames.
|
Chris@18
|
605 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
|
Chris@18
|
606 }
|
Chris@18
|
607
|
Chris@18
|
608 // A URI or path may already have a trailing slash or look like "public://".
|
Chris@18
|
609 if (substr($directory, -1) == '/') {
|
Chris@18
|
610 $separator = '';
|
Chris@18
|
611 }
|
Chris@18
|
612 else {
|
Chris@18
|
613 $separator = '/';
|
Chris@18
|
614 }
|
Chris@18
|
615
|
Chris@18
|
616 $destination = $directory . $separator . $basename;
|
Chris@18
|
617
|
Chris@18
|
618 if (file_exists($destination)) {
|
Chris@18
|
619 // Destination file already exists, generate an alternative.
|
Chris@18
|
620 $pos = strrpos($basename, '.');
|
Chris@18
|
621 if ($pos !== FALSE) {
|
Chris@18
|
622 $name = substr($basename, 0, $pos);
|
Chris@18
|
623 $ext = substr($basename, $pos);
|
Chris@18
|
624 }
|
Chris@18
|
625 else {
|
Chris@18
|
626 $name = $basename;
|
Chris@18
|
627 $ext = '';
|
Chris@18
|
628 }
|
Chris@18
|
629
|
Chris@18
|
630 $counter = 0;
|
Chris@18
|
631 do {
|
Chris@18
|
632 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
|
Chris@18
|
633 } while (file_exists($destination));
|
Chris@18
|
634 }
|
Chris@18
|
635
|
Chris@18
|
636 return $destination;
|
Chris@18
|
637 }
|
Chris@18
|
638
|
Chris@0
|
639 }
|