SparkPostAdmin::getEditForm()   F
last analyzed

Complexity

Conditions 21
Paths 10922

Size

Total Lines 214
Code Lines 131

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 21
eloc 131
nc 10922
nop 2
dl 0
loc 214
rs 0
c 3
b 0
f 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\SparkPost;
4
5
use Exception;
6
use 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\ORM\DataObject;
12
use SilverStripe\View\ArrayData;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Forms\DateField;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\FormField;
17
use SilverStripe\Forms\TextField;
18
use SilverStripe\Control\Director;
19
use SilverStripe\Core\Environment;
20
use SilverStripe\Forms\FormAction;
21
use SilverStripe\Admin\LeftAndMain;
22
use SilverStripe\Forms\HiddenField;
23
use SilverStripe\Security\Security;
24
use SilverStripe\View\ViewableData;
25
use SilverStripe\Forms\LiteralField;
26
use SilverStripe\Control\Email\Email;
27
use SilverStripe\Control\HTTPRequest;
28
use SilverStripe\Forms\DropdownField;
29
use SilverStripe\Security\Permission;
30
use LeKoala\SparkPost\SparkPostHelper;
31
use SilverStripe\Control\HTTPResponse;
32
use SilverStripe\Core\Convert;
33
use SilverStripe\Forms\CompositeField;
34
use SilverStripe\SiteConfig\SiteConfig;
35
use SilverStripe\Forms\GridField\GridField;
36
use SilverStripe\Security\PermissionProvider;
37
use SilverStripe\Security\DefaultAdminService;
38
use SilverStripe\Forms\GridField\GridFieldConfig;
39
use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer;
40
use SilverStripe\Forms\GridField\GridFieldFooter;
41
use SilverStripe\Forms\GridField\GridFieldDetailForm;
42
use SilverStripe\Forms\GridField\GridFieldDataColumns;
43
use Symbiote\GridFieldExtensions\GridFieldTitleHeader;
44
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
45
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
46
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
47
48
/**
49
 * Allow you to see messages sent through the api key used to send messages
50
 *
51
 * @author LeKoala <[email protected]>
52
 */
53
class SparkPostAdmin extends LeftAndMain implements PermissionProvider
54
{
55
    const MESSAGE_CACHE_MINUTES = 5;
56
    const WEBHOOK_CACHE_MINUTES = 1440; // 1 day
57
    const SENDINGDOMAIN_CACHE_MINUTES = 1440; // 1 day
58
59
    /**
60
     * @var string
61
     */
62
    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...
63
64
    /**
65
     * @var string
66
     */
67
    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...
68
69
    /**
70
     * @var string
71
     */
72
    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...
73
74
    /**
75
     * @var string
76
     */
77
    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...
78
79
    /**
80
     * @var array<string>
81
     */
82
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
83
        'settings',
84
        'SearchForm',
85
        'doSearch',
86
        "doInstallHook",
87
        "doUninstallHook",
88
        "doInstallDomain",
89
        "doUninstallDomain",
90
        "send_test",
91
    ];
92
93
    /**
94
     * @var boolean
95
     */
96
    private static $cache_enabled = true;
97
98
    /**
99
     * @var bool
100
     */
101
    protected $subaccountKey = false;
102
103
    /**
104
     * @var Exception|null
105
     */
106
    protected $lastException = null;
107
108
    /**
109
     * @var ViewableData|null
110
     */
111
    protected $currentMessage = null;
112
113
    /**
114
     * Inject public dependencies into the controller
115
     *
116
     * @var array<string,string>
117
     */
118
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
119
        'logger' => '%$Psr\Log\LoggerInterface',
120
        'cache' => '%$Psr\SimpleCache\CacheInterface.sparkpost', // see _config/cache.yml
121
    ];
122
123
    /**
124
     * @var \Psr\Log\LoggerInterface
125
     */
126
    public $logger;
127
128
    /**
129
     * @var \Psr\SimpleCache\CacheInterface
130
     */
131
    public $cache;
132
133
    /**
134
     * @return void
135
     */
136
    public function init()
137
    {
138
        parent::init();
139
140
        if (isset($_GET['refresh'])) {
141
            $this->getCache()->clear();
142
        }
143
    }
144
145
    /**
146
     * @param HTTPRequest $request
147
     * @return HTTPResponse
148
     */
149
    public function settings($request)
150
    {
151
        return parent::index($request);
152
    }
153
154
    /**
155
     * @param HTTPRequest $request
156
     * @return HTTPResponse|string
157
     */
158
    public function send_test($request)
159
    {
160
        if (!$this->CanConfigureApi()) {
161
            return $this->httpError(404);
162
        }
163
        $service = DefaultAdminService::create();
164
        $to = $request->getVar('to');
165
        if (!$to) {
166
            $to = $service->findOrCreateDefaultAdmin()->Email;
167
        }
168
        $email = Email::create();
169
        $email->setSubject("Test email");
170
        $email->setBody("Test " . date('Y-m-d H:i:s'));
171
        $email->setTo($to);
172
173
        $email->send();
174
        return 'Email sent';
175
    }
176
177
    /**
178
     * @return Session
179
     */
180
    public function getSession()
181
    {
182
        return $this->getRequest()->getSession();
183
    }
184
185
    /**
186
     * Returns a GridField of messages
187
     * @param mixed $id
188
     * @param mixed $fields
189
     * @return null|Form
190
     */
191
    public function getEditForm($id = null, $fields = null)
