MailgunController   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 7
dl 0
loc 220
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A index() 0 7 1
A test() 0 17 3
A configure_inbound_emails() 0 26 5
B incoming() 0 48 8
A verifyCall() 0 5 1
A processPayload() 0 34 1
A getLogger() 0 4 1
1
<?php
2
3
namespace LeKoala\Mailgun;
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\Core\Injector\Injector;
13
14
/**
15
 * Provide extensions points for handling the webhook
16
 *
17
 * @link https://www.mailgun.com/guides/your-guide-to-webhooks/
18
 * @author LeKoala <[email protected]>
19
 */
20
class MailgunController extends Controller
21
{
22
    const TYPE_CLICKED = 'clicked';
23
    const TYPE_COMPLAINED = 'complained';
24
    const TYPE_DELIVERED = 'delivered';
25
    const TYPE_OPENED = 'opened';
26
    const TYPE_PERMANENT_FAIL = 'permanent_fail';
27
    const TYPE_TEMPORARY_FAIL = 'temporary_fail';
28
    const TYPE_UNSUBSCRIBED = 'unsubscribed';
29
30
    protected $eventsCount = 0;
31
    protected $skipCount = 0;
32
    private static $allowed_actions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
33
        'incoming',
34
        'test',
35
        'configure_inbound_emails'
36
    ];
37
38
    /**
39
     * Inject public dependencies into the controller
40
     *
41
     * @var array
42
     */
43
    private static $dependencies = [
44
        'logger' => '%$Psr\Log\LoggerInterface',
45
    ];
46
47
    /**
48
     * @var Psr\Log\LoggerInterface
49
     */
50
    public $logger;
51
52
    public function index(HTTPRequest $req)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed.

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

Loading history...
53
    {
54
        return $this->render([
55
            'Title' => 'Mailgun',
56
            'Content' => 'Please use a dedicated action'
57
        ]);
58
    }
59
60
    /**
61
     * You can also see /resources/webhook.txt
62
     *
63
     * @param HTTPRequest $req
64
     */
65
    public function test(HTTPRequest $req)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed.

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

Loading history...
66
    {
67
        if (!Director::isDev()) {
68
            return 'You can only test in dev mode';
69
        }
70
71
        $file = $this->getRequest()->getVar('file');
72
        if ($file) {
73
            $data = file_get_contents(Director::baseFolder() . '/' . rtrim($file, '/'));
74
        } else {
75
            $data = file_get_contents(dirname(__DIR__) . '/resources/webhook.txt');
76
        }
77
78
        $this->processPayload($data, 'TEST');
79
80
        return 'TEST OK - ' . $this->eventsCount . ' events processed / ' . $this->skipCount . ' events skipped';
81
    }
82
83
    /**
84
     * @link https://support.Mailgun.com/customer/portal/articles/2039614-enabling-inbound-email-relaying-relay-webhooks
85
     * @param HTTPRequest $req
86
     * @return string
87
     */
88
    public function configure_inbound_emails(HTTPRequest $req)
89
    {
90
        if (!Director::isDev() && !Permission::check('ADMIN')) {
91
            return 'You must be in dev mode or be logged as an admin';
92
        }
93
94
        $clearExisting = $req->getVar('clear_existing');
95
        $clearWebhooks = $req->getVar('clear_webhooks');
0 ignored issues
show
Unused Code introduced by
$clearWebhooks is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
96
        $clearInbound = $req->getVar('clear_inbound');
0 ignored issues
show
Unused Code introduced by
$clearInbound is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
97
        if ($clearExisting) {
98
            echo '<strong>Existing inbounddomains and relay webhooks will be cleared</strong><br/>';
99
        } else {
100
            echo 'You can clear existing inbound domains and relay webhooks by passing ?clear_existing=1&clear_webhooks=1&clear_inbound=1<br/>';
101
        }
102
103
        $client = MailgunHelper::getClient();
0 ignored issues
show
Unused Code introduced by
$client is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
104
105
        $inbound_domain = Environment::getEnv('MAILGUN_INBOUND_DOMAIN');
106
        if (!$inbound_domain) {
107
            die('You must define a key MAILGUN_INBOUND_DOMAIN');
108
        }
109
110
        //TODO : use routing to implement this
111
112
        throw new Exception("Not implemented yet");
113
    }
114
115
    /**
116
     * Handle incoming webhook
117
     *
118
     * @param HTTPRequest $req
119
     */
120
    public function incoming(HTTPRequest $req)
