comparison core/lib/Drupal/Core/File/FileSystem.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents c75dbcec494b
children
comparison
equal deleted inserted replaced
4:a9cd425dd02b 5:12f9dff5fda9
1 <?php 1 <?php
2 2
3 namespace Drupal\Core\File; 3 namespace Drupal\Core\File;
4 4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\File\Exception\DirectoryNotReadyException;
7 use Drupal\Core\File\Exception\FileException;
8 use Drupal\Core\File\Exception\FileExistsException;
9 use Drupal\Core\File\Exception\FileNotExistsException;
10 use Drupal\Core\File\Exception\FileWriteException;
11 use Drupal\Core\File\Exception\NotRegularFileException;
5 use Drupal\Core\Site\Settings; 12 use Drupal\Core\Site\Settings;
6 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; 13 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
7 use Psr\Log\LoggerInterface; 14 use Psr\Log\LoggerInterface;
8 15
9 /** 16 /**
299 return FALSE; 306 return FALSE;
300 } 307 }
301 return class_exists($this->streamWrapperManager->getClass($scheme)); 308 return class_exists($this->streamWrapperManager->getClass($scheme));
302 } 309 }
303 310
311 /**
312 * {@inheritdoc}
313 */
314 public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
315 $this->prepareDestination($source, $destination, $replace);
316
317 // Perform the copy operation.
318 if (!@copy($source, $destination)) {
319 // If the copy failed and realpaths exist, retry the operation using them
320 // instead.
321 $real_source = $this->realpath($source) ?: $source;
322 $real_destination = $this->realpath($destination) ?: $destination;
323 if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
324 $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [
325 '%source' => $source,
326 '%destination' => $destination,
327 ]);
328 throw new FileWriteException("The specified file '$source' could not be copied to '$destination'.");
329 }
330 }
331
332 // Set the permissions on the new file.
333 $this->chmod($destination);
334
335 return $destination;
336 }
337
338 /**
339 * {@inheritdoc}
340 */
341 public function delete($path) {
342 if (is_file($path)) {
343 if (!$this->unlink($path)) {
344 $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]);
345 throw new FileException("Failed to unlink file '$path'.");
346 }
347 return TRUE;
348 }
349
350 if (is_dir($path)) {
351 $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]);
352 throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead.");
353 }
354
355 // Return TRUE for non-existent file, but log that nothing was actually
356 // deleted, as the current state is the intended result.
357 if (!file_exists($path)) {
358 $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
359 return TRUE;
360 }
361
362 // We cannot handle anything other than files and directories.
363 // Throw an exception for everything else (sockets, symbolic links, etc).
364 $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]);
365 throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted.");
366 }
367
368 /**
369 * {@inheritdoc}
370 */
371 public function deleteRecursive($path, callable $callback = NULL) {
372 if ($callback) {
373 call_user_func($callback, $path);
374 }
375
376 if (is_dir($path)) {
377 $dir = dir($path);
378 while (($entry = $dir->read()) !== FALSE) {
379 if ($entry == '.' || $entry == '..') {
380 continue;
381 }
382 $entry_path = $path . '/' . $entry;
383 $this->deleteRecursive($entry_path, $callback);
384 }
385 $dir->close();
386
387 return $this->rmdir($path);
388 }
389
390 return $this->delete($path);
391 }
392
393 /**
394 * {@inheritdoc}
395 */
396 public function move($source, $destination, $replace = self::EXISTS_RENAME) {
397 $this->prepareDestination($source, $destination, $replace);
398
399 // Ensure compatibility with Windows.
400 // @see \Drupal\Core\File\FileSystemInterface::unlink().
401 $scheme = $this->uriScheme($source);
402 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
403 chmod($source, 0600);
404 }
405 // Attempt to resolve the URIs. This is necessary in certain
406 // configurations (see above) and can also permit fast moves across local
407 // schemes.
408 $real_source = $this->realpath($source) ?: $source;
409 $real_destination = $this->realpath($destination) ?: $destination;
410 // Perform the move operation.
411 if (!@rename($real_source, $real_destination)) {
412 // Fall back to slow copy and unlink procedure. This is necessary for
413 // renames across schemes that are not local, or where rename() has not
414 // been implemented. It's not necessary to use drupal_unlink() as the
415 // Windows issue has already been resolved above.
416 if (!@copy($real_source, $real_destination)) {
417 $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [
418 '%source' => $source,
419 '%destination' => $destination,
420 ]);
421 throw new FileWriteException("The specified file '$source' could not be moved to '$destination'.");
422 }
423 if (!@unlink($real_source)) {
424 $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
425 '%source' => $source,
426 '%destination' => $destination,
427 ]);
428 throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'.");
429 }
430 }
431
432 // Set the permissions on the new file.
433 $this->chmod($destination);
434
435 return $destination;
436 }
437
438 /**
439 * Prepares the destination for a file copy or move operation.
440 *
441 * - Checks if $source and $destination are valid and readable/writable.
442 * - Checks that $source is not equal to $destination; if they are an error
443 * is reported.
444 * - If file already exists in $destination either the call will error out,
445 * replace the file or rename the file based on the $replace parameter.
446 *
447 * @param string $source
448 * A string specifying the filepath or URI of the source file.
449 * @param string|null $destination
450 * A URI containing the destination that $source should be moved/copied to.
451 * The URI may be a bare filepath (without a scheme) and in that case the
452 * default scheme (file://) will be used.
453 * @param int $replace
454 * Replace behavior when the destination file already exists:
455 * - FILE_EXISTS_REPLACE - Replace the existing file.
456 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename
457 * is unique.
458 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
459 *
460 * @see \Drupal\Core\File\FileSystemInterface::copy()
461 * @see \Drupal\Core\File\FileSystemInterface::move()
462 */
463 protected function prepareDestination($source, &$destination, $replace) {
464 $original_source = $source;
465
466 // Assert that the source file actually exists.
467 if (!file_exists($source)) {
468 if (($realpath = $this->realpath($original_source)) !== FALSE) {
469 $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
470 '%original_source' => $original_source,
471 '%realpath' => $realpath,
472 ]);
473 throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist.");
474 }
475 else {
476 $this->logger->error("File '%original_source' could not be copied because it does not exist.", [
477 '%original_source' => $original_source,
478 ]);
479 throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist.");
480 }
481 }
482
483 // Prepare the destination directory.
484 if ($this->prepareDirectory($destination)) {
485 // The destination is already a directory, so append the source basename.
486 $destination = file_stream_wrapper_uri_normalize($destination . '/' . $this->basename($source));
487 }
488 else {
489 // Perhaps $destination is a dir/file?
490 $dirname = $this->dirname($destination);
491 if (!$this->prepareDirectory($dirname)) {
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.", [
493 '%original_source' => $original_source,
494 ]);
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.");
496 }
497 }
498
499 // Determine whether we can perform this operation based on overwrite rules.
500 $destination = $this->getDestinationFilename($destination, $replace);
501 if ($destination === FALSE) {
502 $this->logger->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [
503 '%original_source' => $original_source,
504 '%destination' => $destination,
505 ]);
506 throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination').");
507 }
508
509 // Assert that the source and destination filenames are not the same.
510 $real_source = $this->realpath($source);
511 $real_destination = $this->realpath($destination);
512 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
513 $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [
514 '%source' => $source,
515 ]);
516 throw new FileException("File '$source' could not be copied because it would overwrite itself.");
517 }
518 // Make sure the .htaccess files are present.
519 // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
520 file_ensure_htaccess();
521 }
522
523 /**
524 * {@inheritdoc}
525 */
526 public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
527 // Write the data to a temporary file.
528 $temp_name = $this->tempnam('temporary://', 'file');
529 if (file_put_contents($temp_name, $data) === FALSE) {
530 $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]);
531 throw new FileWriteException("Temporary file '$temp_name' could not be created.");
532 }
533
534 // Move the file to its final destination.
535 return $this->move($temp_name, $destination, $replace);
536 }
537
538 /**
539 * {@inheritdoc}
540 */
541 public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
542 if (!$this->validScheme($this->uriScheme($directory))) {
543 // Only trim if we're not dealing with a stream.
544 $directory = rtrim($directory, '/\\');
545 }
546
547 // Check if directory exists.
548 if (!is_dir($directory)) {
549 // Let mkdir() recursively create directories and use the default
550 // directory permissions.
551 if ($options & static::CREATE_DIRECTORY) {
552 return @$this->mkdir($directory, NULL, TRUE);
553 }
554 return FALSE;
555 }
556 // The directory exists, so check to see if it is writable.
557 $writable = is_writable($directory);
558 if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
559 return $this->chmod($directory);
560 }
561
562 return $writable;
563 }
564
565 /**
566 * {@inheritdoc}
567 */
568 public function getDestinationFilename($destination, $replace) {
569 $basename = $this->basename($destination);
570 if (!Unicode::validateUtf8($basename)) {
571 throw new FileException(sprintf("Invalid filename '%s'", $basename));
572 }
573 if (file_exists($destination)) {
574 switch ($replace) {
575 case FileSystemInterface::EXISTS_REPLACE:
576 // Do nothing here, we want to overwrite the existing file.
577 break;
578
579 case FileSystemInterface::EXISTS_RENAME:
580 $directory = $this->dirname($destination);
581 $destination = $this->createFilename($basename, $directory);
582 break;
583
584 case FileSystemInterface::EXISTS_ERROR:
585 // Error reporting handled by calling function.
586 return FALSE;
587 }
588 }
589 return $destination;
590 }
591
592 /**
593 * {@inheritdoc}
594 */
595 public function createFilename($basename, $directory) {
596 $original = $basename;
597 // Strip control characters (ASCII value < 32). Though these are allowed in
598 // some filesystems, not many applications handle them well.
599 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
600 if (preg_last_error() !== PREG_NO_ERROR) {
601 throw new FileException(sprintf("Invalid filename '%s'", $original));
602 }
603 if (substr(PHP_OS, 0, 3) == 'WIN') {
604 // These characters are not allowed in Windows filenames.
605 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
606 }
607
608 // A URI or path may already have a trailing slash or look like "public://".
609 if (substr($directory, -1) == '/') {
610 $separator = '';
611 }
612 else {
613 $separator = '/';
614 }
615
616 $destination = $directory . $separator . $basename;
617
618 if (file_exists($destination)) {
619 // Destination file already exists, generate an alternative.
620 $pos = strrpos($basename, '.');
621 if ($pos !== FALSE) {
622 $name = substr($basename, 0, $pos);
623 $ext = substr($basename, $pos);
624 }
625 else {
626 $name = $basename;
627 $ext = '';
628 }
629
630 $counter = 0;
631 do {
632 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
633 } while (file_exists($destination));
634 }
635
636 return $destination;
637 }
638
304 } 639 }