systemPath = storage_path('system/notifications.json'); } public function systemConfig(): array { $config = decode_json_file($this->systemPath, []); $changed = false; if (!isset($config['cron_token']) || !is_string($config['cron_token']) || $config['cron_token'] === '') { $config['cron_token'] = bin2hex(random_bytes(24)); $changed = true; } if (!isset($config['subject']) || !is_string($config['subject']) || $config['subject'] === '') { $host = parse_url(app_origin(), PHP_URL_HOST); $host = is_string($host) && $host !== '' ? $host : 'localhost'; $config['subject'] = 'mailto:hello@' . $host; $changed = true; } if ($changed) { $this->writeJson($this->systemPath, $config); } return $config; } public function saveVapidKeys(string $publicKey, string $privateKey): void { $config = $this->systemConfig(); $config['vapid_public_key'] = $publicKey; $config['vapid_private_key'] = $privateKey; $this->writeJson($this->systemPath, $config); } public function subscriptionsForUser(string $username): array { $payload = decode_json_file($this->subscriptionsPath($username), ['subscriptions' => []]); return array_values(array_filter($payload['subscriptions'] ?? [], 'is_array')); } public function saveSubscription(string $username, array $subscription): void { $endpoint = trim((string) ($subscription['endpoint'] ?? '')); if ($endpoint === '') { throw new RuntimeException('Die Subscription ist unvollständig.'); } $subscriptions = $this->subscriptionsForUser($username); $saved = false; foreach ($subscriptions as &$entry) { if (($entry['endpoint'] ?? '') === $endpoint) { $entry = array_merge($entry, $subscription, [ 'updated_at' => date(DATE_ATOM), ]); $saved = true; break; } } unset($entry); if (!$saved) { $subscription['created_at'] = date(DATE_ATOM); $subscription['updated_at'] = date(DATE_ATOM); $subscriptions[] = $subscription; } $this->writeJson($this->subscriptionsPath($username), ['subscriptions' => array_values($subscriptions)]); } public function removeSubscription(string $username, string $endpoint): void { $endpoint = trim($endpoint); if ($endpoint === '') { return; } $subscriptions = array_values(array_filter( $this->subscriptionsForUser($username), static fn (array $entry): bool => ($entry['endpoint'] ?? '') !== $endpoint )); $this->writeJson($this->subscriptionsPath($username), ['subscriptions' => $subscriptions]); } public function removeInvalidSubscriptions(string $username, array $endpoints): void { foreach ($endpoints as $endpoint) { if (is_string($endpoint) && $endpoint !== '') { $this->removeSubscription($username, $endpoint); } } } public function reminderState(string $username): array { return decode_json_file($this->reminderStatePath($username), []); } public function saveReminderState(string $username, array $state): void { $this->writeJson($this->reminderStatePath($username), $state); } public function subscriptionCount(string $username): int { return count($this->subscriptionsForUser($username)); } private function subscriptionsPath(string $username): string { return storage_path('users/' . normalize_username($username) . '/push-subscriptions.json'); } private function reminderStatePath(string $username): string { return storage_path('users/' . normalize_username($username) . '/notification-state.json'); } private function writeJson(string $path, array $payload): void { $directory = dirname($path); if (!is_dir($directory)) { mkdir($directory, 0775, true); } $bytes = file_put_contents( $path, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX ); if ($bytes === false) { throw new RuntimeException('Die Benachrichtigungsdaten konnten nicht gespeichert werden.'); } } }