- 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
136 lines
3.8 KiB
PHP
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;
|
|
}
|
|
}
|