Files
speedBB/app/Http/Controllers/SystemStatusController.php

211 lines
7.3 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\Process\Process;
class SystemStatusController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$user = $request->user();
if (!$user || !$user->roles()->where('name', 'ROLE_ADMIN')->exists()) {
return response()->json(['message' => 'Forbidden'], 403);
}
$phpDefaultPath = $this->resolveBinary('php');
$phpDefaultVersion = $phpDefaultPath ? $this->resolvePhpVersion($phpDefaultPath) : null;
$phpConfiguredPath = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
$phpSelectedPath = null;
$phpSelectedVersion = null;
$phpSelectedOk = false;
if ($phpConfiguredPath !== '') {
$resolvedConfiguredPhpPath = $this->resolveConfiguredPhpBinaryPath($phpConfiguredPath);
$phpSelectedPath = $resolvedConfiguredPhpPath ?: $phpConfiguredPath;
$phpSelectedVersion = $resolvedConfiguredPhpPath ? $this->resolvePhpVersion($resolvedConfiguredPhpPath) : null;
$phpSelectedOk = $resolvedConfiguredPhpPath !== null && $phpSelectedVersion !== null;
} else {
$phpSelectedPath = PHP_BINARY ?: $phpDefaultPath;
$phpSelectedVersion = $phpSelectedPath
? ($this->resolvePhpVersion($phpSelectedPath) ?? $phpDefaultVersion ?? PHP_VERSION)
: null;
$phpSelectedOk = $phpSelectedPath !== null && $phpSelectedVersion !== null;
}
$minVersions = $this->resolveMinVersions();
$composerPath = $this->resolveBinary('composer');
$nodePath = $this->resolveBinary('node');
$npmPath = $this->resolveBinary('npm');
$tarPath = $this->resolveBinary('tar');
$rsyncPath = $this->resolveBinary('rsync');
$procFunctions = [
'proc_open',
'proc_get_status',
'proc_close',
];
$disabledFunctions = array_filter(array_map('trim', explode(',', (string) ini_get('disable_functions'))));
$disabledLookup = array_fill_keys($disabledFunctions, true);
$procFunctionStatus = [];
foreach ($procFunctions as $function) {
$procFunctionStatus[$function] = function_exists($function) && !isset($disabledLookup[$function]);
}
return response()->json([
'php' => PHP_VERSION,
'php_web_path' => PHP_BINARY ?: null,
'php_web_version' => PHP_VERSION ?: null,
'php_default' => $phpDefaultPath,
'php_default_version' => $phpDefaultVersion,
'php_configured' => $phpConfiguredPath ?: null,
'php_selected_path' => $phpSelectedPath,
'php_selected_ok' => $phpSelectedOk,
'php_selected_version' => $phpSelectedVersion,
'min_versions' => $minVersions,
'composer' => $composerPath,
'composer_version' => $this->resolveBinaryVersion($composerPath, ['--version']),
'node' => $nodePath,
'node_version' => $this->resolveBinaryVersion($nodePath, ['--version']),
'npm' => $npmPath,
'npm_version' => $this->resolveBinaryVersion($npmPath, ['--version']),
'tar' => $tarPath,
'tar_version' => $this->resolveBinaryVersion($tarPath, ['--version']),
'rsync' => $rsyncPath,
'rsync_version' => $this->resolveBinaryVersion($rsyncPath, ['--version']),
'proc_functions' => $procFunctionStatus,
'storage_writable' => is_writable(storage_path()),
'storage_public_linked' => $this->isPublicStorageLinked(),
'updates_writable' => is_writable(storage_path('app/updates')) || @mkdir(storage_path('app/updates'), 0755, true),
]);
}
private function isPublicStorageLinked(): bool
{
$publicStorage = public_path('storage');
$storagePublic = storage_path('app/public');
if (!is_link($publicStorage)) {
return false;
}
$target = readlink($publicStorage);
if ($target === false) {
return false;
}
$targetPath = $target;
if (!str_starts_with($target, DIRECTORY_SEPARATOR) && !preg_match('/^[A-Za-z]:[\\\\\\/]/', $target)) {
$targetPath = dirname($publicStorage) . DIRECTORY_SEPARATOR . $target;
}
$resolvedTarget = realpath($targetPath);
$expectedTarget = realpath($storagePublic);
return $resolvedTarget !== false && $expectedTarget !== false && $resolvedTarget === $expectedTarget;
}
private function resolveBinary(string $name): ?string
{
$process = new Process(['sh', '-lc', "command -v {$name}"]);
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
return $output !== '' ? $output : null;
}
private function resolvePhpVersion(string $path): ?string
{
$process = new Process([$path, '-r', 'echo PHP_VERSION;']);
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
return $output !== '' ? $output : null;
}
private function resolveConfiguredPhpBinaryPath(string $binary): ?string
{
$value = trim($binary);
if ($value === '') {
return null;
}
if (str_contains($value, '/')) {
return is_executable($value) ? $value : null;
}
return $this->resolveBinary($value);
}
private function resolveBinaryVersion(?string $path, array $args): ?string
{
if (!$path) {
return null;
}
$process = new Process(array_merge([$path], $args));
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
if ($output === '') {
return null;
}
$line = strtok($output, "\n") ?: $output;
if (preg_match('/(\\d+\\.\\d+(?:\\.\\d+)?)/', $line, $matches)) {
return $matches[1];
}
return null;
}
private function resolveMinVersions(): array
{
$composerJson = $this->readJson(base_path('composer.json'));
$packageJson = $this->readJson(base_path('package.json'));
$php = $composerJson['require']['php'] ?? null;
$node = $packageJson['engines']['node'] ?? null;
$npm = $packageJson['engines']['npm'] ?? null;
$composer = $composerJson['require']['composer-runtime-api'] ?? null;
return [
'php' => is_string($php) ? $php : null,
'node' => is_string($node) ? $node : null,
'npm' => is_string($npm) ? $npm : null,
'composer' => is_string($composer) ? $composer : null,
];
}
private function readJson(string $path): array
{
if (!is_file($path)) {
return [];
}
$contents = file_get_contents($path);
if ($contents === false) {
return [];
}
$data = json_decode($contents, true);
return is_array($data) ? $data : [];
}
}