|
|
@@ -0,0 +1,545 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+// Manage client core: configuration defaults, filesystem helpers, the
|
|
|
+// authenticated HTTP transport and version file handling.
|
|
|
+//
|
|
|
+// This is the only file a host project needs to require. It pulls in the rest
|
|
|
+// of the library, so both the CLI and the GUI panel start here:
|
|
|
+//
|
|
|
+// require_once __DIR__ . "/manage-client/lib/client.php";
|
|
|
+// $status = manageClientStatus();
|
|
|
+
|
|
|
+$manageClientConfigFile = dirname(__DIR__) . "/config.php";
|
|
|
+if (is_file($manageClientConfigFile)) {
|
|
|
+ require_once $manageClientConfigFile;
|
|
|
+}
|
|
|
+
|
|
|
+if (!defined("MANAGE_SERVER_URL")) {
|
|
|
+ define("MANAGE_SERVER_URL", "");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_INSTANCE")) {
|
|
|
+ define("MANAGE_INSTANCE", "");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_TOKEN")) {
|
|
|
+ define("MANAGE_TOKEN", "");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_HTTP_TIMEOUT")) {
|
|
|
+ define("MANAGE_HTTP_TIMEOUT", 15);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_HTTP_TIMEOUT_LONG")) {
|
|
|
+ define("MANAGE_HTTP_TIMEOUT_LONG", 300);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_APP_ROOT")) {
|
|
|
+ define("MANAGE_APP_ROOT", dirname(__DIR__, 2));
|
|
|
+}
|
|
|
+if (!defined("MANAGE_VERSION_FILE")) {
|
|
|
+ define("MANAGE_VERSION_FILE", MANAGE_APP_ROOT . "/VERSION");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_VERSION_CONSTANT")) {
|
|
|
+ define("MANAGE_VERSION_CONSTANT", null);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_WORK_DIR")) {
|
|
|
+ define("MANAGE_WORK_DIR", MANAGE_APP_ROOT . "/data/manage/work/");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_UPDATE_BACKUP_DIR")) {
|
|
|
+ define("MANAGE_UPDATE_BACKUP_DIR", MANAGE_APP_ROOT . "/data/manage/updates/");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_DIR")) {
|
|
|
+ define("MANAGE_BACKUP_DIR", MANAGE_APP_ROOT . "/data/manage/backups/");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_LOG_FILE")) {
|
|
|
+ define("MANAGE_LOG_FILE", MANAGE_APP_ROOT . "/data/manage/manage-client.log");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_UPDATE_PROTECTED_PATHS")) {
|
|
|
+ define("MANAGE_UPDATE_PROTECTED_PATHS", ["config.php", "data/", ".git/", "manage-client/config.php"]);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_UPDATE_SANITY_PATHS")) {
|
|
|
+ define("MANAGE_UPDATE_SANITY_PATHS", ["index.php"]);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_UPDATE_POST_HOOK")) {
|
|
|
+ define("MANAGE_UPDATE_POST_HOOK", null);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_MIGRATIONS_DIR")) {
|
|
|
+ define("MANAGE_MIGRATIONS_DIR", MANAGE_APP_ROOT . "/migrations");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_MIGRATIONS_STATE")) {
|
|
|
+ define("MANAGE_MIGRATIONS_STATE", MANAGE_APP_ROOT . "/data/manage/migrations.json");
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_SOURCES")) {
|
|
|
+ define("MANAGE_BACKUP_SOURCES", [["as" => "data", "glob" => "data/*.json"]]);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_DATABASE")) {
|
|
|
+ define("MANAGE_BACKUP_DATABASE", null);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_LOCAL_RETENTION")) {
|
|
|
+ define("MANAGE_BACKUP_LOCAL_RETENTION", 4);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_AUTO_INTERVAL_SECONDS")) {
|
|
|
+ define("MANAGE_BACKUP_AUTO_INTERVAL_SECONDS", 604800);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_COMPRESS")) {
|
|
|
+ define("MANAGE_BACKUP_COMPRESS", true);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_UPLOAD")) {
|
|
|
+ define("MANAGE_BACKUP_UPLOAD", true);
|
|
|
+}
|
|
|
+if (!defined("MANAGE_BACKUP_REMOTE_TARGETS")) {
|
|
|
+ define("MANAGE_BACKUP_REMOTE_TARGETS", []);
|
|
|
+}
|
|
|
+
|
|
|
+// Raised when a remote upload fails. Carries a scrubbed debug context that is
|
|
|
+// safe to log: credentials are never part of it.
|
|
|
+class ManageRemoteUploadException extends RuntimeException
|
|
|
+{
|
|
|
+ private array $debugContext;
|
|
|
+
|
|
|
+ public function __construct(string $message, array $debugContext = [])
|
|
|
+ {
|
|
|
+ parent::__construct($message);
|
|
|
+ $this->debugContext = $debugContext;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getDebugContext(): array
|
|
|
+ {
|
|
|
+ return $this->debugContext;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Paths
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function manageClientAppRoot(): string
|
|
|
+{
|
|
|
+ $root = realpath((string) MANAGE_APP_ROOT);
|
|
|
+ if ($root === false) {
|
|
|
+ throw new RuntimeException("MANAGE_APP_ROOT existiert nicht: " . MANAGE_APP_ROOT);
|
|
|
+ }
|
|
|
+
|
|
|
+ return rtrim($root, "/\\");
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientPath(string $relative): string
|
|
|
+{
|
|
|
+ return manageClientAppRoot() . DIRECTORY_SEPARATOR . ltrim($relative, "/\\");
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientNormalizePath(string $path): string
|
|
|
+{
|
|
|
+ return str_replace("\\", "/", $path);
|
|
|
+}
|
|
|
+
|
|
|
+function manageEnsureDir(string $dir): void
|
|
|
+{
|
|
|
+ if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
|
|
|
+ throw new RuntimeException("Verzeichnis konnte nicht erstellt werden: " . $dir);
|
|
|
+ }
|
|
|
+
|
|
|
+ @chmod($dir, 02775);
|
|
|
+}
|
|
|
+
|
|
|
+function manageRemoveDir(string $dir): void
|
|
|
+{
|
|
|
+ if (!is_dir($dir)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $items = new RecursiveIteratorIterator(
|
|
|
+ new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
|
|
|
+ RecursiveIteratorIterator::CHILD_FIRST,
|
|
|
+ );
|
|
|
+
|
|
|
+ foreach ($items as $item) {
|
|
|
+ if ($item->isDir()) {
|
|
|
+ @rmdir($item->getPathname());
|
|
|
+ } else {
|
|
|
+ @unlink($item->getPathname());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @rmdir($dir);
|
|
|
+}
|
|
|
+
|
|
|
+function manageIsTemporaryFile(string $path): bool
|
|
|
+{
|
|
|
+ $name = basename($path);
|
|
|
+
|
|
|
+ return $name === "" ||
|
|
|
+ $name[0] === "." ||
|
|
|
+ str_ends_with($name, ".tmp") ||
|
|
|
+ str_ends_with($name, ".part");
|
|
|
+}
|
|
|
+
|
|
|
+function manageFormatBytes(int $bytes): string
|
|
|
+{
|
|
|
+ if ($bytes >= 1073741824) {
|
|
|
+ return number_format($bytes / 1073741824, 2, ",", ".") . " GB";
|
|
|
+ }
|
|
|
+ if ($bytes >= 1048576) {
|
|
|
+ return number_format($bytes / 1048576, 2, ",", ".") . " MB";
|
|
|
+ }
|
|
|
+ if ($bytes >= 1024) {
|
|
|
+ return number_format($bytes / 1024, 1, ",", ".") . " KB";
|
|
|
+ }
|
|
|
+
|
|
|
+ return $bytes . " B";
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// JSON state files
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function manageReadJson(string $file): array
|
|
|
+{
|
|
|
+ if (!is_file($file)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $content = file_get_contents($file);
|
|
|
+ if ($content === false || trim($content) === "") {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $decoded = json_decode($content, true);
|
|
|
+
|
|
|
+ return is_array($decoded) ? $decoded : [];
|
|
|
+}
|
|
|
+
|
|
|
+function manageWriteJson(string $file, array $data): void
|
|
|
+{
|
|
|
+ manageEnsureDir(dirname($file));
|
|
|
+
|
|
|
+ $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
+ if ($json === false) {
|
|
|
+ throw new RuntimeException("JSON konnte nicht kodiert werden: " . basename($file));
|
|
|
+ }
|
|
|
+
|
|
|
+ $tmpFile = $file . ".tmp";
|
|
|
+ if (file_put_contents($tmpFile, $json . PHP_EOL, LOCK_EX) === false) {
|
|
|
+ throw new RuntimeException("Datei konnte nicht geschrieben werden: " . basename($file));
|
|
|
+ }
|
|
|
+
|
|
|
+ @chmod($tmpFile, 0664);
|
|
|
+ if (!rename($tmpFile, $file)) {
|
|
|
+ @unlink($tmpFile);
|
|
|
+ throw new RuntimeException("Datei konnte nicht gespeichert werden: " . basename($file));
|
|
|
+ }
|
|
|
+
|
|
|
+ @chmod($file, 0664);
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Logging
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+// Appends one JSON line. Never throws: a failed log write must not abort an
|
|
|
+// update or a backup.
|
|
|
+function manageClientLog(string $level, string $message, array $context = []): void
|
|
|
+{
|
|
|
+ $file = (string) MANAGE_LOG_FILE;
|
|
|
+ if ($file === "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ manageEnsureDir(dirname($file));
|
|
|
+ } catch (Throwable $exception) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Simple size cap; the host application owns its own log rotation.
|
|
|
+ if (is_file($file) && (int) (filesize($file) ?: 0) > 2097152) {
|
|
|
+ @rename($file, $file . ".1");
|
|
|
+ }
|
|
|
+
|
|
|
+ $line = json_encode([
|
|
|
+ "timestamp" => date("Y-m-d H:i:s"),
|
|
|
+ "level" => $level,
|
|
|
+ "message" => $message,
|
|
|
+ "context" => $context,
|
|
|
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
+
|
|
|
+ if (is_string($line)) {
|
|
|
+ @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Version file
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function manageIsVersionString(string $version): bool
|
|
|
+{
|
|
|
+ return preg_match('/^v\d+\.\d+\.\d+$/', trim($version)) === 1;
|
|
|
+}
|
|
|
+
|
|
|
+function manageVersionCompareValue(string $version): string
|
|
|
+{
|
|
|
+ return ltrim(trim($version), "vV");
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Reads the installed version. Supports both supported layouts:
|
|
|
+ * - a PHP file defining a constant (MANAGE_VERSION_CONSTANT)
|
|
|
+ * - a plain text file containing only the version
|
|
|
+ *
|
|
|
+ * Returns "" when the version cannot be determined, which the callers treat as
|
|
|
+ * "unknown" rather than as an error.
|
|
|
+ */
|
|
|
+function manageClientVersion(): string
|
|
|
+{
|
|
|
+ $file = (string) MANAGE_VERSION_FILE;
|
|
|
+ if ($file === "" || !is_file($file)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ $constant = MANAGE_VERSION_CONSTANT;
|
|
|
+ if (is_string($constant) && $constant !== "") {
|
|
|
+ // The constant may already be defined by the host application.
|
|
|
+ if (defined($constant)) {
|
|
|
+ $value = (string) constant($constant);
|
|
|
+ if (manageIsVersionString($value)) {
|
|
|
+ return trim($value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Otherwise parse it out of the file without executing it: the file may
|
|
|
+ // have side effects, and including it twice would fatal on redefinition.
|
|
|
+ $content = (string) file_get_contents($file);
|
|
|
+ $pattern = '/define\s*\(\s*["\']' . preg_quote($constant, "/") . '["\']\s*,\s*["\'](v?\d+\.\d+\.\d+)["\']/';
|
|
|
+ if (preg_match($pattern, $content, $matches) === 1) {
|
|
|
+ return trim($matches[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ $value = trim((string) file_get_contents($file));
|
|
|
+
|
|
|
+ return manageIsVersionString($value) ? $value : "";
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// HTTP transport
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function manageClientConfigured(): bool
|
|
|
+{
|
|
|
+ return trim((string) MANAGE_SERVER_URL) !== "" &&
|
|
|
+ trim((string) MANAGE_INSTANCE) !== "" &&
|
|
|
+ trim((string) MANAGE_TOKEN) !== "";
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientRequireConfigured(): void
|
|
|
+{
|
|
|
+ if (manageClientConfigured()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new RuntimeException(
|
|
|
+ "Manage-Client ist nicht konfiguriert. MANAGE_SERVER_URL, MANAGE_INSTANCE und MANAGE_TOKEN " .
|
|
|
+ "müssen in manage-client/config.php gesetzt sein.",
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientEndpoint(string $path): string
|
|
|
+{
|
|
|
+ $base = rtrim(trim((string) MANAGE_SERVER_URL), "/");
|
|
|
+
|
|
|
+ return $base . "/api/v1/" . ltrim($path, "/");
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientUserAgent(): string
|
|
|
+{
|
|
|
+ $version = manageClientVersion();
|
|
|
+
|
|
|
+ return "Manage-Client/1.0 (" . (string) MANAGE_INSTANCE . "; app " . ($version !== "" ? $version : "unknown") . ")";
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientAuthHeaders(): string
|
|
|
+{
|
|
|
+ return "X-Manage-Instance: " . (string) MANAGE_INSTANCE . "\r\n" .
|
|
|
+ "X-Manage-Token: " . (string) MANAGE_TOKEN . "\r\n" .
|
|
|
+ "User-Agent: " . manageClientUserAgent() . "\r\n";
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientStatusFromHeaders(array $headers): int
|
|
|
+{
|
|
|
+ $status = 0;
|
|
|
+ foreach ($headers as $header) {
|
|
|
+ if (preg_match('/^HTTP\/\S+\s+(\d+)/', (string) $header, $matches) === 1) {
|
|
|
+ $status = (int) $matches[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $status;
|
|
|
+}
|
|
|
+
|
|
|
+function manageClientResponseHeaders($legacyHeaders): array
|
|
|
+{
|
|
|
+ if (function_exists("http_get_last_response_headers")) {
|
|
|
+ $lastHeaders = http_get_last_response_headers();
|
|
|
+ return is_array($lastHeaders) ? $lastHeaders : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return is_array($legacyHeaders) ? $legacyHeaders : [];
|
|
|
+}
|
|
|
+
|
|
|
+// Turns a server error response into a message worth reading. The API always
|
|
|
+// answers with {"success":false,"error":"..."}; anything else is truncated.
|
|
|
+function manageClientErrorMessage(int $status, $body): string
|
|
|
+{
|
|
|
+ $suffix = $status > 0 ? " (HTTP " . $status . ")" : "";
|
|
|
+
|
|
|
+ if (is_string($body) && $body !== "") {
|
|
|
+ $decoded = json_decode($body, true);
|
|
|
+ if (is_array($decoded) && isset($decoded["error"])) {
|
|
|
+ return trim((string) $decoded["error"]) . $suffix;
|
|
|
+ }
|
|
|
+
|
|
|
+ $excerpt = substr(trim(preg_replace('/\s+/', " ", $body) ?? ""), 0, 300);
|
|
|
+ if ($excerpt !== "") {
|
|
|
+ return $excerpt . $suffix;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "Anfrage fehlgeschlagen" . ($suffix !== "" ? $suffix : " (keine Antwort vom Server)") . ".";
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Performs an authenticated request against the manage server.
|
|
|
+ *
|
|
|
+ * @param string $method GET or POST
|
|
|
+ * @param string $path endpoint below api/v1/
|
|
|
+ * @param string|null $body raw request body for POST
|
|
|
+ * @param string $contentType
|
|
|
+ * @param int|null $timeout seconds; defaults to MANAGE_HTTP_TIMEOUT
|
|
|
+ *
|
|
|
+ * @return array{status: int, body: string}
|
|
|
+ */
|
|
|
+function manageClientRequest(
|
|
|
+ string $method,
|
|
|
+ string $path,
|
|
|
+ ?string $body = null,
|
|
|
+ string $contentType = "application/json",
|
|
|
+ ?int $timeout = null,
|
|
|
+): array {
|
|
|
+ manageClientRequireConfigured();
|
|
|
+
|
|
|
+ $url = manageClientEndpoint($path);
|
|
|
+ if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
|
|
+ throw new RuntimeException("Ungültige Server-URL: " . $url);
|
|
|
+ }
|
|
|
+
|
|
|
+ $headers = manageClientAuthHeaders() . "Accept: application/json\r\n";
|
|
|
+ $options = [
|
|
|
+ "method" => $method,
|
|
|
+ "timeout" => $timeout ?? (int) MANAGE_HTTP_TIMEOUT,
|
|
|
+ "ignore_errors" => true,
|
|
|
+ "follow_location" => 0,
|
|
|
+ "protocol_version" => 1.1,
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($body !== null) {
|
|
|
+ $headers .= "Content-Type: " . $contentType . "\r\n";
|
|
|
+ $headers .= "Content-Length: " . strlen($body) . "\r\n";
|
|
|
+ $options["content"] = $body;
|
|
|
+ }
|
|
|
+
|
|
|
+ $options["header"] = $headers;
|
|
|
+ $context = stream_context_create(["http" => $options]);
|
|
|
+
|
|
|
+ $response = @file_get_contents($url, false, $context);
|
|
|
+ $status = manageClientStatusFromHeaders(manageClientResponseHeaders($http_response_header ?? null));
|
|
|
+
|
|
|
+ if ($response === false && $status === 0) {
|
|
|
+ throw new RuntimeException("Manage-Server ist nicht erreichbar: " . $url);
|
|
|
+ }
|
|
|
+
|
|
|
+ return ["status" => $status, "body" => is_string($response) ? $response : ""];
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Authenticated request that expects a JSON object and a 2xx status.
|
|
|
+ */
|
|
|
+function manageClientRequestJson(
|
|
|
+ string $method,
|
|
|
+ string $path,
|
|
|
+ ?array $payload = null,
|
|
|
+ ?int $timeout = null,
|
|
|
+): array {
|
|
|
+ $body = $payload === null ? null : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
+ if ($payload !== null && $body === false) {
|
|
|
+ throw new RuntimeException("Anfrage konnte nicht kodiert werden.");
|
|
|
+ }
|
|
|
+
|
|
|
+ $response = manageClientRequest($method, $path, $body, "application/json", $timeout);
|
|
|
+
|
|
|
+ if ($response["status"] < 200 || $response["status"] >= 300) {
|
|
|
+ throw new RuntimeException(manageClientErrorMessage($response["status"], $response["body"]));
|
|
|
+ }
|
|
|
+
|
|
|
+ $decoded = json_decode($response["body"], true);
|
|
|
+ if (!is_array($decoded)) {
|
|
|
+ throw new RuntimeException("Antwort des Servers ist kein gültiges JSON.");
|
|
|
+ }
|
|
|
+
|
|
|
+ return $decoded;
|
|
|
+}
|
|
|
+
|
|
|
+require_once __DIR__ . "/zip.php";
|
|
|
+require_once __DIR__ . "/mysql.php";
|
|
|
+require_once __DIR__ . "/remote.php";
|
|
|
+require_once __DIR__ . "/backup.php";
|
|
|
+require_once __DIR__ . "/hooks.php";
|
|
|
+require_once __DIR__ . "/updater.php";
|
|
|
+require_once __DIR__ . "/heartbeat.php";
|
|
|
+
|
|
|
+/**
|
|
|
+ * Aggregate status used by the CLI, the GUI panel and any host integration.
|
|
|
+ * Never throws: every remote failure is reported inside the returned array, so
|
|
|
+ * a settings page can render even when the server is unreachable.
|
|
|
+ */
|
|
|
+function manageClientStatus(): array
|
|
|
+{
|
|
|
+ $status = [
|
|
|
+ "instance" => (string) MANAGE_INSTANCE,
|
|
|
+ "server_url" => (string) MANAGE_SERVER_URL,
|
|
|
+ "configured" => manageClientConfigured(),
|
|
|
+ "version" => manageClientVersion(),
|
|
|
+ "php_version" => PHP_VERSION,
|
|
|
+ "update" => null,
|
|
|
+ "update_error" => null,
|
|
|
+ "backups" => [],
|
|
|
+ "last_backup_at" => null,
|
|
|
+ "pending_migrations" => [],
|
|
|
+ "errors" => [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ $status["backups"] = manageBackupList();
|
|
|
+ $status["last_backup_at"] = $status["backups"] === []
|
|
|
+ ? null
|
|
|
+ : (string) ($status["backups"][0]["created_at"] ?? "");
|
|
|
+ } catch (Throwable $exception) {
|
|
|
+ $status["errors"][] = $exception->getMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $status["pending_migrations"] = manageUpdatePendingMigrations();
|
|
|
+ } catch (Throwable $exception) {
|
|
|
+ $status["errors"][] = $exception->getMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($status["configured"]) {
|
|
|
+ try {
|
|
|
+ $status["update"] = manageUpdateCheck();
|
|
|
+ } catch (Throwable $exception) {
|
|
|
+ $status["update_error"] = $exception->getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $status;
|
|
|
+}
|