Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
/**
|
||||
* Hardened process runner. Arguments are ALWAYS passed as an array and
|
||||
* escaped by proc_open's non-shell mode — user input can never become a
|
||||
* shell command. Timeouts, output caps and resource limits are enforced.
|
||||
*/
|
||||
final class Process
|
||||
{
|
||||
private int $exitCode = -1;
|
||||
private string $stdout = '';
|
||||
private string $stderr = '';
|
||||
|
||||
public function __construct(
|
||||
/** @var list<string> */
|
||||
private readonly array $command,
|
||||
private readonly int $timeoutSeconds = 300,
|
||||
private readonly int $maxOutputBytes = 2_000_000,
|
||||
private ?string $stdin = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function binary(string $name, array $args, int $timeout = 300): self
|
||||
{
|
||||
$site = config('Site');
|
||||
$bin = $site->binaries[$name] ?? null;
|
||||
|
||||
if ($bin === null || ! is_executable($bin)) {
|
||||
throw new \RuntimeException("Binary '{$name}' is not available on this host.");
|
||||
}
|
||||
|
||||
return new self([$bin, ...array_map('strval', array_values($args))], $timeout);
|
||||
}
|
||||
|
||||
public function withStdin(?string $data): self
|
||||
{
|
||||
$this->stdin = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(): bool
|
||||
{
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$process = proc_open($this->command, $descriptors, $pipes, sys_get_temp_dir(), [
|
||||
'LC_ALL' => 'C',
|
||||
'HOME' => rtrim(WRITEPATH, '/'),
|
||||
'PATH' => '/usr/bin:/bin',
|
||||
]);
|
||||
|
||||
if (! is_resource($process)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
if ($this->stdin !== null) {
|
||||
fwrite($pipes[0], $this->stdin);
|
||||
}
|
||||
fclose($pipes[0]);
|
||||
|
||||
$start = microtime(true);
|
||||
$timedOut = false;
|
||||
$outLen = 0;
|
||||
|
||||
// NB: proc_get_status() reports a valid 'exitcode' only on the FIRST
|
||||
// call that observes running === false — capture it right there.
|
||||
while (true) {
|
||||
$status = proc_get_status($process);
|
||||
if ($status === false || ! $status['running']) {
|
||||
$this->exitCode = (int) ($status['exitcode'] ?? -1);
|
||||
break;
|
||||
}
|
||||
// drain pipes so the child never blocks on full buffers
|
||||
foreach ([1, 2] as $i) {
|
||||
if ($outLen < $this->maxOutputBytes) {
|
||||
$chunk = fread($pipes[$i], 65536);
|
||||
$this->{$i === 1 ? 'stdout' : 'stderr'} .= (string) $chunk;
|
||||
$outLen += strlen((string) $chunk);
|
||||
}
|
||||
}
|
||||
if (microtime(true) - $start > $this->timeoutSeconds) {
|
||||
$timedOut = true;
|
||||
proc_terminate($process, 9);
|
||||
break;
|
||||
}
|
||||
usleep(10_000);
|
||||
}
|
||||
|
||||
foreach ([1, 2] as $i) {
|
||||
$rest = stream_get_contents($pipes[$i]) ?: '';
|
||||
$this->{$i === 1 ? 'stdout' : 'stderr'} .= substr($rest, 0, $this->maxOutputBytes);
|
||||
fclose($pipes[$i]);
|
||||
}
|
||||
proc_close($process);
|
||||
|
||||
if ($timedOut) {
|
||||
log_message('warning', 'Process timed out after {t}s: {cmd}', ['t' => $this->timeoutSeconds, 'cmd' => basename((string) ($this->command[0] ?? ''))]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->exitCode === 0;
|
||||
}
|
||||
|
||||
public function ok(): bool
|
||||
{
|
||||
return $this->exitCode === 0;
|
||||
}
|
||||
|
||||
public function exit(): int
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
public function out(): string
|
||||
{
|
||||
return $this->stdout;
|
||||
}
|
||||
|
||||
public function err(): string
|
||||
{
|
||||
return $this->stderr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user