Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Latest
------

## Fixes
* [#343](https://github.com/cleverage/process-bundle/issues/343) The temporary files of `FileSplitterTask` (`file_splitter`), `CsvSplitterTask` (`csv_splitter`) and of the JSON buffer of `ProcessLauncherTask` (`json_buffering`) are created with a random name, exclusively (an existing path, symbolic links included, is never reused) and readable by their owner only, instead of a name derived from the time (`uniqid()`), opened in `w` mode with the permissions of the umask. Update documentation, add tests.

v5.4.1
-----

Expand Down
5 changes: 3 additions & 2 deletions docs/reference/tasks/csv_splitter_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Accepted inputs
Possible outputs
----------------

`string`: path of a temporary CSV file (created in the system temporary directory) containing the headers and the next
`max_lines` data lines of the source file (the last file may contain fewer lines).
`string`: path of a temporary CSV file (created in the system temporary directory, with a random name and readable by
its owner only) containing the headers and the next `max_lines` data lines of the source file (the last file may
contain fewer lines).

Options
-------
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/tasks/file_splitter_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ file path as input. Any other input is ignored.
Possible outputs
----------------

`string`: path of a temporary file (created in the system temporary directory, with a `.tmp` extension) containing the
next `max_lines` lines of the source file (the last file may contain fewer lines).
`string`: path of a temporary file (created in the system temporary directory, with a random name, a `.tmp` extension
and readable by its owner only) containing the next `max_lines` lines of the source file (the last file may contain
fewer lines).

Options
-------
Expand Down
41 changes: 41 additions & 0 deletions src/Filesystem/TemporaryFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Filesystem;

use CleverAge\ProcessBundle\Exception\RuntimeException;

/**
* Creates the temporary files of the bundle in the system temporary directory: random name, exclusive creation (an
* existing path, symbolic links included, is never reused) and readable by their owner only.
*
* @internal
*/
final class TemporaryFile
{
/**
* Creates an empty file and returns its path.
*/
public static function create(string $prefix, string $suffix = ''): string
{
$path = sys_get_temp_dir().\DIRECTORY_SEPARATOR.$prefix.bin2hex(random_bytes(16)).$suffix;
$file = @fopen($path, 'x');
if (false === $file) {
throw new RuntimeException("Unable to create temporary file {$path}");
}
fclose($file);
chmod($path, 0o600);

return $path;
}
}
10 changes: 7 additions & 3 deletions src/Model/SubprocessInstance.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace CleverAge\ProcessBundle\Model;

use CleverAge\ProcessBundle\Exception\RuntimeException;
use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
Expand All @@ -29,7 +30,10 @@ class SubprocessInstance

protected Process $process;

protected string $bufferPath;
/**
* JSON buffer, in the system temporary directory (the project directory may be read-only, e.g. in a container).
*/
protected string $bufferPath = '';

/**
* @var array<string, mixed>
Expand Down Expand Up @@ -75,8 +79,6 @@ public function __construct(
$consolePath = $consolePath->getProjectDir().'/bin/console';
}
$this->consolePath = $consolePath;
// The project directory may be read-only (e.g. in a container)
$this->bufferPath = sys_get_temp_dir().'/cdm_buffer_'.uniqid('', true).'.json-stream';
}

/**
Expand Down Expand Up @@ -104,6 +106,8 @@ public function buildProcess(): static
}

if ($this->options[self::OPTION_JSON_BUFFERING]) {
// Created before the sub-process writes into it: random name, exclusive creation, owner only
$this->bufferPath = TemporaryFile::create('cdm_buffer_', '.json-stream');
$arguments = [...$arguments, '--output='.$this->bufferPath, '--output-format=json-stream'];
}

Expand Down
3 changes: 2 additions & 1 deletion src/Task/File/Csv/CsvSplitterTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CleverAge\ProcessBundle\Exception\RuntimeException;
use CleverAge\ProcessBundle\Filesystem\CsvFile;
use CleverAge\ProcessBundle\Filesystem\CsvResource;
use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;

Expand Down Expand Up @@ -93,7 +94,7 @@ public function finalize(ProcessState $state): void

protected function splitCsv(CsvResource $csv, int $maxLines): string
{
$tmpFilePath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_'.uniqid('process', false).'.csv';
$tmpFilePath = TemporaryFile::create('php_process', '.csv');
$tmpFile = fopen($tmpFilePath, 'wb+');
if (false === $tmpFile) {
throw new RuntimeException("Unable to open temporary file {$tmpFilePath}");
Expand Down
3 changes: 2 additions & 1 deletion src/Task/File/FileSplitterTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace CleverAge\ProcessBundle\Task\File;

use CleverAge\ProcessBundle\Filesystem\SplFile;
use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\IterableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessState;
Expand Down Expand Up @@ -78,7 +79,7 @@ public function next(ProcessState $state): bool

protected function splitFile(SplFile $file, int $maxLines): string
{
$tmpFilePath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_'.uniqid('process', false).'.tmp';
$tmpFilePath = TemporaryFile::create('php_process', '.tmp');
$splitFile = new SplFile($tmpFilePath, 'wb', []);

$writtenLines = 0;
Expand Down
47 changes: 47 additions & 0 deletions tests/Filesystem/TemporaryFileTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Tests\Filesystem;

use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use PHPUnit\Framework\TestCase;

#[\PHPUnit\Framework\Attributes\CoversClass(TemporaryFile::class)]
class TemporaryFileTest extends TestCase
{
public function testAnEmptyFileReadableByItsOwnerOnlyIsCreated(): void
{
$path = TemporaryFile::create('temporary_file_test_', '.csv');
try {
self::assertMatchesRegularExpression(
'#^'.preg_quote(sys_get_temp_dir(), '#').'/temporary_file_test_[0-9a-f]{32}\.csv$#',
$path
);
self::assertFileExists($path);
self::assertSame(0, filesize($path));
self::assertSame(0o600, fileperms($path) & 0o777);
} finally {
unlink($path);
}
}

public function testEachFileHasItsOwnName(): void
{
$first = TemporaryFile::create('temporary_file_test_');
$second = TemporaryFile::create('temporary_file_test_');
unlink($first);
unlink($second);

self::assertNotSame($first, $second);
}
}
11 changes: 10 additions & 1 deletion tests/Model/SubprocessInstanceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@

namespace CleverAge\ProcessBundle\Tests\Model;

use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use CleverAge\ProcessBundle\Model\SubprocessInstance;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;

#[CoversClass(SubprocessInstance::class)]
#[UsesClass(TemporaryFile::class)]
class SubprocessInstanceTest extends TestCase
{
private string $projectDir;
Expand Down Expand Up @@ -61,9 +64,15 @@ public function testJsonBufferIsWrittenInTheTemporaryDirectory(): void
->buildProcess();

$this->assertMatchesRegularExpression(
'#\'--output='.preg_quote(sys_get_temp_dir(), '#').'/cdm_buffer_[^/\']+\.json-stream\' \'--output-format=json-stream\'#',
'#\'--output=('.preg_quote(sys_get_temp_dir(), '#').'/cdm_buffer_[0-9a-f]{32}\.json-stream)\' \'--output-format=json-stream\'#',
$subprocess->getProcess()->getCommandLine()
);
preg_match('#--output=([^\']+)#', $subprocess->getProcess()->getCommandLine(), $matches);
$bufferPath = $matches[1] ?? '';
// Created before the sub-process writes into it, readable by its owner only
$this->assertFileExists($bufferPath);
$this->assertSame(0o600, fileperms($bufferPath) & 0o777);
unlink($bufferPath);
}

public function testMissingConsoleThrows(): void
Expand Down
2 changes: 2 additions & 0 deletions tests/Task/File/Csv/CsvSplitterTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
use CleverAge\ProcessBundle\Filesystem\CsvFile;
use CleverAge\ProcessBundle\Filesystem\CsvResource;
use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessHistory;
use CleverAge\ProcessBundle\Model\ProcessState;
Expand All @@ -33,6 +34,7 @@
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;

#[\PHPUnit\Framework\Attributes\CoversClass(CsvSplitterTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(TemporaryFile::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(CsvReaderTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(InputCsvReaderTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractCsvTask::class)]
Expand Down
2 changes: 2 additions & 0 deletions tests/Task/File/FileSplitterTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
use CleverAge\ProcessBundle\Filesystem\SplFile;
use CleverAge\ProcessBundle\Filesystem\TemporaryFile;
use CleverAge\ProcessBundle\Model\ProcessHistory;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Task\File\FileSplitterTask;
Expand All @@ -25,6 +26,7 @@
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;

#[\PHPUnit\Framework\Attributes\CoversClass(FileSplitterTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(TemporaryFile::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(SplFile::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfiguration::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(TaskConfiguration::class)]
Expand Down
Loading