192
    {
193
        if (!$id) {
194
            $id = $this->currentPageID();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Admin\LeftAndMain::currentPageID() has been deprecated: 5.4.0 use currentRecordID() instead. ( Ignorable by Annotation )

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

194
            $id = /** @scrutinizer ignore-deprecated */ $this->currentPageID();

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...
195
        }
196
197
        /** @var DataObject|null $record */
198
        $record = $this->getRecord($id);
199
200
        // Check if this record is viewable
201
        if ($record && !$record->canView()) {
202
            $response = Security::permissionFailure($this);
203
            $this->setResponse($response);
204
            return null;
205
        }
206
207
        // Build gridfield
208
        $messageListConfig = GridFieldConfig::create()->addComponents(
209
            new GridFieldSortableHeader(),
210
            new GridFieldDataColumns(),
211
            new GridFieldFooter()
212
        );
213
214
        $messages = $this->Messages();
215
        if (is_string($messages)) {
216
            // The api returned an error
217
            $messagesList = new LiteralField("MessageAlert", $this->MessageHelper($messages, 'bad'));
218
        } else {
219
            $messagesList = GridField::create(
220
                'Messages',
221
                false,
222
                $messages,
223
                $messageListConfig
224
            )->addExtraClass("messages_grid");
225
226
            /** @var GridFieldDataColumns $columns  */
227
            $columns = $messageListConfig->getComponentByType(GridFieldDataColumns::class);
228
            $columns->setDisplayFields([
229
                'transmission_id' => _t('SparkPostAdmin.EventTransmissionId', 'Id'),
230
                'timestamp' => _t('SparkPostAdmin.EventDate', 'Date'),
231
                'type' => _t('SparkPostAdmin.EventType', 'Type'),
232
                'rcpt_to' => _t('SparkPostAdmin.EventRecipient', 'Recipient'),
233
                'subject' => _t('SparkPostAdmin.EventSubject', 'Subject'),
234
                'friendly_from' => _t('SparkPostAdmin.EventSender', 'Sender'),
235
            ]);
236
237
            $columns->setFieldFormatting([
238
                'timestamp' => function ($value, &$item) {
239
                    return date('Y-m-d H:i:s', strtotime($value));
240
                },
241
            ]);
242
243
            // Validator setup
244
            $validator = null;
245
            if ($record && method_exists($record, 'getValidator')) {
246
                $validator = $record->getValidator();
247
            }
248
249
            if ($validator) {
250
                /** @var GridFieldDetailForm|null $detailForm  */
251
                $detailForm = $messageListConfig->getComponentByType(GridFieldDetailForm::class);
252
                if ($detailForm) {
253
                    $detailForm->setValidator($validator);
254
                }
255
            }
256
        }
257
258
        // Create tabs
259
        $messagesTab = new Tab(
260
            'Messages',
261
            _t('SparkPostAdmin.Messages', 'Messages'),
262
            $this->SearchFields(),
263
            $messagesList,
264
            // necessary for tree node selection in LeftAndMain.EditForm.js
265
            new HiddenField('ID', null, 0)
266
        );
267
268
        $fields = new FieldList([
269
            $root = new TabSet('Root', $messagesTab)
270
        ]);
271
272
        $suppressionsTab = new Tab('Suppressions', _t('SparkPostAdmin.Suppressions', 'Suppressions'));
273
274
        // Show the summary
275
        $summary = $this->getCachedData('suppressionSummary');
276
        if ($summary) {
277
            $callback = function ($k, $v) {
278
                return ArrayData::create([
279
                    'Title' => $k,
280
                    'Value' => $v,
281
                ]);
282
            };
283
            $summaryArray = array_map($callback, array_keys($summary), array_values($summary));
284
            $summaryGrid = GridField::create(
285
                'SuppressionSummary',
286
                _t('SparkPostAdmin.Suppressionsummary', 'Suppression summary'),
287
                new ArrayList($summaryArray)
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\ORM\ArrayList has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\List\ArrayList ( Ignorable by Annotation )

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

287
                /** @scrutinizer ignore-deprecated */ new ArrayList($summaryArray)
Loading history...
288
            );
289
            //@link https://docs.silverstripe.org/en/5/developer_guides/forms/using_gridfield_with_arbitrary_data/
290
            $summaryGrid->getConfig()->removeComponentsByType(GridFieldFilterHeader::class);
291
            /** @var GridFieldDataColumns $columns */
292
            $columns = $summaryGrid->getConfig()->getComponentByType(GridFieldDataColumns::class);
293
            $columns->setDisplayFields([
294
                'Title' => _t('SparkPostAdmin.Title', 'Title'),
295
                'Value' => _t('SparkPostAdmin.Value', 'Value'),
296
            ]);
297
            $suppressionsTab->push($summaryGrid);
298
        }
299
300
301
        // Show recent suppressions
302
        $client = SparkPostHelper::getClient();
303
        $suppressions = $this->getCachedData('searchSuppressions', [
304
            'from' => $client->createValidDatetime('-90 days')
305
        ]);
306
        if ($suppressions) {
307
            $suppressionGrid = GridField::create(
308
                'RecentSuppressions',
309
                _t('SparkPostAdmin.RecentSuppressions', 'Recent suppressions'),
310
                new ArrayList(array_map(function ($item) {
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\ORM\ArrayList has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\List\ArrayList ( Ignorable by Annotation )

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

310
                /** @scrutinizer ignore-deprecated */ new ArrayList(array_map(function ($item) {
Loading history...
311
                    return ArrayData::create($item);
312
                }, $suppressions))
313
            );
314
            //@link https://docs.silverstripe.org/en/5/developer_guides/forms/using_gridfield_with_arbitrary_data/
315
            $suppressionGrid->getConfig()->removeComponentsByType(GridFieldFilterHeader::class);
316
            /** @var GridFieldDataColumns $columns */
317
            $columns = $suppressionGrid->getConfig()->getComponentByType(GridFieldDataColumns::class);
318
            $columns->setDisplayFields([
319
                'recipient' => _t('SparkPostAdmin.Recipient', 'Recipient'),
320
                'type' => _t('SparkPostAdmin.Type', 'Type'),
321
                'source' => _t('SparkPostAdmin.Source', 'Source'),
322
                'description' => _t('SparkPostAdmin.Description', 'Description'),
323
                'created' => _t('SparkPostAdmin.Created', 'Created'),
324
                // 'updated' => _t('SparkPostAdmin.Updated', 'Updated'),
325
            ]);
326
            $suppressionsTab->push($suppressionGrid);
327
        }
328
329
        $fields->addFieldToTab('Root', $suppressionsTab);
330
331
        $settingsTab = new Tab('Settings', _t('SparkPostAdmin.Settings', 'Settings'));
332
        if ($this->CanConfigureApi()) {
333
            $domainTabData = $this->DomainTab();
334
            $settingsTab->push($domainTabData);
335
336
            // Show webhook options if not using a subaccount key
337
            if (!SparkPostHelper::getSubaccountId() && self::config()->show_webhook_tab) {
338
                $webhookTabData = $this->WebhookTab();
339
                $settingsTab->push($webhookTabData);
340
            }
341
342
            $toolsHtml = '<h2>Tools</h2>';
343
344
            // Show default from email
345
            $defaultEmail = SparkPostHelper::resolveDefaultFromEmail();
346
            $defaultEmailDisplayed = EmailUtils::stringify($defaultEmail);
347
            $toolsHtml .= "<p>Default sending email: " . Convert::raw2xml($defaultEmailDisplayed) . " (" . SparkPostHelper::resolveDefaultFromEmailType() . ")</p>";
348
            if (!SparkPostHelper::isEmailDomainReady($defaultEmailDisplayed)) {
349
                $toolsHtml .= '<p style="color:red">The default email is not ready to send emails</p>';
350
            }
351
352
            // Show constants
353
            if (SparkPostHelper::getEnvSendingDisabled()) {
354
                $toolsHtml .= '<p style="color:red">Sending is disabled by .env configuration</p>';
355
            }
356
            if (SparkPostHelper::getEnvEnableLogging()) {
357
                $toolsHtml .= '<p style="color:orange">Logging is enabled by .env configuration</p>';
358
            }
359
            if (SparkPostHelper::getSubaccountId()) {
360
                $toolsHtml .= '<p style="color:orange">Using subaccount id</p>';
361
            }
362
            if (SparkPostHelper::getEnvForceSender()) {
363
                $toolsHtml .= '<p style="color:orange">Sender is forced to ' . SparkPostHelper::getEnvForceSender() . '</p>';
364
            }
365
366
            // Add a refresh button
367
            $toolsHtml .= $this->ButtonHelper(
368
                $this->Link() . '?refresh=true',
369
                _t('SparkPostAdmin.REFRESH', 'Force data refresh from the API')
370
            );
371
372
            $toolsHtml = $this->FormGroupHelper($toolsHtml);
373
            $Tools = new LiteralField('Tools', $toolsHtml);
374
            $settingsTab->push($Tools);
375
376
            $fields->addFieldToTab('Root', $settingsTab);
377
        }
378
379
        // Tab nav in CMS is rendered through separate template
380
        $root->setTemplate('SilverStripe\\Forms\\CMSTabSet');
381
382
        // Manage tabs state
383
        $actionParam = $this->getRequest()->param('Action');
384
        if ($actionParam == 'setting') {
385
            $settingsTab->addExtraClass('ui-state-active');
386
        } elseif ($actionParam == 'messages') {
387
            $messagesTab->addExtraClass('ui-state-active');
388
        }
389
390
        // Build replacement form
391
        $form = Form::create(
392
            $this,
393
            'EditForm',
394
            $fields,
395
            new FieldList()
396
        )->setHTMLID('Form_EditForm');
397
        $form->addExtraClass('cms-edit-form fill-height');
398
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
399
        $form->addExtraClass('ss-tabset cms-tabset ' . $this->BaseCSSClasses());
400
        $form->setAttribute('data-pjax-fragment', 'CurrentForm');
401
402
        $this->extend('updateEditForm', $form);
403
404
        return $form;
405
    }
406
407
    /**
408
     * Get logger
409
     *
410
     * @return \Psr\Log\LoggerInterface
411
     */
412
    public function getLogger()
413
    {
414
        return $this->logger;
415
    }
416
417
    /**
418
     * Get the cache
419
     *
420
     * @return \Psr\SimpleCache\CacheInterface
421
     */
422
    public function getCache()
423
    {
424
        return $this->cache;
425
    }
426
427
    /**
428
     * @return boolean
429
     */
430
    public function getCacheEnabled()
431
    {
432
        if (isset($_GET['disable_cache'])) {
433
            return false;
434
        }
435
        if (Environment::getEnv('SPARKPOST_DISABLE_CACHE')) {
436
            return false;
437
        }
438
        $v = $this->config()->cache_enabled;
439
        if ($v === null) {
440
            $v = self::$cache_enabled;
441
        }
442
        return $v;
443
    }
444
445
    /**
446
     * A simple cache helper
447
     *
448
     * @param string $method
449
     * @param array<mixed>|null|string|bool $params
450
     * @param int $expireInSeconds
451
     * @return array<mixed>|false
452
     */
453
    protected function getCachedData($method, $params = null, $expireInSeconds = 60)
454
    {
455
        $enabled = $this->getCacheEnabled();
456
        $cacheResult = false;
457
        $cache = $this->getCache();
458
        if ($enabled) {
459
            $key = $method . '_' . md5(serialize($params));
460
            $cacheResult = $cache->get($key);
461
        }
462
        if ($enabled && $cacheResult) {
463
            $data = unserialize($cacheResult);
464
        } else {
465
            try {
466
                $client = SparkPostHelper::getClient();
467
                $data = $client->$method($params);
468
            } catch (Exception $ex) {
469
                $this->lastException = $ex;
470
                $this->getLogger()->debug($ex);
471
                $data = false;
472
            }
473
474
            //5 minutes cache
475
            if ($enabled) {
476
                $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...
477
            }
478
        }
479
480
        return $data;
481
    }
482
483
    /**
484
     * @return array<mixed>
485
     */
486
    public function getParams()
487
    {
488
        $params = $this->config()->default_search_params;
489
        if (!$params) {
490
            $params = [];
491
        }
492
        $data = $this->getSession()->get(__class__ . '.Search');
493
        if (!$data) {
494
            $data = [];
495
        }
496
497
        $params = array_merge($params, $data);
498
499
        // Respect api formats
500
        if (!empty($params['to'])) {
501
            //@phpstan-ignore-next-line
502
            $params['to'] = date('Y-m-d', strtotime(str_replace('/', '-', $params['to']))) . 'T00:00';
503
        }
504
        if (!empty($params['from'])) {
505
            //@phpstan-ignore-next-line
506
            $params['from'] = date('Y-m-d', strtotime(str_replace('/', '-', $params['from']))) . 'T23:59';
507
        }
508
509
        $params = array_filter($params);
510
511
        return $params;
512
    }
513
514
    /**
515
     * @param string $name
516
     * @param mixed $default
517
     * @return mixed
518
     */
519
    public function getParam($name, $default = null)
520
    {
521
        $data = $this->getSession()->get(__class__ . '.Search');
522
        if (!$data) {
523
            return $default;
524
        }
525
        return (isset($data[$name]) && strlen($data[$name])) ? $data[$name] : $default;
526
    }
527
528
    /**
529
     * @return CompositeField
530
     */
531
    public function SearchFields()
532
    {
533
        $disabled_filters = $this->config()->disabled_search_filters;
534
        if (!$disabled_filters) {
535
            $disabled_filters = [];
536
        }
537
538
        $fields = new CompositeField();
539
        $fields->push($from = new DateField('params[from]', _t('SparkPostAdmin.DATEFROM', 'From'), $this->getParam('from')));
540
        // $from->setConfig('min', date('Y-m-d', strtotime('-10 days')));
541
542
        $fields->push(new DateField('params[to]', _t('SparkPostAdmin.DATETO', 'To'), $to = $this->getParam('to')));
543
544
        if (!in_array('friendly_froms', $disabled_filters)) {
545
            $fields->push($friendly_froms = new TextField('params[friendly_froms]', _t('SparkPostAdmin.FRIENDLYFROM', 'Sender'), $this->getParam('friendly_froms')));
546
            $friendly_froms->setAttribute('placeholder', '[email protected],[email protected]');
547
        }
548
549
        if (!in_array('recipients', $disabled_filters)) {
550
            $fields->push($recipients = new TextField('params[recipients]', _t('SparkPostAdmin.RECIPIENTS', 'Recipients'), $this->getParam('recipients')));
551
            $recipients->setAttribute('placeholder', '[email protected],[email protected]');
552
        }
553
554
        // Only allow filtering by subaccount if a master key is defined
555
        if (SparkPostHelper::config()->master_api_key && !in_array('subaccounts', $disabled_filters)) {
556
            $fields->push($subaccounts = new TextField('params[subaccounts]', _t('SparkPostAdmin.SUBACCOUNTS', 'Subaccounts'), $this->getParam('subaccounts')));
557
            $subaccounts->setAttribute('placeholder', '101,102');
558
        }
559
560
        $fields->push(new DropdownField('params[per_page]', _t('SparkPostAdmin.PERPAGE', 'Number of results'), array(
561
            100 => 100,
562
            500 => 500,
563
            1000 => 1000,
564
            10000 => 10000,
565
        ), $this->getParam('per_page', 100)));
566
567
        foreach ($fields->FieldList() as $field) {
568
            $field->addExtraClass('no-change-track');
569
        }
570
571
        // This is a ugly hack to allow embedding a form into another form
572
        $fields->push($doSearch = new FormAction('doSearch', _t('SparkPostAdmin.DOSEARCH', 'Search')));
573
        $doSearch->addExtraClass("btn-primary");
574
        $doSearch->setAttribute('onclick', "jQuery('#Form_SearchForm').append(jQuery('#Form_EditForm input,#Form_EditForm select').clone()).submit();");
575
576
        return $fields;
577
    }
578
579
    /**
580
     * @return Form
581
     */
582
    public function SearchForm()
583
    {
584
        $SearchForm = new Form($this, 'SearchForm', new FieldList(), new FieldList([
585
            new FormAction('doSearch')
586
        ]));
587
        $SearchForm->setAttribute('style', 'display:none');
588
        return $SearchForm;
589
    }
590
591
    /**
592
     * @param array<mixed> $data
593
     * @param Form $form
594
     * @return HTTPResponse
595
     */
596
    public function doSearch($data, Form $form)
597
    {
598
        $post = $this->getRequest()->postVar('params');
599
        if (!$post) {
600
            return $this->redirectBack();
601
        }
602
        $params = [];
603
604
        $validFields = [];
605
        foreach ($this->SearchFields()->FieldList()->dataFields() as $field) {
606
            $validFields[] = str_replace(['params[', ']'], '', $field->getName());
607
        }
608
609
        foreach ($post as $k => $v) {
610
            if (in_array($k, $validFields)) {
611
                $params[$k] = $v;
612
            }
613
        }
614
615
        $this->getSession()->set(__class__ . '.Search', $params);
616
        $this->getSession()->save($this->getRequest());
617
618
        return $this->redirectBack();
619
    }
620
621
    /**
622
     * List of messages events
623
     *
624
     * Messages are cached to avoid hammering the api
625
     *
626
     * @return ArrayList|string
627
     */
628
    public function Messages()
629
    {
630
        $params = $this->getParams();
631
632
        $messages = $this->getCachedData('searchEvents', $params, 60 * self::MESSAGE_CACHE_MINUTES);
633
        if ($messages === false || !$messages) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $messages of type array<mixed,mixed> 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...
634
            if ($this->lastException) {
635
                return $this->lastException->getMessage();
636
            }
637
            return _t('SparkpostAdmin.NO_MESSAGES', 'No messages');
638
        }
639
640
        // Consolidate Subject/Sender for open and click events
641
        $transmissions = [];
642
        foreach ($messages as $message) {
643
            if (empty($message['transmission_id']) || empty($message['subject'])) {
644
                continue;
645
            }
646
            if (isset($transmissions[$message['transmission_id']])) {
647
                continue;
648
            }
649
            $transmissions[$message['transmission_id']] = $message;
650
        }
651
652
        $list = new ArrayList();
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\ORM\ArrayList has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\List\ArrayList ( Ignorable by Annotation )

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

652
        $list = /** @scrutinizer ignore-deprecated */ new ArrayList();
Loading history...
653
        foreach ($messages as $message) {
654
            // If we have a transmission id but no subject, try to find the transmission details
655
            if (isset($message['transmission_id']) && empty($message['subject']) && isset($transmissions[$message['transmission_id']])) {
656
                $message = array_merge($transmissions[$message['transmission_id']], $message);
657
            }
658
            // In some case (errors, etc) we don't have a friendly from
659
            if (empty($message['friendly_from']) && isset($message['msg_from'])) {
660
                $message['friendly_from'] = $message['msg_from'];
661
            }
662
            $m = new ArrayData($message);
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\View\ArrayData has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\ArrayData ( Ignorable by Annotation )

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

662
            $m = /** @scrutinizer ignore-deprecated */ new ArrayData($message);
Loading history...
663
            $list->push($m);
664
        }
665
666
        return $list;
667
    }
668
669
    /**
670
     * Provides custom permissions to the Security section
671
     *
672
     * @return array<string,mixed>
673
     */
674
    public function providePermissions()
675
    {
676
        $title = _t("SparkPostAdmin.MENUTITLE", LeftAndMain::menu_title('SparkPost'));
677
        return [
678
            "CMS_ACCESS_SparkPost" => [
679
                'name' => _t('SparkPostAdmin.ACCESS', "Access to '{title}' section", ['title' => $title]),
680
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
681
                'help' => _t(
682
                    'SparkPostAdmin.ACCESS_HELP',
683
                    'Allow use of SparkPost admin section'
684
                )
685
            ],
686
        ];
687
    }
688
689
    /**
690
     * Message helper
691
     *
692
     * @param string $message
693
     * @param string $status
694
     * @return string
695
     */
696
    protected function MessageHelper($message, $status = 'info')
697
    {
698
        return '<div class="message ' . $status . '">' . $message . '</div>';
699
    }
700
701
    /**
702
     * Button helper
703
     *
704
     * @param string $link
705
     * @param string $text
706
     * @param boolean $confirm
707
     * @return string
708
     */
709
    protected function ButtonHelper($link, $text, $confirm = false)
710
    {
711
        $link = '<a class="btn btn-primary" href="' . $link . '"';
712
        if ($confirm) {
713
            $link .= ' onclick="return confirm(\'' . _t('SparkPostAdmin.CONFIRM_MSG', 'Are you sure?') . '\')"';
714
        }
715
        $link .= '>' . $text . '</a>';
716
        return $link;
717
    }
718
719
    /**
720
     * Wrap html in a form group
721
     *
722
     * @param string $html
723
     * @return string
724
     */
725
    protected function FormGroupHelper($html)
726
    {
727
        return '<div class="form-group"><div class="form__fieldgroup form__field-holder form__field-holder--no-label">' . $html . '</div></div>';
728
    }
729
730
    /**
731
     * A template accessor to check the ADMIN permission
732
     *
733
     * @return bool
734
     */
735
    public function IsAdmin()
736
    {
737
        return boolval(Permission::check("ADMIN"));
738
    }
739
740
    /**
741
     * Check the permission for current user
742
     *
743
     * @return bool
744
     */
745
    public function canView($member = null)
746
    {
747
        $mailer = SparkPostHelper::getMailer();
748
        $transport = SparkPostHelper::getTransportFromMailer($mailer);
749
        // Another custom mailer has been set
750
        if (!($transport instanceof SparkPostApiTransport)) {
751
            return false;
752
        }
753
        return boolval(Permission::check("CMS_ACCESS_SparkPost", 'any', $member));
754
    }
755
756
    /**
757
     * @return bool
758
     */
759
    public function CanConfigureApi()
760
    {
761
        return Permission::check('ADMIN') || Director::isDev();
762
    }
763
764
    /**
765
     * Check if webhook is installed
766
     *
767
     * @return array<mixed>|false
768
     */
769
    public function WebhookInstalled()
770
    {
771
        $list = $this->getCachedData('listAllWebhooks', null, 60 * self::WEBHOOK_CACHE_MINUTES);
772
773
        if (empty($list)) {
774
            return false;
775
        }
776
        $url = $this->WebhookUrl();
777
        foreach ($list as $el) {
778
            if (!empty($el['target']) && $el['target'] === $url) {
779
                /*
780
                ^ array:13 [▼
781
  "auth_token" => false
782
  "auth_type" => "basic"
783
  "name" => "Testing Webhook"
784
  "auth_credentials" => array:2 [▶]
785
  "events" => array:19 [▶]
786
  "target" => "https://mydomain.com/sparkpost/incoming"
787
  "custom_headers" => []
788
  "auth_request_details" => []
789
  "id" => "xxxxxx"
790
  "last_successful" => null
791
  "last_failure" => null
792
  "active" => false
793
  "links" => array:1 [▼
794
    0 => array:3 [▶]
795
  ]
796
]
797
*/
798
                return $el;
799
            }
800
        }
801
        return false;
802
    }
803
804
    /**
805
     * Hook details for template
806
     * @return ArrayData|null
807
     */
808
    public function WebhookDetails()
809
    {
810
        $el = $this->WebhookInstalled();
811
        if ($el) {
812
            return new ArrayData($el);
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\View\ArrayData has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\ArrayData ( Ignorable by Annotation )

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

812
            return /** @scrutinizer ignore-deprecated */ new ArrayData($el);
Loading history...
813
        }
814
        return null;
815
    }
816
817
    /**
818
     * Get content of the tab
819
     *
820
     * @return FormField
821
     */
822
    public function WebhookTab()
823
    {
824
        $webhook = $this->WebhookInstalled();
825
        if ($webhook) {
826
            return $this->UninstallHookForm($webhook);
827
        }
828
        return $this->InstallHookForm();
829
    }
830
831
    /**
832
     * @return string
833
     */
834
    public function WebhookUrl()
835
    {
836
        if (self::config()->webhook_base_url) {
837
            return rtrim(self::config()->webhook_base_url, '/') . '/sparkpost/incoming';
838
        }
839
        if (Director::isLive()) {
840
            $absurl = Director::absoluteURL('/sparkpost/incoming');
841
            if ($absurl && is_string($absurl)) {
842
                return $absurl;
843
            }
844
        }
845
        $protocol = Director::protocol();
846
        $domain = $this->getDomain();
847
        if (!$domain) {
848
            throw new Exception("No domain for webhook");
849
        }
850
        return $protocol . $domain . '/sparkpost/incoming';
851
    }
852
853
    /**
854
     * Install hook form
855
     *
856
     * @return FormField
857
     */
858
    public function InstallHookForm()
859
    {
860
        $fields = new CompositeField();
861
        $fields->push(new LiteralField('Info', $this->MessageHelper(
862
            _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()]),
863
            'bad'
864
        )));
865
        $fields->push(new LiteralField('doInstallHook', $this->ButtonHelper(
866
            $this->Link('doInstallHook'),
867
            _t('SparkPostAdmin.DOINSTALL_WEBHOOK', 'Install webhook')
868
        )));
869
        return $fields;
870
    }
