Passed
Push — master ( 418dab...364f8f )
by Thomas
03:49 queued 50s
created

SparkPostAdmin::index()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace LeKoala\SparkPost;
4
5
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception 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...
6
use SilverStripe\Forms\Tab;
7
use SilverStripe\Forms\Form;
8
use SilverStripe\Forms\TabSet;
9
use SilverStripe\ORM\ArrayLib;
10
use SilverStripe\ORM\ArrayList;
11
use SilverStripe\View\ArrayData;
12
use SilverStripe\Control\Session;
13
use SilverStripe\Forms\DateField;
14
use SilverStripe\Forms\FieldList;
15
use SilverStripe\Forms\FormField;
16
use SilverStripe\Forms\TextField;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Core\Environment;
19
use SilverStripe\Forms\FormAction;
20
use SilverStripe\Admin\LeftAndMain;
21
use SilverStripe\Forms\HiddenField;
22
use SilverStripe\Security\Security;
23
use SilverStripe\View\ViewableData;
24
use SilverStripe\Forms\LiteralField;
25
use SilverStripe\Control\Email\Email;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Security\Permission;
28
use LeKoala\SparkPost\SparkPostHelper;
29
use SilverStripe\Forms\CompositeField;
30
use SilverStripe\SiteConfig\SiteConfig;
31
use SilverStripe\Forms\GridField\GridField;
32
use SilverStripe\Security\PermissionProvider;
33
use SilverStripe\Security\DefaultAdminService;
34
use SilverStripe\Forms\GridField\GridFieldConfig;
35
use SilverStripe\Forms\GridField\GridFieldFooter;
36
use SilverStripe\Forms\GridField\GridFieldDetailForm;
37
use SilverStripe\Forms\GridField\GridFieldDataColumns;
38
use Symbiote\GridFieldExtensions\GridFieldTitleHeader;
39
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
40
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
41
42
/**
43
 * Allow you to see messages sent through the api key used to send messages
44
 *
45
 * @author LeKoala <[email protected]>
46
 */
47
class SparkPostAdmin extends LeftAndMain implements PermissionProvider
48
{
49
50
    const MESSAGE_CACHE_MINUTES = 5;
51
    const WEBHOOK_CACHE_MINUTES = 1440; // 1 day
52
    const SENDINGDOMAIN_CACHE_MINUTES = 1440; // 1 day
53
54
    private static $menu_title = "SparkPost";
0 ignored issues
show
introduced by
The private property $menu_title is not used, and could be removed.
Loading history...
55
    private static $url_segment = "sparkpost";
0 ignored issues
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
56
    private static $menu_icon = "sparkpost/images/sparkpost-icon.png";
0 ignored issues
show
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
57
    private static $url_rule = '/$Action/$ID/$OtherID';
0 ignored issues
show
introduced by
The private property $url_rule is not used, and could be removed.
Loading history...
58
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
59
        'settings',
60
        'SearchForm',
61
        'doSearch',
62
        "doInstallHook",
63
        "doUninstallHook",
64
        "doInstallDomain",
65
        "doUninstallDomain",
66
        "send_test",
67
    ];
68
69
    private static $cache_enabled = true;
70
71
    /**
72
     * @var bool
73
     */
74
    protected $subaccountKey = false;
75
76
    /**
77
     * @var Exception
78
     */
79
    protected $lastException;
80
81
    /**
82
     * @var ViewableData
83
     */
84
    protected $currentMessage;
85
86
    /**
87
     * Inject public dependencies into the controller
88
     *
89
     * @var array
90
     */
91
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
92
        'logger' => '%$Psr\Log\LoggerInterface',
93
        'cache' => '%$Psr\SimpleCache\CacheInterface.sparkpost', // see _config/cache.yml
94
    ];
95
96
    /**
97
     * @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...
98
     */
99
    public $logger;
100
101
    /**
102
     * @var 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...
103
     */
104
    public $cache;
105
106
    public function init()
107
    {
108
        parent::init();
109
110
        if (isset($_GET['refresh'])) {
111
            $this->getCache()->clear();
112
        }
113
    }
114
115
    public function settings($request)
116
    {
117
        return parent::index($request);
118
    }
119
120
    public function send_test($request)
121
    {
122
        if (!$this->CanConfigureApi()) {
123
            return $this->httpError(404);
124
        }
125
        $service = DefaultAdminService::create();
126
        $to = $request->getVar('to');
127
        if (!$to) {
128
            $to = $service->findOrCreateDefaultAdmin()->Email;
129
        }
130
        $email = Email::create();
131
        $email->setSubject("Test email");
132
        $email->setBody("Test " . date('Y-m-d H:i:s'));
133
        $email->setTo($to);
134
135
        $result = $email->send();
136
        var_dump($result);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($result) looks like debug code. Are you sure you do not want to remove it?
Loading history...
137
    }
138
139
    /**
140
     * @return Session
141
     */
142
    public function getSession()
143
    {
144
        return $this->getRequest()->getSession();
145
    }
