CampaignAdmin   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 825
Duplicated Lines 0 %

Importance

Changes 28
Bugs 1 Features 2
Metric Value
wmc 81
eloc 383
c 28
b 1
f 2
dl 0
loc 825
rs 2

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getClientConfig() 0 33 1
A getPlaceholderGroups() 0 26 3
B getChangeSetResource() 0 59 8
A getListItems() 0 15 3
A readCampaigns() 0 7 1
A EditForm() 0 4 1
A init() 0 6 1
B getChangeSetItemResource() 0 72 8
A getListResource() 0 23 2
A getEditForm() 0 16 1
A getCampaignEditForm() 0 62 3
A SetLink() 0 5 1
A providePermissions() 0 13 1
A shouldCampaignSync() 0 12 3
B removeCampaignItem() 0 29 8
A deleteCampaign() 0 24 6
A ItemLink() 0 5 1
A campaignCreateForm() 0 3 1
A getCampaignCreateForm() 0 39 2
B publishCampaign() 0 33 7
B save() 0 54 10
A campaignEditForm() 0 13 3
A readCampaign() 0 28 6

How to fix   Complexity   

Complex Class

Complex classes like CampaignAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CampaignAdmin, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\CampaignAdmin;
4
5
use LogicException;
6
use SilverStripe\Admin\LeftAndMain;
7
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\Forms\FieldList;
15
use SilverStripe\Forms\Form;
16
use SilverStripe\Forms\FormAction;
17
use SilverStripe\Forms\HiddenField;
18
use SilverStripe\Forms\RequiredFields;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\FieldType\DBDatetime;
21
use SilverStripe\ORM\SS_List;
22
use SilverStripe\ORM\UnexpectedDataException;
23
use SilverStripe\ORM\ValidationResult;
24
use SilverStripe\Security\PermissionProvider;
25
use SilverStripe\Security\Security;
26
use SilverStripe\Security\SecurityToken;
27
use SilverStripe\Versioned\ChangeSet;
28
use SilverStripe\Versioned\ChangeSetItem;
29
use SilverStripe\View\Requirements;
30
31
/**
32
 * Campaign section of the CMS
33
 */
34
class CampaignAdmin extends LeftAndMain implements PermissionProvider
35
{
36
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
37
        'set',
38
        'sets',
39
        'EditForm',
40
        'campaignEditForm',
41
        'campaignCreateForm',
42
        'readCampaigns',
43
        'readCampaign',
44
        'deleteCampaign',
45
        'publishCampaign',
46
        'removeCampaignItem'
47
    ];
48
49
    private static $menu_priority = 3;
0 ignored issues
show
introduced by
The private property $menu_priority is not used, and could be removed.
Loading history...
50
51
    /**
52
     * When listing campaigns, re-sync items automatically after this many seconds.
53
     * This can prevent unnecessary and expensive database requests on every view.
54
     *
55
     * @config
56
     * @var int
57
     */
58
    private static $sync_expires = 300;
0 ignored issues
show
introduced by
The private property $sync_expires is not used, and could be removed.
Loading history...
59
60
    private static $menu_title = 'Campaigns';
0 ignored issues
show
introduced by
The private property $menu_title is not used, and could be removed.
Loading history...
61
62
    private static $menu_icon_class = 'font-icon-page-multiple';
0 ignored issues
show
introduced by
The private property $menu_icon_class is not used, and could be removed.
Loading history...
63
64
    private static $tree_class = ChangeSet::class;
0 ignored issues
show
introduced by
The private property $tree_class is not used, and could be removed.
Loading history...
65
66
    /**
67
     * Show published changesets
68
     *
69
     * Note: Experimental API (will be changed in the near future)
70
     *
71
     * @config
72
     * @var bool
73
     */
74
    private static $show_published = true;
0 ignored issues
show
introduced by
The private property $show_published is not used, and could be removed.
Loading history...
75
76
    /**
77
     * Show inferred changesets (automatically created when you publish a page)
78
     *
79
     * Note: Experimental API (will be changed in the near future)
80
     *
81
     * @config
82
     * @var bool
83
     */
84
    private static $show_inferred = false;