871
872
    /**
873
     * @return HTTPResponse
874
     */
875
    public function doInstallHook()
876
    {
877
        if (!$this->CanConfigureApi()) {
878
            return $this->redirectBack();
879
        }
880
881
        $client = SparkPostHelper::getClient();
882
883
        $url = $this->WebhookUrl();
884
        $description = SiteConfig::current_site_config()->Title;
885
886
        $webhookUser = Environment::getEnv('SS_DEFAULT_ADMIN_USERNAME');
887
        $webhookPassword = Environment::getEnv('SS_DEFAULT_ADMIN_PASSWORD');
888
889
        if (SparkPostHelper::getWebhookUsername()) {
890
            $webhookUser = SparkPostHelper::getWebhookUsername();
891
            $webhookPassword = SparkPostHelper::getWebhookPassword();
892
        }
893
894
        try {
895
            if ($webhookUser && $webhookPassword) {
896
                $client->createSimpleWebhook($description, $url, null, true, ['username' => $webhookUser, 'password' => $webhookPassword]);
897
            } else {
898
                $client->createSimpleWebhook($description, $url); // This will add a default credentials that is sparkpost/sparkpost
899
            }
900
            $this->getCache()->clear();
901
        } catch (Exception $ex) {
902
            $this->getLogger()->debug($ex);
903
        }
904
905
        return $this->redirectBack();
906
    }
