FileBackupRepository.php
- Pfad:
src/Infrastructure/Persistence/FileBackupRepository.php - Namespace: Infrastructure\Persistence
- Zeilen: 297 | Größe: 8,925 Bytes
- Geändert: 2025-12-27 23:50:18 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 92
- Dependencies: 100 (25%)
- LOC: 68 (20%)
- Methods: 90 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 5
- implements Domain\Repository\FileBackupRepositoryInterface
- constructor PDO
- use Domain\Constants
- use Domain\Repository\FileBackupRepositoryInterface
- use PDO
Klassen 1
-
FileBackupRepositoryclass Zeile 13
Funktionen 11
-
__construct()public Zeile 26 -
findAll()public Zeile 36 -
count()public Zeile 71 -
findById()public Zeile 95 -
findByFilePath()public Zeile 109 -
getStatistics()public Zeile 128 -
restore()public Zeile 171 -
validateRestorePath()Zeile 209 -
logSecurityViolation()Zeile 248 -
logRestore()Zeile 266 -
getContentPreview()Zeile 280
Verwendet von 1
Versionen 11
-
v11
2025-12-27 23:50 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v10
2025-12-27 23:49 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v9
2025-12-25 12:51 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v8
2025-12-25 10:34 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v7
2025-12-25 10:32 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v6
2025-12-23 08:05 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v5
2025-12-23 04:22 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v4
2025-12-20 19:53 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v3
2025-12-20 19:40 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v2
2025-12-20 19:24 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v1
2025-12-20 19:24 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation
Code
<?php
declare(strict_types=1);
namespace Infrastructure\Persistence;
// @responsibility: Persistenz für Datei-Backup-Historie (Restore-Validierung)
use Domain\Constants;
use Domain\Repository\FileBackupRepositoryInterface;
use PDO;
class FileBackupRepository implements FileBackupRepositoryInterface
{
private PDO $db;
/**
* Allowlist of base paths where file restoration is permitted.
* Only files within these directories can be restored.
*/
private const ALLOWED_RESTORE_PATHS = [
'/var/www/dev.campus.systemische-tools.de/',
'/var/www/prod.campus.systemische-tools.de/',
];
public function __construct(PDO $pdo)
{
$this->db = $pdo;
}
/**
* Find all backups with optional filters.
*
* @param array<string, mixed> $filters
*/
public function findAll(array $filters = [], int $limit = 50, int $offset = 0): array
{
$sql = 'SELECT id, file_path, content_hash, file_size, version, change_type,
changed_at, changed_by, reason
FROM file_backup_history WHERE 1=1';
$params = [];
if (!empty($filters['search'])) {
$sql .= ' AND file_path LIKE :search';
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['change_type'])) {
$sql .= ' AND change_type = :change_type';
$params['change_type'] = $filters['change_type'];
}
$sql .= ' ORDER BY changed_at DESC LIMIT :limit OFFSET :offset';
$stmt = $this->db->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue(':' . $key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Count total backups with filters.
*
* @param array<string, mixed> $filters
*/
public function count(array $filters = []): int
{
$sql = 'SELECT COUNT(*) FROM file_backup_history WHERE 1=1';
$params = [];
if (!empty($filters['search'])) {
$sql .= ' AND file_path LIKE :search';
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['change_type'])) {
$sql .= ' AND change_type = :change_type';
$params['change_type'] = $filters['change_type'];
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetchColumn();
}
/**
* Find backup by ID.
*/
public function findById(int $id): ?array
{
$stmt = $this->db->prepare(
'SELECT * FROM file_backup_history WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result !== false ? $result : null;
}
/**
* Find all backups for a specific file path.
*/
public function findByFilePath(string $path): array
{
$stmt = $this->db->prepare(
'SELECT id, file_path, content_hash, file_size, version, change_type,
changed_at, changed_by, reason
FROM file_backup_history
WHERE file_path = :path
ORDER BY version DESC'
);
$stmt->execute(['path' => $path]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Get statistics about backups.
*
* @return array<string, int>
*/
public function getStatistics(): array
{
$stats = [];
// Total backups
$stats['total'] = (int) $this->db->query(
'SELECT COUNT(*) FROM file_backup_history'
)->fetchColumn();
// Unique files
$stats['files'] = (int) $this->db->query(
'SELECT COUNT(DISTINCT file_path) FROM file_backup_history'
)->fetchColumn();
// Total versions (max version per file summed)
$stats['versions'] = (int) $this->db->query(
'SELECT COALESCE(SUM(max_version), 0) FROM (
SELECT MAX(version) as max_version FROM file_backup_history GROUP BY file_path
) as v'
)->fetchColumn();
// Last 24 hours
$stats['recent'] = (int) $this->db->query(
'SELECT COUNT(*) FROM file_backup_history WHERE changed_at >= NOW() - INTERVAL ' . Constants::HOURS_PER_DAY . ' HOUR'
)->fetchColumn();
// By change type
$stats['created'] = (int) $this->db->query(
"SELECT COUNT(*) FROM file_backup_history WHERE change_type = 'created'"
)->fetchColumn();
$stats['modified'] = (int) $this->db->query(
"SELECT COUNT(*) FROM file_backup_history WHERE change_type = 'modified'"
)->fetchColumn();
return $stats;
}
/**
* Restore a file from backup.
*
* @throws \RuntimeException If restore fails or path validation fails
*/
public function restore(int $id): bool
{
$backup = $this->findById($id);
if ($backup === null) {
throw new \RuntimeException('Backup not found');
}
$filePath = $backup['file_path'];
$content = $backup['file_content'];
// SECURITY: Validate the restore path before writing
$this->validateRestorePath($filePath);
// Check if directory exists
$dir = dirname($filePath);
if (!is_dir($dir)) {
throw new \RuntimeException("Directory does not exist: {$dir}");
}
// Write content back to file
$result = file_put_contents($filePath, $content);
if ($result === false) {
throw new \RuntimeException("Failed to write to file: {$filePath}");
}
// Log the restore action
$this->logRestore($id, $filePath);
return true;
}
/**
* Validate that a file path is safe for restoration.
*
* @throws \RuntimeException If path is not allowed
*/
private function validateRestorePath(string $filePath): void
{
// Check for path traversal attempts
if (str_contains($filePath, '..')) {
$this->logSecurityViolation('path_traversal', $filePath);
throw new \RuntimeException('Invalid path: path traversal detected');
}
// Normalize the path
$dir = dirname($filePath);
$realDir = realpath($dir);
if ($realDir === false) {
throw new \RuntimeException('Invalid path: directory does not exist');
}
// Reconstruct full path with real directory
$normalizedPath = $realDir . '/' . basename($filePath);
// Check against allowlist
$isAllowed = false;
foreach (self::ALLOWED_RESTORE_PATHS as $allowedPath) {
if (str_starts_with($normalizedPath, $allowedPath)) {
$isAllowed = true;
break;
}
}
if (!$isAllowed) {
$this->logSecurityViolation('path_not_allowed', $filePath);
throw new \RuntimeException('Invalid path: restore location not permitted');
}
}
/**
* Log security violation attempts.
*/
private function logSecurityViolation(string $type, string $filePath): void
{
$stmt = $this->db->prepare(
"INSERT INTO mcp_log (tool, operation, parameters, result, logged_at)
VALUES ('backup_restore', 'security_violation', :params, 'blocked', NOW())"
);
$stmt->execute([
'params' => json_encode([
'violation_type' => $type,
'attempted_path' => $filePath,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]),
]);
}
/**
* Log restore action to mcp_log.
*/
private function logRestore(int $backupId, string $filePath): void
{
$stmt = $this->db->prepare(
"INSERT INTO mcp_log (tool, operation, parameters, result, logged_at)
VALUES ('backup_restore', 'restore', :params, 'success', NOW())"
);
$stmt->execute([
'params' => json_encode(['backup_id' => $backupId, 'file_path' => $filePath]),
]);
}
/**
* Get content preview (first N lines).
*/
public function getContentPreview(int $id, int $maxLines = 500): ?string
{
$backup = $this->findById($id);
if ($backup === null || empty($backup['file_content'])) {
return null;
}
$lines = explode("\n", $backup['file_content']);
if (count($lines) <= $maxLines) {
return $backup['file_content'];
}
return implode("\n", array_slice($lines, 0, $maxLines)) . "\n\n... (" . (count($lines) - $maxLines) . ' weitere Zeilen)';
}
}