Passed
Push — master ( 149af1...3220be )
by Thomas
02:36
created

SparkPostController::sent_emails()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 56
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 35
nc 6
nop 1
dl 0
loc 56
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\SparkPost;
4
5
use Exception;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Security\Permission;
12
use SilverStripe\Control\HTTPResponse;
13
use LeKoala\SparkPost\Api\SparkPostApiClient;
14
use SilverStripe\Control\HTTP;
15
use SilverStripe\ORM\ArrayList;
16
17
/**
18
 * Provide extensions points for handling the webhook
19
 *
20
 * @author LeKoala <[email protected]>
21
 */
22
class SparkPostController extends Controller
23
{
24
    /**
25
     * @var int
26
     */
27
    protected $eventsCount = 0;
28
29
    /**
30
     * @var int
31
     */
32
    protected $skipCount = 0;
33
34
    /**
35
     * @var array<string>
36
     */
37
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
38
        'incoming',
39
        'test',
40
        'configure_inbound_emails',
41
        'sent_emails',
42
    ];
43
44
    /**
45
     * Inject public dependencies into the controller
46
     *
47
     * @var array<string,string>
48
     */
49
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
50
        'logger' => '%$Psr\Log\LoggerInterface',
51
    ];
52
53
    /**
54
     * @var \Psr\Log\LoggerInterface
55
     */
56
    public $logger;
57
58
    /**
59
     * @param HTTPRequest $req
60
     * @return HTTPResponse|string
61
     */
62
    public function index(HTTPRequest $req)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

62
    public function index(/** @scrutinizer ignore-unused */ HTTPRequest $req)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
63
    {
64
        return $this->render([
65
            'Title' => 'SparkPost',
66
            'Content' => 'Please use a dedicated action'
67
        ]);
68
    }
69
70
    public function sent_emails(HTTPRequest $req)
71
    {
72
        if (!Director::isDev() && !Permission::check('ADMIN')) {
73
            return $this->httpError(404);
74
        }
75
76
        $logFolder = SparkPostHelper::getLogFolder();
77
        $view = $req->getVar('view');
78
        $download = $req->getVar('download');
79
        $base = Director::baseFolder();
80
81
        if ($download) {
82
            $file = $logFolder . '/' . $download;
83
            if (!is_file($file) || dirname($file) != $logFolder) {
84
                return $this->httpError(404);
85
            }
86
87
            $fileData = file_get_contents($file);
88
            $fileName = $download;
89
            return HTTPRequest::send_file($fileData, $fileName);
90
        }
91
92
        if ($view) {
93
            $file = $logFolder . '/' . $view;
94
            if (!is_file($file) || dirname($file) != $logFolder) {
95
                return $this->httpError(404);
96
            }
97
98
            $content = file_get_contents($file);
99
            $content = str_replace($logFolder . '/', '/__sparkpost/sent_emails?download=', $content);
100
            $content = str_replace($base, '', $content);
101
102
            $customFields = [
103
                'Email' => $content
104
            ];
105
        } else {
106
            $emails = new ArrayList();
107
            $items = glob("$logFolder/*.html", GLOB_NOSORT);
108
            usort($items, function ($a, $b) {
109
                return filemtime($b) - filemtime($a);
110
            });
111
112
            foreach ($items as $email) {
113
                $emails->push([
114
                    'File' => $email,
115
                    'Name' => basename($email),
116
                    'Date' => date('Y-m-d H:i:s', filemtime($email)),
117
                ]);
118
            }
119
120
            $customFields = [
121
                'Emails' => $emails
122
            ];
123
        }
124
125
        return $this->renderWith('LeKoala/SparkPost/SparkPostController_sent_emails', $customFields);
126
    }
127
128
    /**
129
     * You can also see /resources/sample.json
130
     *
131
     * @param HTTPRequest $req
132
     * @return string
133
     */
134
    public function test(HTTPRequest $req)