907
908
    /**
909
     * Uninstall hook form
910
     *
911
     * @param array<mixed> $data
912
     * @return FormField
913
     */
914
    public function UninstallHookForm($data)
915
    {
916
        $fields = new CompositeField();
917
918
        if ($data['active']) {
919
            $fields->push(new LiteralField('Info', $this->MessageHelper(
920
                _t('SparkPostAdmin.WebhookInstalled', 'Webhook is installed but inactive at the following url {url}.', ['url' => $this->WebhookUrl()]),
921
                'warning'
922
            )));
923
        } else {
924
            $fields->push(new LiteralField('Info', $this->MessageHelper(
925
                _t('SparkPostAdmin.WebhookInstalled', 'Webhook is installed and accessible at the following url {url}.', ['url' => $this->WebhookUrl()]),
926
                'good'
927
            )));
928
        }
929
930
        if ($data['last_successful']) {
931
            $fields->push(new LiteralField('LastSuccess', $this->MessageHelper(
932
                _t('SparkPostAdmin.WebhookLastSuccess', 'Webhook was last called successfully at {date}.', ['date' => $data['last_successful']]),
933
                'good'
934
            )));
935
        }
936
        if ($data['last_failure']) {
937
            $fields->push(new LiteralField('LastFailure', $this->MessageHelper(
938
                _t('SparkPostAdmin.WebhookLastFailure', 'Webhook last failure was at {date}.', ['date' => $data['last_failure']]),
939
                'bad'
940
            )));
941
        }
942
943
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
944
            $this->Link('doUninstallHook'),
945
            _t('SparkPostAdmin.DOUNINSTALL_WEBHOOK', 'Uninstall webhook'),
946
            true
947
        )));
