31

I have a script that checks a zipfile containing a number of matching PDF+textfiles. I want to unpack, or somehow read the textfiles from the zipfile, and just pick out some information from the textfile to see that the file version is correct.

I was looking at the tempnam() function to find an equivalent to make a tempdir, but maybe someone has a better solution for the problem.

The indexfile looks something like this. (-> is for TAB char). I have made the function to extract the version from the textfile and to check if its correct already, its only the unpacking, tmpdir or some other solution im looking for.

1000->filename->file version->program version->customer no->company no->distribution
2000->pagenumber->more info->more info->...
Will
  • 24,082
  • 14
  • 97
  • 108
Arto Uusikangas
  • 1,889
  • 5
  • 20
  • 33

8 Answers8

36

quite easy (I took partly it from the PHP manual):

<?php

function tempdir() {
    $tempfile=tempnam(sys_get_temp_dir(),'');
    // tempnam creates file on disk
    if (file_exists($tempfile)) { unlink($tempfile); }
    mkdir($tempfile);
    if (is_dir($tempfile)) { return $tempfile; }
}

/*example*/

echo tempdir();
// returns: /tmp/8e9MLi

See: https://www.php.net/manual/en/function.tempnam.php

Please look at Will's solution below.

=> My answer should not be the accepted answer anymore.

Peter
  • 16,453
  • 8
  • 51
  • 77
Mario Mueller
  • 1,450
  • 2
  • 13
  • 16
  • 21
    This implementation is subject to race condition. Between unlink and mkdir. Highly unlikely it'll give you any issues, but something to note if it does. – Kevin Mark Sep 17 '12 at 04:01
  • 20
    why tempdir function accepts $dir and $prefix but never are used?? – AgelessEssence Feb 01 '13 at 22:39
  • That's beautiful! I think the race-condition goes away (I didn't verify) if you move the unlink further down in the code, and add something, say an underscore, to the end or beginning of the new directory to be created. – Harry Pehkonen Nov 13 '13 at 19:24
  • But this does not have the special property of `tmpfile()`, which will automagically wipe itself when all handles to it are closed... then again, directories don't have handles. – Milind R Jan 08 '14 at 12:39
  • 1
    @MilindR I used `register_shutdown_function(function() use ($tmpfile) { \`rm -rf "$tmpfile"\`; });` – Max Tsepkov Feb 21 '15 at 05:17
  • 6
    Nice @MaxTsepkov! With a bit of luck, one can find an exploit to set $tmpfile to "/" and remove quite some files... – Patrick Allaert Aug 18 '15 at 12:46
  • 3
    @PatrickAllaert if a malicious user has write access to code he already can execute `rm -rf /` with php privilege. It can be exploited if you still running `register_globals on` though in this case you most probably have outdated code and hence lot more issues. – Max Tsepkov Aug 18 '15 at 18:26
  • What do you mean by "in a linux system a file and a directory are theoretically the same thing" ? – Jack Feb 13 '16 at 11:15
  • @AgelessEssence Since between choosing a unique name and creating an object with such name there can be a time where someone else create the object with your very same name, you are requested to give a $prefix that minimize the chances for a name clash. Usually give your application name as $prefix is a good thing – Jack Feb 13 '16 at 11:18
  • Seems messy to me to create a file and delete it immediately just to generate a random string. Why not use something you can guarantee is unique, such as some combination of username, timestamp, and a random string? If you're still worried, you could do a `do-while` to make sure it doesn't exist before you create it. – Autumn Leonard Aug 16 '18 at 16:42
21

So I first found a post by Ron Korving on PHP.net, which I then modified to make a bit safer (from endless loops, invalid characters, and unwritable parent dirs) and use a bit more entropy.

<?php
/**
 * Creates a random unique temporary directory, with specified parameters,
 * that does not already exist (like tempnam(), but for dirs).
 *
 * Created dir will begin with the specified prefix, followed by random
 * numbers.
 *
 * @link https://php.net/manual/en/function.tempnam.php
 *
 * @param string|null $dir Base directory under which to create temp dir.
 *     If null, the default system temp dir (sys_get_temp_dir()) will be
 *     used.
 * @param string $prefix String with which to prefix created dirs.
 * @param int $mode Octal file permission mask for the newly-created dir.
 *     Should begin with a 0.
 * @param int $maxAttempts Maximum attempts before giving up (to prevent
 *     endless loops).
 * @return string|bool Full path to newly-created dir, or false on failure.
 */