135
    {
136
        if (!Director::isDev()) {
137
            return $this->httpError(404);
138
        }
139
140
        $file = $this->getRequest()->getVar('file');
141
        if ($file) {
142
            $data = file_get_contents(Director::baseFolder() . '/' . rtrim($file, '/'));
143
        } else {
144
            $data = file_get_contents(dirname(__DIR__) . '/resources/sample.json');
145
        }
146
        if (!$data) {
147
            throw new Exception("Failed to get data");
148
        }
149
        $payload = json_decode($data, true);
150
        $payload['@headers'] = $req->getHeaders();
151
152
        $this->processPayload($payload, 'TEST');
153
154
        return 'TEST OK - ' . $this->eventsCount . ' events processed / ' . $this->skipCount . ' events skipped';
155
    }
156
157
    /**
158
     * @link https://support.sparkpost.com/customer/portal/articles/2039614-enabling-inbound-email-relaying-relay-webhooks
159
     * @param HTTPRequest $req
160
     * @return string
161
     */
162
    public function configure_inbound_emails(HTTPRequest $req)
163
    {
164
        if (!Director::isDev() && !Permission::check('ADMIN')) {
165
            return 'You must be in dev mode or be logged as an admin';
166
        }
167
168
        $clearExisting = $req->getVar('clear_existing');
169
        $clearWebhooks = $req->getVar('clear_webhooks');
170
        $clearInbound = $req->getVar('clear_inbound');
171
        if ($clearExisting) {
172
            echo '<strong>Existing inbounddomains and relay webhooks will be cleared</strong><br/>';
173
        } else {
174
            echo 'You can clear existing inbound domains and relay webhooks by passing ?clear_existing=1&clear_webhooks=1&clear_inbound=1<br/>';
175
        }
176
177
        $client = SparkPostHelper::getMasterClient();
178
179
        $inbound_domain = Environment::getEnv('SPARKPOST_INBOUND_DOMAIN');
180
        if (!$inbound_domain) {
181
            die('You must define a key SPARKPOST_INBOUND_DOMAIN');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
182
        }
183
184
        /*
185
         *  "name": "Replies Webhook",
186
         *  "target": "https://webhooks.customer.example/replies",
187
         * "auth_token": "5ebe2294ecd0e0f08eab7690d2a6ee69",
188
         *  "match": {
189
         *  "protocol": "SMTP",
190
         * "domain": "email.example.com"
191
         */
192
193
        $listWebhooks = $client->listRelayWebhooks();
194
        $listInboundDomains = $client->listInboundDomains();
195
196
        if ($clearExisting) {
197
            // we need to delete relay webhooks first!
198
            if ($clearWebhooks) {
199
                foreach ($listWebhooks as $wh) {
200
                    $client->deleteRelayWebhook($wh['id']);
201
                    echo 'Delete relay webhook ' . $wh['id'] . '<br/>';
202
                }
203
            }
204
            if ($clearInbound) {
205
                foreach ($listInboundDomains as $id) {
206
                    $client->deleteInboundDomain($id['domain']);
207
                    echo 'Delete domain ' . $id['domain'] . '<br/>';
208
                }
209
            }
210
211
            $listWebhooks = $client->listRelayWebhooks();
212
            $listInboundDomains = $client->listInboundDomains();
213
        }
214
215
        echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
216
        echo 'List Inbounds Domains:<br/>';
217
        print_r($listInboundDomains);
218
        echo '</pre>';
219
220
        $found = false;
221
222
        foreach ($listInboundDomains as $id) {
223
            if ($id['domain'] == $inbound_domain) {
224
                $found = true;
225
            }
226
        }
227
228
        if (!$found) {
229
            echo "Domain is not found, we create it<br/>";
230
231
            // This is the domain that users will send email to.
232
            $result = $client->createInboundDomain($inbound_domain);
233
234
            echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
235
            echo 'Create Inbound Domain:<br/>';
236
            print_r($result);
237
            echo '</pre>';
238
        } else {
239
            echo "Domain is already configured<br/>";
240
        }
241
242
        // Now that you have your InboundDomain set up, you can create your Relay Webhook by sending a POST request to
243
        // https://api.sparkpost.com/api/v1/relay-webhooks. This step links your consumer with the Inbound Domain.
244
245
        echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
246
        echo 'List Webhooks:<br/>';
247
        print_r($listWebhooks);
248
        echo '</pre>';
249
250
        $found = false;
251
252
        foreach ($listWebhooks as $wh) {
253
            if ($wh['match']['domain'] == $inbound_domain) {
254
                $found = true;
255
            }
256
        }
257
258
        if (!$found) {
259
            //  The match.domain property should be the same as the Inbound Domain you set up in the previous step
260
            $webhookResult = $client->createRelayWebhook([
261
                'name' => 'Inbound Webhook',
262
                'target' => Director::absoluteURL('sparkpost/incoming'),
263
                'match' => [
264
                    'domain' => $inbound_domain
265
                ]
266
            ]);
267
268
            echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
269
            echo 'Webhook result:<br/>';
270
            print_r($webhookResult);
271
            echo '</pre>';
272
273
            if ($webhookResult['id']) {
274
                echo "New webhook created with id " . $webhookResult['id'];
275
            }
276
        } else {
277
            echo "Webhook already configured";
278
        }
279
        return '';
280
    }