948
        return $fields;
949
    }
950
951
    /**
952
     * @param array<mixed> $data
953
     * @param Form $form
954
     * @return HTTPResponse
955
     */
956
    public function doUninstallHook($data, Form $form)
957
    {
958
        if (!$this->CanConfigureApi()) {
959
            return $this->redirectBack();
960
        }
961
962
        $client = SparkPostHelper::getClient();
963
964
        try {
965
            $el = $this->WebhookInstalled();
966
            if ($el && !empty($el['id'])) {
967
                $client->deleteWebhook($el['id']);
968
            }
969
            $this->getCache()->clear();
970
        } catch (Exception $ex) {
971
            $this->getLogger()->debug($ex);
972
        }
973
974
        return $this->redirectBack();
975
    }
976
977
    /**
978
     * Check if sending domain is installed
979
     *
980
     * @return array<mixed>|false
981
     */
982
    public function SendingDomainInstalled()
983
    {
984
        $domain = $this->getCachedData('getSendingDomain', $this->getDomain(), 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
985
986
        if (empty($domain)) {
987
            return false;
988
        }
989
        return $domain;
990
    }
991
992
    /**
993
     * Trigger request to check if sending domain is verified
994
     *
995
     * @return array<mixed>|false
996
     */
997
    public function VerifySendingDomain()
998
    {
999
        $client = SparkPostHelper::getClient();
1000
1001
        $host = $this->getDomain();
1002
        if (!$host || is_bool($host)) {
1003
            return false;
1004
        }
1005
1006
        $verification = $client->verifySendingDomain($host);
1007
        if (empty($verification)) {
1008
            return false;
1009
        }
1010
        return $verification;
1011
    }
1012
1013
    /**
1014
     * Get content of the tab
1015
     *
1016
     * @return FormField
1017
     */
1018
    public function DomainTab()
1019
    {
1020
        $defaultDomain = $this->getDomain();
1021
        $defaultDomainInfos = null;
1022
1023
        $domains = $this->getCachedData('listAllSendingDomains', null, 60 * self::SENDINGDOMAIN_CACHE_MINUTES);
1024
1025
        $fields = new CompositeField();
1026
1027
        $list = new ArrayList();
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\ORM\ArrayList has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\List\ArrayList ( Ignorable by Annotation )

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

1027
        $list = /** @scrutinizer ignore-deprecated */ new ArrayList();
Loading history...
1028
        if ($domains) {
1029
            foreach ($domains as $domain) {
1030
                // Sometimes the api or the cache returns invalid data...
1031
                if (!is_array($domain)) {
1032
                    continue;
1033
                }
1034
                $list->push(new ArrayData([
0 ignored issues
show
Deprecated Code introduced by
The class SilverStripe\View\ArrayData has been deprecated: 5.4.0 Will be renamed to SilverStripe\Model\ArrayData ( Ignorable by Annotation )

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

1034
                $list->push(/** @scrutinizer ignore-deprecated */ new ArrayData([
Loading history...
1035
                    'Domain' => $domain['domain'],
1036
                    'SPF' => $domain['status']['spf_status'],
1037
                    'DKIM' => $domain['status']['dkim_status'],
1038
                    'Compliance' => $domain['status']['compliance_status'],
1039
                    'Verified' => $domain['status']['ownership_verified'],
1040
                ]));
1041
1042
                if ($domain['domain'] == $defaultDomain) {
1043
                    $defaultDomainInfos = $domain;
1044
                }
1045
            }
1046
        }
1047
1048
        $config = GridFieldConfig::create();
1049
        $config->addComponent(new GridFieldToolbarHeader());
1050
        $config->addComponent(new GridFieldTitleHeader());
1051
        $config->addComponent($columns = new GridFieldDataColumns());
1052
        $columns->setDisplayFields(ArrayLib::valuekey(['Domain', 'SPF', 'DKIM', 'Compliance', 'Verified']));
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\ArrayLib::valuekey() has been deprecated: 5.4.0 Will be renamed to SilverStripe\Core\ArrayLib::valuekey() ( Ignorable by Annotation )

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

1052
        $columns->setDisplayFields(/** @scrutinizer ignore-deprecated */ ArrayLib::valuekey(['Domain', 'SPF', 'DKIM', 'Compliance', 'Verified']));

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...
1053
        $domainsList = new GridField('SendingDomains', _t('SparkPostAdmin.ALL_SENDING_DOMAINS', 'Configured sending domains'), $list, $config);
1054
        $domainsList->addExtraClass('mb-2');
1055
        $fields->push($domainsList);
1056
1057
        if (!$defaultDomainInfos) {
1058
            $this->InstallDomainForm($fields);
1059
        } else {
1060
            $this->UninstallDomainForm($fields);
1061
        }
1062
1063
        return $fields;
1064
    }
1065
1066
    /**
1067
     * @return ?string
1068
     */
1069
    public function InboundUrl()
1070
    {
1071
        $subdomain = self::config()->inbound_subdomain;
1072
        $domain = $this->getDomain();
1073
        if ($domain) {
1074
            return $subdomain . '.' . $domain;
1075
        }
1076
        return null;
1077
    }
1078
1079
    /**
1080
     * Get domain name from current host
1081
     *
1082
     * @return boolean|string
1083
     */
1084
    public function getDomainFromHost()
1085
    {
1086
        $base = Environment::getEnv('SS_BASE_URL');
1087
        if (!$base) {
1088
            $base = Director::protocolAndHost();
1089
        }
1090
        $host = parse_url($base, PHP_URL_HOST);
1091
        if (!$host) {
1092
            return false;
1093
        }
1094
        $hostParts = explode('.', $host);
1095
        $parts = count($hostParts);
1096
        if ($parts < 2) {
1097
            return false;
1098
        }
1099
        $domain = $hostParts[$parts - 2] . "." . $hostParts[$parts - 1];
1100
        return $domain;
1101
    }
1102
1103
    /**
1104
     * Get domain from admin email
1105
     *
1106
     * @return boolean|string
1107
     */
1108
    public function getDomainFromEmail()
1109
    {
1110
        $email = SparkPostHelper::resolveDefaultFromEmail(null, false);
1111
        if ($email && is_string($email)) {
1112
            $emailat = (string)strrchr($email, "@");
1113
            $domain = substr($emailat, 1);
1114
            if (!$domain) {
1115
                return false;
1116
            }
1117
            return $domain;
1118
        }
1119
        return false;
1120
    }
1121
1122
    /**
1123
     * Get domain
1124
     *
1125
     * @return boolean|string
1126
     */
1127
    public function getDomain()
1128
    {
1129
        $domain = $this->getDomainFromEmail();
1130
        if (!$domain) {
1131
            return $this->getDomainFromHost();
1132
        }
1133
        return $domain;
1134
    }
1135
1136
    /**
1137
     * Install domain form
1138
     *
1139
     * @param CompositeField $fields
1140
     * @return void
1141
     */
1142
    public function InstallDomainForm(CompositeField $fields)
1143
    {
1144
        $host = $this->getDomain();
1145
1146
        $fields->push(new LiteralField('Info', $this->MessageHelper(
1147
            _t('SparkPostAdmin.DomainNotInstalled', 'Default sending domain {domain} is not installed.', ['domain' => $host]),
1148
            "bad"
1149
        )));
1150
        $fields->push(new LiteralField('doInstallDomain', $this->ButtonHelper(
1151
            $this->Link('doInstallDomain'),
1152
            _t('SparkPostAdmin.DOINSTALLDOMAIN', 'Install domain')
1153
        )));
1154
    }
1155
1156
    /**
1157
     * @return HTTPResponse
1158
     */
1159
    public function doInstallDomain()
1160
    {
1161
        if (!$this->CanConfigureApi()) {
1162
            return $this->redirectBack();
1163
        }
1164
1165
        $client = SparkPostHelper::getClient();
1166
1167
        $domain = $this->getDomain();
1168
1169
        if (!$domain || is_bool($domain)) {
1170
            return $this->redirectBack();
1171
        }
1172
1173
        try {
1174
            $client->createSimpleSendingDomain($domain);
1175
            $this->getCache()->clear();
1176
        } catch (Exception $ex) {
1177
            $this->getLogger()->debug($ex);
1178
        }
1179
1180
        return $this->redirectBack();
1181
    }
1182
1183
    /**
1184
     * Uninstall domain form
1185
     *
1186
     * @param CompositeField $fields
1187
     * @return void
1188
     */
1189
    public function UninstallDomainForm(CompositeField $fields)
1190
    {
1191
        $domainInfos = $this->SendingDomainInstalled();
1192
1193
        $domain = $this->getDomain();
1194
1195
        if ($domainInfos && $domainInfos['status']['ownership_verified']) {
1196
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1197
                _t('SparkPostAdmin.DomainInstalled', 'Default domain {domain} is installed.', ['domain' => $domain]),
1198
                'good'
1199
            )));
1200
        } else {
1201
            $fields->push(new LiteralField('Info', $this->MessageHelper(
1202
                _t('SparkPostAdmin.DomainInstalledBut', 'Default domain {domain} is installed, but is not properly configured.'),
1203
                'warning'
1204
            )));
1205
        }
1206
        $fields->push(new LiteralField('doUninstallHook', $this->ButtonHelper(
1207
            $this->Link('doUninstallHook'),
1208
            _t('SparkPostAdmin.DOUNINSTALLDOMAIN', 'Uninstall domain'),
1209
            true
1210
        )));
1211
    }
1212
1213
    /**
1214
     * @param array<mixed> $data
1215
     * @param Form $form
1216
     * @return HTTPResponse
1217
     */
1218
    public function doUninstallDomain($data, Form $form)
1219
    {
1220
        if (!$this->CanConfigureApi()) {
1221
            return $this->redirectBack();
1222
        }
1223
1224
        $client = SparkPostHelper::getClient();
1225
1226
        $domain = $this->getDomain();
1227
1228
        if (!$domain || is_bool($domain)) {
1229
            return $this->redirectBack();
1230
        }
1231
1232
        try {
1233
            $el = $this->SendingDomainInstalled();
1234
            if ($el) {
1235
                $client->deleteSendingDomain($domain);
1236
            }
1237
            $this->getCache()->clear();
1238
        } catch (Exception $ex) {
1239
            $this->getLogger()->debug($ex);
1240
        }
1241
1242
        return $this->redirectBack();
1243
    }
1244
}
1245