Passed
Push — master ( 4707af...d36b17 )
by Thomas
03:29
created

SparkPostAdmin::send_test()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

569
        $title = _t("SparkPostAdmin.MENUTITLE", /** @scrutinizer ignore-deprecated */ LeftAndMain::menu_title_for_class('SparkPost'));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
570
        return [
571
            "CMS_ACCESS_SparkPost" => [
572
                'name' => _t('SparkPostAdmin.ACCESS', "Access to '{title}' section", ['title' => $title]),
573
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
574
                'help' => _t(
575
                    'SparkPostAdmin.ACCESS_HELP',
576
                    'Allow use of SparkPost admin section'
577
                )
578
            ],
579
        ];
580
    }
581
582
    /**
583
     * Message helper
584
     *
585
     * @param string $message
586
     * @param string $status
587
     * @return string
588
     */
589
    protected function MessageHelper($message, $status = 'info')
590
    {
591
        return '<div class="message ' . $status . '">' . $message . '</div>';
592
    }
593
594
    /**
595
     * Button helper
596
     *
597
     * @param string $link
598
     * @param string $text
599
     * @param boolean $confirm
600
     * @return string
601
     */
602
    protected function ButtonHelper($link, $text, $confirm = false)
603
    {
604
        $link = '<a class="btn btn-primary" href="' . $link . '"';
605
        if ($confirm) {
606
            $link .= ' onclick="return confirm(\'' . _t('SparkPostAdmin.CONFIRM_MSG', 'Are you sure?') . '\')"';
607
        }
608
        $link .= '>' . $text . '</a>';
609
        return $link;
610
    }
611
612
    /**
613
     * Wrap html in a form group
614
     *
615
     * @param string $html
616
     * @return string
617
     */
618
    protected function FormGroupHelper($html)
619
    {
620
        return '<div class="form-group"><div class="form__fieldgroup form__field-holder form__field-holder--no-label">' . $html . '</div></div>';
621
    }
622
623
    /**
624
     * A template accessor to check the ADMIN permission
625
     *
626
     * @return bool
627
     */
628
    public function IsAdmin()
629
    {
630
        return Permission::check("ADMIN");
631
    }
632
633
    /**
634
     * Check the permission for current user
635
     *
636
     * @return bool
637
     */
638
    public function canView($member = null)
639
    {
640
        $mailer = SparkPostHelper::getMailer();
641
        // Another custom mailer has been set
642
        if (!$mailer instanceof SwiftMailer) {
643
            return false;
644
        }
645
        // Doesn't use the proper transport
646
        if (!$mailer->getSwiftMailer()->getTransport() instanceof SparkPostSwiftTransport) {
647
            return false;
648
        }
649
        return Permission::check("CMS_ACCESS_SparkPost", 'any', $member);
650
    }
651
652
    /**
653
     *
654
     * @return bool
655
     */
656
    public function CanConfigureApi()
657
    {
658
        return Permission::check('ADMIN') || Director::isDev();
659
    }
660
661
    /**
662
     * Check if webhook is installed
663
     *
664
     * @return array
665
     */
666
    public function WebhookInstalled()
667
    {
668
        $list = $this->getCachedData('listAllWebhooks', null, 60 * self::WEBHOOK_CACHE_MINUTES);
669
670
        if (empty($list)) {
671
            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...
672
        }
673
        $url = $this->WebhookUrl();
674
        foreach ($list as $el) {
675
            if (!empty($el['target']) && $el['target'] === $url) {
676
                return $el;
677
            }
678
        }
679
        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...
680
    }
681
682
    /**
683
     * Hook details for template
684
     * @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...
685
     */
686
    public function WebhookDetails()
687
    {
688
        $el = $this->WebhookInstalled();
689
        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...
690
            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...
691
        }
692
    }
693
694
    /**
695
     * Get content of the tab
696
     *
697
     * @return FormField
698
     */
699
    public function WebhookTab()
700
    {
701
        if ($this->WebhookInstalled()) {
702
            return $this->UninstallHookForm();
703
        }
704
        return $this->InstallHookForm();
705
    }
706
707
    /**
708
     * @return string
709
     */
710
    public function WebhookUrl()
711
    {
712
        if (self::config()->webhook_base_url) {
713
            return rtrim(self::config()->webhook_base_url, '/') . '/sparkpost/incoming';
714
        }
715
        if (Director::isLive()) {
716
            return Director::absoluteURL('/sparkpost/incoming');
717
        }
718
        $protocol = Director::protocol();
719
        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

719
        return $protocol . /** @scrutinizer ignore-type */ $this->getDomain() . '/sparkpost/incoming';
Loading history...
720
    }
721
722
    /**
723
     * Install hook form
724
     *
725
     * @return FormField
726
     */
727
    public function InstallHookForm()
728
    {
729
        $fields = new CompositeField();
730
        $fields->push(new LiteralField('Info', $this->MessageHelper(
731
            _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()]),
732
            'bad'
733
        )));
734
        $fields->push(new LiteralField('doInstallHook', $this->ButtonHelper(
735
            $this->Link('doInstallHook'),
736
            _t('SparkPostAdmin.DOINSTALL_WEBHOOK', 'Install webhook')
737
        )));