281
282
    /**
283
     * Handle incoming webhook
284
     *
285
     * @link https://developers.sparkpost.com/api/#/reference/webhooks/create-a-webhook
286
     * @link https://www.sparkpost.com/blog/webhooks-beyond-the-basics/
287
     * @link https://support.sparkpost.com/customer/portal/articles/1976204-webhook-event-reference
288
     * @param HTTPRequest $req
289
     * @return HTTPResponse
290
     */
291
    public function incoming(HTTPRequest $req)
292
    {
293
        // Each webhook batch contains the header X-Messagesystems-Batch-Id,
294
        // which is useful for auditing and prevention of processing duplicate batches.
295
        $batchId = $req->getHeader('X-Messagesystems-Batch-Id');
296
        if (!$batchId) {
297
            $batchId = uniqid();
298
        }
299
300
        $json = file_get_contents('php://input');
301
302
        // By default, return a valid response
303
        $response = $this->getResponse();
304
        $response->setStatusCode(200);
305
        $response->setBody('NO DATA');
306
307
        if (!$json) {
308
            return $response;
309
        }
310
311
        $payload = json_decode($json, true);
312
313
        // Check credentials if defined
314
        $isAuthenticated = true;
315
        $authError = null;
316
        if (SparkPostHelper::getWebhookUsername()) {
317
            try {
318
                $this->authRequest($req);
319
            } catch (Exception $e) {
320
                $isAuthenticated = false;
321
                $authError = $e->getMessage();
322
            }
323
        }
324
325
        $webhookLogDir = Environment::getEnv('SPARKPOST_WEBHOOK_LOG_DIR');
326
        if ($webhookLogDir) {
327
            $dir = rtrim(Director::baseFolder(), '/') . '/' . rtrim($webhookLogDir, '/');
328
329
            if (!is_dir($dir) && Director::isDev()) {
330
                mkdir($dir, 0755, true);
331
            }
332
333
            if (is_dir($dir)) {
334
                $storedPayload = array_merge([], $payload);
335
                $storedPayload['@headers'] = $req->getHeaders();
336
                $storedPayload['@isAuthenticated'] = $isAuthenticated;
337
                $storedPayload['@authError'] = $authError;
338
                $prettyPayload = json_encode($storedPayload, JSON_PRETTY_PRINT);
339
                $time = date('Ymd-His');
340
                file_put_contents($dir . '/' . $time . '_' . $batchId . '.json', $prettyPayload);
341
            } else {
342
                $this->getLogger()->debug("Directory $dir does not exist");
343
            }
344
        }
345
346
        if (!$isAuthenticated) {
347
            return $response;
348
        }
349
350
        try {
351
            $this->processPayload($payload, $batchId);
352
        } catch (Exception $ex) {
353
            // Maybe processing payload will create exceptions, but we
354
            // catch them to send a proper response to the API
355
            $logLevel = self::config()->log_level ? self::config()->log_level : 7;
356
            $this->getLogger()->log($ex->getMessage(), $logLevel);
357
        }
358
359
        $response->setBody('OK');
360
        return $response;
361
    }
362
363
    /**
364
     * @param HTTPRequest $req
365
     * @return void
366
     */
367
    protected function authRequest(HTTPRequest $req)
