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