0 ignored issues
show
introduced by
The private property $show_inferred is not used, and could be removed.
Loading history...
85
86
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
87
        'GET sets' => 'readCampaigns',
88
        'POST set/$ID/publish' => 'publishCampaign',
89
        'GET set/$ID/$Name' => 'readCampaign',
90
        'DELETE set/$ID' => 'deleteCampaign',
91
        'campaignEditForm/$ID' => 'campaignEditForm',
92
        'campaignCreateForm' => 'campaignCreateForm',
93
        'POST removeCampaignItem/$CampaignID/$ItemID' => 'removeCampaignItem',
94
    ];
95
96
    private static $url_segment = 'campaigns';
0 ignored issues
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
97
98
    /**
99
     * Size of thumbnail width
100
     *
101
     * @config
102
     * @var int
103
     */
104
    private static $thumbnail_width = 64;
0 ignored issues
show
introduced by
The private property $thumbnail_width is not used, and could be removed.
Loading history...
105
106
    /**
107
     * Size of thumbnail height
108
     *
109
     * @config
110
     * @var int
111
     */
112
    private static $thumbnail_height = 64;
0 ignored issues
show
introduced by
The private property $thumbnail_height is not used, and could be removed.
Loading history...
113
114
    private static $required_permission_codes = 'CMS_ACCESS_CampaignAdmin';
0 ignored issues
show
introduced by
The private property $required_permission_codes is not used, and could be removed.
Loading history...
115
116
    public function getClientConfig()
117
    {
118
        return array_merge(parent::getClientConfig(), [
119
            'reactRouter' => true,
120
            'form' => [
121
                // TODO Use schemaUrl instead
122
                'EditForm' => [
123
                    'schemaUrl' => $this->Link('schema/EditForm')
124
                ],
125
                'campaignEditForm' => [
126
                    'schemaUrl' => $this->Link('schema/campaignEditForm')
127
                ],
128
                'campaignCreateForm' => [
129
                    'schemaUrl' => $this->Link('schema/campaignCreateForm')
130
                ],
131
            ],
132
            'readCampaignsEndpoint' => [
133
                'url' => $this->Link() . 'sets',
134
                'method' => 'get'
135
            ],
136
            'itemListViewEndpoint' => [
137
                'url' => $this->Link() . 'set/:id/show',
138
                'method' => 'get'
139
            ],
140
            'publishEndpoint' => [
141
                'url' => $this->Link() . 'set/:id/publish',
142
                'method' => 'post'
143
            ],
144
            'removeCampaignItemEndpoint' => [
145
                'url' => $this->Link() . 'removeCampaignItem/:id/:itemId',
146
                'method' => 'post'
147
            ],
148
            'treeClass' => $this->config()->get('tree_class')
149
        ]);
150
    }
151
152
    public function init()
153
    {
154
        parent::init();
155
        Requirements::add_i18n_javascript('silverstripe/campaign-admin: client/lang', false, true);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Requir...::add_i18n_javascript() has been deprecated. ( Ignorable by Annotation )

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

155
        /** @scrutinizer ignore-deprecated */ Requirements::add_i18n_javascript('silverstripe/campaign-admin: client/lang', false, true);
Loading history...
156
        Requirements::javascript('silverstripe/campaign-admin: client/dist/js/bundle.js');
157
        Requirements::css('silverstripe/campaign-admin: client/dist/styles/bundle.css');
158
    }
159
160
    public function getEditForm($id = null, $fields = null)
161
    {
162
        $fields = new FieldList(
163
            CampaignAdminList::create('ChangeSets')
164
        );
165
        $actions = new FieldList();
166
        $form = Form::create($this, 'EditForm', $fields, $actions);
167
        $form->addExtraClass('form--padded');
168
169
        // Set callback response
170
        $form->setValidationResponseCallback(function () use ($form) {
171
            $schemaId = $this->Link('schema/EditForm');
172
            return $this->getSchemaResponse($form, $schemaId);
0 ignored issues
show
Bug introduced by
$schemaId of type string is incompatible with the type SilverStripe\Forms\Form expected by parameter $form of SilverStripe\Admin\LeftA...in::getSchemaResponse(). ( Ignorable by Annotation )

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

172
            return $this->getSchemaResponse($form, /** @scrutinizer ignore-type */ $schemaId);
Loading history...
173
        });
174
175
        return $form;
176
    }
