321 lines
10 KiB
PHP
321 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
final class SummaryRepository
|
|
{
|
|
private EntryCrypto $crypto;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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] ?? ''));
|
|
}
|
|
}
|