crypto = new EntryCrypto(); } public function all(string $username): array { $items = array_merge( $this->readKind($username, 'weekly'), $this->readKind($username, 'monthly') ); usort($items, static function (array $left, array $right): int { $leftDate = (string) ($left['date_to'] ?? ''); $rightDate = (string) ($right['date_to'] ?? ''); $byDate = strcmp($rightDate, $leftDate); if ($byDate !== 0) { return $byDate; } return strcmp((string) ($right['summary_key'] ?? ''), (string) ($left['summary_key'] ?? '')); }); return $items; } public function weekly(string $username): array { return $this->readKind($username, 'weekly'); } public function monthly(string $username): array { return $this->readKind($username, 'monthly'); } public function find(string $username, string $kind, string $key): ?array { $path = $this->pathFor($username, $kind, $key); if (!is_file($path)) { return null; } $content = (string) file_get_contents($path); $plaintext = $this->crypto->decrypt($content); if ($this->crypto->shouldEncrypt() && !$this->crypto->isEncrypted($content)) { file_put_contents($path, $this->crypto->encrypt($plaintext), LOCK_EX); } return $this->parse($plaintext, $kind, $key); } public function save(string $username, string $kind, string $key, array $summary): void { $normalized = $this->normalizeSummary($kind, $key, $summary); $path = $this->pathFor($username, $kind, $key); $directory = dirname($path); if (!is_dir($directory)) { mkdir($directory, 0775, true); } file_put_contents($path, $this->crypto->encrypt($this->toText($normalized)), LOCK_EX); } public function exportBackupFiles(string $username): array { $exports = []; foreach (['weekly', 'monthly'] as $kind) { foreach ($this->readKind($username, $kind) as $summary) { $exports[] = [ 'path' => 'summaries/' . $kind . '/' . (string) $summary['summary_key'] . '.txt', 'content' => $this->toText($summary), ]; } } return $exports; } public function importBackupFile(string $username, string $fileName, string $content): bool { $detected = $this->detectBackupFile($fileName); if ($detected === null) { return false; } $summary = $this->parse($content, $detected['kind'], $detected['key']); if ($summary === null) { throw new RuntimeException('Eine KI-Zusammenfassung aus dem Backup konnte nicht gelesen werden.'); } $this->save($username, $detected['kind'], $detected['key'], $summary); return true; } private function readKind(string $username, string $kind): array { $directory = $this->directoryFor($username, $kind); if (!is_dir($directory)) { return []; } $files = glob($directory . '/*.txt') ?: []; rsort($files, SORT_STRING); $summaries = []; foreach ($files as $file) { $key = basename($file, '.txt'); $content = (string) file_get_contents($file); $plaintext = $this->crypto->decrypt($content); if ($this->crypto->shouldEncrypt() && !$this->crypto->isEncrypted($content)) { file_put_contents($file, $this->crypto->encrypt($plaintext), LOCK_EX); } $summary = $this->parse($plaintext, $kind, $key); if ($summary !== null) { $summaries[] = $summary; } } return $summaries; } private function parse(string $content, string $kind, string $key): ?array { $plaintext = $this->crypto->decrypt($content); $kind = $this->normalizeKind($kind); if ($kind === null || !$this->isValidKey($kind, $key)) { return null; } $title = $this->extract('/^Titel:\s*(.+)$/mu', $plaintext); $type = $this->extract('/^Typ:\s*(.+)$/mu', $plaintext); $createdAt = $this->extract('/^Erstellt am:\s*(.+)$/mu', $plaintext); if (!preg_match('/^Zeitraum:\s*(\d{4}-\d{2}-\d{2})\s+bis\s+(\d{4}-\d{2}-\d{2})$/mu', $plaintext, $rangeMatch)) { return null; } if (!preg_match('/^Zeitraum:\s*.+$\R\R([\s\S]*)\z/mu', $plaintext, $textMatch)) { return null; } if ($title === null || $type === null || $createdAt === null) { return null; } $expectedType = $kind === 'weekly' ? 'ki-wochenauswertung' : 'ki-monatsauswertung'; if (trim($type) !== $expectedType) { return null; } $summary = [ 'summary_kind' => $kind, 'summary_key' => $key, 'title' => trim($title), 'type' => $expectedType, 'created_at' => trim($createdAt), 'date_from' => trim((string) ($rangeMatch[1] ?? '')), 'date_to' => trim((string) ($rangeMatch[2] ?? '')), 'text' => trim((string) ($textMatch[1] ?? '')), ]; if (!$this->isValidDate($summary['date_from']) || !$this->isValidDate($summary['date_to'])) { return null; } return $summary; } private function toText(array $summary): string { $normalized = $this->normalizeSummary( (string) $summary['summary_kind'], (string) $summary['summary_key'], $summary ); return implode("\n", [ 'Titel: ' . $normalized['title'], 'Typ: ' . $normalized['type'], 'Erstellt am: ' . $normalized['created_at'], 'Zeitraum: ' . $normalized['date_from'] . ' bis ' . $normalized['date_to'], '', trim((string) $normalized['text']), '', ]); } private function normalizeSummary(string $kind, string $key, array $summary): array { $kind = $this->normalizeKind($kind); if ($kind === null || !$this->isValidKey($kind, $key)) { throw new RuntimeException('Die Zusammenfassung hat einen ungültigen Schlüssel.'); } $dateFrom = trim((string) ($summary['date_from'] ?? '')); $dateTo = trim((string) ($summary['date_to'] ?? '')); $createdAt = trim((string) ($summary['created_at'] ?? date(DATE_ATOM))); $text = trim((string) ($summary['text'] ?? '')); if (!$this->isValidDate($dateFrom) || !$this->isValidDate($dateTo)) { throw new RuntimeException('Die Zusammenfassung hat einen ungültigen Zeitraum.'); } if ($text === '') { throw new RuntimeException('Die Zusammenfassung darf nicht leer sein.'); } return [ 'summary_kind' => $kind, 'summary_key' => $key, 'title' => trim((string) ($summary['title'] ?? $this->defaultTitle($kind, $key))), 'type' => $kind === 'weekly' ? 'ki-wochenauswertung' : 'ki-monatsauswertung', 'created_at' => $createdAt, 'date_from' => $dateFrom, 'date_to' => $dateTo, 'text' => $text, ]; } private function defaultTitle(string $kind, string $key): string { if ($kind === 'weekly' && preg_match('/^(\d{4})-KW-(\d{2})$/', $key, $matches)) { return 'Wochenzusammenfassung KW ' . $matches[2] . ' / ' . $matches[1]; } if ($kind === 'monthly' && preg_match('/^(\d{4})-(\d{2})$/', $key, $matches)) { return 'Monatszusammenfassung ' . $matches[2] . ' / ' . $matches[1]; } return 'KI-Zusammenfassung'; } private function detectBackupFile(string $fileName): ?array { $normalized = str_replace('\\', '/', trim($fileName)); $baseName = basename($normalized); if (preg_match('/^(\d{4}-KW-\d{2})\.txt$/', $baseName, $matches)) { return [ 'kind' => 'weekly', 'key' => (string) $matches[1], ]; } if (preg_match('/^(\d{4}-\d{2})\.txt$/', $baseName, $matches)) { return [ 'kind' => 'monthly', 'key' => (string) $matches[1], ]; } return null; } private function directoryFor(string $username, string $kind): string { return storage_path('users/' . normalize_username($username) . '/summaries/' . $kind); } private function pathFor(string $username, string $kind, string $key): string { return $this->directoryFor($username, $kind) . '/' . $key . '.txt'; } private function normalizeKind(string $kind): ?string { $kind = trim($kind); return in_array($kind, ['weekly', 'monthly'], true) ? $kind : null; } private function isValidKey(string $kind, string $key): bool { if ($kind === 'weekly' && preg_match('/^\d{4}-KW-(\d{2})$/', $key, $matches) === 1) { $week = (int) ($matches[1] ?? 0); return $week >= 1 && $week <= 53; } if ($kind === 'monthly' && preg_match('/^\d{4}-(\d{2})$/', $key, $matches) === 1) { $month = (int) ($matches[1] ?? 0); return $month >= 1 && $month <= 12; } return false; } private function isValidDate(string $date): bool { $parsed = DateTimeImmutable::createFromFormat('Y-m-d', $date); return $parsed !== false && $parsed->format('Y-m-d') === $date; } private function extract(string $pattern, string $content): ?string { if (preg_match($pattern, $content, $matches) !== 1) { return null; } return trim((string) ($matches[1] ?? '')); } }