Browse Source

Add log rotation and retention settings for error and access logs
- Implemented log rotation functionality in the logging system to manage log file sizes and retention.

Josef Straßl 1 tháng trước cách đây
mục cha
commit
cada25a903

+ 5 - 0
config.sample.php

@@ -100,6 +100,11 @@ define('BACKUP_REMOTE_TARGETS', [
     // ],
 ]);
 
+// Log retention settings
+define('LOG_MAX_BYTES', 1048576);
+define('LOG_KEEP_FILES', 5);
+define('LOG_MAX_AGE_SECONDS', 2592000);
+
 // Session settings
 if (session_status() === PHP_SESSION_NONE) {
     $isHttps =

+ 1 - 0
docs/BACKUP_CONFIGURATION.md

@@ -224,4 +224,5 @@ Uploads von unbekannten Instanzen werden abgelehnt. Entfernte Instanzen können
 - `data/` muss für PHP beschreibbar sein.
 - Auf Apache-Hosting muss `.htaccess` aktiv bleiben, damit `data/backups/` nicht direkt erreichbar ist.
 - Remote-Ziele sollten nach Konfigurationsänderungen mit einem manuellen Backup getestet werden.
+- Remote-Backup-Erfolge werden knapp in `data/logs/access.log` protokolliert; Fehler mit Debug-Kontext in `data/logs/error.log`. Die App-Logs rotieren nach `LOG_MAX_BYTES` und entfernen alte rotierte Logs nach `LOG_MAX_AGE_SECONDS`.
 - Lokale Backups sollten nur über den authentifizierten Adminbereich heruntergeladen werden.

+ 1 - 0
docs/CONFIG_REFERENCE.md

@@ -57,6 +57,7 @@ Der Startseiten-Introtext wird unter **FAQ** gepflegt (`startpage_intro_text` in
 - Rate-Limit-Zähler liegen unter `data/ratelimit/` (wird bei Bedarf angelegt).
 - Wenn das Verzeichnis nicht beschreibbar ist, gelten Limits als **nicht aktiv** (Anfragen werden zugelassen — Verfügbarkeit auf Shared Hosting).
 - Zugriffs- und Fehlerprotokolle: `data/logs/` (siehe `logAccess` / `logError` in `includes/functions.php`).
+- Logs werden ab `LOG_MAX_BYTES` rotiert, es bleiben `LOG_KEEP_FILES` rotierte Dateien erhalten, und rotierte Logs älter als `LOG_MAX_AGE_SECONDS` werden entfernt.
 
 ## Backups
 

+ 161 - 7
includes/backup.php

@@ -15,6 +15,22 @@ if (!defined("BACKUP_REMOTE_TARGETS")) {
     define("BACKUP_REMOTE_TARGETS", []);
 }
 
+class BackupRemoteUploadException 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;
+    }
+}
+
 function backupGetDirectory(): string
 {
     return rtrim((string) BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR;
@@ -422,6 +438,87 @@ function backupGetTargetLabel(array $target, int $index): string
     return $type . "-" . ($index + 1);
 }
 
+function backupGetSafeTargetContext(array $target): array
+{
+    $safe = [];
+    $allowedKeys = [
+        "name",
+        "type",
+        "url",
+        "instance",
+        "bucket",
+        "region",
+        "prefix",
+        "endpoint",
+        "host",
+        "port",
+        "username",
+        "path",
+        "file",
+        "callback",
+        "timeout",
+    ];
+
+    foreach ($allowedKeys as $key) {
+        if (array_key_exists($key, $target)) {
+            $safe[$key] = is_scalar($target[$key]) ? (string) $target[$key] : gettype($target[$key]);
+        }
+    }
+
+    return $safe;
+}
+
+function backupGetHttpUserAgent(): string
+{
+    $version = defined("APP_VERSION") ? trim((string) APP_VERSION) : "";
+    if ($version === "") {
+        $version = "unknown";
+    }
+
+    return "PSA-Orderform-Backup/" . $version;
+}
+
+function backupFormatResponseExcerpt($response): string
+{
+    if (!is_string($response) || $response === "") {
+        return "";
+    }
+
+    $response = preg_replace('/\s+/', " ", trim($response));
+    if (!is_string($response)) {
+        return "";
+    }
+
+    return substr($response, 0, 500);
+}
+
+function backupFormatHeaderExcerpt(array $headers): array
+{
+    $result = [];
+    foreach ($headers as $header) {
+        $header = trim((string) $header);
+        if ($header === "") {
+            continue;
+        }
+        $result[] = substr($header, 0, 500);
+        if (count($result) >= 20) {
+            break;
+        }
+    }
+
+    return $result;
+}
+
+function backupGetLastPhpErrorMessage(): string
+{
+    $error = error_get_last();
+    if (!is_array($error)) {
+        return "";
+    }
+
+    return substr(trim((string) ($error["message"] ?? "")), 0, 500);
+}
+
 function backupRemoteCapabilities(): array
 {
     $targets = backupGetRemoteTargets();
@@ -546,19 +643,21 @@ function backupUploadToS3(string $archivePath, array $metadata, array $target):
         "http" => [
             "method" => "PUT",
             "timeout" => (int) ($target["timeout"] ?? 120),
-            "ignore_errors" => false,
+            "ignore_errors" => true,
             "header" =>
                 "Content-Type: application/zip\r\n" .
                 "Content-Length: " . strlen($payload) . "\r\n" .
                 "Host: " . $host . "\r\n" .
                 "X-Amz-Date: " . $now . "\r\n" .
                 "X-Amz-Content-Sha256: " . $payloadHash . "\r\n" .
-                "Authorization: " . $authorization . "\r\n",
+                "Authorization: " . $authorization . "\r\n" .
+                "User-Agent: " . backupGetHttpUserAgent() . "\r\n",
             "content" => $payload,
         ],
     ]);
 
     $response = @file_get_contents($url, false, $context);