121
    {
122
        $batchId = uniqid();
123
124
        $json = file_get_contents('php://input');
125
126
        // By default, return a valid response
127
        $response = $this->getResponse();
128
        $response->setStatusCode(200);
129
        $response->setBody('NO DATA');
130
131
        if (!$json) {
132
            return $response;
133
        }
134
135
        $webhookLogDir = Environment::getEnv('MAILGUN_WEBHOOK_LOG_DIR');
136
        if ($webhookLogDir) {
137
            $dir = rtrim(Director::baseFolder(), '/') . '/' . rtrim($webhookLogDir, '/');
138
139
            if (!is_dir($dir) && Director::isDev()) {
140
                mkdir($dir, 0755, true);
141
            }
142
143
            if (is_dir($dir)) {
144
                $payload['@headers'] = $req->getHeaders();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$payload was never initialized. Although not strictly required by PHP, it is generally a good practice to add $payload = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
145
                $prettyPayload = json_encode(json_decode($json), JSON_PRETTY_PRINT);
146
                $time = date('Ymd-His');
147
                file_put_contents($dir . '/' . $time . '_' . $batchId . '.json', $prettyPayload);
148
            } else {
149
                $this->getLogger()->debug("Directory $dir does not exist");
150
            }
151
        }
152
153
        $payload = json_decode($json, JSON_OBJECT_AS_ARRAY);
154
155
        try {
156
            $this->processPayload($payload, $batchId);
157
        } catch (Exception $ex) {
158
            // Maybe processing payload will create exceptions, but we
159
            // catch them to send a proper response to the API
160
            $logLevel = self::config()->log_level ? self::config()->log_level : 7;
161
            $this->getLogger()->log($ex->getMessage(), $logLevel);
162
        }
163
164
        $response->setBody('OK');
165
166
        return $response;
167
    }
168
169
    /**
170
     * A receiving URI must be public, so webhooks should be secured with a signature,
171
     * time stamp and token to create a hash map using an API key to verify that the data
172
     * is coming from the developer’s ESP. Users should program their application to check
173
     * that hash map and compare it to that of the ESP, and then allow the post to be made only if it matches.
174
     *
175
     * To verify the webhook is originating from their ESP, users should concatenate time stamp and token values,
176
     * encode the resulting string with the HMAC algorithm (using the ESP’s supplied API Key as a key and SHA256 digest mode),
177
     * and compare the resulting hexdigest to the signature.
178
     * Optionally, users can cache the token value locally and not honor any subsequent request with the same token. This will prevent replay attacks.
179
     *
180
     * @return bool
181
     */
182
    protected function verifyCall()
183
    {
184
        //TODO: implement this
185
        return true;
186
    }
187
188
    /**
189
     * Process data
190
     *
191
     * @param string $payload
192
     * @param string $batchId
193
     */
194
    protected function processPayload($payload, $batchId = null)
195
    {
196
        $this->extend('beforeProcessPayload', $payload, $batchId);
197
198
        // TODO: parse payload properly
199
        // foreach ($payload as $r) {
200
        //     $this->eventsCount++;
201
        //     $this->extend('onAnyEvent', $data, $type);
202
203
        //     switch ($type) {
204
        //             //Click, Open
205
        //         case self::TYPE_CLICKED:
206
        //         case self::TYPE_OPENED:
207
        //             $this->extend('onEngagementEvent', $data, $type);
208
        //             break;
209
        //             //Generation Failure, Generation Rejection
210
        //         case self::TYPE_DELIVERED:
211
        //             $this->extend('onGenerationEvent', $data, $type);
212
        //             break;
213
        //             //Bounce, Delivery, Injection, SMS Status, Spam Complaint, Out of Band, Policy Rejection, Delay
214
        //         case self::TYPE_COMPLAINED:
215
        //         case self::TYPE_PERMANENT_FAIL:
216
        //         case self::TYPE_TEMPORARY_FAIL:
217
        //             $this->extend('onMessageEvent', $data, $type);
218
        //             break;
219
        //             //List Unsubscribe, Link Unsubscribe
220
        //         case self::TYPE_UNSUBSCRIBED:
221
        //             $this->extend('onUnsubscribeEvent', $data, $type);
222
        //             break;
223
        //     }
224
        // }
225
226
        $this->extend('afterProcessPayload', $payload, $batchId);
227
    }
228
229
230
    /**
231
     * Get logger
232
     *
233
     * @return Psr\SimpleCache\CacheInterface
234
     */
235
    public function getLogger()
236
    {
237
        return $this->logger;
238
    }
239
}
240