Skip to content

Commit c6a7747

Browse files
markchallonerpierredup
authored andcommitted
[Filesystem] added tempnam() stream wrapper aware version of PHP's native tempnam() and fixed dumpFile to allow dumping to streams
1 parent 0bb46c1 commit c6a7747

File tree

5 files changed

+432
-1
lines changed

5 files changed

+432
-1
lines changed

src/Symfony/Component/Filesystem/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
2.8.0
5+
-----
6+
7+
* added tempnam() a stream aware version of PHP's native tempnam()
8+
49
2.6.0
510
-----
611

src/Symfony/Component/Filesystem/Filesystem.php

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,53 @@ public function isAbsolutePath($file)
452452
);
453453
}
454454

455+
/**
456+
* Creates a temporary file with support for custom stream wrappers.
457+
*
458+
* @param string $dir The directory where the temporary filename will be created.
459+
* @param string $prefix The prefix of the generated temporary filename.
460+
* Note: Windows uses only the first three characters of prefix.
461+
*
462+
* @return string The new temporary filename (with path), or false on failure.
463+
*/
464+
public function tempnam($dir, $prefix)
465+
{
466+
$limit = 10;
467+
list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir);
468+
469+
// If no scheme or scheme is "file" create temp file in local filesystem
470+
if (null === $scheme || 'file' === $scheme) {
471+
$tmpFile = tempnam($hierarchy, $prefix);
472+
473+
// If tempnam failed or no scheme return the filename otherwise prepend the scheme
474+
return false === $tmpFile || null === $scheme ? $tmpFile : $scheme.'://'.$tmpFile;
475+
}
476+
477+
// Loop until we create a valid temp file or have reached $limit attempts
478+
for ($i = 0; $i < $limit; $i++) {
479+
480+
// Create a unique filename
481+
$tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true);
482+
483+
// Use fopen instead of file_exists as some streams do not support stat
484+
// Use mode 'x' to atomically check existence and create to avoid a TOCTOU vulnerability
485+
$handle = @fopen($tmpFile, 'x');
486+
487+
// If unsuccessful restart the loop
488+
if (false === $handle) {
489+
continue;
490+
}
491+
492+
// Close the file if it was successfully opened
493+
@fclose($handle);
494+
495+
return $tmpFile;
496+
497+
}
498+
499+
return false;
500+
}
501+
455502
/**
456503
* Atomically dumps content into a file.
457504
*
@@ -472,7 +519,7 @@ public function dumpFile($filename, $content, $mode = 0666)
472519
throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
473520
}
474521

475-
$tmpFile = tempnam($dir, basename($filename));
522+
$tmpFile = $this->tempnam($dir, basename($filename));
476523

477524
if (false === @file_put_contents($tmpFile, $content)) {
478525
throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
@@ -501,4 +548,19 @@ private function toIterator($files)
501548

502549
return $files;
503550
}
551+
552+
/**
553+
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)).
554+
*
555+
* @param string $filename The filename to be parsed.
556+
*
557+
* @return array The filename scheme and hierarchical part
558+
*/
559+
private function getSchemeAndHierarchy($filename)
560+
{
561+
$components = explode('://', $filename, 2);
562+
563+
return count($components) >= 2 ? array($components[0], $components[1]) : array(null, $components[0]);
564+
}
565+
504566
}

src/Symfony/Component/Filesystem/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ $filesystem->rename($origin, $target);
3030

3131
$filesystem->symlink($originDir, $targetDir, $copyOnWindows = false);
3232

33+
$filesystem->tempnam($dir, $prefix);
34+
3335
$filesystem->makePathRelative($endPath, $startPath);
3436

3537
$filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array());
3638