738
        return $fields;
739
    }
740
741
    public function doInstallHook()
742
    {
743
        if (!$this->CanConfigureApi()) {
744
            return $this->redirectBack();
745
        }
746
747
        $client = SparkPostHelper::getClient();
748
749
        $url = $this->WebhookUrl();
750
        $description = SiteConfig::current_site_config()->Title;
751
752
        $defaultAdmin = Environment::getEnv('SS_DEFAULT_ADMIN_USERNAME');
753
        $defaultPassword = Environment::getEnv('SS_DEFAULT_ADMIN_PASSWORD');
754
755
        try {
756
            if ($defaultAdmin) {
757
                $client->createSimpleWebhook($description, $url, null, true, ['username' => $defaultAdmin, 'password' => $defaultPassword]);
758
            } else {
759
                $client->createSimpleWebhook($description, $url);
760
            }
761
            $this->getCache()->clear();
762
        } catch (Exception $ex) {
763
            $this->getLogger()->debug($ex);
764
        }
765
766
        return $this->redirectBack();
767
    }
768
769
    /**
770
     * Uninstall hook form
771
     *
772
     * @return FormField
773
     */
774
    public function UninstallHookForm()
775
    {
776
        $fields = new CompositeField();
777
        $fields->push(new LiteralField('Info', $this->MessageHelper(
778
            _t('SparkPostAdmin.WebhookInstalled', 'Webhook is installed and accessible at the following url {url}.', ['url' => $this->WebhookUrl()]),
779
            'good'
780
        )));
781
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
782
            $this->Link('doUninstallHook'),
783
            _t('SparkPostAdmin.DOUNINSTALL_WEBHOOK', 'Uninstall webhook'),
784
            true
785
        )));
786
        return $fields;
787
    }
788
789
    public function doUninstallHook($data, Form $form)
790
    {
791
        if (!$this->CanConfigureApi()) {
792
            return $this->redirectBack();
793
        }
794
795
        $client = SparkPostHelper::getClient();
796
797
        try {
798
            $el = $this->WebhookInstalled();
799
            if ($el && !empty($el['id'])) {
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...
800
                $client->deleteWebhook($el['id']);
801
            }
802
            $this->getCache()->clear();
803
        } catch (Exception $ex) {
804
            $this->getLogger()->debug($ex);
805
        }
806
807
        return $this->redirectBack();
808
    }
809
810
    /**
811
     * Check if sending domain is installed
812
     *
813
     * @return array
814
     */
815
    public function SendingDomainInstalled()
