Passed
Push — master ( c65031...5c5fd2 )
by Thomas
03:11
created

SparkPostController::incoming()   D

Complexity

Conditions 14
Paths 282

Size

Total Lines 70
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 41
nc 282
nop 1
dl 0
loc 70
rs 4.4083
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 LeKoala\SparkPost\Api\SparkPostApiClient;
13
14
/**
15
 * Provide extensions points for handling the webhook
16
 *
17
 * @author LeKoala <[email protected]>
18
 */
19
class SparkPostController extends Controller
20
{
21
    protected $eventsCount = 0;
22
    protected $skipCount = 0;
23
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
24
        'incoming',
25
        'test',
26
        'configure_inbound_emails'
27
    ];
28
29
    /**
30
     * Inject public dependencies into the controller
31
     *
32
     * @var array
33
     */
34
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
35
        'logger' => '%$Psr\Log\LoggerInterface',
36
    ];
37
38
    /**
39
     * @var Psr\Log\LoggerInterface
0 ignored issues
show
Bug introduced by
The type LeKoala\SparkPost\Psr\Log\LoggerInterface was not found. Did you mean Psr\Log\LoggerInterface? If so, make sure to prefix the type with \.
Loading history...
40
     */
41
    public $logger;
42
43
    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

43
    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...
44
    {
45
        return $this->render([
46
            'Title' => 'SparkPost',
47
            'Content' => 'Please use a dedicated action'
48
        ]);
49
    }
50
51
    /**
52
     * You can also see /resources/sample.json
53
     *
54
     * @param HTTPRequest $req
55
     */