+    $phpError = $response === false ? backupGetLastPhpErrorMessage() : "";
     if (function_exists("http_get_last_response_headers")) {
         $lastHeaders = http_get_last_response_headers();
         $headers = is_array($lastHeaders) ? $lastHeaders : [];
@@ -571,8 +670,18 @@ function backupUploadToS3(string $archivePath, array $metadata, array $target):
     $status = backupGetHttpStatusFromHeaders($headers);
 
     if ($response === false || $status < 200 || $status >= 300) {
-        throw new RuntimeException(
+        throw new BackupRemoteUploadException(
             "S3-Upload fehlgeschlagen" . ($status > 0 ? " (HTTP " . $status . ")" : "") . ".",
+            [
+                "http_status" => $status,
+                "response_excerpt" => backupFormatResponseExcerpt($response),
+                "response_headers" => backupFormatHeaderExcerpt($headers),
+                "php_error" => $phpError,
+                "bucket" => $bucket,
+                "region" => $region,
+                "key" => $key,
+                "endpoint" => $endpoint,
+            ],
         );
     }
 
@@ -732,6 +841,8 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
             "header" =>
                 "Content-Type: multipart/form-data; boundary=" .
                 $boundary .
+                "\r\nAccept: application/json\r\nUser-Agent: " .
+                backupGetHttpUserAgent() .
                 "\r\nContent-Length: " .
                 strlen($body) .
                 "\r\n",
@@ -740,6 +851,7 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
     ]);
 
     $response = @file_get_contents($url, false, $context);