177
178
    public function EditForm($request = null)
179
    {
180
        // Note: Edit form doesn't have ID, and simply populates a top level gridfield
181
        return $this->getEditForm();
182
    }
183
184
    /**
185
     * REST endpoint to get a list of campaigns.
186
     *
187
     * @return HTTPResponse
188
     */
189
    public function readCampaigns()
190
    {
191
        $response = new HTTPResponse();
192
        $response->addHeader('Content-Type', 'application/json');
193
        $hal = $this->getListResource();
194
        $response->setBody(json_encode($hal));
195
        return $response;
196
    }
197
198
    /**
199
     * @return array
200
     */
201
    protected function getPlaceholderGroups()
202
    {
203
        $groups = [];
204
205
        $classes = Config::inst()->get(ChangeSet::class, 'important_classes');
206
207
        foreach ($classes as $class) {
208
            if (!class_exists($class ?? '')) {
209
                continue;
210
            }
211
            /** @var DataObject $item */
212
            $item = Injector::inst()->get($class);
213
            $groups[] = [
214
                'baseClass' => DataObject::getSchema()->baseDataClass($class),
215
                'singular' => $item->i18n_singular_name(),
216
                'plural' => $item->i18n_plural_name(),
217
                'noItemsText' => _t(__CLASS__.'.NOITEMSTEXT', 'Add items from the {section} section', [
218
                    'section' => $item->i18n_plural_name(),
219
                ]),
220
                'items' => []
221
            ];
222
        }
223
224
        $this->extend('updatePlaceholderGroups', $groups);
225
226
        return $groups;
227
    }
228
229
    /**
230
     * Get list contained as a hal wrapper
231
     *
232
     * @return array
233
     */
234
    protected function getListResource()
235
    {
236
        $items = $this->getListItems();
237
        $count = $items->count();
238
        /** @var string $treeClass */
239
        $treeClass = $this->config()->get('tree_class');
240
        $hal = [
241
            'count' => $count,
242
            'total' => $count,
243
            '_links' => [
244
                'self' => [
245
                    'href' => $this->Link('items')
246
                ]
247
            ],
248
            '_embedded' => [$treeClass => []]
249
        ];
250
        /** @var ChangeSet $item */
251
        foreach ($items as $item) {
252
            $sync = $this->shouldCampaignSync($item);
253
            $resource = $this->getChangeSetResource($item, $sync);
254
            $hal['_embedded'][$treeClass][] = $resource;
255
        }
256
        return $hal;
257
    }
258
259
    /**
260
     * Build item resource from a changeset
261
     *
262
     * @param ChangeSet $changeSet
263
     * @param bool $sync Set to true to force async of this changeset
264
     * @return array
265
     */
266
    protected function getChangeSetResource(ChangeSet $changeSet, $sync = false)
