ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
ilNotificationPushHandler.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Notifications;
22
23use Firebase\JWT\JWT;
31use ilLogger;
32use ilObjUser;
33use ilSetting;
34use ilUtil;
35use Random\Randomizer;
36
46{
48 protected ilLogger $logger;
49 protected string $public_key;
50 protected string $private_key;
51 protected string $sub;
52 protected bool $is_enabled;
55
57 {
58 global $DIC;
59 $this->provider = $provider;
60 $this->logger = $DIC->logger()->root();
61 $this->sub = $DIC->settings()->get('admin_email');
62 $this->subscription_repo = new PushRepository($DIC->database(), $DIC->user());
63 $settings = new ilSetting('notifications');
64 $this->public_key = $settings->get('application_server_key');
65 $this->private_key = file_get_contents($settings->get('private_key_path'));
66 $this->is_enabled = $settings->get('enable_push') === '1';
67 }
68
73 public function notify(ilNotificationObject $notification): void
74 {
75 $this->resetLastQueueResult();
76
77 if (!$this->is_enabled) {
78 $this->logger->debug('Notifications are globaly disabled.');
80 return;
81 }
82
83 if (!$this->provider instanceof InternalPushProvider) {
84 $id = $notification->handlerParams['setting']['user_pref'];
85 if (!$this->validateForUser($notification->user, $id)) {
86 $this->logger->debug('Notification for ' . $id . ' not send due to user preferences.');
88 return;
89 }
90 }
91
92 $subscriptions = $this->subscription_repo->getUserSubscriptions($notification->user->getId());
93 if ($subscriptions === []) {
94 $this->logger->debug('User ' . $notification->user->getId() . ' has no active Subscriptions');
96 return;
97 }
98
99 $salt = (new Randomizer())->getBytes(16);
100 $local_key = $this->base64UrlDecode($this->public_key);
101 $content = $this->buildContent($notification);
102 $ttl = $notification->handlerParams['setting']['ttl'] ?? 60;
103 foreach ($subscriptions as $subscription) {
104 $url_parts = parse_url($subscription->getEndpoint());
105 $data = [
106 'sub' => 'mailto:' . $this->sub,
107 'aud' => $url_parts['scheme'] . '://' . $url_parts['host'],
108 'exp' => time() + (int) $ttl
109 ];
110
111 $user_key = $this->base64UrlDecode($subscription->getP256dh());
112 $pre_key = $this->hash(
113 openssl_pkey_derive($this->publicVapidToPEM($user_key), $this->private_key, 256),
114 $this->base64UrlDecode($subscription->getAuth())
115 );
116 $key = $this->hash($this->hash($user_key . $local_key, $pre_key, 32, 'WebPush: info'), $salt);
117 $encryptedText = openssl_encrypt(
118 $content,
119 'aes-128-gcm',
120 $this->hash('', $key, 16, 'Content-Encoding: aes128gcm'),
121 OPENSSL_RAW_DATA,
122 $this->hash('', $key, 12, 'Content-Encoding: nonce'),
123 $tag
124 );
125 $post = $salt . $this->padKey($local_key) . $encryptedText . $tag;
126
127 $curl = new ilCurlConnection($subscription->getEndpoint());
128 $curl->init();
129 $curl->setOpt(CURLOPT_HTTPHEADER, [
130 'Content-Encoding: aes128gcm',
131 'Authorization: vapid t=' . JWT::encode($data, $this->private_key, 'ES256') . ', k=' . $this->public_key,
132 'Ttl: ' . $ttl
133 ]);
134 $curl->setOpt(CURLOPT_POST, 1);
135 $curl->setOpt(CURLOPT_RETURNTRANSFER, 1);
136 $curl->setOpt(CURLOPT_POSTFIELDS, $post);
137 $response = $curl->exec();
138
139 if ($response === false) {
140 $this->logger->error('Push notification [' . $subscription->getAuth() . '] request failed.');
142 } else {
143 $this->setLastQueueResult(
144 $this->handleResponse($curl->getInfo(CURLINFO_HTTP_CODE), $response, $subscription->getAuth())
145 );
146 }
147 $curl->close();
148 }
149 }
150
151 private function base64UrlDecode(string $data): string
152 {
153 return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
154 }
155
156 private function publicVapidToPEM(string $key): string
157 {
158 $der_key = pack('H*', '3059301306072a8648ce3d020106082a8648ce3d030107034200') . $key;
159 return "-----BEGIN PUBLIC KEY-----\n" .
160 chunk_split(base64_encode($der_key), 64, "\n") .
161 "-----END PUBLIC KEY-----\n";
162 }
163
167 private function hash(string $data, string $key, int $length = PHP_INT_MAX, ?string $preffix = null): string
168 {
169 if ($preffix !== null) {
170 $data = $preffix . \chr(0) . $data . \chr(1);
171 }
172 return mb_substr(hash_hmac('sha256', $data, $key, true), 0, $length, '8bit');
173 }
174
179 private function padKey(string $key): string
180 {
181 return pack('N*', 4096) . pack('C*', mb_strlen($key, '8bit')) . $key;
182 }
183
188 protected function buildContent(ilNotificationObject $notification): string
189 {
190 $actions = [];
191 foreach ($notification->links as $link) {
192 $actions[] = [
193 'title' => $link->getTitle(),
194 'action' => $link->getUrl(),
195 ];
196 }
197
198 return base64_encode(json_encode([
199 $notification->title,
200 [
201 'data' => ['action' => $notification->action ?? '/'],
202 'icon' => $notification->iconPath ?: ilUtil::getImagePath('logo/HeaderIconResponsive.svg'),
203 'body' => $notification->shortDescription,
204 'actions' => $actions
205 ]
206 ], JSON_THROW_ON_ERROR)) . \chr(2);
207 }
208
209 protected function handleResponse(int $http_code, string $response, string $auth): PushQueueResult
210 {
211 if ($response !== '') {
212 $this->logger->info("Push notification [$auth] response: $response");
213 }
214 switch ($http_code) {
217 $this->logger->debug("Push notification [$auth] successful.");
222 $this->logger->error("Push notification [$auth] request was invalid.");
226 $this->subscription_repo->deleteSubscription($auth);
227 $this->logger->debug("Push notification [$auth] endpoint outdated. Subscription removed.");
231 $this->logger->debug("Push notification [$auth] endpoint blocked due to heavy usage or spam.");
233 default:
234 $this->logger->info("Push notification [$auth] went into unkown/browser-specific handling.");
235 return PushQueueResult::UNKNOWN;
236 }
237 }
238
239 protected function resetLastQueueResult(): void
240 {
241 $this->last_queue_result = null;
242 }
243
244 protected function setLastQueueResult(PushQueueResult $result): void
245 {
246 if (
247 $this->last_queue_result === null ||
248 $this->last_queue_result === PushQueueResult::FAILED ||
250 ) {
251 $this->last_queue_result = $result;
252 }
253 }
254
255 protected function validateForUser(ilObjUser $user, string $id): bool
256 {
257 return \in_array($id, json_decode($user->getPref('push_notification_provider') ?? '[]'), true);
258 }
259
261 {
263 }
264}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
This is the lowest common denominator of all popular browsers.
hash(string $data, string $key, int $length=PHP_INT_MAX, ?string $preffix=null)
padKey(string $key)
The key needs to be preffixed for the request to fit the endpoints requirement.
handleResponse(int $http_code, string $response, string $auth)
Component logger with individual log levels by component id.
User class.
getPref(string $a_keyword)
ILIAS Setting Class.
Util class various functions, usage as namespace.
static getImagePath(string $image_name, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
const HTTP_REQUEST_ENTITY_TOO_LARGE
Definition: StatusCode.php:69
$post
Definition: ltitoken.php:46
global $DIC
Definition: shib_login.php:26
$response
Definition: xapitoken.php:93