816
    {
817
        $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

817
        $domain = $this->getCachedData('getSendingDomain', /** @scrutinizer ignore-type */ $this->getDomain(), 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
Loading history...
818
819
        if (empty($domain)) {
820
            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...
821
        }
822
        return $domain;
823
    }
824
825
    /**
826
     * Trigger request to check if sending domain is verified
827
     *
828
     * @return array
829
     */
830
    public function VerifySendingDomain()
831
    {
832
        $client = SparkPostHelper::getClient();
833
834
        $host = $this->getDomain();
835
836
        $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

836
        $verification = $client->verifySendingDomain(/** @scrutinizer ignore-type */ $host);
Loading history...
837
838
        if (empty($verification)) {
839
            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...
840
        }
841
        return $verification;
842
    }
843
844
    /**
845
     * Get content of the tab
846
     *
847
     * @return FormField
848
     */
849
    public function DomainTab()
850
    {
851
        $defaultDomain = $this->getDomain();
852
        $defaultDomainInfos = null;
853
854
        $domains = $this->getCachedData('listAllSendingDomains', null, 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
855
856
        $fields = new CompositeField();
857
858
        $list = new ArrayList();
859
        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...
860
            foreach ($domains as $domain) {
861
                // We are using a subaccount api key
862
                if (!isset($domain['shared_with_subaccounts'])) {
863
                    $this->subaccountKey = true;
864
                }
865
866
                $list->push(new ArrayData([
867
                    'Domain' => $domain['domain'],
868
                    'SPF' => $domain['status']['spf_status'],
869
                    'DKIM' => $domain['status']['dkim_status'],
870
                    'Compliance' => $domain['status']['compliance_status'],
871
                    'Verified' => $domain['status']['ownership_verified'],
872
                ]));
873
874
                if ($domain['domain'] == $defaultDomain) {
875
                    $defaultDomainInfos = $domain;
876
                }
877
            }
878
        }
879
880
        $config = GridFieldConfig::create();
881
        $config->addComponent(new GridFieldToolbarHeader());
882
        $config->addComponent(new GridFieldTitleHeader());
883
        $config->addComponent($columns = new GridFieldDataColumns());
884
        $columns->setDisplayFields(ArrayLib::valuekey(['Domain', 'SPF', 'DKIM', 'Compliance', 'Verified']));
885
        $domainsList = new GridField('SendingDomains', _t('SparkPostAdmin.ALL_SENDING_DOMAINS', 'Configured sending domains'), $list, $config);
886
        $domainsList->addExtraClass('mb-2');
887
        $fields->push($domainsList);
888
889
        if (!$defaultDomainInfos) {
890
            $this->InstallDomainForm($fields);
891
        } else {
892
            $this->UninstallDomainForm($fields);
893
        }
894
895
        return $fields;
896
    }
897
898
    /**
899
     * @return string
900
     */
901
    public function InboundUrl()
902
    {
903
        $subdomain = self::config()->inbound_subdomain;
904
        $domain = $this->getDomain();
905
        if ($domain) {
906
            return $subdomain . '.' . $domain;
907
        }
908
        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...
909
    }
910
911
    /**
912
     * Get domain name from current host
913
     *
914
     * @return boolean|string
915
     */
916
    public function getDomainFromHost()
917
    {
918
        $base = Environment::getEnv('SS_BASE_URL');
919
        if (!$base) {
920
            $base = Director::protocolAndHost();
921
        }
922
        $host = parse_url($base, PHP_URL_HOST);
923
        $hostParts = explode('.', $host);
924
        $parts = count($hostParts);
925
        if ($parts < 2) {
926
            return false;
927
        }
928
        $domain = $hostParts[$parts - 2] . "." . $hostParts[$parts - 1];
929
        return $domain;
930
    }
931
932
    /**
933
     * Get domain from admin email
934
     *
935
     * @return boolean|string
936
     */
937
    public function getDomainFromEmail()
938
    {
939
        $email = SparkPostHelper::resolveDefaultFromEmail(null, false);
940
        if ($email) {
941
            $domain = substr(strrchr($email, "@"), 1);
942
            return $domain;
943
        }
944
        return false;
945
    }
946
947
    /**
948
     * Get domain
949
     *
950
     * @return boolean|string
951
     */
952
    public function getDomain()
953
    {
954
        $domain = $this->getDomainFromEmail();
955
        if (!$domain) {
956
            return $this->getDomainFromHost();
957
        }
958
        return $domain;
959
    }
960
961
    /**
962
     * Install domain form
963
     *
964
     * @param CompositeField $fieldsd
965
     * @return FormField
966
     */
967
    public function InstallDomainForm(CompositeField $fields)
968
    {
969
        $host = $this->getDomain();
970
971
        $fields->push(new LiteralField('Info', $this->MessageHelper(
972
            _t('SparkPostAdmin.DomainNotInstalled', 'Default sending domain {domain} is not installed.', ['domain' => $host]),
973
            "bad"
974
        )));
975
        $fields->push(new LiteralField('doInstallDomain', $this->ButtonHelper(
976
            $this->Link('doInstallDomain'),
977
            _t('SparkPostAdmin.DOINSTALLDOMAIN', 'Install domain')
978
        )));
979
    }
980
981
    public function doInstallDomain()
982
    {
983
        if (!$this->CanConfigureApi()) {
984
            return $this->redirectBack();
985
        }
986
987
        $client = SparkPostHelper::getClient();
988
989
        $domain = $this->getDomain();
990
991
        if (!$domain) {
992
            return $this->redirectBack();
993
        }
994
995
        try {
996
            $client->createSimpleSendingDomain($domain);
997
            $this->getCache()->clear();
998
        } catch (Exception $ex) {
999
            $this->getLogger()->debug($ex);
1000
        }
1001
1002
        return $this->redirectBack();
1003
    }
1004
1005
    /**
1006
     * Uninstall domain form
1007
     *
1008
     * @param CompositeField $fieldsd
1009
     * @return FormField
1010
     */
1011
    public function UninstallDomainForm(CompositeField $fields)
1012
    {
1013
        $domainInfos = $this->SendingDomainInstalled();
1014
1015
        $domain = $this->getDomain();
1016
1017
        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...
1018
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1019
                _t('SparkPostAdmin.DomainInstalled', 'Default domain {domain} is installed.', ['domain' => $domain]),
1020
                'good'
1021
            )));
1022
        } else {
1023
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1024
                _t('SparkPostAdmin.DomainInstalledBut', 'Default domain {domain} is installed, but is not properly configured.'),
1025
                'warning'
1026
            )));
1027
        }
1028
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
1029
            $this->Link('doUninstallHook'),
1030
            _t('SparkPostAdmin.DOUNINSTALLDOMAIN', 'Uninstall domain'),
1031
            true
1032
        )));
1033
    }
1034
1035
    public function doUninstallDomain($data, Form $form)
1036
    {
1037
        if (!$this->CanConfigureApi()) {
1038
            return $this->redirectBack();
1039
        }
1040
1041
        $client = SparkPostHelper::getClient();
1042
1043
        $domain = $this->getDomain();
1044
1045
        if (!$domain) {
1046
            return $this->redirectBack();
1047
        }
1048
1049
        try {
1050
            $el = $this->SendingDomainInstalled();
1051
            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...
1052
                $client->deleteSendingDomain($domain);
1053
            }
1054
            $this->getCache()->clear();
1055
        } catch (Exception $ex) {
1056
            $this->getLogger()->debug($ex);
1057
        }
1058
1059
        return $this->redirectBack();
1060
    }
1061
}
1062