267
    {
268
        $stateLabel = sprintf(
269
            '<span class="campaign-status campaign-status--%s"></span>%s',
270
            $changeSet->State,
271
            $changeSet->getStateLabel()
272
        );
273
274
        $hal = [
275
            '_links' => [
276
                'self' => [
277
                    'href' => $this->SetLink($changeSet->ID)
278
                ]
279
            ],
280
            'ID' => $changeSet->ID,
281
            'Name' => $changeSet->Name,
282
            'Created' => $changeSet->Created,
283
            'LastEdited' => $changeSet->LastEdited,
284
            'State' => $changeSet->State,
285
            'StateLabel' => [ 'html' => $stateLabel ],
286
            'IsInferred' => $changeSet->IsInferred,
287
            'canEdit' => $changeSet->canEdit(),
288
            'canPublish' => false,
289
            '_embedded' => ['items' => []],
290
            'placeholderGroups' => $this->getPlaceholderGroups(),
291
        ];
292
293
        // Before presenting the changeset to the client,
294
        // synchronise it with new changes.
295
        try {
296
            if ($sync) {
297
                $changeSet->sync();
298
            }
299
            $hal['PublishedLabel'] = $changeSet->getPublishedLabel() ?: '-';
300
            $hal['Details'] = $changeSet->getDetails();
301
            $hal['canPublish'] = $changeSet->canPublish() && $changeSet->hasChanges();
302
303
            foreach ($changeSet->Changes() as $changeSetItem) {
304
                if (!$changeSetItem) {
305
                    continue;
306
                }
307
308
                /** @var ChangesetItem $changeSetItem */
309
                $resource = $this->getChangeSetItemResource($changeSetItem);
310
                if (!empty($resource)) {
311
                    $hal['_embedded']['items'][] = $resource;
312
                }
313
            }
314
315
            // An unexpected data exception means that the database is corrupt
316
        } catch (UnexpectedDataException $e) {
317
            $hal['PublishedLabel'] = '-';
318
            $hal['Details'] = 'Corrupt database! ' . $e->getMessage();
319
            $hal['canPublish'] = false;
320
        }
321
322
        $this->extend('updateChangeSetResource', $hal, $changeSet);
323
324
        return $hal;
325
    }
326
327
    /**
328
     * Build item resource from a changesetitem
329
     *
330
     * @param ChangeSetItem $changeSetItem
331
     * @return array
332
     */
333
    protected function getChangeSetItemResource(ChangeSetItem $changeSetItem)
334
    {
335
        $baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
336
        $baseSingleton = DataObject::singleton($baseClass);
337
338
        // Allow items to opt out of being displayed in changesets
339
        if ($baseSingleton->config()->get('hide_in_campaigns')) {
340
            return [];
341
        }
342
343
        $thumbnailWidth = (int)$this->config()->get('thumbnail_width');
344
        $thumbnailHeight = (int)$this->config()->get('thumbnail_height');
345
        $hal = [
346
            '_links' => [
347
                'self' => [
348
                    'id' => $changeSetItem->ID,
349
                    'href' => $this->ItemLink($changeSetItem->ID)
350
                ]
351
            ],
352
            'ID' => $changeSetItem->ID,
353
            'Created' => $changeSetItem->Created,
354
            'LastEdited' => $changeSetItem->LastEdited,
355
            'Title' => $changeSetItem->getTitle(),
356
            'ChangeType' => $changeSetItem->getChangeType(),
357
            'Added' => $changeSetItem->Added,
358
            'ObjectClass' => $changeSetItem->ObjectClass,
359
            'ObjectID' => $changeSetItem->ObjectID,
360
            'BaseClass' => $baseClass,
361
            'Singular' => $baseSingleton->i18n_singular_name(),
362
            'Plural' => $baseSingleton->i18n_plural_name(),
363
            'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight),
364
        ];
365
        // Get preview urls
366
        $previews = $changeSetItem->getPreviewLinks();
367
        if ($previews) {
368
            $hal['_links']['preview'] = $previews;
369
        }
370
371
        // Get edit link
372
        $editLink = $changeSetItem->CMSEditLink();
373
        if ($editLink) {
374
            $hal['_links']['edit'] = [
375
                'href' => $editLink,
376
            ];
377
        }
378
379
        // Depending on whether the object was added implicitly or explicitly, set
380
        // other related objects.
381
        if ($changeSetItem->Added === ChangeSetItem::IMPLICITLY) {
382
            $referencedItems = $changeSetItem->ReferencedBy();
383
            $referencedBy = [];
384
            foreach ($referencedItems as $referencedItem) {
385
                $referencedBy[] = [
386
                    'href' => $this->SetLink($referencedItem->ID),
387
                    'ChangeSetItemID' => $referencedItem->ID
388
                ];
389
            }
390
            if ($referencedBy) {
391
                $hal['_links']['referenced_by'] = $referencedBy;
392
            }
393
        }
394
395
        $referToItems = $changeSetItem->References();
396
        $referTo = [];
397
        foreach ($referToItems as $referToItem) {
398
            $referTo[] = [
399
                'ChangeSetItemID' => $referToItem->ID,
400
            ];
401
        }
402
        $hal['_links']['references'] = $referTo;
403
404
        return $hal;
405
    }