146
147
    /**
148
     * Returns a GridField of messages
149
     * @return CMSForm
0 ignored issues
show
Bug introduced by
The type LeKoala\SparkPost\CMSForm 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...
150
     */
151
    public function getEditForm($id = null, $fields = null)
152
    {
153
        if (!$id) {
154
            $id = $this->currentPageID();
155
        }
156
157
        $form = parent::getEditForm($id);
0 ignored issues
show
Unused Code introduced by
The assignment to $form is dead and can be removed.
Loading history...
158
159
        $record = $this->getRecord($id);
160
161
        // Check if this record is viewable
162
        if ($record && !$record->canView()) {
163
            $response = Security::permissionFailure($this);
164
            $this->setResponse($response);
165
            return null;
166
        }
167
168
        // Build gridfield
169
        $messageListConfig = GridFieldConfig::create()->addComponents(
170
            new GridFieldSortableHeader(),
171
            new GridFieldDataColumns(),
172
            new GridFieldFooter()
173
        );
174
175
        $messages = $this->Messages();
176
        if (is_string($messages)) {
0 ignored issues
show
introduced by
The condition is_string($messages) is always false.
Loading history...
177
            // The api returned an error
178
            $messagesList = new LiteralField("MessageAlert", $this->MessageHelper($messages, 'bad'));
179
        } else {
180
            $messagesList = GridField::create(
181
                'Messages',
182
                false,
183
                $messages,
184
                $messageListConfig
185
            )->addExtraClass("messages_grid");
186
187
            /** @var GridFieldDataColumns $columns  */
188
            $columns = $messageListConfig->getComponentByType(GridFieldDataColumns::class);
189
            $columns->setDisplayFields([
190
                'transmission_id' => _t('SparkPostAdmin.EventTransmissionId', 'Id'),
191
                'timestamp' => _t('SparkPostAdmin.EventDate', 'Date'),
192
                'type' => _t('SparkPostAdmin.EventType', 'Type'),
193
                'rcpt_to' => _t('SparkPostAdmin.EventRecipient', 'Recipient'),
194
                'subject' => _t('SparkPostAdmin.EventSubject', 'Subject'),
195
                'friendly_from' => _t('SparkPostAdmin.EventSender', 'Sender'),
196
            ]);
197
198
            $columns->setFieldFormatting([
199
                'timestamp' => function ($value, &$item) {
200
                    return date('Y-m-d H:i:s', strtotime($value));
201
                },
202
            ]);
203
204
            // Validator setup
205
            $validator = null;
206
            if ($record && method_exists($record, 'getValidator')) {
207
                $validator = $record->getValidator();
208
            }
209
210
            if ($validator) {
211
                /** @var GridFieldDetailForm $detailForm  */
212
                $detailForm = $messageListConfig->getComponentByType(GridFieldDetailForm::class);
213
                if ($detailForm) {
0 ignored issues
show
introduced by
$detailForm is of type SilverStripe\Forms\GridField\GridFieldDetailForm, thus it always evaluated to true.
Loading history...
214
                    $detailForm->setValidator($validator);
215
                }
216
            }
217
        }
218
219
        // Create tabs
220
        $messagesTab = new Tab(
221
            'Messages',
222
            _t('SparkPostAdmin.Messages', 'Messages'),
223
            $this->SearchFields(),
224
            $messagesList,
225
            // necessary for tree node selection in LeftAndMain.EditForm.js
226
            new HiddenField('ID', false, 0)
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type SilverStripe\View\ViewableData|null|string expected by parameter $title of SilverStripe\Forms\HiddenField::__construct(). ( Ignorable by Annotation )

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

226
            new HiddenField('ID', /** @scrutinizer ignore-type */ false, 0)
Loading history...
227
        );
228
229
        $fields = new FieldList([
230
            $root = new TabSet('Root', $messagesTab)
231
        ]);
232
233
        if ($this->CanConfigureApi()) {
234
            $settingsTab = new Tab('Settings', _t('SparkPostAdmin.Settings', 'Settings'));
235
236
            $domainTabData = $this->DomainTab();
237
            $settingsTab->push($domainTabData);
238
239
            // Show webhook options if not using a subaccount key
240
            if (!SparkPostHelper::getSubaccountId() && self::config()->show_webhook_tab) {
241
                $webhookTabData = $this->WebhookTab();
242
                $settingsTab->push($webhookTabData);
243
            }
244
245
            $toolsHtml = '<h2>Tools</h2>';
246
247
            // Show default from email
248
            $defaultEmail =  SparkPostHelper::resolveDefaultFromEmail();
249
            $toolsHtml .= "<p>Default sending email: " . $defaultEmail . " (" . SparkPostHelper::resolveDefaultFromEmailType() . ")</p>";
250
            if (!SparkPostHelper::isEmailDomainReady($defaultEmail)) {
251
                $toolsHtml .= '<p style="color:red">The default email is not ready to send emails</p>';
252
            }
253
254
            // Show constants
255
            if (SparkPostHelper::getEnvSendingDisabled()) {
256
                $toolsHtml .= '<p style="color:red">Sending is disabled by .env configuration</p>';
257
            }
258
            if (SparkPostHelper::getEnvEnableLogging()) {
259
                $toolsHtml .= '<p style="color:orange">Logging is enabled by .env configuration</p>';
260
            }
261
            if (SparkPostHelper::getSubaccountId()) {
262
                $toolsHtml .= '<p style="color:orange">Using subaccount id</p>';
263
            }
264
            if (SparkPostHelper::getEnvForceSender()) {
265
                $toolsHtml .= '<p style="color:orange">Sender is forced to ' . SparkPostHelper::getEnvForceSender() . '</p>';
266
            }
267
268
            // Add a refresh button
269
            $toolsHtml .= $this->ButtonHelper(
270
                $this->Link() . '?refresh=true',
271
                _t('SparkPostAdmin.REFRESH', 'Force data refresh from the API')
272
            );
273
274
            $toolsHtml = $this->FormGroupHelper($toolsHtml);
275
            $Tools = new LiteralField('Tools', $toolsHtml);
276
            $settingsTab->push($Tools);
277
278
            $fields->addFieldToTab('Root', $settingsTab);
279
        }
280
281
        // Tab nav in CMS is rendered through separate template
282
        $root->setTemplate('SilverStripe\\Forms\\CMSTabSet');
283
284
        // Manage tabs state
285
        $actionParam = $this->getRequest()->param('Action');
286
        if ($actionParam == 'setting') {
287
            $settingsTab->addExtraClass('ui-state-active');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $settingsTab does not seem to be defined for all execution paths leading up to this point.
Loading history...
288
        } elseif ($actionParam == 'messages') {
289
            $messagesTab->addExtraClass('ui-state-active');
290
        }
291
292
        $actions = new FieldList();
0 ignored issues
show
Unused Code introduced by
The assignment to $actions is dead and can be removed.
Loading history...
293
294
295
        // Build replacement form
296
        $form = Form::create(
297
            $this,
298
            'EditForm',
299
            $fields,
300
            new FieldList()
301
        )->setHTMLID('Form_EditForm');
302
        $form->addExtraClass('cms-edit-form fill-height');
303
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
304
        $form->addExtraClass('ss-tabset cms-tabset ' . $this->BaseCSSClasses());
305
        $form->setAttribute('data-pjax-fragment', 'CurrentForm');
306
307
        $this->extend('updateEditForm', $form);
308
309
        return $form;
310
    }
311
312
    /**
313
     * Get logger
314
     *
315
     * @return  Psr\Log\LoggerInterface
316
     */
317
    public function getLogger()
318
    {
319
        return $this->logger;
320
    }
321
322
    /**
323
     * Get the cache
324
     *
325
     * @return Psr\SimpleCache\CacheInterface
326
     */
327
    public function getCache()
328
    {
329
        return $this->cache;
330
    }
331
332
    /**
333
     * @return boolean
334
     */
335
    public function getCacheEnabled()
336
    {
337
        if (isset($_GET['disable_cache'])) {
338
            return false;
339
        }
340
        if (Environment::getEnv('SPARKPOST_DISABLE_CACHE')) {
341
            return false;
342
        }
343
        $v = $this->config()->cache_enabled;
344
        if ($v === null) {
345
            $v = self::$cache_enabled;
346
        }
347
        return $v;
348
    }
349
350
    /**
351
     * A simple cache helper
352
     *
353
     * @param string $method
354
     * @param array $params
355
     * @param int $expireInSeconds
356
     * @return array
357
     */
358
    protected function getCachedData($method, $params, $expireInSeconds = 60)
359
    {
360
        $enabled = $this->getCacheEnabled();
361
        if ($enabled) {
362
            $cache = $this->getCache();
363
            $key = md5(serialize($params));
364
            $cacheResult = $cache->get($key);
365
        }
366
        if ($enabled && $cacheResult) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cacheResult does not seem to be defined for all execution paths leading up to this point.
Loading history...
367
            $data = unserialize($cacheResult);
368
        } else {
369
            try {
370
                $client = SparkPostHelper::getClient();
371
                $data = $client->$method($params);
372
            } catch (Exception $ex) {
373
                $this->lastException = $ex;
374
                $this->getLogger()->debug($ex);
375
                $data = false;
376
            }
377
378
            //5 minutes cache
379
            if ($enabled) {
380
                $cache->set($key, serialize($data), $expireInSeconds);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $cache does not seem to be defined for all execution paths leading up to this point.
Loading history...
381
            }
382
        }
383
384
        return $data;
385
    }
386
387
    public function getParams()
388
    {
389
        $params = $this->config()->default_search_params;
390
        if (!$params) {
391
            $params = [];
392
        }
393
        $data = $this->getSession()->get(__class__ . '.Search');
394
        if (!$data) {
395
            $data = [];
396
        }
397
398
        $params = array_merge($params, $data);
399
400
        // Respect api formats
401
        if (!empty($params['to'])) {
402
            $params['to'] = date('Y-m-d', strtotime(str_replace('/', '-', $params['to']))) . 'T00:00';
403
        }
404
        if (!empty($params['from'])) {
405
            $params['from'] = date('Y-m-d', strtotime(str_replace('/', '-', $params['from']))) . 'T23:59';
406
        }
407
408
        $params = array_filter($params);
409
410
        return $params;
411
    }
412
413
    public function getParam($name, $default = null)
414
    {
415
        $data = $this->getSession()->get(__class__ . '.Search');
416
        if (!$data) {
417
            return $default;
418
        }
419
        return (isset($data[$name]) && strlen($data[$name])) ? $data[$name] : $default;
420
    }
421
422
    public function SearchFields()
423
    {
424
        $disabled_filters = $this->config()->disabled_search_filters;
425
        if (!$disabled_filters) {
426
            $disabled_filters = [];
427
        }
428
429
        $fields = new CompositeField();
430
        $fields->push($from = new DateField('params[from]', _t('SparkPostAdmin.DATEFROM', 'From'), $this->getParam('from')));
431
        // $from->setConfig('min', date('Y-m-d', strtotime('-10 days')));
432
433
        $fields->push(new DateField('params[to]', _t('SparkPostAdmin.DATETO', 'To'), $to = $this->getParam('to')));
434
435
        if (!in_array('friendly_froms', $disabled_filters)) {
436
            $fields->push($friendly_froms = new TextField('params[friendly_froms]', _t('SparkPostAdmin.FRIENDLYFROM', 'Sender'), $this->getParam('friendly_froms')));
437
            $friendly_froms->setAttribute('placeholder', '[email protected],[email protected]');
438
        }
439
440
        if (!in_array('recipients', $disabled_filters)) {
441
            $fields->push($recipients = new TextField('params[recipients]', _t('SparkPostAdmin.RECIPIENTS', 'Recipients'), $this->getParam('recipients')));
442
            $recipients->setAttribute('placeholder', '[email protected],[email protected]');
443
        }
444
445
        // Only allow filtering by subaccount if a master key is defined
446
        if (SparkPostHelper::config()->master_api_key && !in_array('subaccounts', $disabled_filters)) {
447
            $fields->push($subaccounts = new TextField('params[subaccounts]', _t('SparkPostAdmin.SUBACCOUNTS', 'Subaccounts'), $this->getParam('subaccounts')));
448
            $subaccounts->setAttribute('placeholder', '101,102');
449
        }
450
451
        $fields->push(new DropdownField('params[per_page]', _t('SparkPostAdmin.PERPAGE', 'Number of results'), array(
452
            100 => 100,
453
            500 => 500,
454
            1000 => 1000,
455
            10000 => 10000,
456
        ), $this->getParam('per_page', 100)));
457
458
        foreach ($fields->FieldList() as $field) {
459
            $field->addExtraClass('no-change-track');
460
        }
461
462
        // This is a ugly hack to allow embedding a form into another form
463
        $fields->push($doSearch = new FormAction('doSearch', _t('SparkPostAdmin.DOSEARCH', 'Search')));
464
        $doSearch->addExtraClass("btn-primary");
465
        $doSearch->setAttribute('onclick', "jQuery('#Form_SearchForm').append(jQuery('#Form_EditForm input,#Form_EditForm select').clone()).submit();");
466
467
        return $fields;
468
    }
469
470
    public function SearchForm()
471
    {
472
        $SearchForm = new Form($this, 'SearchForm', new FieldList(), new FieldList([
473
            new FormAction('doSearch')
474
        ]));
475
        $SearchForm->setAttribute('style', 'display:none');
476
        return $SearchForm;
477
    }
478
479
    public function doSearch($data, Form $form)
480
    {
481
        $post = $this->getRequest()->postVar('params');
482
        if (!$post) {
483
            return $this->redirectBack();
484
        }
485
        $params = [];
486
487
        $validFields = [];
488
        foreach ($this->SearchFields()->FieldList()->dataFields() as $field) {
489
            $validFields[] = str_replace(['params[', ']'], '', $field->getName());
490
        }
491
492
        foreach ($post as $k => $v) {
493
            if (in_array($k, $validFields)) {
494
                $params[$k] = $v;
495
            }
496
        }
497
498
        $this->getSession()->set(__class__ . '.Search', $params);
499
        $this->getSession()->save($this->getRequest());
500
501
        return $this->redirectBack();
502
    }
503
504
    /**
505
     * List of messages events
506
     *
507
     * Messages are cached to avoid hammering the api
508
     *
509
     * @return ArrayList|string
510
     */
511
    public function Messages()
512
    {
513
        $params = $this->getParams();
514
515
        $messages = $this->getCachedData('searchEvents', $params, 60 * self::MESSAGE_CACHE_MINUTES);
516
        if ($messages === false) {
0 ignored issues
show
introduced by
The condition $messages === false is always false.
Loading history...
517
            if ($this->lastException) {
518
                return $this->lastException->getMessage();
519
            }
520
            return _t('SparkpostAdmin.NO_MESSAGES', 'No messages');
521
        }
522
523
        // Consolidate Subject/Sender for open and click events
524
        $transmissions = [];
525
        foreach ($messages as $message) {
526
            if (empty($message['transmission_id']) || empty($message['subject'])) {
527
                continue;
528
            }
529
            if (isset($transmissions[$message['transmission_id']])) {
530
                continue;
531
            }
532
            $transmissions[$message['transmission_id']] = $message;
533
        }
534
535
        $list = new ArrayList();
536
        if ($messages) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $messages of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
537
            foreach ($messages as $message) {
538
                // If we have a transmission id but no subject, try to find the transmission details
539
                if (isset($message['transmission_id']) && empty($message['subject']) && isset($transmissions[$message['transmission_id']])) {
540
                    $message = array_merge($transmissions[$message['transmission_id']], $message);
541
                }
542
                // In some case (errors, etc) we don't have a friendly from
543
                if (empty($message['friendly_from']) && isset($message['msg_from'])) {
544
                    $message['friendly_from'] = $message['msg_from'];
545
                }
546
                $m = new ArrayData($message);
547
                $list->push($m);
548
            }
549
        }
550
551
        return $list;
552
    }
553
554
    /**
555
     * Provides custom permissions to the Security section
556
     *
557
     * @return array
558
     */
559
    public function providePermissions()
560
    {
561
        $title = _t("SparkPostAdmin.MENUTITLE", LeftAndMain::menu_title('SparkPost'));
562
        return [
563
            "CMS_ACCESS_SparkPost" => [
564
                'name' => _t('SparkPostAdmin.ACCESS', "Access to '{title}' section", ['title' => $title]),
565
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
566
                'help' => _t(
567
                    'SparkPostAdmin.ACCESS_HELP',
568
                    'Allow use of SparkPost admin section'
569
                )
570
            ],
571
        ];
572
    }
573
574
    /**
575
     * Message helper
576
     *
577
     * @param string $message
578
     * @param string $status
579
     * @return string
580
     */
581
    protected function MessageHelper($message, $status = 'info')
582
    {
583
        return '<div class="message ' . $status . '">' . $message . '</div>';
584
    }
585
586
    /**
587
     * Button helper
588
     *
589
     * @param string $link
590
     * @param string $text
591
     * @param boolean $confirm
592
     * @return string
593
     */
594
    protected function ButtonHelper($link, $text, $confirm = false)
595
    {
596
        $link = '<a class="btn btn-primary" href="' . $link . '"';
597
        if ($confirm) {
598
            $link .= ' onclick="return confirm(\'' . _t('SparkPostAdmin.CONFIRM_MSG', 'Are you sure?') . '\')"';
599
        }
600
        $link .= '>' . $text . '</a>';
601
        return $link;
602
    }
603
604
    /**
605
     * Wrap html in a form group
606
     *
607
     * @param string $html
608
     * @return string
609
     */
610
    protected function FormGroupHelper($html)
611
    {
612
        return '<div class="form-group"><div class="form__fieldgroup form__field-holder form__field-holder--no-label">' . $html . '</div></div>';
613
    }
614
615
    /**
616
     * A template accessor to check the ADMIN permission
617
     *
618
     * @return bool
619
     */
620
    public function IsAdmin()
621
    {
622
        return Permission::check("ADMIN");
623
    }
624
625
    /**
626
     * Check the permission for current user
627
     *
628
     * @return bool
629
     */
630
    public function canView($member = null)
631
    {
632
        $mailer = SparkPostHelper::getMailer();
633
        $transport = SparkPostHelper::getTransportFromMailer($mailer);
634
        // Another custom mailer has been set
635
        if (!($transport instanceof SparkPostApiTransport)) {
636
            return false;
637
        }
638
        return Permission::check("CMS_ACCESS_SparkPost", 'any', $member);
639
    }
640
641
    /**
642
     *
643
     * @return bool
644
     */
645
    public function CanConfigureApi()
646
    {
647
        return Permission::check('ADMIN') || Director::isDev();
648
    }
649
650
    /**
651
     * Check if webhook is installed
652
     *
653
     * @return array|false
654
     */
655
    public function WebhookInstalled()
656
    {
657
        $list = $this->getCachedData('listAllWebhooks', null, 60 * self::WEBHOOK_CACHE_MINUTES);
658
659
        if (empty($list)) {
660
            return false;
661
        }
662
        $url = $this->WebhookUrl();
663
        foreach ($list as $el) {
664
            if (!empty($el['target']) && $el['target'] === $url) {
665
                /*
666
                ^ array:13 [▼
667
  "auth_token" => false
668
  "auth_type" => "basic"
669
  "name" => "Testing Webhook"
670
  "auth_credentials" => array:2 [▶]
671
  "events" => array:19 [▶]
672
  "target" => "https://mydomain.com/sparkpost/incoming"
673
  "custom_headers" => []
674
  "auth_request_details" => []
675
  "id" => "xxxxxx"
676
  "last_successful" => null
677
  "last_failure" => null
678
  "active" => false
679
  "links" => array:1 [▼
680
    0 => array:3 [▶]
681
  ]
682
]
683
*/
684
                return $el;
685
            }
686
        }
687
        return false;
688
    }
689
690
    /**
691
     * Hook details for template
692
     * @return \ArrayData
0 ignored issues
show
Bug introduced by
The type ArrayData 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...
693
     */
694
    public function WebhookDetails()
695
    {
696
        $el = $this->WebhookInstalled();
697
        if ($el) {
698
            return new ArrayData($el);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new SilverStripe\View\ArrayData($el) returns the type SilverStripe\View\ArrayData which is incompatible with the documented return type ArrayData.
Loading history...
699
        }
700
    }
701
702
    /**
703
     * Get content of the tab
704
     *
705
     * @return FormField
706
     */
707
    public function WebhookTab()
708
    {
709
        $webhook = $this->WebhookInstalled();
710
        if ($webhook) {
711
            return $this->UninstallHookForm($webhook);
712
        }
713
        return $this->InstallHookForm($webhook);
0 ignored issues
show
Bug introduced by
It seems like $webhook can also be of type false; however, parameter $data of LeKoala\SparkPost\SparkP...dmin::InstallHookForm() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

713
        return $this->InstallHookForm(/** @scrutinizer ignore-type */ $webhook);
Loading history...
714
    }
715
716
    /**
717
     * @return string
718
     */
719
    public function WebhookUrl()
720
    {
721
        if (self::config()->webhook_base_url) {
722
            return rtrim(self::config()->webhook_base_url, '/') . '/sparkpost/incoming';
723
        }
724
        if (Director::isLive()) {
725
            return Director::absoluteURL('/sparkpost/incoming');
0 ignored issues
show
Bug Best Practice introduced by
The expression return SilverStripe\Cont...('/sparkpost/incoming') could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
726
        }
727
        $protocol = Director::protocol();
728
        return $protocol . $this->getDomain() . '/sparkpost/incoming';
0 ignored issues
show
Bug introduced by
Are you sure $this->getDomain() of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

728
        return $protocol . /** @scrutinizer ignore-type */ $this->getDomain() . '/sparkpost/incoming';
Loading history...
729
    }
730
731
    /**
732
     * Install hook form
733
     *
734
     * @param array $data
735
     * @return FormField
736
     */
737
    public function InstallHookForm($data)
738
    {
739
        $fields = new CompositeField();
740
        $fields->push(new LiteralField('Info', $this->MessageHelper(
741
            _t('SparkPostAdmin.WebhookNotInstalled', 'Webhook is not installed. It should be configured using the following url {url}. This url must be publicly visible to be used as a hook.', ['url' => $this->WebhookUrl()]),
742
            'bad'
743
        )));
744
        $fields->push(new LiteralField('doInstallHook', $this->ButtonHelper(
745
            $this->Link('doInstallHook'),
746
            _t('SparkPostAdmin.DOINSTALL_WEBHOOK', 'Install webhook')
747
        )));
748
        return $fields;
749
    }
750
751
    public function doInstallHook()
752
    {
753
        if (!$this->CanConfigureApi()) {
754
            return $this->redirectBack();
755
        }
756
757
        $client = SparkPostHelper::getClient();
758
759
        $url = $this->WebhookUrl();
760
        $description = SiteConfig::current_site_config()->Title;
761
762
        $webhookUser = Environment::getEnv('SS_DEFAULT_ADMIN_USERNAME');
763
        $webhookPassword = Environment::getEnv('SS_DEFAULT_ADMIN_PASSWORD');
764
765
        if (SparkPostHelper::getWebhookUsername()) {
766
            $webhookUser = SparkPostHelper::getWebhookUsername();
767
            $webhookPassword = SparkPostHelper::getWebhookPassword();
768
        }
769
770
        try {
771
            if ($webhookUser && $webhookPassword) {
772
                $client->createSimpleWebhook($description, $url, null, true, ['username' => $webhookUser, 'password' => $webhookPassword]);
773
            } else {
774
                $client->createSimpleWebhook($description, $url); // This will add a default credentials that is sparkpost/sparkpost
775
            }
776
            $this->getCache()->clear();
777
        } catch (Exception $ex) {
778
            $this->getLogger()->debug($ex);
779
        }
780
781
        return $this->redirectBack();
782
    }
783
784
    /**
785
     * Uninstall hook form
786
     *
787
     * @param array $data
788
     * @return FormField
789
     */
790
    public function UninstallHookForm($data)
791
    {
792
        $fields = new CompositeField();
793
794
        if ($data['active']) {
795
            $fields->push(new LiteralField('Info', $this->MessageHelper(
796
                _t('SparkPostAdmin.WebhookInstalled', 'Webhook is installed but inactive at the following url {url}.', ['url' => $this->WebhookUrl()]),
797
                'warning'
798
            )));
799
        } else {
800
            $fields->push(new LiteralField('Info', $this->MessageHelper(
801
                _t('SparkPostAdmin.WebhookInstalled', 'Webhook is installed and accessible at the following url {url}.', ['url' => $this->WebhookUrl()]),
802
                'good'
803
            )));
804
        }
805
806
        if ($data['last_successful']) {
807
            $fields->push(new LiteralField('LastSuccess', $this->MessageHelper(
808
                _t('SparkPostAdmin.WebhookLastSuccess', 'Webhook was last called successfully at {date}.', ['date' => $data['last_successful']]),
809
                'good'
810
            )));
811
        }
812
        if ($data['last_failure']) {
813
            $fields->push(new LiteralField('LastFailure', $this->MessageHelper(
814
                _t('SparkPostAdmin.WebhookLastFailure', 'Webhook last failure was at {date}.', ['date' => $data['last_failure']]),
815
                'bad'
816
            )));
817
        }
818
819
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
820
            $this->Link('doUninstallHook'),
821
            _t('SparkPostAdmin.DOUNINSTALL_WEBHOOK', 'Uninstall webhook'),
822
            true
823
        )));
824
        return $fields;
825
    }
826
827
    public function doUninstallHook($data, Form $form)
828
    {
829
        if (!$this->CanConfigureApi()) {
830
            return $this->redirectBack();
831
        }
832
833
        $client = SparkPostHelper::getClient();
834
835
        try {
836
            $el = $this->WebhookInstalled();
837
            if ($el && !empty($el['id'])) {
838
                $client->deleteWebhook($el['id']);
839
            }
840
            $this->getCache()->clear();
841
        } catch (Exception $ex) {
842
            $this->getLogger()->debug($ex);
843
        }
844
845
        return $this->redirectBack();
846
    }
847
848
    /**
849
     * Check if sending domain is installed
850
     *
851
     * @return array
852
     */
853
    public function SendingDomainInstalled()
