Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Filesystem.php

Zuletzt modifiziert: 01.05.2026, 11:26 - Dateigröße: 29.07 KiB


 001  <?php
 002   
 003  /*
 004   * This file is part of the Symfony package.
 005   *
 006   * (c) Fabien Potencier <fabien@symfony.com>
 007   *
 008   * For the full copyright and license information, please view the LICENSE
 009   * file that was distributed with this source code.
 010   */
 011   
 012  namespace Symfony\Component\Filesystem;
 013   
 014  use Symfony\Component\Filesystem\Exception\FileNotFoundException;
 015  use Symfony\Component\Filesystem\Exception\IOException;
 016   
 017  /**
 018   * Provides basic utility to manipulate the file system.
 019   *
 020   * @author Fabien Potencier <fabien@symfony.com>
 021   */
 022  class Filesystem
 023  {
 024      private static $lastError;
 025   
 026      /**
 027       * Copies a file.
 028       *
 029       * If the target file is older than the origin file, it's always overwritten.
 030       * If the target file is newer, it is overwritten only when the
 031       * $overwriteNewerFiles option is set to true.
 032       *
 033       * @param string $originFile          The original filename
 034       * @param string $targetFile          The target filename
 035       * @param bool   $overwriteNewerFiles If true, target files newer than origin files are overwritten
 036       *
 037       * @throws FileNotFoundException When originFile doesn't exist
 038       * @throws IOException           When copy fails
 039       */
 040      public function copy($originFile, $targetFile, $overwriteNewerFiles = false)
 041      {
 042          $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
 043          if ($originIsLocal && !is_file($originFile)) {
 044              throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
 045          }
 046   
 047          $this->mkdir(\dirname($targetFile));
 048   
 049          $doCopy = true;
 050          if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
 051              $doCopy = filemtime($originFile) > filemtime($targetFile);
 052          }
 053   
 054          if ($doCopy) {
 055              // https://bugs.php.net/64634
 056              if (false === $source = @fopen($originFile, 'r')) {
 057                  throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading.', $originFile, $targetFile), 0, null, $originFile);
 058              }
 059   
 060              // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
 061              if (false === $target = @fopen($targetFile, 'w', null, stream_context_create(['ftp' => ['overwrite' => true]]))) {
 062                  throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing.', $originFile, $targetFile), 0, null, $originFile);
 063              }
 064   
 065              $bytesCopied = stream_copy_to_stream($source, $target);
 066              fclose($source);
 067              fclose($target);
 068              unset($source, $target);
 069   
 070              if (!is_file($targetFile)) {
 071                  throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
 072              }
 073   
 074              if ($originIsLocal) {
 075                  // Like `cp`, preserve executable permission bits
 076                  @chmod($targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
 077   
 078                  if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
 079                      throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
 080                  }
 081              }
 082          }
 083      }
 084   
 085      /**
 086       * Creates a directory recursively.
 087       *
 088       * @param string|iterable $dirs The directory path
 089       * @param int             $mode The directory mode
 090       *
 091       * @throws IOException On any directory creation failure
 092       */
 093      public function mkdir($dirs, $mode = 0777)
 094      {
 095          foreach ($this->toIterable($dirs) as $dir) {
 096              if (is_dir($dir)) {
 097                  continue;
 098              }
 099   
 100              if (!self::box('mkdir', $dir, $mode, true)) {
 101                  if (!is_dir($dir)) {
 102                      // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one
 103                      if (self::$lastError) {
 104                          throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
 105                      }
 106                      throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir);
 107                  }
 108              }
 109          }
 110      }
 111   
 112      /**
 113       * Checks the existence of files or directories.
 114       *
 115       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
 116       *
 117       * @return bool true if the file exists, false otherwise
 118       */
 119      public function exists($files)
 120      {
 121          $maxPathLength = \PHP_MAXPATHLEN - 2;
 122   
 123          foreach ($this->toIterable($files) as $file) {
 124              if (\strlen($file) > $maxPathLength) {
 125                  throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
 126              }
 127   
 128              if (!file_exists($file)) {
 129                  return false;
 130              }
 131          }
 132   
 133          return true;
 134      }
 135   
 136      /**
 137       * Sets access and modification time of file.
 138       *
 139       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
 140       * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
 141       * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
 142       *
 143       * @throws IOException When touch fails
 144       */
 145      public function touch($files, $time = null, $atime = null)
 146      {
 147          foreach ($this->toIterable($files) as $file) {
 148              $touch = $time ? @touch($file, $time, $atime) : @touch($file);
 149              if (true !== $touch) {
 150                  throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file);
 151              }
 152          }
 153      }
 154   
 155      /**
 156       * Removes files or directories.
 157       *
 158       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
 159       *
 160       * @throws IOException When removal fails
 161       */
 162      public function remove($files)
 163      {
 164          if ($files instanceof \Traversable) {
 165              $files = iterator_to_array($files, false);
 166          } elseif (!\is_array($files)) {
 167              $files = [$files];
 168          }
 169          $files = array_reverse($files);
 170          foreach ($files as $file) {
 171              if (is_link($file)) {
 172                  // See https://bugs.php.net/52176
 173                  if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
 174                      throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
 175                  }
 176              } elseif (is_dir($file)) {
 177                  $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
 178   
 179                  if (!self::box('rmdir', $file) && file_exists($file)) {
 180                      throw new IOException(sprintf('Failed to remove directory "%s": ', $file).self::$lastError);
 181                  }
 182              } elseif (!self::box('unlink', $file) && (false !== strpos(self::$lastError, 'Permission denied') || file_exists($file))) {
 183                  throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
 184              }
 185          }
 186      }
 187   
 188      /**
 189       * Change mode for an array of files or directories.
 190       *
 191       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
 192       * @param int             $mode      The new mode (octal)
 193       * @param int             $umask     The mode mask (octal)
 194       * @param bool            $recursive Whether change the mod recursively or not
 195       *
 196       * @throws IOException When the change fails
 197       */
 198      public function chmod($files, $mode, $umask = 0000, $recursive = false)
 199      {
 200          foreach ($this->toIterable($files) as $file) {
 201              if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && true !== @chmod($file, $mode & ~$umask)) {
 202                  throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file);
 203              }
 204              if ($recursive && is_dir($file) && !is_link($file)) {
 205                  $this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
 206              }
 207          }
 208      }
 209   
 210      /**
 211       * Change the owner of an array of files or directories.
 212       *
 213       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
 214       * @param string|int      $user      A user name or number
 215       * @param bool            $recursive Whether change the owner recursively or not
 216       *
 217       * @throws IOException When the change fails
 218       */
 219      public function chown($files, $user, $recursive = false)
 220      {
 221          foreach ($this->toIterable($files) as $file) {
 222              if ($recursive && is_dir($file) && !is_link($file)) {
 223                  $this->chown(new \FilesystemIterator($file), $user, true);
 224              }
 225              if (is_link($file) && \function_exists('lchown')) {
 226                  if (true !== @lchown($file, $user)) {
 227                      throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
 228                  }
 229              } else {
 230                  if (true !== @chown($file, $user)) {
 231                      throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
 232                  }
 233              }
 234          }
 235      }
 236   
 237      /**
 238       * Change the group of an array of files or directories.
 239       *
 240       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
 241       * @param string|int      $group     A group name or number
 242       * @param bool            $recursive Whether change the group recursively or not
 243       *
 244       * @throws IOException When the change fails
 245       */
 246      public function chgrp($files, $group, $recursive = false)
 247      {
 248          foreach ($this->toIterable($files) as $file) {
 249              if ($recursive && is_dir($file) && !is_link($file)) {
 250                  $this->chgrp(new \FilesystemIterator($file), $group, true);
 251              }
 252              if (is_link($file) && \function_exists('lchgrp')) {
 253                  if (true !== @lchgrp($file, $group) || (\defined('HHVM_VERSION') && !posix_getgrnam($group))) {
 254                      throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
 255                  }
 256              } else {
 257                  if (true !== @chgrp($file, $group)) {
 258                      throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
 259                  }
 260              }
 261          }
 262      }
 263   
 264      /**
 265       * Renames a file or a directory.
 266       *
 267       * @param string $origin    The origin filename or directory
 268       * @param string $target    The new filename or directory
 269       * @param bool   $overwrite Whether to overwrite the target if it already exists
 270       *
 271       * @throws IOException When target file or directory already exists
 272       * @throws IOException When origin cannot be renamed
 273       */
 274      public function rename($origin, $target, $overwrite = false)
 275      {
 276          // we check that target does not exist
 277          if (!$overwrite && $this->isReadable($target)) {
 278              throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
 279          }
 280   
 281          if (true !== @rename($origin, $target)) {
 282              if (is_dir($origin)) {
 283                  // See https://bugs.php.net/54097 & https://php.net/rename#113943
 284                  $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
 285                  $this->remove($origin);
 286   
 287                  return;
 288              }
 289              throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target);
 290          }
 291      }
 292   
 293      /**
 294       * Tells whether a file exists and is readable.
 295       *
 296       * @param string $filename Path to the file
 297       *
 298       * @return bool
 299       *
 300       * @throws IOException When windows path is longer than 258 characters
 301       */
 302      private function isReadable($filename)
 303      {
 304          $maxPathLength = \PHP_MAXPATHLEN - 2;
 305   
 306          if (\strlen($filename) > $maxPathLength) {
 307              throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
 308          }
 309   
 310          return is_readable($filename);
 311      }
 312   
 313      /**
 314       * Creates a symbolic link or copy a directory.
 315       *
 316       * @param string $originDir     The origin directory path
 317       * @param string $targetDir     The symbolic link name
 318       * @param bool   $copyOnWindows Whether to copy files if on Windows
 319       *
 320       * @throws IOException When symlink fails
 321       */
 322      public function symlink($originDir, $targetDir, $copyOnWindows = false)
 323      {
 324          if ('\\' === \DIRECTORY_SEPARATOR) {
 325              $originDir = strtr($originDir, '/', '\\');
 326              $targetDir = strtr($targetDir, '/', '\\');
 327   
 328              if ($copyOnWindows) {
 329                  $this->mirror($originDir, $targetDir);
 330   
 331                  return;
 332              }
 333          }
 334   
 335          $this->mkdir(\dirname($targetDir));
 336   
 337          if (is_link($targetDir)) {
 338              if (readlink($targetDir) === $originDir) {
 339                  return;
 340              }
 341              $this->remove($targetDir);
 342          }
 343   
 344          if (!self::box('symlink', $originDir, $targetDir)) {
 345              $this->linkException($originDir, $targetDir, 'symbolic');
 346          }
 347      }
 348   
 349      /**
 350       * Creates a hard link, or several hard links to a file.
 351       *
 352       * @param string          $originFile  The original file
 353       * @param string|string[] $targetFiles The target file(s)
 354       *
 355       * @throws FileNotFoundException When original file is missing or not a file
 356       * @throws IOException           When link fails, including if link already exists
 357       */
 358      public function hardlink($originFile, $targetFiles)
 359      {
 360          if (!$this->exists($originFile)) {
 361              throw new FileNotFoundException(null, 0, null, $originFile);
 362          }
 363   
 364          if (!is_file($originFile)) {
 365              throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
 366          }
 367   
 368          foreach ($this->toIterable($targetFiles) as $targetFile) {
 369              if (is_file($targetFile)) {
 370                  if (fileinode($originFile) === fileinode($targetFile)) {
 371                      continue;
 372                  }
 373                  $this->remove($targetFile);
 374              }
 375   
 376              if (!self::box('link', $originFile, $targetFile)) {
 377                  $this->linkException($originFile, $targetFile, 'hard');
 378              }
 379          }
 380      }
 381   
 382      /**
 383       * @param string $origin
 384       * @param string $target
 385       * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
 386       */
 387      private function linkException($origin, $target, $linkType)
 388      {
 389          if (self::$lastError) {
 390              if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos(self::$lastError, 'error code(1314)')) {
 391                  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), 0, null, $target);
 392              }
 393          }
 394          throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target);
 395      }
 396   
 397      /**
 398       * Resolves links in paths.
 399       *
 400       * With $canonicalize = false (default)
 401       *      - if $path does not exist or is not a link, returns null
 402       *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
 403       *
 404       * With $canonicalize = true
 405       *      - if $path does not exist, returns null
 406       *      - if $path exists, returns its absolute fully resolved final version
 407       *
 408       * @param string $path         A filesystem path
 409       * @param bool   $canonicalize Whether or not to return a canonicalized path
 410       *
 411       * @return string|null
 412       */
 413      public function readlink($path, $canonicalize = false)
 414      {
 415          if (!$canonicalize && !is_link($path)) {
 416              return null;
 417          }
 418   
 419          if ($canonicalize) {
 420              if (!$this->exists($path)) {
 421                  return null;
 422              }
 423   
 424              if ('\\' === \DIRECTORY_SEPARATOR) {
 425                  $path = readlink($path);
 426              }
 427   
 428              return realpath($path);
 429          }
 430   
 431          if ('\\' === \DIRECTORY_SEPARATOR) {
 432              return realpath($path);
 433          }
 434   
 435          return readlink($path);
 436      }
 437   
 438      /**
 439       * Given an existing path, convert it to a path relative to a given starting path.
 440       *
 441       * @param string $endPath   Absolute path of target
 442       * @param string $startPath Absolute path where traversal begins
 443       *
 444       * @return string Path of target relative to starting path
 445       */
 446      public function makePathRelative($endPath, $startPath)
 447      {
 448          if (!$this->isAbsolutePath($endPath) || !$this->isAbsolutePath($startPath)) {
 449              @trigger_error(sprintf('Support for passing relative paths to %s() is deprecated since Symfony 3.4 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED);
 450          }
 451   
 452          // Normalize separators on Windows
 453          if ('\\' === \DIRECTORY_SEPARATOR) {
 454              $endPath = str_replace('\\', '/', $endPath);
 455              $startPath = str_replace('\\', '/', $startPath);
 456          }
 457   
 458          $splitDriveLetter = function ($path) {
 459              return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
 460                  ? [substr($path, 2), strtoupper($path[0])]
 461                  : [$path, null];
 462          };
 463   
 464          $splitPath = function ($path, $absolute) {
 465              $result = [];
 466   
 467              foreach (explode('/', trim($path, '/')) as $segment) {
 468                  if ('..' === $segment && ($absolute || \count($result))) {
 469                      array_pop($result);
 470                  } elseif ('.' !== $segment && '' !== $segment) {
 471                      $result[] = $segment;
 472                  }
 473              }
 474   
 475              return $result;
 476          };
 477   
 478          list($endPath, $endDriveLetter) = $splitDriveLetter($endPath);
 479          list($startPath, $startDriveLetter) = $splitDriveLetter($startPath);
 480   
 481          $startPathArr = $splitPath($startPath, static::isAbsolutePath($startPath));
 482          $endPathArr = $splitPath($endPath, static::isAbsolutePath($endPath));
 483   
 484          if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
 485              // End path is on another drive, so no relative path exists
 486              return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
 487          }
 488   
 489          // Find for which directory the common path stops
 490          $index = 0;
 491          while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
 492              ++$index;
 493          }
 494   
 495          // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
 496          if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
 497              $depth = 0;
 498          } else {
 499              $depth = \count($startPathArr) - $index;
 500          }
 501   
 502          // Repeated "../" for each level need to reach the common path
 503          $traverser = str_repeat('../', $depth);
 504   
 505          $endPathRemainder = implode('/', \array_slice($endPathArr, $index));
 506   
 507          // Construct $endPath from traversing to the common path, then to the remaining $endPath
 508          $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
 509   
 510          return '' === $relativePath ? './' : $relativePath;
 511      }
 512   
 513      /**
 514       * Mirrors a directory to another.
 515       *
 516       * Copies files and directories from the origin directory into the target directory. By default:
 517       *
 518       *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
 519       *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
 520       *
 521       * @param string            $originDir The origin directory
 522       * @param string            $targetDir The target directory
 523       * @param \Traversable|null $iterator  Iterator that filters which files and directories to copy, if null a recursive iterator is created
 524       * @param array             $options   An array of boolean options
 525       *                                     Valid options are:
 526       *                                     - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
 527       *                                     - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
 528       *                                     - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
 529       *
 530       * @throws IOException When file type is unknown
 531       */
 532      public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = [])
 533      {
 534          $targetDir = rtrim($targetDir, '/\\');
 535          $originDir = rtrim($originDir, '/\\');
 536          $originDirLen = \strlen($originDir);
 537   
 538          // Iterate in destination folder to remove obsolete entries
 539          if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
 540              $deleteIterator = $iterator;
 541              if (null === $deleteIterator) {
 542                  $flags = \FilesystemIterator::SKIP_DOTS;
 543                  $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
 544              }
 545              $targetDirLen = \strlen($targetDir);
 546              foreach ($deleteIterator as $file) {
 547                  $origin = $originDir.substr($file->getPathname(), $targetDirLen);
 548                  if (!$this->exists($origin)) {
 549                      $this->remove($file);
 550                  }
 551              }
 552          }
 553   
 554          $copyOnWindows = false;
 555          if (isset($options['copy_on_windows'])) {
 556              $copyOnWindows = $options['copy_on_windows'];
 557          }
 558   
 559          if (null === $iterator) {
 560              $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
 561              $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
 562          }
 563   
 564          if ($this->exists($originDir)) {
 565              $this->mkdir($targetDir);
 566          }
 567   
 568          foreach ($iterator as $file) {
 569              $target = $targetDir.substr($file->getPathname(), $originDirLen);
 570   
 571              if ($copyOnWindows) {
 572                  if (is_file($file)) {
 573                      $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
 574                  } elseif (is_dir($file)) {
 575                      $this->mkdir($target);
 576                  } else {
 577                      throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
 578                  }
 579              } else {
 580                  if (is_link($file)) {
 581                      $this->symlink($file->getLinkTarget(), $target);
 582                  } elseif (is_dir($file)) {
 583                      $this->mkdir($target);
 584                  } elseif (is_file($file)) {
 585                      $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
 586                  } else {
 587                      throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
 588                  }
 589              }
 590          }
 591      }
 592   
 593      /**
 594       * Returns whether the file path is an absolute path.
 595       *
 596       * @param string $file A file path
 597       *
 598       * @return bool
 599       */
 600      public function isAbsolutePath($file)
 601      {
 602          return '' !== (string) $file && (strspn($file, '/\\', 0, 1)
 603              || (\strlen($file) > 3 && ctype_alpha($file[0])
 604                  && ':' === $file[1]
 605                  && strspn($file, '/\\', 2, 1)
 606              )
 607              || null !== parse_url($file, \PHP_URL_SCHEME)
 608          );
 609      }
 610   
 611      /**
 612       * Creates a temporary file with support for custom stream wrappers.
 613       *
 614       * @param string $dir    The directory where the temporary filename will be created
 615       * @param string $prefix The prefix of the generated temporary filename
 616       *                       Note: Windows uses only the first three characters of prefix
 617       *
 618       * @return string The new temporary filename (with path), or throw an exception on failure
 619       */
 620      public function tempnam($dir, $prefix)
 621      {
 622          list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir);
 623   
 624          // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
 625          if (null === $scheme || 'file' === $scheme || 'gs' === $scheme) {
 626              $tmpFile = @tempnam($hierarchy, $prefix);
 627   
 628              // If tempnam failed or no scheme return the filename otherwise prepend the scheme
 629              if (false !== $tmpFile) {
 630                  if (null !== $scheme && 'gs' !== $scheme) {
 631                      return $scheme.'://'.$tmpFile;
 632                  }
 633   
 634                  return $tmpFile;
 635              }
 636   
 637              throw new IOException('A temporary file could not be created.');
 638          }
 639   
 640          // Loop until we create a valid temp file or have reached 10 attempts
 641          for ($i = 0; $i < 10; ++$i) {
 642              // Create a unique filename
 643              $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true);
 644   
 645              // Use fopen instead of file_exists as some streams do not support stat
 646              // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
 647              $handle = @fopen($tmpFile, 'x+');
 648   
 649              // If unsuccessful restart the loop
 650              if (false === $handle) {
 651                  continue;
 652              }
 653   
 654              // Close the file if it was successfully opened
 655              @fclose($handle);
 656   
 657              return $tmpFile;
 658          }
 659   
 660          throw new IOException('A temporary file could not be created.');
 661      }
 662   
 663      /**
 664       * Atomically dumps content into a file.
 665       *
 666       * @param string $filename The file to be written to
 667       * @param string $content  The data to write into the file
 668       *
 669       * @throws IOException if the file cannot be written to
 670       */
 671      public function dumpFile($filename, $content)
 672      {
 673          $dir = \dirname($filename);
 674   
 675          if (!is_dir($dir)) {
 676              $this->mkdir($dir);
 677          }
 678   
 679          if (!is_writable($dir)) {
 680              throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
 681          }
 682   
 683          // Will create a temp file with 0600 access rights
 684          // when the filesystem supports chmod.
 685          $tmpFile = $this->tempnam($dir, basename($filename));
 686   
 687          if (false === @file_put_contents($tmpFile, $content)) {
 688              throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
 689          }
 690   
 691          @chmod($tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
 692   
 693          $this->rename($tmpFile, $filename, true);
 694      }
 695   
 696      /**
 697       * Appends content to an existing file.
 698       *
 699       * @param string $filename The file to which to append content
 700       * @param string $content  The content to append
 701       *
 702       * @throws IOException If the file is not writable
 703       */
 704      public function appendToFile($filename, $content)
 705      {
 706          $dir = \dirname($filename);
 707   
 708          if (!is_dir($dir)) {
 709              $this->mkdir($dir);
 710          }
 711   
 712          if (!is_writable($dir)) {
 713              throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
 714          }
 715   
 716          if (false === @file_put_contents($filename, $content, \FILE_APPEND)) {
 717              throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
 718          }
 719      }
 720   
 721      /**
 722       * @param mixed $files
 723       *
 724       * @return array|\Traversable
 725       */
 726      private function toIterable($files)
 727      {
 728          return \is_array($files) || $files instanceof \Traversable ? $files : [$files];
 729      }
 730   
 731      /**
 732       * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
 733       *
 734       * @param string $filename The filename to be parsed
 735       *
 736       * @return array The filename scheme and hierarchical part
 737       */
 738      private function getSchemeAndHierarchy($filename)
 739      {
 740          $components = explode('://', $filename, 2);
 741   
 742          return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
 743      }
 744   
 745      /**
 746       * @param callable $func
 747       *
 748       * @return mixed
 749       */
 750      private static function box($func)
 751      {
 752          self::$lastError = null;
 753          set_error_handler(__CLASS__.'::handleError');
 754          try {
 755              $result = \call_user_func_array($func, \array_slice(\func_get_args(), 1));
 756              restore_error_handler();
 757   
 758              return $result;
 759          } catch (\Throwable $e) {
 760          } catch (\Exception $e) {
 761          }
 762          restore_error_handler();
 763   
 764          throw $e;
 765      }
 766   
 767      /**
 768       * @internal
 769       */
 770      public static function handleError($type, $msg)
 771      {
 772          self::$lastError = $msg;
 773      }
 774  }
 775