406
407
    /**
408
     * Gets viewable list of campaigns
409
     *
410
     * @return SS_List
411
     */
412
    protected function getListItems()
413
    {
414
        $changesets = ChangeSet::get();
415
        // Filter out published items if disabled
416
        if (!$this->config()->get('show_published')) {
417
            $changesets = $changesets->filter('State', ChangeSet::STATE_OPEN);
418
        }
419
        // Filter out automatically created changesets
420
        if (!$this->config()->get('show_inferred')) {
421
            $changesets = $changesets->filter('IsInferred', 0);
422
        }
423
        return $changesets
424
            ->filterByCallback(function ($item) {
425
                /** @var ChangeSet $item */
426
                return ($item->canView());
427
            });
428
    }
429
430
431
    /**
432
     * REST endpoint to get a campaign.
433
     *
434
     * @param HTTPRequest $request
435
     *
436
     * @return HTTPResponse
437
     */
438
    public function readCampaign(HTTPRequest $request)
439
    {
440
        $response = new HTTPResponse();
441
        $accepts = $request->getAcceptMimetypes();
442
443
        //accept 'text/json' for legacy reasons
444
        if (in_array('application/json', $accepts ?? []) || in_array('text/json', $accepts ?? [])) {
445
            $response->addHeader('Content-Type', 'application/json');
446
            if (!$request->param('Name')) {
447
                return (new HTTPResponse(null, 400));
448
            }
449
450
            /** @var ChangeSet $changeSet */
451
            $changeSet = ChangeSet::get()->filter('IsInferred', 0)->byID($request->param('ID'));
0 ignored issues
show
Bug introduced by
$request->param('ID') of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataList::byID(). ( Ignorable by Annotation )

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

451
            $changeSet = ChangeSet::get()->filter('IsInferred', 0)->byID(/** @scrutinizer ignore-type */ $request->param('ID'));
Loading history...
452
453
            if (!$changeSet) {
0 ignored issues
show
introduced by
$changeSet is of type SilverStripe\Versioned\ChangeSet, thus it always evaluated to true.
Loading history...
454
                return (new HTTPResponse(null, 404));
455
            }
456
457
            if (!$changeSet->canView()) {
458
                return (new HTTPResponse(null, 403));
459
            }
460
461
            $body = json_encode($this->getChangeSetResource($changeSet, true));
462
            return (new HTTPResponse($body, 200))
463
                ->addHeader('Content-Type', 'application/json');
464
        } else {
465
            return $this->index($request);
466
        }
467
    }
468
469
    /**
470
     * REST endpoint to delete a campaign item.
471
     *
472
     * @param HTTPRequest $request
473
     *
474
     * @return HTTPResponse
475
     */
476
    public function removeCampaignItem(HTTPRequest $request)
477
    {
478
        // Check security ID
479
        if (!SecurityToken::inst()->checkRequest($request)) {
480
            return new HTTPResponse(null, 400);
481
        }
482
483
        $campaignID = $request->param('CampaignID');
484
        $itemID = $request->param('ItemID');
485
486
        if (!$campaignID ||
487
            !is_numeric($campaignID) ||
488
            !$itemID ||
489
            !is_numeric($itemID)) {
490
            return (new HTTPResponse(null, 400));
491
        }
492
493
        /** @var ChangeSet $campaign */
494
        $campaign = ChangeSet::get()->byID($campaignID);
0 ignored issues
show
Bug introduced by
$campaignID of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataList::byID(). ( Ignorable by Annotation )

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

494
        $campaign = ChangeSet::get()->byID(/** @scrutinizer ignore-type */ $campaignID);
Loading history...
495
        /** @var ChangeSetItem $item */
496
        $item = ChangeSetItem::get()->byID($itemID);
497
        if (!$campaign || !$item) {
0 ignored issues
show
introduced by
$campaign is of type SilverStripe\Versioned\ChangeSet, thus it always evaluated to true.
Loading history...
introduced by
$item is of type SilverStripe\Versioned\ChangeSetItem, thus it always evaluated to true.
Loading history...
498
            return (new HTTPResponse(null, 404));
499
        }
500
501
502
        $campaign->removeObject($item->Object());
503
504
        return (new HTTPResponse(null, 204));
505
    }