3739
$filesystem->isAbsolutePath($file);
40+
41+
$filesystem->dumpFile($file, $content);
3842
```
3943

4044
Resources

src/Symfony/Component/Filesystem/Tests/FilesystemTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Filesystem\Tests;
1313

14+
use Phar;
1415
/**
1516
* Test class for Filesystem.
1617
*/
@@ -946,6 +947,116 @@ public function providePathsForIsAbsolutePath()
946947
);
947948
}
948949

950+
public function testTempnam()
951+
{
952+
$dirname = $this->workspace;
953+
954+
$filename = $this->filesystem->tempnam($dirname, 'foo');
955+
956+
$this->assertNotFalse($filename);
957+
$this->assertFileExists($filename);
958+
}
959+
960+
public function testTempnamWithFileScheme()
961+
{
962+
$scheme = 'file://';
963+
$dirname = $scheme.$this->workspace;
964+
965+
$filename = $this->filesystem->tempnam($dirname, 'foo');
966+
967+
$this->assertNotFalse($filename);
968+
$this->assertStringStartsWith($scheme, $filename);
969+
$this->assertFileExists($filename);
970+
}
971+
972+
public function testTempnamWithMockScheme()
973+
{
974+
// We avoid autoloading via ClassLoader as stream_wrapper_register creates the object
975+
if (!@include __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MockStream'.DIRECTORY_SEPARATOR.'MockStream.php') {
976+
$this->markTestSkipped('Unable to load mock:// stream.');
977+
}
978+
979+
stream_wrapper_register('mock', 'MockStream\MockStream');
980+
981+
$scheme = 'mock://';
982+
$dirname = $scheme.$this->workspace;
983+
984+
$filename = $this->filesystem->tempnam($dirname, 'foo');
985+
986+
$this->assertNotFalse($filename);
987+
$this->assertStringStartsWith($scheme, $filename);
988+
$this->assertFileExists($filename);
989+
}
990+
991+
public function testTempnamWithZlibSchemeFails()
992+
{
993+
$scheme = 'compress.zlib://';
994+
$dirname = $scheme.$this->workspace;
995+
996+
$filename = $this->filesystem->tempnam($dirname, 'bar');
997+
998+
// The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false
999+
$this->assertFalse($filename);
1000+
}
1001+
1002+
public function testTempnamWithPHPTempSchemeFails()
1003+
{
1004+
$scheme = 'php://temp';
1005+
$dirname = $scheme;
1006+
1007+
$filename = $this->filesystem->tempnam($dirname, 'bar');
1008+
1009+
$this->assertNotFalse($filename);
1010+
$this->assertStringStartsWith($scheme, $filename);
1011+
1012+
// The php://temp stream deletes the file after close
1013+
$this->assertFileNotExists($filename);
1014+
}
1015+
1016+
public function testTempnamWithPharSchemeFails()
1017+
{
1018+
// Skip test if Phar disabled phar.readonly must be 0 in php.ini
1019+
if (!Phar::canWrite()) {
1020+
$this->markTestSkipped('This test cannot run when phar.readonly is 1.');
1021+
}
1022+
1023+
$scheme = 'phar://';
1024+
$dirname = $scheme.$this->workspace;
1025+
$pharname = 'foo.phar';
1026+
1027+
$p = new Phar($this->workspace.'/'.$pharname, 0, $pharname);
1028+
$filename = $this->filesystem->tempnam($dirname, $pharname.'/bar');
1029+
1030+
// The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false
1031+
$this->assertFalse($filename);
1032+
}
1033+
1034+
public function testTempnamWithHTTPSchemeFails()
1035+
{
1036+
$scheme = 'http://';
1037+
$dirname = $scheme.$this->workspace;
1038+
1039+
$filename = $this->filesystem->tempnam($dirname, 'bar');
1040+
1041+
// The http:// scheme is read-only
1042+
$this->assertFalse($filename);
1043+
}
1044+
1045+
public function testTempnamOnUnwritableFallsBackToSysTmp()
1046+
{
1047+
$scheme = 'file://';
1048+
$dirname = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'does_not_exist';
1049+
1050+
$filename = $this->filesystem->tempnam($dirname, 'bar');
1051+
1052+
$this->assertNotFalse($filename);
1053+
$this->assertStringStartsWith(rtrim($scheme.sys_get_temp_dir(), DIRECTORY_SEPARATOR), $filename);
1054+
$this->assertFileExists($filename);
1055+
1056+
// Tear down
1057+
unlink($filename);
1058+
}
1059+
9491060
public function testDumpFile()
9501061
{
9511062
$filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
@@ -1000,6 +1111,29 @@ public function testDumpFileOverwritesAnExistingFile()
10001111
$this->assertSame('bar', file_get_contents($filename));
10011112
}
10021113

1114+
public function testDumpFileWithFileScheme()
1115+
{
1116+
$scheme = 'file://';
1117+
$filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
1118+
1119+
$this->filesystem->dumpFile($filename, 'bar', null);
1120+
1121+
$this->assertFileExists($filename);
1122+
$this->assertSame('bar', file_get_contents($filename));
1123+
}
1124+
1125+
public function testDumpFileWithZlibScheme()
1126+
{
1127+
$scheme = 'compress.zlib://';
1128+
$filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
1129+
1130+
$this->filesystem->dumpFile($filename, 'bar', null);
1131+
1132+
// Zlib stat uses file:// wrapper so remove scheme
1133+
$this->assertFileExists(str_replace($scheme, '', $filename));
1134+
$this->assertSame('bar', file_get_contents($filename));
1135+
}
1136+
10031137
public function testCopyShouldKeepExecutionPermission()
10041138
{
10051139
$this->markAsSkippedIfChmodIsMissing();

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy