vapid = $this->getVapid(); } public static function isValidP256dh(string $key): bool { if (!preg_match('/^[A-Za-z0-9_-]{87}=*$/', $key)) { return false; } try { Utils::unserializePublicKey(Base64Url::decode($key)); } catch (\InvalidArgumentException) { return false; } return true; } public static function isValidAuth(string $auth): bool { if (!preg_match('/^[A-Za-z0-9_-]{22}=*$/', $auth)) { return false; } try { $a = Base64Url::decode($auth); } catch (\InvalidArgumentException) { return false; } return strlen($a) === 16; } private function getClient(): WebPush { if (isset($this->client)) { return $this->client; } $this->client = new WebPush(auth: ['VAPID' => $this->vapid]); $this->client->setReuseVAPIDHeaders(true); return $this->client; } /** * @return array * @psalm-return array{publicKey: string, privateKey: string, subject: string} */ private function getVapid(): array { // Do not use lazy for now try { $publicKey = $this->appConfig->getAppValueString('webpush_vapid_pubkey'); $privateKey = $this->appConfig->getAppValueString('webpush_vapid_privkey'); } catch (\Throwable) { // Decryption failed (e.g. mismatched instance secret), regenerate keys $publicKey = ''; $privateKey = ''; } if ($publicKey === '' || $privateKey === '') { /** @var array{publicKey: string, privateKey: string} $vapid */ $vapid = VAPID::createVapidKeys(); $this->appConfig->setAppValueString( 'webpush_vapid_pubkey', $vapid['publicKey'] ); $this->appConfig->setAppValueString( 'webpush_vapid_privkey', $vapid['privateKey'], sensitive: true ); } else { $vapid = [ 'publicKey' => $publicKey, 'privateKey' => $privateKey, ]; } $vapid['subject'] = 'https://nextcloud.com/contact/'; return $vapid; } /** * @return string */ public function getVapidPublicKey(): string { return $this->vapid['publicKey']; } /** * Send one notification - blocking (should be avoided most of the time) */ public function notify(string $endpoint, string $uaPublicKey, string $auth, string $body): void { $c = $this->getClient(); $c->queueNotification( new Subscription($endpoint, $uaPublicKey, $auth, 'aes128gcm'), $body ); // the callback could be defined by the caller // For the moment, it is used during registration only - no need to catch 404 &co // as the registration isn't activated $callback = function ($r): void { }; $c->flushPooled($callback); } /** * Queue one notification. [flush] needs to be called to actually send the notifications * @throws \ErrorException */ public function enqueue(string $endpoint, string $uaPublicKey, string $auth, string $body, string $urgency = 'normal'): void { $c = $this->getClient(); $c->queueNotification( new Subscription($endpoint, $uaPublicKey, $auth, 'aes128gcm'), $body, options: [ 'urgency' => $urgency ] ); } /** * @param callable $callback * @psalm-param $callback callable(MessageSentReport): void */ public function flush(callable $callback): void { $c = $this->getClient(); $c->flushPooled($callback); } }