506
507
    /**
508
     * REST endpoint to delete a campaign.
509
     *
510
     * @param HTTPRequest $request
511
     *
512
     * @return HTTPResponse
513
     */
514
    public function deleteCampaign(HTTPRequest $request)
515
    {
516
        // Check security ID
517
        if (!SecurityToken::inst()->checkRequest($request)) {
518
            return new HTTPResponse(null, 400);
519
        }
520
521
        $id = $request->param('ID');
522
        if (!$id || !is_numeric($id)) {
523
            return (new HTTPResponse(null, 400));
524
        }
525
526
        $record = ChangeSet::get()->byID($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataList::byID(). ( Ignorable by Annotation )

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

526
        $record = ChangeSet::get()->byID(/** @scrutinizer ignore-type */ $id);
Loading history...
527
        if (!$record) {
528
            return (new HTTPResponse(null, 404));
529
        }
530
531
        if (!$record->canDelete()) {
532
            return (new HTTPResponse(null, 403));
533
        }
534
535
        $record->delete();
536
537
        return (new HTTPResponse(null, 204));
538
    }
539
540
    /**
541
     * REST endpoint to publish a {@link ChangeSet} and all of its items.
542
     *
543
     * @param HTTPRequest $request
544
     *
545
     * @return HTTPResponse
546
     */
547
    public function publishCampaign(HTTPRequest $request)
548
    {
549
        // Protect against CSRF on destructive action
550
        if (!SecurityToken::inst()->checkRequest($request)) {
551
            return (new HTTPResponse(null, 400));
552
        }
553
554
        $id = $request->param('ID');
555
        if (!$id || !is_numeric($id)) {
556
            return (new HTTPResponse(null, 400));
557
        }
558
559
        /** @var ChangeSet $record */
560
        $record = ChangeSet::get()->byID($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataList::byID(). ( Ignorable by Annotation )

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

560
        $record = ChangeSet::get()->byID(/** @scrutinizer ignore-type */ $id);
Loading history...
561
        if (!$record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\Versioned\ChangeSet, thus it always evaluated to true.
Loading history...
562
            return (new HTTPResponse(null, 404));
563
        }
564
565
        if (!$record->canPublish()) {
566
            return (new HTTPResponse(null, 403));
567
        }
568
569
        try {
570
            $record->publish();
571
        } catch (LogicException $e) {
572
            return (new HTTPResponse(json_encode(['status' => 'error', 'message' => $e->getMessage()]), 401))
573
                ->addHeader('Content-Type', 'application/json');
574
        }
575
576
        return (new HTTPResponse(
577
            json_encode($this->getChangeSetResource($record)),
578
            200
579
        ))->addHeader('Content-Type', 'application/json');
580
    }
581
582
    /**
583
     * Url handler for edit form
584
     *
585
     * @param HTTPRequest $request
586
     * @return Form
587
     */
588
    public function campaignEditForm($request)
589
    {
590
        // Get ID either from posted back value, or url parameter
591
        if (!$request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
592
            $this->httpError(400);
593
            return null;
594
        }
595
        $id = $request->param('ID');
596
        if (!$id) {
597
            $this->httpError(400);
598
            return null;
599
        }
600
        return $this->getCampaignEditForm($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\CampaignAdm...::getCampaignEditForm(). ( Ignorable by Annotation )

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

600
        return $this->getCampaignEditForm(/** @scrutinizer ignore-type */ $id);
Loading history...
601
    }
602
603
    /**
604
     * @todo Use GridFieldDetailForm once it can handle structured data and form schemas
605
     * @todo move to FormBuilder
606
     *
607
     * @param int $id
608
     * @return Form
609
     */
610
    public function getCampaignEditForm($id)
611
    {
612
        // Get record-specific fields
613
        $record = ChangeSet::get()->byID($id);
614
        if (!$record) {
615
            $this->httpError(404);
616
            return null;
617
        }
618
        if (!$record->canView()) {
619
            $this->httpError(403);
620
            return null;
621
        }
622
623
        $fields = $record->getCMSFields();
624
625
        // Add standard fields
626
        $fields->push(HiddenField::create('ID'));
627
        $form = Form::create(
628
            $this,
629
            'campaignEditForm',
630
            $fields,
631
            FieldList::create(
632
                FormAction::create('save', _t(__CLASS__.'SAVE', 'Save'))
633
                    ->setIcon('save')
634
                    ->setSchemaState([
635
                        'data' => [
636
                            'pristineTitle' => _t(__CLASS__.'SAVED', 'Saved'),
637
                            'pristineIcon' => 'tick',
638
                            'dirtyTitle' => _t(__CLASS__.'SAVE', 'Save'),
639
                            'dirtyIcon' => 'save',
640
                            'pristineClass' => 'btn-outline-primary',
641
                            'dirtyClass' => '',
642
                        ],
643
                    ]),
644
                FormAction::create('cancel', _t(__CLASS__.'.CANCEL', 'Cancel'))
645
                    ->setUseButtonTag(true)
646
            ),
647
            new RequiredFields('Name')
648
        );
649
650
        // Load into form
651
        $form->loadDataFrom($record);
652
653
        // Set form action handler with ID included
654
        $form->setRequestHandler(
655
            LeftAndMainFormRequestHandler::create($form, [ $id ])
656
        );
657
658
        // Configure form to respond to validation errors with form schema
659
        // if requested via react.
660
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id, $record) {
0 ignored issues
show
Unused Code introduced by
The import $record is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
661
            $schemaId = Controller::join_links(
662
                $this->Link('schema'),
663
                'campaignEditForm',
664
                $id
665
            );
666
            return $this->getSchemaResponse($schemaId, $form, $errors);
667
        });
668
669
        $form->setNotifyUnsavedChanges(true);
670
671
        return $form;
672
    }
673
674
    /**
675
     * Url handler for create form
676
     *
677
     * @param HTTPRequest $request
678
     * @return Form
679
     */
680
    public function campaignCreateForm($request)
681
    {
682
        return $this->getCampaignCreateForm();
683
    }
684
685
    /**
686
     * Build create form
687
     * @todo Move to form builder
688
     *
689
     * @return Form
690
     */
691
    public function getCampaignCreateForm()
692
    {
693
        $record = ChangeSet::singleton();
694
        if (!$record->canCreate()) {
695
            $this->httpError(403);
696
            return null;
697
        }
698
        $fields = $record->getCMSFields();
699
700
        // Add standard fields
701
        $fields->push(HiddenField::create('ID'));
702
        $form = Form::create(
703
            $this,
704
            'campaignCreateForm',
705
            $fields,
706
            FieldList::create(
707
                FormAction::create('save', _t(__CLASS__.'.CREATE', 'Create'))
708
                    ->setIcon('plus'),
709
                FormAction::create('cancel', _t(__CLASS__.'.CANCEL', 'Cancel'))
710
                    ->setUseButtonTag(true)
711
            ),
712
            new RequiredFields('Name')
713
        );
714
715
        // Custom form handler
716
        $form->setRequestHandler(
717
            LeftAndMainFormRequestHandler::create($form)
718
        );
719
720
        // Configure form to respond to validation errors with form schema
721
        // if requested via react.
722
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $record) {
0 ignored issues
show
Unused Code introduced by
The import $record is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
723
            $schemaId = $this->Link('schema/campaignCreateForm');
724
            return $this->getSchemaResponse($schemaId, $form, $errors);
725
        });
