PushSubscription   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 117
c 3
b 0
f 0
dl 0
loc 286
rs 9.28
wmc 39

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getWebpushHandler() 0 25 5
A sendMessage() 0 20 5
A getPrivateKey() 0 6 2
A doTest() 0 12 3
A createSubscription() 0 4 1
A getPublicKey() 0 6 2
A getCMSFields() 0 4 1
A getByEndpoint() 0 4 1
A deleteSubscriptions() 0 9 2
A deleteEndpoint() 0 8 2
A createNew() 0 15 4
A getCMSActions() 0 7 2
B sendPushNotifications() 0 51 9
1
<?php
2
3
namespace LeKoala\SsPwa;
4
5
use Exception;
6
use SilverStripe\ORM\DataList;
7
use Minishlink\WebPush\WebPush;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\Forms\FieldList;
10
use SilverStripe\Security\Member;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Environment;
13
use LeKoala\CmsActions\CustomAction;
0 ignored issues
show
Bug introduced by
The type LeKoala\CmsActions\CustomAction was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Minishlink\WebPush\Subscription;
15
use Minishlink\WebPush\MessageSentReport;
16
17
/**
18
 * Save push subscription
19
 *
20
 * Subscription details is saved in a raw format as json text in Subscription field
21
 *
22
 * It looks like this:
23
 * {
24
 * "endpoint":"https:\/\/fcm.googleapis.com\/fcm\/...",
25
 * "expirationTime":null,
26
 * "keys":{"p256dh":"BJXOZk_g9pi7goy_I1SHctH_9NRV6fYfmQiEQ-piLe9j...","auth":"egZhXUK..."}
27
 * }
28
 *
29
 * @link https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe
30
 * @link https://web.dev/push-notifications-subscribing-a-user/
31
 * @property string $Endpoint
32
 * @property string $Subscription
33
 * @property string $Platform
34
 * @property string $LastCalled
35
 * @property bool $LastCallError
36
 * @property string $LastCallErrorReason
37
 * @property int $MemberID
38
 * @method \SilverStripe\Security\Member Member()
39
 */
40
class PushSubscription extends DataObject
41
{
42
    const WEBPUSH  = "webpush";
43
    const FIREBASE = "firebase";
44
    const APN = "apn";
45
46
    /**
47
     * @var string
48
     */
49
    private static $table_name = 'PushSubscription';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
50
51
    /**
52
     * @var array<string,string>
53
     */
54
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
55
        'Endpoint' => 'Varchar(255)',
56
        'Subscription' => 'Text',
57
        'Platform' => "Enum(',webpush,firebase,apn')",
58
        'LastCalled' => 'Datetime',
59
        'LastCallError' => 'Boolean',
60
        'LastCallErrorReason' => 'Text',
61
    ];
62
63
    /**
64
     * @var array<string,string>
65
     */
66
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
67
        'Member' => Member::class
68
    ];
69
70
    /**
71
     * @var array<string>
72
     */
73
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
74
        'Created', 'Member.Title'
75
    ];
76
77
    /**
78
     * @var array<string,mixed>
79
     */
80
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
81
        'Endpoint' => true,
82
        'LastCalled' => true,
83
    ];
84
85
    /**
86
     * @return string
87
     */
88
    public static function getPublicKey()
89
    {
90
        if (Environment::getEnv('PUSH_PUBLIC_KEY')) {
91
            return Environment::getEnv('PUSH_PUBLIC_KEY');
92
        }
93
        return self::config()->push_public_key;
94
    }
95
96
    /**
97
     * @return string
98
     */
99
    public static function getPrivateKey()
100
    {
101
        if (Environment::getEnv('PUSH_PRIVATE_KEY')) {
102
            return Environment::getEnv('PUSH_PRIVATE_KEY');
103
        }
104
        return self::config()->push_private_key;
105
    }
106
107
    /**
108
     * @return FieldList
109
     */
110
    public function getCMSFields()
111
    {
112
        $fields = parent::getCMSFields();
113
        return $fields;
114
    }
115
116
    /**
117
     * @return FieldList
118
     */
119
    public function getCMSActions()
120
    {
121
        $actions = parent::getCMSActions();
122
        if (class_exists(CustomAction::class)) {
123
            $actions->push(new CustomAction("doTest", "Test notification"));
124
        }
125
        return $actions;
126
    }
127
128
    /**
129
     * @return WebPush
130
     */
131
    public static function getWebpushHandler()
132
    {
133
        $publicKey = self::getPublicKey();
134
        $privateKey = self::getPrivateKey();
135
        if (!$publicKey || !$privateKey) {
136
            throw new Exception("Missing public or private key");
137
        }
138
        $auth = [
139
            'VAPID' => [
140
                'subject' => Director::absoluteBaseURL(), // can be a mailto: or your website address
141
                'publicKey' => $publicKey,
142
                'privateKey' => $privateKey,
143
            ],
144
        ];
145
        $defaultOptions = [];
146
        $timeout = 30;
147
        $clientOptions = [];
148
        // This fixes ca cert issues if server is not configured properly
149
        if (ini_get('curl.cainfo') === false || ini_get('curl.cainfo') === '') {
150
            $clientOptions['verify'] = \Composer\CaBundle\CaBundle::getBundledCaBundlePath();
151
        }
152
        $webPush = new WebPush($auth, $defaultOptions, $timeout, $clientOptions);
153
        $webPush->setReuseVAPIDHeaders(true);
154
155
        return $webPush;
156
    }
