vendor/symfony/filesystem/Filesystem.php line 690

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Filesystem;
  11. use Symfony\Component\Filesystem\Exception\FileNotFoundException;
  12. use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
  13. use Symfony\Component\Filesystem\Exception\IOException;
  14. /**
  15.  * Provides basic utility to manipulate the file system.
  16.  *
  17.  * @author Fabien Potencier <fabien@symfony.com>
  18.  */
  19. class Filesystem
  20. {
  21.     private static $lastError;
  22.     /**
  23.      * Copies a file.
  24.      *
  25.      * If the target file is older than the origin file, it's always overwritten.
  26.      * If the target file is newer, it is overwritten only when the
  27.      * $overwriteNewerFiles option is set to true.
  28.      *
  29.      * @throws FileNotFoundException When originFile doesn't exist
  30.      * @throws IOException           When copy fails
  31.      */
  32.     public function copy(string $originFilestring $targetFilebool $overwriteNewerFiles false)
  33.     {
  34.         $originIsLocal stream_is_local($originFile) || === stripos($originFile'file://');
  35.         if ($originIsLocal && !is_file($originFile)) {
  36.             throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.'$originFile), 0null$originFile);
  37.         }
  38.         $this->mkdir(\dirname($targetFile));
  39.         $doCopy true;
  40.         if (!$overwriteNewerFiles && !parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
  41.             $doCopy filemtime($originFile) > filemtime($targetFile);
  42.         }
  43.         if ($doCopy) {
  44.             // https://bugs.php.net/64634
  45.             if (!$source self::box('fopen'$originFile'r')) {
  46.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: '$originFile$targetFile).self::$lastError0null$originFile);
  47.             }
  48.             // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
  49.             if (!$target self::box('fopen'$targetFile'w'falsestream_context_create(['ftp' => ['overwrite' => true]]))) {
  50.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: '$originFile$targetFile).self::$lastError0null$originFile);
  51.             }
  52.             $bytesCopied stream_copy_to_stream($source$target);
  53.             fclose($source);
  54.             fclose($target);
  55.             unset($source$target);
  56.             if (!is_file($targetFile)) {
  57.                 throw new IOException(sprintf('Failed to copy "%s" to "%s".'$originFile$targetFile), 0null$originFile);
  58.             }
  59.             if ($originIsLocal) {
  60.                 // Like `cp`, preserve executable permission bits
  61.                 self::box('chmod'$targetFilefileperms($targetFile) | (fileperms($originFile) & 0111));
  62.                 // Like `cp`, preserve the file modification time
  63.                 self::box('touch'$targetFilefilemtime($originFile));
  64.                 if ($bytesCopied !== $bytesOrigin filesize($originFile)) {
  65.                     throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).'$originFile$targetFile$bytesCopied$bytesOrigin), 0null$originFile);
  66.                 }
  67.             }
  68.         }
  69.     }
  70.     /**
  71.      * Creates a directory recursively.
  72.      *
  73.      * @param string|iterable $dirs The directory path
  74.      *
  75.      * @throws IOException On any directory creation failure
  76.      */
  77.     public function mkdir($dirsint $mode 0777)
  78.     {
  79.         foreach ($this->toIterable($dirs) as $dir) {
  80.             if (is_dir($dir)) {
  81.                 continue;
  82.             }
  83.             if (!self::box('mkdir'$dir$modetrue) && !is_dir($dir)) {
  84.                 throw new IOException(sprintf('Failed to create "%s": '$dir).self::$lastError0null$dir);
  85.             }
  86.         }
  87.     }
  88.     /**
  89.      * Checks the existence of files or directories.
  90.      *
  91.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
  92.      *
  93.      * @return bool
  94.      */
  95.     public function exists($files)
  96.     {
  97.         $maxPathLength = \PHP_MAXPATHLEN 2;
  98.         foreach ($this->toIterable($files) as $file) {
  99.             if (\strlen($file) > $maxPathLength) {
  100.                 throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.'$maxPathLength), 0null$file);
  101.             }
  102.             if (!file_exists($file)) {
  103.                 return false;
  104.             }
  105.         }
  106.         return true;
  107.     }
  108.     /**
  109.      * Sets access and modification time of file.
  110.      *
  111.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
  112.      * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
  113.      * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
  114.      *
  115.      * @throws IOException When touch fails
  116.      */
  117.     public function touch($files, ?int $time null, ?int $atime null)
  118.     {
  119.         foreach ($this->toIterable($files) as $file) {
  120.             if (!($time self::box('touch'$file$time$atime) : self::box('touch'$file))) {
  121.                 throw new IOException(sprintf('Failed to touch "%s": '$file).self::$lastError0null$file);
  122.             }
  123.         }
  124.     }
  125.     /**
  126.      * Removes files or directories.
  127.      *
  128.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
  129.      *
  130.      * @throws IOException When removal fails
  131.      */
  132.     public function remove($files)
  133.     {
  134.         if ($files instanceof \Traversable) {
  135.             $files iterator_to_array($filesfalse);
  136.         } elseif (!\is_array($files)) {
  137.             $files = [$files];
  138.         }
  139.         self::doRemove($filesfalse);
  140.     }
  141.     private static function doRemove(array $filesbool $isRecursive): void
  142.     {
  143.         $files array_reverse($files);
  144.         foreach ($files as $file) {
  145.             if (is_link($file)) {
  146.                 // See https://bugs.php.net/52176
  147.                 if (!(self::box('unlink'$file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir'$file)) && file_exists($file)) {
  148.                     throw new IOException(sprintf('Failed to remove symlink "%s": '$file).self::$lastError);
  149.                 }
  150.             } elseif (is_dir($file)) {
  151.                 if (!$isRecursive) {
  152.                     $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=''-!'));
  153.                     if (file_exists($tmpName)) {
  154.                         try {
  155.                             self::doRemove([$tmpName], true);
  156.                         } catch (IOException $e) {
  157.                         }
  158.                     }
  159.                     if (!file_exists($tmpName) && self::box('rename'$file$tmpName)) {
  160.                         $origFile $file;
  161.                         $file $tmpName;
  162.                     } else {
  163.                         $origFile null;
  164.                     }
  165.                 }
  166.                 $files = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
  167.                 self::doRemove(iterator_to_array($filestrue), true);
  168.                 if (!self::box('rmdir'$file) && file_exists($file) && !$isRecursive) {
  169.                     $lastError self::$lastError;
  170.                     if (null !== $origFile && self::box('rename'$file$origFile)) {
  171.                         $file $origFile;
  172.                     }
  173.                     throw new IOException(sprintf('Failed to remove directory "%s": '$file).$lastError);
  174.                 }
  175.             } elseif (!self::box('unlink'$file) && ((self::$lastError && str_contains(self::$lastError'Permission denied')) || file_exists($file))) {
  176.                 throw new IOException(sprintf('Failed to remove file "%s": '$file).self::$lastError);
  177.             }
  178.         }
  179.     }
  180.     /**
  181.      * Change mode for an array of files or directories.
  182.      *
  183.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
  184.      * @param int             $mode      The new mode (octal)
  185.      * @param int             $umask     The mode mask (octal)
  186.      * @param bool            $recursive Whether change the mod recursively or not
  187.      *
  188.      * @throws IOException When the change fails
  189.      */
  190.     public function chmod($filesint $modeint $umask 0000bool $recursive false)
  191.     {
  192.         foreach ($this->toIterable($files) as $file) {
  193.             if ((\PHP_VERSION_ID 80000 || \is_int($mode)) && !self::box('chmod'$file$mode & ~$umask)) {
  194.                 throw new IOException(sprintf('Failed to chmod file "%s": '$file).self::$lastError0null$file);
  195.             }
  196.             if ($recursive && is_dir($file) && !is_link($file)) {
  197.                 $this->chmod(new \FilesystemIterator($file), $mode$umasktrue);
  198.             }
  199.         }
  200.     }
  201.     /**
  202.      * Change the owner of an array of files or directories.
  203.      *
  204.      * This method always throws on Windows, as the underlying PHP function is not supported.
  205.      *
  206.      * @see https://www.php.net/chown
  207.      *
  208.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
  209.      * @param string|int      $user      A user name or number
  210.      * @param bool            $recursive Whether change the owner recursively or not
  211.      *
  212.      * @throws IOException When the change fails
  213.      */
  214.     public function chown($files$userbool $recursive false)
  215.     {
  216.         foreach ($this->toIterable($files) as $file) {
  217.             if ($recursive && is_dir($file) && !is_link($file)) {
  218.                 $this->chown(new \FilesystemIterator($file), $usertrue);
  219.             }
  220.             if (is_link($file) && \function_exists('lchown')) {
  221.                 if (!self::box('lchown'$file$user)) {
  222.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  223.                 }
  224.             } else {
  225.                 if (!self::box('chown'$file$user)) {
  226.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  227.                 }
  228.             }
  229.         }
  230.     }
  231.     /**
  232.      * Change the group of an array of files or directories.
  233.      *
  234.      * This method always throws on Windows, as the underlying PHP function is not supported.
  235.      *
  236.      * @see https://www.php.net/chgrp
  237.      *
  238.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
  239.      * @param string|int      $group     A group name or number
  240.      * @param bool            $recursive Whether change the group recursively or not
  241.      *
  242.      * @throws IOException When the change fails
  243.      */
  244.     public function chgrp($files$groupbool $recursive false)
  245.     {
  246.         foreach ($this->toIterable($files) as $file) {
  247.             if ($recursive && is_dir($file) && !is_link($file)) {
  248.                 $this->chgrp(new \FilesystemIterator($file), $grouptrue);
  249.             }
  250.             if (is_link($file) && \function_exists('lchgrp')) {
  251.                 if (!self::box('lchgrp'$file$group)) {
  252.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  253.                 }
  254.             } else {
  255.                 if (!self::box('chgrp'$file$group)) {
  256.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  257.                 }
  258.             }
  259.         }
  260.     }
  261.     /**
  262.      * Renames a file or a directory.
  263.      *
  264.      * @throws IOException When target file or directory already exists
  265.      * @throws IOException When origin cannot be renamed
  266.      */
  267.     public function rename(string $originstring $targetbool $overwrite false)
  268.     {
  269.         // we check that target does not exist
  270.         if (!$overwrite && $this->isReadable($target)) {
  271.             throw new IOException(sprintf('Cannot rename because the target "%s" already exists.'$target), 0null$target);
  272.         }
  273.         if (!self::box('rename'$origin$target)) {
  274.             if (is_dir($origin)) {
  275.                 // See https://bugs.php.net/54097 & https://php.net/rename#113943
  276.                 $this->mirror($origin$targetnull, ['override' => $overwrite'delete' => $overwrite]);
  277.                 $this->remove($origin);
  278.                 return;
  279.             }
  280.             throw new IOException(sprintf('Cannot rename "%s" to "%s": '$origin$target).self::$lastError0null$target);
  281.         }
  282.     }
  283.     /**
  284.      * Tells whether a file exists and is readable.
  285.      *
  286.      * @throws IOException When windows path is longer than 258 characters
  287.      */
  288.     private function isReadable(string $filename): bool
  289.     {
  290.         $maxPathLength = \PHP_MAXPATHLEN 2;
  291.         if (\strlen($filename) > $maxPathLength) {
  292.             throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.'$maxPathLength), 0null$filename);
  293.         }
  294.         return is_readable($filename);
  295.     }
  296.     /**
  297.      * Creates a symbolic link or copy a directory.
  298.      *
  299.      * @throws IOException When symlink fails
  300.      */
  301.     public function symlink(string $originDirstring $targetDirbool $copyOnWindows false)
  302.     {
  303.         self::assertFunctionExists('symlink');
  304.         if ('\\' === \DIRECTORY_SEPARATOR) {
  305.             $originDir strtr($originDir'/''\\');
  306.             $targetDir strtr($targetDir'/''\\');
  307.             if ($copyOnWindows) {
  308.                 $this->mirror($originDir$targetDir);
  309.                 return;
  310.             }
  311.         }
  312.         $this->mkdir(\dirname($targetDir));
  313.         if (is_link($targetDir)) {
  314.             if (readlink($targetDir) === $originDir) {
  315.                 return;
  316.             }
  317.             $this->remove($targetDir);
  318.         }
  319.         if (!self::box('symlink'$originDir$targetDir)) {
  320.             $this->linkException($originDir$targetDir'symbolic');
  321.         }
  322.     }
  323.     /**
  324.      * Creates a hard link, or several hard links to a file.
  325.      *
  326.      * @param string|string[] $targetFiles The target file(s)
  327.      *
  328.      * @throws FileNotFoundException When original file is missing or not a file
  329.      * @throws IOException           When link fails, including if link already exists
  330.      */
  331.     public function hardlink(string $originFile$targetFiles)
  332.     {
  333.         self::assertFunctionExists('link');
  334.         if (!$this->exists($originFile)) {
  335.             throw new FileNotFoundException(null0null$originFile);
  336.         }
  337.         if (!is_file($originFile)) {
  338.             throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.'$originFile));
  339.         }
  340.         foreach ($this->toIterable($targetFiles) as $targetFile) {
  341.             if (is_file($targetFile)) {
  342.                 if (fileinode($originFile) === fileinode($targetFile)) {
  343.                     continue;
  344.                 }
  345.                 $this->remove($targetFile);
  346.             }
  347.             if (!self::box('link'$originFile$targetFile)) {
  348.                 $this->linkException($originFile$targetFile'hard');
  349.             }
  350.         }
  351.     }
  352.     /**
  353.      * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
  354.      */
  355.     private function linkException(string $originstring $targetstring $linkType)
  356.     {
  357.         if (self::$lastError) {
  358.             if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError'error code(1314)')) {
  359.                 throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?'$linkType), 0null$target);
  360.             }
  361.         }
  362.         throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": '$linkType$origin$target).self::$lastError0null$target);
  363.     }
  364.     /**
  365.      * Resolves links in paths.
  366.      *
  367.      * With $canonicalize = false (default)
  368.      *      - if $path does not exist or is not a link, returns null
  369.      *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
  370.      *
  371.      * With $canonicalize = true
  372.      *      - if $path does not exist, returns null
  373.      *      - if $path exists, returns its absolute fully resolved final version
  374.      *
  375.      * @return string|null
  376.      */
  377.     public function readlink(string $pathbool $canonicalize false)
  378.     {
  379.         if (!$canonicalize && !is_link($path)) {
  380.             return null;
  381.         }
  382.         if ($canonicalize) {
  383.             if (!$this->exists($path)) {
  384.                 return null;
  385.             }
  386.             if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID 70410) {
  387.                 $path readlink($path);
  388.             }
  389.             return realpath($path);
  390.         }
  391.         if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID 70400) {
  392.             return realpath($path);
  393.         }
  394.         return readlink($path);
  395.     }
  396.     /**
  397.      * Given an existing path, convert it to a path relative to a given starting path.
  398.      *
  399.      * @return string
  400.      */
  401.     public function makePathRelative(string $endPathstring $startPath)
  402.     {
  403.         if (!$this->isAbsolutePath($startPath)) {
  404.             throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.'$startPath));
  405.         }
  406.         if (!$this->isAbsolutePath($endPath)) {
  407.             throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.'$endPath));
  408.         }
  409.         // Normalize separators on Windows
  410.         if ('\\' === \DIRECTORY_SEPARATOR) {
  411.             $endPath str_replace('\\''/'$endPath);
  412.             $startPath str_replace('\\''/'$startPath);
  413.         }
  414.         $splitDriveLetter = function ($path) {
  415.             return (\strlen($path) > && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
  416.                 ? [substr($path2), strtoupper($path[0])]
  417.                 : [$pathnull];
  418.         };
  419.         $splitPath = function ($path) {
  420.             $result = [];
  421.             foreach (explode('/'trim($path'/')) as $segment) {
  422.                 if ('..' === $segment) {
  423.                     array_pop($result);
  424.                 } elseif ('.' !== $segment && '' !== $segment) {
  425.                     $result[] = $segment;
  426.                 }
  427.             }
  428.             return $result;
  429.         };
  430.         [$endPath$endDriveLetter] = $splitDriveLetter($endPath);
  431.         [$startPath$startDriveLetter] = $splitDriveLetter($startPath);
  432.         $startPathArr $splitPath($startPath);
  433.         $endPathArr $splitPath($endPath);
  434.         if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
  435.             // End path is on another drive, so no relative path exists
  436.             return $endDriveLetter.':/'.($endPathArr implode('/'$endPathArr).'/' '');
  437.         }
  438.         // Find for which directory the common path stops
  439.         $index 0;
  440.         while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
  441.             ++$index;
  442.         }
  443.         // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
  444.         if (=== \count($startPathArr) && '' === $startPathArr[0]) {
  445.             $depth 0;
  446.         } else {
  447.             $depth = \count($startPathArr) - $index;
  448.         }
  449.         // Repeated "../" for each level need to reach the common path
  450.         $traverser str_repeat('../'$depth);
  451.         $endPathRemainder implode('/', \array_slice($endPathArr$index));
  452.         // Construct $endPath from traversing to the common path, then to the remaining $endPath
  453.         $relativePath $traverser.('' !== $endPathRemainder $endPathRemainder.'/' '');
  454.         return '' === $relativePath './' $relativePath;
  455.     }
  456.     /**
  457.      * Mirrors a directory to another.
  458.      *
  459.      * Copies files and directories from the origin directory into the target directory. By default:
  460.      *
  461.      *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
  462.      *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
  463.      *
  464.      * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
  465.      * @param array             $options  An array of boolean options
  466.      *                                    Valid options are:
  467.      *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
  468.      *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
  469.      *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
  470.      *
  471.      * @throws IOException When file type is unknown
  472.      */
  473.     public function mirror(string $originDirstring $targetDir, ?\Traversable $iterator null, array $options = [])
  474.     {
  475.         $targetDir rtrim($targetDir'/\\');
  476.         $originDir rtrim($originDir'/\\');
  477.         $originDirLen = \strlen($originDir);
  478.         if (!$this->exists($originDir)) {
  479.             throw new IOException(sprintf('The origin directory specified "%s" was not found.'$originDir), 0null$originDir);
  480.         }
  481.         // Iterate in destination folder to remove obsolete entries
  482.         if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
  483.             $deleteIterator $iterator;
  484.             if (null === $deleteIterator) {
  485.                 $flags = \FilesystemIterator::SKIP_DOTS;
  486.                 $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir$flags), \RecursiveIteratorIterator::CHILD_FIRST);
  487.             }
  488.             $targetDirLen = \strlen($targetDir);
  489.             foreach ($deleteIterator as $file) {
  490.                 $origin $originDir.substr($file->getPathname(), $targetDirLen);
  491.                 if (!$this->exists($origin)) {
  492.                     $this->remove($file);
  493.                 }
  494.             }
  495.         }
  496.         $copyOnWindows $options['copy_on_windows'] ?? false;
  497.         if (null === $iterator) {
  498.             $flags $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
  499.             $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir$flags), \RecursiveIteratorIterator::SELF_FIRST);
  500.         }
  501.         $this->mkdir($targetDir);
  502.         $filesCreatedWhileMirroring = [];
  503.         foreach ($iterator as $file) {
  504.             if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
  505.                 continue;
  506.             }
  507.             $target $targetDir.substr($file->getPathname(), $originDirLen);
  508.             $filesCreatedWhileMirroring[$target] = true;
  509.             if (!$copyOnWindows && is_link($file)) {
  510.                 $this->symlink($file->getLinkTarget(), $target);
  511.             } elseif (is_dir($file)) {
  512.                 $this->mkdir($target);
  513.             } elseif (is_file($file)) {
  514.                 $this->copy($file$target$options['override'] ?? false);
  515.             } else {
  516.                 throw new IOException(sprintf('Unable to guess "%s" file type.'$file), 0null$file);
  517.             }
  518.         }
  519.     }
  520.     /**
  521.      * Returns whether the file path is an absolute path.
  522.      *
  523.      * @return bool
  524.      */
  525.     public function isAbsolutePath(string $file)
  526.     {
  527.         return '' !== $file && (strspn($file'/\\'01)
  528.             || (\strlen($file) > && ctype_alpha($file[0])
  529.                 && ':' === $file[1]
  530.                 && strspn($file'/\\'21)
  531.             )
  532.             || null !== parse_url($file, \PHP_URL_SCHEME)
  533.         );
  534.     }
  535.     /**
  536.      * Creates a temporary file with support for custom stream wrappers.
  537.      *
  538.      * @param string $prefix The prefix of the generated temporary filename
  539.      *                       Note: Windows uses only the first three characters of prefix
  540.      * @param string $suffix The suffix of the generated temporary filename
  541.      *
  542.      * @return string The new temporary filename (with path), or throw an exception on failure
  543.      */
  544.     public function tempnam(string $dirstring $prefix/* , string $suffix = '' */)
  545.     {
  546.         $suffix = \func_num_args() > func_get_arg(2) : '';
  547.         [$scheme$hierarchy] = $this->getSchemeAndHierarchy($dir);
  548.         // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
  549.         if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
  550.             // If tempnam failed or no scheme return the filename otherwise prepend the scheme
  551.             if ($tmpFile self::box('tempnam'$hierarchy$prefix)) {
  552.                 if (null !== $scheme && 'gs' !== $scheme) {
  553.                     return $scheme.'://'.$tmpFile;
  554.                 }
  555.                 return $tmpFile;
  556.             }
  557.             throw new IOException('A temporary file could not be created: '.self::$lastError);
  558.         }
  559.         // Loop until we create a valid temp file or have reached 10 attempts
  560.         for ($i 0$i 10; ++$i) {
  561.             // Create a unique filename
  562.             $tmpFile $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
  563.             // Use fopen instead of file_exists as some streams do not support stat
  564.             // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
  565.             if (!$handle self::box('fopen'$tmpFile'x+')) {
  566.                 continue;
  567.             }
  568.             // Close the file if it was successfully opened
  569.             self::box('fclose'$handle);
  570.             return $tmpFile;
  571.         }
  572.         throw new IOException('A temporary file could not be created: '.self::$lastError);
  573.     }
  574.     /**
  575.      * Atomically dumps content into a file.
  576.      *
  577.      * @param string|resource $content The data to write into the file
  578.      *
  579.      * @throws IOException if the file cannot be written to
  580.      */
  581.     public function dumpFile(string $filename$content)
  582.     {
  583.         if (\is_array($content)) {
  584.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  585.         }
  586.         $dir = \dirname($filename);
  587.         if (is_link($filename) && $linkTarget $this->readlink($filename)) {
  588.             $this->dumpFile(Path::makeAbsolute($linkTarget$dir), $content);
  589.             return;
  590.         }
  591.         if (!is_dir($dir)) {
  592.             $this->mkdir($dir);
  593.         }
  594.         // Will create a temp file with 0600 access rights
  595.         // when the filesystem supports chmod.
  596.         $tmpFile $this->tempnam($dirbasename($filename));
  597.         try {
  598.             if (false === self::box('file_put_contents'$tmpFile$content)) {
  599.                 throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  600.             }
  601.             self::box('chmod'$tmpFileself::box('fileperms'$filename) ?: 0666 & ~umask());
  602.             $this->rename($tmpFile$filenametrue);
  603.         } finally {
  604.             if (file_exists($tmpFile)) {
  605.                 if ('\\' === \DIRECTORY_SEPARATOR && !is_writable($tmpFile)) {
  606.                     self::box('chmod'$tmpFileself::box('fileperms'$tmpFile) | 0200);
  607.                 }
  608.                 self::box('unlink'$tmpFile);
  609.             }
  610.         }
  611.     }
  612.     /**
  613.      * Appends content to an existing file.
  614.      *
  615.      * @param string|resource $content The content to append
  616.      * @param bool            $lock    Whether the file should be locked when writing to it
  617.      *
  618.      * @throws IOException If the file is not writable
  619.      */
  620.     public function appendToFile(string $filename$content/* , bool $lock = false */)
  621.     {
  622.         if (\is_array($content)) {
  623.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  624.         }
  625.         $dir = \dirname($filename);
  626.         if (!is_dir($dir)) {
  627.             $this->mkdir($dir);
  628.         }
  629.         $lock = \func_num_args() > && func_get_arg(2);
  630.         if (false === self::box('file_put_contents'$filename$content, \FILE_APPEND | ($lock ? \LOCK_EX 0))) {
  631.             throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  632.         }
  633.     }
  634.     private function toIterable($files): iterable
  635.     {
  636.         return is_iterable($files) ? $files : [$files];
  637.     }
  638.     /**
  639.      * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
  640.      */
  641.     private function getSchemeAndHierarchy(string $filename): array
  642.     {
  643.         $components explode('://'$filename2);
  644.         return === \count($components) ? [$components[0], $components[1]] : [null$components[0]];
  645.     }
  646.     private static function assertFunctionExists(string $func): void
  647.     {
  648.         if (!\function_exists($func)) {
  649.             throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.'$func));
  650.         }
  651.     }
  652.     /**
  653.      * @param mixed ...$args
  654.      *
  655.      * @return mixed
  656.      */
  657.     private static function box(string $func, ...$args)
  658.     {
  659.         self::assertFunctionExists($func);
  660.         self::$lastError null;
  661.         set_error_handler(__CLASS__.'::handleError');
  662.         try {
  663.             return $func(...$args);
  664.         } finally {
  665.             restore_error_handler();
  666.         }
  667.     }
  668.     /**
  669.      * @internal
  670.      */
  671.     public static function handleError(int $typestring $msg)
  672.     {
  673.         self::$lastError $msg;
  674.     }
  675. }