function tempdir($dir = null, $prefix = 'tmp_', $mode = 0700, $maxAttempts = 1000)
{
    /* Use the system temp dir by default. */
    if (is_null($dir))
    {
        $dir = sys_get_temp_dir();
    }

    /* Trim trailing slashes from $dir. */
    $dir = rtrim($dir, DIRECTORY_SEPARATOR);

    /* If we don't have permission to create a directory, fail, otherwise we will
     * be stuck in an endless loop.
     */
    if (!is_dir($dir) || !is_writable($dir))
    {
        return false;
    }

    /* Make sure characters in prefix are safe. */
    if (strpbrk($prefix, '\\/:*?"<>|') !== false)
    {
        return false;
    }

    /* Attempt to create a random directory until it works. Abort if we reach
     * $maxAttempts. Something screwy could be happening with the filesystem
     * and our loop could otherwise become endless.
     */
    $attempts = 0;
    do
    {
        $path = sprintf('%s%s%s%s', $dir, DIRECTORY_SEPARATOR, $prefix, mt_rand(100000, mt_getrandmax()));
    } while (
        !mkdir($path, $mode) &&
        $attempts++ < $maxAttempts
    );

    return $path;
}
?>

So, let's try it out:

<?php
echo "\n";
$dir1 = tempdir();
echo $dir1, "\n";
var_dump(is_dir($dir1), is_writable($dir1));
var_dump(rmdir($dir1));

echo "\n";
$dir2 = tempdir('/tmp', 'stack_');
echo $dir2, "\n";
var_dump(is_dir($dir2), is_writable($dir2));
var_dump(rmdir($dir2));

echo "\n";
$dir3 = tempdir(null, 'stack_');
echo $dir3, "\n";
var_dump(is_dir($dir3), is_writable($dir3));
var_dump(rmdir($dir3));
?>

Result:

/var/folders/v4/647wm24x2ysdjwx6z_f07_kw0000gp/T/tmp_900342820
bool(true)
bool(true)
bool(true)

/tmp/stack_1102047767
bool(true)
bool(true)
bool(true)

/var/folders/v4/647wm24x2ysdjwx6z_f07_kw0000gp/T/stack_638989419
bool(true)
bool(true)
bool(true)
Will
  • 24,082
  • 14
  • 97
  • 108
  • Care to comment on the downvote? I originally landed on this question attempting to solve the problem cleanly, and couldn't find an answer I was satisfied with using in production, so I wanted to share what I wrote for others that end up here looking for the same thing. SO etiquette prefers that you leave a comment or suggest an edit or improvement when downvoting something. – Will May 03 '15 at 07:50
  • Nice. Should `Attenot` be `Attempt`? – TRiG Aug 30 '17 at 14:19
  • 5
    I like your solution but if the code in the loop fails too many times it'll just return the most-recently-attempted path rather than indicating that it failed to find a valid path, which could make code that uses this function fail. – ke4ukz Oct 22 '18 at 15:31
  • Rather than using '/', perhaps this should utilize DIRECTORY_SEPARATOR? – Gary Reckard Nov 15 '18 at 19:16
12

Another option if running on linux with mktemp and access to the exec function is the following:

<?php

function tempdir($dir=NULL,$prefix=NULL) {
  $template = "{$prefix}XXXXXX";
  if (($dir) && (is_dir($dir))) { $tmpdir = "--tmpdir=$dir"; }
  else { $tmpdir = '--tmpdir=' . sys_get_temp_dir(); }
  return exec("mktemp -d $tmpdir $template");
}

/*example*/

$dir = tempdir();
echo "$dir\n";
rmdir($dir);

$dir = tempdir('/tmp/foo', 'bar');
echo "$dir\n";
rmdir($dir);

// returns:
//   /tmp/BN4Wcd
//   /tmp/foo/baruLWFsN (if /tmp/foo exists, /tmp/baruLWFsN otherwise)

?>

This avoids the potential (although unlikely) race issue above and has the same behavior as the tempnam function.

zelanix
  • 3,326
  • 1
  • 25
  • 35
7

I wanted to add a refinement to @Mario Mueller's answer, as his is subject to possible race conditions, however I believe the following should not be:

function tempdir(int $mode = 0700): string {
    do { $tmp = sys_get_temp_dir() . '/' . mt_rand(); }
    while (!@mkdir($tmp, $mode));
    return $tmp;
}

This works because mkdir returns false if $tmp already exists, causing the loop to repeat and try another name.

Note also that I've added handling for $mode, with a default that ensures the directory is accessible to the current user only, as mkdir's default is 0777 otherwise.

It is strongly advised that you use a shutdown function to ensure the directory is removed when no longer needed, even if your script exits by unexpected means*. To facilitate this, the full function that I use does this automatically unless the $auto_delete argument is set to false.

// Deletes a non-empty directory
function destroydir(string $dir): bool { 
    if (!is_dir($dir)) { return false; }

    $files = array_diff(scandir($dir), ['.', '..']);
    foreach ($files as $file) {
        if (is_dir("$dir/$file")) { destroydir("$dir/$file"); }
        else { unlink("$dir/$file"); }
    }
    return rmdir($dir); 
}

