Issues (51)

src/CampaignAdmin.php (4 issues)

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 = [
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;
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;
59
60
    private static $menu_title = 'Campaigns';
61
62
    private static $menu_icon_class = 'font-icon-page-multiple';
63
64
    private static $tree_class = ChangeSet::class;
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;
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;
85
86
    private static $url_handlers = [
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';
97
98
    /**
99
     * Size of thumbnail width
100
     *
101
     * @config
102
     * @var int
103
     */
104
    private static $thumbnail_width = 64;
105
106
    /**
107
     * Size of thumbnail height
108
     *
109
     * @config
110
     * @var int
111
     */
112
    private static $thumbnail_height = 64;
113
114
    private static $required_permission_codes = 'CMS_ACCESS_CampaignAdmin';
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);
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);
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'));
452
453
            if (!$changeSet) {
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
$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
$campaign is of type SilverStripe\Versioned\ChangeSet, thus it always evaluated to true.
Loading history...
$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);
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);
561
        if (!$record) {
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) {
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);
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) {
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) {
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
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