Files
deepseek beaf0e1f37 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
2026-08-23 07:10:30 +00:00

136 lines
3.8 KiB
PHP

<?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;
}
}