function tempdir(int $mode = 0700, bool $auto_delete = true): string {
    do { $tmp = sys_get_temp_dir() . '/' . mt_rand(); }
    while (!@mkdir($tmp, $mode));

    if ($auto_delete) {
        register_shutdown_function(function() use ($tmp) { destroydir($tmp); });
    }
    return $tmp;
}

This means that by default any temporary directory created by tempdir() will have permissions of 0700 and will be automatically deleted (along with its contents) when your script ends.

NOTE: *This may not be the case if the script is killed, for this you might need to look into registering a signal handler as well.

Haravikk
  • 3,109
  • 1
  • 33
  • 46
  • 1
    Great answer. Just one detail: I needed to add `unlink($tmp)` inside `do {} while()`. If not, I get an infinite loop since `$tmp` is created as a regular file and therefore `mkdir` always return `false`. – Jordi Nebot Nov 24 '17 at 10:11
  • 2
    WARNING: never use code above as is. It will cause web server (Apache for me) create HUGE junk in /tmp folder. It works like a bomb, eating all i-nodes on partition in which /tmp is located. Server reboot and long time for removing millions of empty files will be required! Fix is simple add unlink($tmp) as in comment above. – NoAngel Jul 13 '18 at 11:06
1

The "mkdir" function raises a warning if the directory already exists, so you can catch this using "@mkdir" and avoid any race condition:

function tempDir($parent = null)
{
    // Prechecks
    if ($parent === null) {
        $parent = sys_get_temp_dir();
    }
    $parent = rtrim($parent, '/');
    if (!is_dir($parent) || !is_writeable($parent)) {
        throw new Exception(sprintf('Parent directory is not writable: %s', $parent));
    }

    // Create directory
    do  { 
        $directory = $parent . '/' . mt_rand();
        $success = @mkdir($directory);
    }
    while (!$success);

    return $directory; 
}
Romain
  • 573
  • 3
  • 8
  • OK, this could probably do with a guard in the while loop in case something crazy happens ($parent points to a file, for example), but the general mechanism looks sound - isn't this more robust than the two higher-rated answers? – Bobby Jack Mar 02 '16 at 10:57
  • I edited my answer to also check if the parent is a directory. So now it can go crazy only if disk is full. And yes I think it's better than the above because it's race safe. – Romain Mar 02 '16 at 15:01
1

There are a lot of overkill answers to this question. One simple answer would be:

$tempdir = tempnam(sys_get_temp_dir()) . 'dir';
mkdir($tempdir);
  1. Obtain a temporary file name.
  2. Create the directory (append a suffix to temp file, to avoid file name collision.)
  3. Done.
Amado Martinez
  • 429
  • 2
  • 8
  • 5
    Your approach has the side-effect of creating an unused temporary file and not removing it. – Seldom 'Where's Monica' Needy Oct 20 '16 at 00:16
  • For all of the effort in hours to get things to clean up correctly, you're almost better using this solution, and not cleaning it up. Instead have a cron or scheduled task go back and remove any directory that's over a day old, at the end of the day or something. I agree everything here is overkill, and I see it a lot. There will always be something the clip doesn't do, and either way, the OP didn't ask about cleanup they just wanted to know how to create a directory. Just noticed how old this is, oh well :) – Dan Chase Nov 11 '18 at 16:09
  • In case of cleanup, this is safe if you remove them in reverse order, removing temporary directory first and then the placeholder file (`$tempdir` without `dir` suffix). That way there is no race condition risk in the clean-up phase. – Zouppen Oct 05 '21 at 10:52
0

Another possibility is to use the temporal file as a kind of semaphore to guarantee the unicity of the directory name. Then, create a directory whose name is based on the file name.

define ('TMP_DIR', '/tmp'); // sys_get_temp_dir() PHP 5 >= 5.2.1
define ('TMP_DIR_PREFIX', 'tmpdir_');
define ('TMP_DIR_SUFFIX', '.d');

/* ************************************************************************** */

function createTmpDir() {
  $tmpFile = tempnam(TMP_DIR, TMP_DIR_PREFIX);
  $tmpDir = $tmpFile.TMP_DIR_SUFFIX;
  mkdir($tmpDir);
  return $tmpDir;
}

function rmTmpDir($tmpDir) {
  $offsetSuffix = -1 * strlen(TMP_DIR_SUFFIX);
  assert(strcmp(substr($tmpDir, $offsetSuffix), TMP_DIR_SUFFIX) === 0);
  $tmpFile = substr($tmpDir, 0, $offsetSuffix);

  // Removes non-empty directory
  $command = "rm -rf $tmpDir/";
  exec($command);
  // rmdir($tmpDir);

  unlink($tmpFile);
}

/* ************************************************************************** */
jap1968
  • 7,747
  • 1
  • 30
  • 37
0

Figure I will proved the easy answer. Just Set Prefix to your application specific string

$tmpDir = sprintf('%s%sPREFIX-%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, mt_rand());

mkdir($tmpDir);
Ricardo Saracino
  • 1,345
  • 2
  • 16
  • 37