+    $phpError = $response === false ? backupGetLastPhpErrorMessage() : "";
     if (function_exists("http_get_last_response_headers")) {
         $lastHeaders = http_get_last_response_headers();
         $headers = is_array($lastHeaders) ? $lastHeaders : [];
@@ -751,16 +863,32 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
     }
     $status = backupGetHttpStatusFromHeaders($headers);
     if ($response === false || $status < 200 || $status >= 300) {
-        throw new RuntimeException(
+        throw new BackupRemoteUploadException(
             "Managed-Backup-Upload fehlgeschlagen" . ($status > 0 ? " (HTTP " . $status . ")" : "") . ".",
+            [
+                "http_status" => $status,
+                "response_excerpt" => backupFormatResponseExcerpt($response),
+                "response_headers" => backupFormatHeaderExcerpt($headers),
+                "php_error" => $phpError,
+                "url" => $url,
+                "instance" => $instance,
+            ],
         );
     }
 
     $decoded = json_decode($response, true);
     if (!is_array($decoded) || empty($decoded["success"])) {
         $error = is_array($decoded) ? trim((string) ($decoded["error"] ?? "")) : "";
-        throw new RuntimeException(
+        throw new BackupRemoteUploadException(
             "Managed-Backup-Upload wurde abgelehnt" . ($error !== "" ? ": " . $error : "."),
+            [
+                "http_status" => $status,
+                "response_excerpt" => backupFormatResponseExcerpt($response),
+                "response_headers" => backupFormatHeaderExcerpt($headers),
+                "url" => $url,
+                "instance" => $instance,
+                "server_error" => $error,
+            ],
         );
     }
 
@@ -810,6 +938,7 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
         $type = trim((string) ($target["type"] ?? ""));
         $label = backupGetTargetLabel($target, (int) $index);
         $startedAt = date("c");
+        $safeTargetContext = backupGetSafeTargetContext($target);
 
         try {
             if ($type === "s3") {
@@ -824,7 +953,7 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
                 throw new RuntimeException("Unbekannter Backup-Zieltyp: " . $type);
             }
 
-            $results[] = array_merge(
+            $result = array_merge(
                 [
                     "target" => $label,
                     "type" => $type,
@@ -834,14 +963,39 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
                 ],
                 is_array($extra) ? $extra : [],
             );
+            $results[] = $result;
+
+            logAccess("Backup remote upload succeeded", [
+                "target" => $label,
+                "type" => $type,
+                "filename" => $metadata["filename"] ?? basename($archivePath),
+                "remote_path" => (string) ($result["remote_path"] ?? ""),
+            ]);
         } catch (Throwable $exception) {
-            $results[] = [
+            $debugContext = $exception instanceof BackupRemoteUploadException
+                ? $exception->getDebugContext()
+                : [];
+            $result = [
                 "target" => $label,
                 "type" => $type !== "" ? $type : "unknown",
                 "success" => false,
                 "started_at" => $startedAt,
                 "error" => $exception->getMessage(),
             ];
+            if (!empty($debugContext)) {
+                $result["debug"] = $debugContext;
+            }
+            $results[] = $result;
+
+            logError("Backup remote upload failed", [
+                "target" => $label,
+                "type" => $type !== "" ? $type : "unknown",
+                "target_config" => $safeTargetContext,
+                "filename" => $metadata["filename"] ?? basename($archivePath),
+                "sha256" => $metadata["sha256"] ?? "",
+                "error" => $exception->getMessage(),
+                "debug" => $debugContext,
+            ]);
         }
     }
 

+ 52 - 0
includes/functions.php

@@ -3394,6 +3394,15 @@ if (!defined("ERROR_LOG_FILE")) {
 if (!defined("ACCESS_LOG_FILE")) {
     define("ACCESS_LOG_FILE", LOG_DIR . "access.log");
 }
+if (!defined("LOG_MAX_BYTES")) {
+    define("LOG_MAX_BYTES", 1048576);
+}
+if (!defined("LOG_KEEP_FILES")) {
+    define("LOG_KEEP_FILES", 5);
+}
+if (!defined("LOG_MAX_AGE_SECONDS")) {
+    define("LOG_MAX_AGE_SECONDS", 2592000);
+}
 
 function initLogging()
 {
@@ -3403,9 +3412,51 @@ function initLogging()
     }
 }
 
+function rotateLogFileIfNeeded(string $logFile): void
+{
+    $maxBytes = max(1024, (int) LOG_MAX_BYTES);
+    $keepFiles = max(1, (int) LOG_KEEP_FILES);
+    $maxAgeSeconds = max(0, (int) LOG_MAX_AGE_SECONDS);
+    $now = time();
+    $rotatedFiles = glob($logFile . ".*") ?: [];
+
+    foreach ($rotatedFiles as $rotatedFile) {
+        if (preg_match('/\.(\d+)$/', $rotatedFile, $matches) !== 1) {
+            continue;
+        }
+
+        $index = (int) $matches[1];
+        $isTooOld =
+            $maxAgeSeconds > 0 &&
+            is_file($rotatedFile) &&
+            ($now - (int) filemtime($rotatedFile)) > $maxAgeSeconds;
+
+        if ($index > $keepFiles || $isTooOld) {
+            @unlink($rotatedFile);
+        }
+    }
+
+    if (!is_file($logFile) || (filesize($logFile) ?: 0) < $maxBytes) {
+        return;
+    }
+
+    for ($i = $keepFiles; $i >= 1; $i--) {
+        $source = $i === 1 ? $logFile : $logFile . "." . ($i - 1);
+        $target = $logFile . "." . $i;
+        if (!is_file($source)) {
+            continue;
+        }
+        if ($i === $keepFiles && is_file($target)) {
+            @unlink($target);
+        }
+        @rename($source, $target);
+    }
+}
+
 function logError($message, $context = [], $level = "ERROR")
 {
     initLogging();
+    rotateLogFileIfNeeded(ERROR_LOG_FILE);
 
     $entry = [
         "timestamp" => date("Y-m-d H:i:s.u"),
@@ -3434,6 +3485,7 @@ function logError($message, $context = [], $level = "ERROR")
 function logAccess($message, $context = [])
 {
     initLogging();
+    rotateLogFileIfNeeded(ACCESS_LOG_FILE);
 
     $entry = [
         "timestamp" => date("Y-m-d H:i:s.u"),

+ 1 - 1
includes/version.php

@@ -1,3 +1,3 @@
 <?php
 
-define("APP_VERSION", "v1.3.5");
+define("APP_VERSION", "v1.3.8");