368
    {
369
        $requestUser = $req->getHeader('php_auth_user');
370
        $requestPassword = $req->getHeader('php_auth_pw');
371
        if (!$requestUser) {
372
            $requestUser = $_SERVER['PHP_AUTH_USER'];
373
        }
374
        if (!$requestPassword) {
375
            $requestPassword = $_SERVER['PHP_AUTH_PW'];
376
        }
377
378
        $authError = null;
379
        $hasSuppliedCredentials = $requestUser && $requestPassword;
380
        if ($hasSuppliedCredentials) {
381
            $user = SparkPostHelper::getWebhookUsername();
382
            $password = SparkPostHelper::getWebhookPassword();
383
            if ($user != $requestUser) {
384
                $authError = "User $requestUser doesn't match";
385
            } elseif ($password != $requestPassword) {
386
                $authError = "Password $requestPassword don't match";
387
            }
388
        } else {
389
            $authError = "No credentials";
390
        }
391
        if ($authError) {
392
            throw new Exception($authError);
393
        }
394
    }
395
396
    /**
397
     * Process data
398
     *
399
     * @param array<mixed> $payload
400
     * @param string $batchId
401
     * @return void
402
     */
403
    protected function processPayload(array $payload, $batchId = null)
404
    {
405
        $this->extend('beforeProcessPayload', $payload, $batchId);
406
407
        $subaccount = SparkPostHelper::getClient()->getSubaccount();
408
409
        foreach ($payload as $idx => $r) {
410
            // This is a test payload
411
            if (empty($r)) {
412
                continue;
413
            }
414
            // This is a custom entry
415
            if (!is_numeric($idx)) {
416
                continue;
417
            }
418
419
            $ev = $r['msys'] ?? null;
420
421
            // Invalid payload: it should always be an object with a msys key containing the event
422
            if ($ev === null) {
423
                $this->getLogger()->warning("Invalid payload: " . substr((string)json_encode($r), 0, 100) . '...');
424
                continue;
425
            }
426
427
            // Check type: it should be an object with the type as key
428
            $type = key($ev);
429
            if (!isset($ev[$type])) {
430
                $this->getLogger()->warning("Invalid type $type in SparkPost payload");
431
                continue;
432
            }
433
            $data = $ev[$type];
434
435
            // Ignore events not related to the subaccount we are managing
436
            if (!empty($data['subaccount_id']) && $subaccount && $subaccount != $data['subaccount_id']) {
437
                $this->skipCount++;
438
                continue;
439
            }
440
441
            $this->eventsCount++;
442
            $this->extend('onAnyEvent', $data, $type);
443
444
            switch ($type) {
445
                    //Click, Open
446
                case SparkPostApiClient::TYPE_ENGAGEMENT:
447
                    $this->extend('onEngagementEvent', $data, $type);
448
                    break;
449
                    //Generation Failure, Generation Rejection
450
                case SparkPostApiClient::TYPE_GENERATION:
451
                    $this->extend('onGenerationEvent', $data, $type);
452
                    break;
453
                    //Bounce, Delivery, Injection, SMS Status, Spam Complaint, Out of Band, Policy Rejection, Delay
454
                case SparkPostApiClient::TYPE_MESSAGE:
455
                    $this->extend('onMessageEvent', $data, $type);
456
                    break;
457
                    //Relay Injection, Relay Rejection, Relay Delivery, Relay Temporary Failure, Relay Permanent Failure
458
                case SparkPostApiClient::TYPE_RELAY:
459
                    $this->extend('onRelayEvent', $data, $type);
460
                    break;
461
                    //List Unsubscribe, Link Unsubscribe
462
                case SparkPostApiClient::TYPE_UNSUBSCRIBE:
463
                    $this->extend('onUnsubscribeEvent', $data, $type);
464
                    break;
465
            }
466
        }
467
468
        $this->extend('afterProcessPayload', $payload, $batchId);
469
    }
470
471
472
    /**
473
     * Get logger
474
     *
475
     * @return \Psr\Log\LoggerInterface
476
     */
477
    public function getLogger()
478
    {
479
        return $this->logger;
480
    }
481
}
482