726
727
        $form->setNotifyUnsavedChanges(true);
728
729
        return $form;
730
    }
731
732
    /**
733
     * Save  handler
734
     *
735
     * @param array $data
736
     * @param Form $form
737
     * @return HTTPResponse
738
     */
739
    public function save($data, $form)
740
    {
741
        $errors = null;
742
743
        // Existing or new record?
744
        $id = empty($data['ID']) ? 0 : $data['ID'];
745
        if ($id) {
746
            $record = ChangeSet::get()->byID($id);
747
            if ($record && !$record->canEdit()) {
748
                return Security::permissionFailure($this);
749
            }
750
            if (!$record || !$record->ID) {
751
                $this->httpError(404, "Bad record ID #" . (int)$id);
752
            }
753
        } else {
754
            if (!ChangeSet::singleton()->canCreate()) {
755
                return Security::permissionFailure($this);
756
            }
757
            $record = ChangeSet::create();
758
        }
759
760
        $hasExistingName = Changeset::get()
761
            ->filter('Name:nocase', $data['Name'])
762
            ->exclude('ID', $id)
763
            ->count() > 0;
764
765
        if (!$hasExistingName) {
766
            // save form data into record
767
            $form->saveInto($record, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type SilverStripe\Forms\FieldList expected by parameter $fieldList of SilverStripe\Forms\Form::saveInto(). ( Ignorable by Annotation )

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

767
            $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
768
            $record->write();
769
            $this->extend('onAfterSave', $record);
770
            $message = _t(__CLASS__.'.SAVEDUP', 'Saved.');
771
            $form->setMessage($message, ValidationResult::TYPE_GOOD);
772
        } else {
773
            $nameDuplicateMsg = _t(__CLASS__ . '.ERROR_DUPLICATE_NAME', 'Name "{Name}" already exists', '', [ 'Name' => $data['Name']]);
774
            $errors = new ValidationResult();
775
            $errors->addFieldMessage('Name', $nameDuplicateMsg);
776
            $message = _t(__CLASS__.'.SAVEDERROR', 'Error.');
777
            // Need to set the form message or the field message won't show up at all
778
            $form->setMessage($message, ValidationResult::TYPE_ERROR);
779
        }
780
781
        if ($id) {
782
            $schemaId = Controller::join_links($this->Link('schema'), 'campaignEditForm', $id);
783
        } else {
784
            $schemaId = Controller::join_links($this->Link('schema'), 'campaignCreateForm');
785
        }
786
787
        // Ensure that newly created records have all their data loaded back into the form.
788
        $form->loadDataFrom($record);
789
        $extra = ['record' => ['id' => $record->ID]];
790
        $response = $this->getSchemaResponse($schemaId, $form, $errors, $extra);
791
        $response->addHeader('X-Status', rawurlencode($message ?? ''));
792
        return $response;
793
    }
794
795
    /**
796
     * Gets user-visible url to edit a specific {@see ChangeSet}
797
     *
798
     * @param $itemID
799
     * @return string
800
     */
801
    public function SetLink($itemID)
802
    {
803
        return Controller::join_links(
804
            $this->Link('set'),
805
            $itemID
806
        );
807
    }
808
809
    /**
810
     * Gets user-visible url to edit a specific {@see ChangeSetItem}
811
     *
812
     * @param int $itemID
813
     * @return string
814
     */
815
    public function ItemLink($itemID)
816
    {
817
        return Controller::join_links(
818
            $this->Link('item'),
819
            $itemID
820
        );
821
    }
822
823
    public function providePermissions()
824
    {
825
        return array(
826
            "CMS_ACCESS_CampaignAdmin" => array(
827
                'name' => _t(
828
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS',
829
                    "Access to '{title}' section",
830
                    array('title' => static::menu_title())
831
                ),
832
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
833
                'help' => _t(
834
                    __CLASS__.'.ACCESS_HELP',
835
                    'Allow viewing of the campaign publishing section.'
836
                )
837
            )
838
        );
839
    }
840
841
    /**
842
     * Check if the given campaign should be synced before view
843
     *
844
     * @param ChangeSet $item
845
     * @return bool
846
     */
847
    protected function shouldCampaignSync(ChangeSet $item)
848
    {
849
        // Don't sync published changesets
850
        if ($item->State !== ChangeSet::STATE_OPEN) {
851
            return false;
852
        }
853
854
        // Get sync threshold
855
        $syncOlderThan = DBDatetime::now()->getTimestamp() - $this->config()->get('sync_expires');
856
        /** @var DBDatetime $lastSynced */
857
        $lastSynced = $item->dbObject('LastSynced');
858
        return !$lastSynced || $lastSynced->getTimestamp() < $syncOlderThan;
859
    }
860
}
861