854
    {
855
        $domain = $this->getCachedData('getSendingDomain', $this->getDomain(), 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
0 ignored issues
show
Bug introduced by
$this->getDomain() of type false|string is incompatible with the type array expected by parameter $params of LeKoala\SparkPost\SparkPostAdmin::getCachedData(). ( Ignorable by Annotation )

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

855
        $domain = $this->getCachedData('getSendingDomain', /** @scrutinizer ignore-type */ $this->getDomain(), 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
Loading history...
856
857
        if (empty($domain)) {
858
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
859
        }
860
        return $domain;
861
    }
862
863
    /**
864
     * Trigger request to check if sending domain is verified
865
     *
866
     * @return array
867
     */
868
    public function VerifySendingDomain()
869
    {
870
        $client = SparkPostHelper::getClient();
871
872
        $host = $this->getDomain();
873
874
        $verification = $client->verifySendingDomain($host);
0 ignored issues
show
Bug introduced by
It seems like $host can also be of type false; however, parameter $id of LeKoala\SparkPost\Api\Sp...::verifySendingDomain() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

874
        $verification = $client->verifySendingDomain(/** @scrutinizer ignore-type */ $host);
Loading history...
875
876
        if (empty($verification)) {
877
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
878
        }
879
        return $verification;
880
    }
881
882
    /**
883
     * Get content of the tab
884
     *
885
     * @return FormField
886
     */
887
    public function DomainTab()
888
    {
889
        $defaultDomain = $this->getDomain();
890
        $defaultDomainInfos = null;
891
892
        $domains = $this->getCachedData('listAllSendingDomains', null, 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
893
894
        $fields = new CompositeField();
895
896
        $list = new ArrayList();
897
        if ($domains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
898
            foreach ($domains as $domain) {
899
                $list->push(new ArrayData([
900
                    'Domain' => $domain['domain'],
901
                    'SPF' => $domain['status']['spf_status'],
902
                    'DKIM' => $domain['status']['dkim_status'],
903
                    'Compliance' => $domain['status']['compliance_status'],
904
                    'Verified' => $domain['status']['ownership_verified'],
905
                ]));
906
907
                if ($domain['domain'] == $defaultDomain) {
908
                    $defaultDomainInfos = $domain;
909
                }
910
            }
911
        }
912
913
        $config = GridFieldConfig::create();
914
        $config->addComponent(new GridFieldToolbarHeader());
915
        $config->addComponent(new GridFieldTitleHeader());
916
        $config->addComponent($columns = new GridFieldDataColumns());
917
        $columns->setDisplayFields(ArrayLib::valuekey(['Domain', 'SPF', 'DKIM', 'Compliance', 'Verified']));
918
        $domainsList = new GridField('SendingDomains', _t('SparkPostAdmin.ALL_SENDING_DOMAINS', 'Configured sending domains'), $list, $config);
919
        $domainsList->addExtraClass('mb-2');
920
        $fields->push($domainsList);
921
922
        if (!$defaultDomainInfos) {
923
            $this->InstallDomainForm($fields);
924
        } else {
925
            $this->UninstallDomainForm($fields);
926
        }
927
928
        return $fields;
929
    }
930
931
    /**
932
     * @return string
933
     */
934
    public function InboundUrl()
935
    {
936
        $subdomain = self::config()->inbound_subdomain;
937
        $domain = $this->getDomain();
938
        if ($domain) {
939
            return $subdomain . '.' . $domain;
940
        }
941
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
942
    }
943
944
    /**
945
     * Get domain name from current host
946
     *
947
     * @return boolean|string
948
     */
949
    public function getDomainFromHost()
950
    {
951
        $base = Environment::getEnv('SS_BASE_URL');
952
        if (!$base) {
953
            $base = Director::protocolAndHost();
954
        }
955
        $host = parse_url($base, PHP_URL_HOST);
956
        $hostParts = explode('.', $host);
957
        $parts = count($hostParts);
958
        if ($parts < 2) {
959
            return false;
960
        }
961
        $domain = $hostParts[$parts - 2] . "." . $hostParts[$parts - 1];
962
        return $domain;
963
    }
964
965
    /**
966
     * Get domain from admin email
967
     *
968
     * @return boolean|string
969
     */
970
    public function getDomainFromEmail()
971
    {
972
        $email = SparkPostHelper::resolveDefaultFromEmail(null, false);
973
        if ($email) {
974
            $domain = substr(strrchr($email, "@"), 1);
975
            return $domain;
976
        }
977
        return false;
978
    }
979
980
    /**
981
     * Get domain
982
     *
983
     * @return boolean|string
984
     */
985
    public function getDomain()
986
    {
987
        $domain = $this->getDomainFromEmail();
988
        if (!$domain) {
989
            return $this->getDomainFromHost();
990
        }
991
        return $domain;
992
    }
993
994
    /**
995
     * Install domain form
996
     *
997
     * @param CompositeField $fieldsd
998
     * @return FormField
999
     */
1000
    public function InstallDomainForm(CompositeField $fields)
1001
    {
1002
        $host = $this->getDomain();
1003
1004
        $fields->push(new LiteralField('Info', $this->MessageHelper(
1005
            _t('SparkPostAdmin.DomainNotInstalled', 'Default sending domain {domain} is not installed.', ['domain' => $host]),
1006
            "bad"
1007
        )));
1008
        $fields->push(new LiteralField('doInstallDomain', $this->ButtonHelper(
1009
            $this->Link('doInstallDomain'),
1010
            _t('SparkPostAdmin.DOINSTALLDOMAIN', 'Install domain')
1011
        )));
1012
    }
1013
1014
    public function doInstallDomain()
1015
    {
1016
        if (!$this->CanConfigureApi()) {
1017
            return $this->redirectBack();
1018
        }
1019
1020
        $client = SparkPostHelper::getClient();
1021
1022
        $domain = $this->getDomain();
1023
1024
        if (!$domain) {
1025
            return $this->redirectBack();
1026
        }
1027
1028
        try {
1029
            $client->createSimpleSendingDomain($domain);
1030
            $this->getCache()->clear();
1031
        } catch (Exception $ex) {
1032
            $this->getLogger()->debug($ex);
1033
        }
1034
1035
        return $this->redirectBack();
1036
    }
1037
1038
    /**
1039
     * Uninstall domain form
1040
     *
1041
     * @param CompositeField $fieldsd
1042
     * @return FormField
1043
     */
1044
    public function UninstallDomainForm(CompositeField $fields)
1045
    {
1046
        $domainInfos = $this->SendingDomainInstalled();
1047
1048
        $domain = $this->getDomain();
1049
1050
        if ($domainInfos && $domainInfos['status']['ownership_verified']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domainInfos of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1051
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1052
                _t('SparkPostAdmin.DomainInstalled', 'Default domain {domain} is installed.', ['domain' => $domain]),
1053
                'good'
1054
            )));
1055
        } else {
1056
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1057
                _t('SparkPostAdmin.DomainInstalledBut', 'Default domain {domain} is installed, but is not properly configured.'),
1058
                'warning'
1059
            )));
1060
        }
1061
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
1062
            $this->Link('doUninstallHook'),
1063
            _t('SparkPostAdmin.DOUNINSTALLDOMAIN', 'Uninstall domain'),
1064
            true
1065
        )));
1066
    }
1067
1068
    public function doUninstallDomain($data, Form $form)
1069
    {
1070
        if (!$this->CanConfigureApi()) {
1071
            return $this->redirectBack();
1072
        }
1073
1074
        $client = SparkPostHelper::getClient();
1075
1076
        $domain = $this->getDomain();
1077
1078
        if (!$domain) {
1079
            return $this->redirectBack();
1080
        }
1081
1082
        try {
1083
            $el = $this->SendingDomainInstalled();
1084
            if ($el) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $el of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1085
                $client->deleteSendingDomain($domain);
1086
            }
1087
            $this->getCache()->clear();
1088
        } catch (Exception $ex) {
1089
            $this->getLogger()->debug($ex);
1090
        }
1091
1092
        return $this->redirectBack();
1093
    }
1094
}
1095