157
158
    public function doTest(): string
159
    {
160
        $payload = "Test at " . date('d/m/Y H:i:s');
161
        $report = $this->sendMessage($payload);
162
163
        if ($report->isSubscriptionExpired()) {
164
            throw new Exception('Error: subscription is expired');
165
        }
166
        if (!$report->isSuccess()) {
167
            throw new Exception('Error: ' . str_replace("\n", "", $report->getReason()));
168
        }
169
        return 'Sent!';
170
    }
171
172
    /**
173
     * @return Subscription
174
     */
175
    public function createSubscription()
176
    {
177
        $data = json_decode($this->Subscription, true);
178
        return Subscription::create($data);
179
    }
180
181
    /**
182
     * @param string|array<string,mixed> $payload
183
     * @return MessageSentReport
184
     */
185
    public function sendMessage($payload)
186
    {
187
        if ($this->Platform && $this->Platform != self::WEBPUSH) {
188
            throw new Exception("Not a webpush");
189
        }
190
191
        $pushSub = $this->createSubscription();
192
        $webPush = self::getWebpushHandler();
193
194
        if (is_array($payload)) {
195
            $payload = json_encode($payload);
196
        }
197
        $payload = $payload ? $payload : null;
198
199
        $report = $webPush->sendOneNotification(
200
            $pushSub,
201
            $payload
202
        );
203
204
        return $report;
205
    }
206
207
    /**
208
     * @param Member $member
209
     * @return bool
210
     */
211
    public static function deleteSubscriptions(Member $member)
212
    {
213
        $i = 0;
214
        //@phpstan-ignore-next-line
215
        foreach ($member->PushSubscriptions() as $sub) {
216
            $i++;
217
            $sub->delete();
218
        }
219
        return $i > 0;
220
    }
221
222
    /**
223
     * @param string $endpoint
224
     * @return bool
225
     */
226
    public static function deleteEndpoint($endpoint)
227
    {
228
        $sub = self::getByEndpoint($endpoint);
229
        if ($sub) {
230
            $sub->delete();
231
            return true;
232
        }
233
        return false;
234
    }
235
236
    /**
237
     * @param array<string,mixed> $data
238
     * @param Member|null $member
239
     * @param string $type see consts
240
     * @return PushSubscription
241
     */
242
    public static function createNew($data, Member $member = null, $type = null)
243
    {
244
        $sub = new PushSubscription();
245
        if ($member) {
246
            $sub->MemberID = $member->ID;
247
        }
248
        $encodedData = json_encode($data);
249
        $encodedData = $encodedData ? $encodedData : '';
250
        $sub->Endpoint = $data['endpoint'] ?? '';
251
        $sub->Subscription = $encodedData;
252
        if ($type) {
253
            $sub->Platform = $type;
254
        }
255
        $sub->write();
256
        return $sub;
257
    }
258
259
    /**
260
     * @param string $endpoint
261
     * @return PushSubscription|null
262
     */
263
    public static function getByEndpoint($endpoint)
264
    {
265
        //@phpstan-ignore-next-line
266
        return self::get()->filter('Endpoint', $endpoint)->first();
267
    }
268
269
    /**
270
     * @link https://github.com/web-push-libs/web-push-php
271
     * @param string|array<mixed> $where
272
     * @param array<string,mixed>|string $payload
273
     * @return int
274
     */
275
    public static function sendPushNotifications($where, $payload)
276
    {
277
        /** @var PushSubscription[]|DataList $subs */
278
        $subs = self::get()->where($where);
279
280
        if (is_array($payload)) {
281
            $payload = json_encode($payload);
282
        }
283
        $payload = $payload ? $payload : null;
284
285
        $webPush = PushSubscription::getWebpushHandler();
286
        $processed = 0;
287
        $subsByEndpoint = [];
288
        /** @var PushSubscription $sub */
289
        foreach ($subs as $sub) {
290
            if ($sub->Platform && $sub->Platform != self::WEBPUSH) {
291
                continue;
292
            }
293
294
            $processed++;
295
296
            $pushSub = $sub->createSubscription();
297
298
            $webPush->queueNotification(
299
                $pushSub,
300
                $payload // optional (defaults null)
301
            );
302
303
            $subsByEndpoint[$pushSub->getEndpoint()] = $sub;
304
        }
305
306
        /**
307
         * Check sent results
308
         * @var MessageSentReport $report
309
         */
310
        foreach ($webPush->flush() as $report) {
311
            $endpoint = $report->getRequest()->getUri()->__toString();
312
313
            $sub = $subsByEndpoint[$endpoint] ?? '';
314
            if (!$sub) {
315
                throw new Exception("Could not find subscription for $endpoint");
316
            }
317
            $sub->LastCallError = !$report->isSuccess();
318
            if (!$report->isSuccess()) {
319
                $sub->LastCallErrorReason = $report->getReason();
320
            }
321
            $sub->LastCalled = date('Y-m-d H:i:s');
322
            $sub->write();
323
        }
324
325
        return $processed;
326
    }
327
}
328