Mercurial > hg > cmmr2012-drupal-site
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 } |