56
    public function test(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

56
    public function test(/** @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...
57
    {
58
        if (!Director::isDev()) {
59
            return 'You can only test in dev mode';
60
        }
61
62
        $client = $this->getClient();
0 ignored issues
show
Bug introduced by
The method getClient() does not exist on LeKoala\SparkPost\SparkPostController. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

62
        /** @scrutinizer ignore-call */ 
63
        $client = $this->getClient();
Loading history...
Unused Code introduced by
The assignment to $client is dead and can be removed.
Loading history...
63
64
        $file = $this->getRequest()->getVar('file');
65
        if ($file) {
66
            $data = file_get_contents(Director::baseFolder() . '/' . rtrim($file, '/'));
67
        } else {
68
            $data = file_get_contents(dirname(__DIR__) . '/resources/sample.json');
69
        }
70
        $payload = json_decode($data, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Bug introduced by
LeKoala\SparkPost\JSON_OBJECT_AS_ARRAY of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

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

70
        $payload = json_decode($data, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
71
72
        $this->processPayload($payload, 'TEST');
73
74
        return 'TEST OK - ' . $this->eventsCount . ' events processed / ' . $this->skipCount . ' events skipped';
75
    }
76
77
    /**
78
     * @link https://support.sparkpost.com/customer/portal/articles/2039614-enabling-inbound-email-relaying-relay-webhooks
79
     * @param HTTPRequest $req
80
     * @return string
81
     */
82
    public function configure_inbound_emails(HTTPRequest $req)
83
    {
84
        if (!Director::isDev() && !Permission::check('ADMIN')) {
85
            return 'You must be in dev mode or be logged as an admin';
86
        }
87
88
        $clearExisting = $req->getVar('clear_existing');
89
        $clearWebhooks = $req->getVar('clear_webhooks');
90
        $clearInbound = $req->getVar('clear_inbound');
91
        if ($clearExisting) {
92
            echo '<strong>Existing inbounddomains and relay webhooks will be cleared</strong><br/>';
93
        } else {
94
            echo 'You can clear existing inbound domains and relay webhooks by passing ?clear_existing=1&clear_webhooks=1&clear_inbound=1<br/>';
95
        }
96
97
        $client = SparkPostHelper::getMasterClient();
98
99
        $inbound_domain = Environment::getEnv('SPARKPOST_INBOUND_DOMAIN');
100
        if (!$inbound_domain) {
101
            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...
102
        }
103
104
        /*
105
         *  "name": "Replies Webhook",
106
         *  "target": "https://webhooks.customer.example/replies",
107
         * "auth_token": "5ebe2294ecd0e0f08eab7690d2a6ee69",
108
         *  "match": {
109
         *  "protocol": "SMTP",
110
         * "domain": "email.example.com"
111
         */
112
113
        $listWebhooks = $client->listRelayWebhooks();
114
        $listInboundDomains = $client->listInboundDomains();
115
116
        if ($clearExisting) {
117
            // we need to delete relay webhooks first!
118
            if ($clearWebhooks) {
119
                foreach ($listWebhooks as $wh) {
120
                    $client->deleteRelayWebhook($wh['id']);
121
                    echo 'Delete relay webhook ' . $wh['id'] . '<br/>';
122
                }
123
            }
124
            if ($clearInbound) {
125
                foreach ($listInboundDomains as $id) {
126
                    $client->deleteInboundDomain($id['domain']);
127
                    echo 'Delete domain ' . $id['domain'] . '<br/>';
128
                }
129
            }
130
131
            $listWebhooks = $client->listRelayWebhooks();
132
            $listInboundDomains = $client->listInboundDomains();
133
        }
134
135
        echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
136
        echo 'List Inbounds Domains:<br/>';
137
        print_r($listInboundDomains);
138
        echo '</pre>';
139
140
        $found = false;
141
142
        foreach ($listInboundDomains as $id) {
143
            if ($id['domain'] == $inbound_domain) {
144
                $found = true;
145
            }
146
        }
147
148
        if (!$found) {
149
            echo "Domain is not found, we create it<br/>";
150
151
            // This is the domain that users will send email to.
152
            $result = $client->createInboundDomain($inbound_domain);
153
154
            echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
155
            echo 'Create Inbound Domain:<br/>';
156
            print_r($result);
157
            echo '</pre>';
158
        } else {
159
            echo "Domain is already configured<br/>";
160
        }
161
162
        // Now that you have your InboundDomain set up, you can create your Relay Webhook by sending a POST request to
163
        // https://api.sparkpost.com/api/v1/relay-webhooks. This step links your consumer with the Inbound Domain.
164
165
        echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
166
        echo 'List Webhooks:<br/>';
167
        print_r($listWebhooks);
168
        echo '</pre>';
169
170
        $found = false;
171
172
        foreach ($listWebhooks as $wh) {
173
            if ($wh['match']['domain'] == $inbound_domain) {
174
                $found = true;
175
            }
176
        }
177
178
        if (!$found) {
179
            //  The match.domain property should be the same as the Inbound Domain you set up in the previous step
180
            $webhookResult = $client->createRelayWebhook([
181
                'name' => 'Inbound Webhook',
182
                'target' => Director::absoluteURL('sparkpost/incoming'),
183
                'match' => [
184
                    'domain' => $inbound_domain
185
                ]
186
            ]);
187
188
            echo '<pre>' . __FILE__ . ':' . __LINE__ . '<br/>';
189
            echo 'Webhook result:<br/>';
190
            print_r($webhookResult);
191
            echo '</pre>';
192
193
            if ($webhookResult['id']) {
194
                echo "New webhook created with id " . $webhookResult['id'];
195
            }
196
        } else {
197
            echo "Webhook already configured";
198
        }
199
    }
200
201
    /**
202
     * Handle incoming webhook
203
     *
204
     * @link https://developers.sparkpost.com/api/#/reference/webhooks/create-a-webhook
205
     * @link https://www.sparkpost.com/blog/webhooks-beyond-the-basics/
206
     * @link https://support.sparkpost.com/customer/portal/articles/1976204-webhook-event-reference
207
     * @param HTTPRequest $req
208
     */
209
    public function incoming(HTTPRequest $req)
210
    {
211
        // Each webhook batch contains the header X-Messagesystems-Batch-Id,
212
        // which is useful for auditing and prevention of processing duplicate batches.
213
        $batchId = $req->getHeader('X-Messagesystems-Batch-Id');
214
        if (!$batchId) {
215
            $batchId = uniqid();
216
        }
217
218
        $json = file_get_contents('php://input');
219
220
        // By default, return a valid response
221
        $response = $this->getResponse();
222
        $response->setStatusCode(200);
223
        $response->setBody('NO DATA');
224
225
        if (!$json) {
226
            return $response;
227
        }
228
229
        $payload = json_decode($json, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Bug introduced by
LeKoala\SparkPost\JSON_OBJECT_AS_ARRAY of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

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

229
        $payload = json_decode($json, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
230
231
        // Check credentials if defined
232
        $isAuthenticated = true;
233
        if (SparkPostHelper::getWebhookUsername()) {
234
            $hasSuppliedCredentials = !(empty($_SERVER['PHP_AUTH_USER']) && empty($_SERVER['PHP_AUTH_PW']));
235
            if ($hasSuppliedCredentials) {
236
                $user = SparkPostHelper::getWebhookUsername();
237
                $password = SparkPostHelper::getWebhookPassword();
238
                $isAuthenticated = ($_SERVER['PHP_AUTH_USER'] == $user || $_SERVER['PHP_AUTH_PW'] == $password);
239
            } else {
240
                $isAuthenticated = false;
241
            }
242
        }
243
244
        $webhookLogDir = Environment::getEnv('SPARKPOST_WEBHOOK_LOG_DIR');
245
        if ($webhookLogDir) {
246
            $dir = rtrim(Director::baseFolder(), '/') . '/' . rtrim($webhookLogDir, '/');
247
248
            if (!is_dir($dir) && Director::isDev()) {
249
                mkdir($dir, 0755, true);
250
            }
251
252
            if (is_dir($dir)) {
253
                $payload['@headers'] = $req->getHeaders();
254
                $payload['@isAuthenticated'] = $isAuthenticated;
255
                $prettyPayload = json_encode($payload, JSON_PRETTY_PRINT);
256
                $time = date('Ymd-His');
257
                file_put_contents($dir . '/' . $time . '_' . $batchId . '.json', $prettyPayload);
258
            } else {
259
                $this->getLogger()->debug("Directory $dir does not exist");
260
            }
261
        }
262
263
        if (!$isAuthenticated) {
264
            return $response;
265
        }
266
267
        try {
268
            $this->processPayload($payload, $batchId);
269
        } catch (Exception $ex) {
270
            // Maybe processing payload will create exceptions, but we
271
            // catch them to send a proper response to the API
272
            $logLevel = self::config()->log_level ? self::config()->log_level : 7;
273
            $this->getLogger()->log($ex->getMessage(), $logLevel);
274
        }
275
276
        $response->setBody('OK');
277
278
        return $response;
279
    }
280
281
    /**
282
     * Process data
283
     *
284
     * @param array $payload
285
     * @param string $batchId
286
     */
287
    protected function processPayload(array $payload, $batchId = null)
288
    {
289
        $this->extend('beforeProcessPayload', $payload, $batchId);
290
291
        $subaccount = SparkPostHelper::getClient()->getSubaccount();
292
293
        foreach ($payload as $r) {
294
            $ev = $r['msys'];
295
296
            // This is a test payload
297
            if (empty($ev)) {
298
                continue;
299
            }
300
301
            $type = key($ev);
302
            if (!isset($ev[$type])) {
303
                $this->getLogger()->warn("Invalid type $type in SparkPost payload");
304
                continue;
305
            }
306
            $data = $ev[$type];
307
308
            // Ignore events not related to the subaccount we are managing
309
            if (!empty($data['subaccount_id']) && $subaccount && $subaccount != $data['subaccount_id']) {
310
                $this->skipCount++;
311
                continue;
312
            }
313
314
            $this->eventsCount++;
315
            $this->extend('onAnyEvent', $data, $type);
316
317
            switch ($type) {
318
                    //Click, Open
319
                case SparkPostApiClient::TYPE_ENGAGEMENT:
320
                    $this->extend('onEngagementEvent', $data, $type);
321
                    break;
322
                    //Generation Failure, Generation Rejection
323
                case SparkPostApiClient::TYPE_GENERATION:
324
                    $this->extend('onGenerationEvent', $data, $type);
325
                    break;
326
                    //Bounce, Delivery, Injection, SMS Status, Spam Complaint, Out of Band, Policy Rejection, Delay
327
                case SparkPostApiClient::TYPE_MESSAGE:
328
                    $this->extend('onMessageEvent', $data, $type);
329
                    break;
330
                    //Relay Injection, Relay Rejection, Relay Delivery, Relay Temporary Failure, Relay Permanent Failure
331
                case SparkPostApiClient::TYPE_RELAY:
332
                    $this->extend('onRelayEvent', $data, $type);
333
                    break;
334
                    //List Unsubscribe, Link Unsubscribe
335
                case SparkPostApiClient::TYPE_UNSUBSCRIBE:
336
                    $this->extend('onUnsubscribeEvent', $data, $type);
337
                    break;
338
            }
339
        }
340
341
        $this->extend('afterProcessPayload', $payload, $batchId);
342
    }
343
344
345
    /**
346
     * Get logger
347
     *
348
     * @return Psr\SimpleCache\CacheInterface
0 ignored issues
show
Bug introduced by
The type LeKoala\SparkPost\Psr\SimpleCache\CacheInterface was not found. Did you mean Psr\SimpleCache\CacheInterface? If so, make sure to prefix the type with \.
Loading history...
349
     */
350
    public function getLogger()
351
    {
352
        return $this->logger;
353
    }
354
}
355