PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.86
WindPress – Tailwind CSS integration for WordPress v3.2.86
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
windpress / vendor / symfony / filesystem / Filesystem.php

Filesystem.php in WindPress – Tailwind CSS integration for WordPress 3.2.86, at vendor/symfony/filesystem/Filesystem.php

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