Passed
Push — master ( 04d765...0e1598 )
by Thomas
11:06
created

ActionsGridFieldItemRequest::addGridState()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
cc 3
eloc 7
c 2
b 1
f 1
nc 3
nop 2
dl 0
loc 12
rs 10
1
<?php
2
3
namespace LeKoala\CmsActions;
4
5
use Exception;
6
use SilverStripe\Forms\Tab;
7
use SilverStripe\Forms\Form;
8
use SilverStripe\Forms\TabSet;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Forms\FieldList;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Forms\FormAction;
14
use SilverStripe\Admin\LeftAndMain;
15
use SilverStripe\ORM\DataExtension;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\HTTPRequest;
18
use SilverStripe\Control\HTTPResponse;
19
use SilverStripe\Forms\CompositeField;
20
use SilverStripe\ORM\ValidationResult;
21
use SilverStripe\SiteConfig\SiteConfig;
0 ignored issues
show
Bug introduced by
The type SilverStripe\SiteConfig\SiteConfig was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use SilverStripe\Core\Config\Configurable;
23
use SilverStripe\ORM\FieldType\DBHTMLText;
24
use SilverStripe\Control\HTTPResponse_Exception;
25
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
26
use SilverStripe\Forms\HiddenField;
27
28
/**
29
 * Decorates GridDetailForm_ItemRequest to use new form actions and buttons.
30
 *
31
 * This is also applied to LeftAndMain to allow actions on pages
32
 * Warning: LeftAndMain doesn't call updateItemEditForm
33
 *
34
 * This is a lightweight version of BetterButtons that use default getCMSActions functionnality
35
 * on DataObjects
36
 *
37
 * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons
38
 * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons/blob/master/src/Extensions/GridFieldBetterButtonsItemRequest.php
39
 * @property LeftAndMain|GridFieldDetailForm_ItemRequest|ActionsGridFieldItemRequest $owner
40
 */
41
class ActionsGridFieldItemRequest extends DataExtension
42
{
43
    use Configurable;
44
    use Extensible;
45
46
    /**
47
     * @config
48
     * @var boolean
49
     */
50
    private static $enable_save_prev_next = true;
51
52
    /**
53
     * @config
54
     * @var boolean
55
     */
56
    private static $enable_save_close = true;
57
58
    /**
59
     * @config
60
     * @var boolean
61
     */
62
    private static $enable_delete_right = true;
63
64
    /**
65
     * @config
66
     * @var boolean
67
     */
68
    private static $enable_utils_prev_next = false;
0 ignored issues
show
introduced by
The private property $enable_utils_prev_next is not used, and could be removed.
Loading history...
69
70
    /**
71
     * @var array Allowed controller actions
72
     */
73
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
74
        'doSaveAndClose',
75
        'doSaveAndNext',
76
        'doSaveAndPrev',
77
        'doCustomAction', // For CustomAction
78
        'doCustomLink', // For CustomLink
79
    ];
80
81
    /**
82
     * @return array
83
     */
84
    protected function getAvailableActions($actions)
85
    {
86
        $list = [];
87
        foreach ($actions as $action) {
88
            if (is_a($action, CompositeField::class)) {
89
                $list = array_merge($list, $this->getAvailableActions($action->FieldList()));
90
            } else {
91
                $list[] = $action->getName();
92
            }
93
        }
94
95
        return $list;
96
    }
97
98
    /**
99
     * Called by CMSMain, typically in the CMS or in the SiteConfig admin
100
     * CMSMain already uses getCMSActions so we are good to go with anything defined there
101
     *
102
     * @param Form $form
103
     * @return void
104
     */
105
    public function updateEditForm(Form $form)
106
    {
107
        $actions = $form->Actions();
108
109
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
110
        // and we don't want to duplicate it
111
        $this->processDropUpMenu($actions);
112
    }
113
114
    /**
115
     * Called by GridField_ItemRequest
116
     * GridField_ItemRequest defines its own set of actions so we need to add ours
117
     * We add our custom save&close, save&next and other tweaks
118
     * Actions can be made readonly after this extension point
119
     */
120
    public function updateFormActions($actions)
121
    {
122
        $record = $this->owner->getRecord();
0 ignored issues
show
Bug introduced by
The method getRecord() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

122
        /** @scrutinizer ignore-call */ 
123
        $record = $this->owner->getRecord();
Loading history...
Bug introduced by
The call to SilverStripe\Admin\LeftAndMain::getRecord() has too few arguments starting with id. ( Ignorable by Annotation )

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

122
        /** @scrutinizer ignore-call */ 
123
        $record = $this->owner->getRecord();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
123
124
        // We get the actions as defined on our record
125
        /** @var FieldList $CMSActions */
126
        $CMSActions = $record->getCMSActions();
127
128
        // The default button group that contains the Save or Create action
129
        // @link https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface/#extending-the-cms-actions
130
        $MajorActions = $actions->fieldByName('MajorActions');
131
132
        // If it doesn't exist, push to default group
133
        if (!$MajorActions) {
134
            $MajorActions = $actions;
0 ignored issues
show
Unused Code introduced by
The assignment to $MajorActions is dead and can be removed.
Loading history...
135
        }
136
137
        // Push our actions that are otherwise ignored by SilverStripe
138
        foreach ($CMSActions as $action) {
139
            // Avoid duplicated actions (eg: when added by SilverStripe\Versioned\VersionedGridFieldItemRequest)
140
            if ($actions->fieldByName($action->getName())) {
141
                continue;
142
            }
143
            $actions->push($action);
144
        }
145
146
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
147
        // and we don't want to duplicate it
148
        $this->processDropUpMenu($actions);
149
150
        // Add extension hook
151
        $this->extend('onBeforeUpdateCMSActions', $actions, $record);
152
        $record->extend('onBeforeUpdateCMSActions', $actions);
153
154
        $ActionMenus = $actions->fieldByName('ActionMenus');
155
        // Re-insert ActionMenus to make sure they always follow the buttons
156
        if ($ActionMenus) {
157
            $actions->remove($ActionMenus);
158
            $actions->push($ActionMenus);
159
        }
160
161
        // We have a 4.4 setup, before that there was no RightGroup
162
        $RightGroup = $actions->fieldByName('RightGroup');
163
164
        // Insert again to make sure our actions are properly placed after apply changes
165
        if ($RightGroup) {
166
            $actions->remove($RightGroup);
167
            $actions->push($RightGroup);
168
        }
169
170
        $opts = [
171
            'save_close'     => self::config()->enable_save_close,
172
            'save_prev_next' => self::config()->enable_save_prev_next,
173
            'delete_right'   => self::config()->enable_delete_right,
174
        ];
175
        if ($record->hasMethod('getCMSActionsOptions')) {
176
            $opts = array_merge($opts, $record->getCMSActionsOptions());
177
        }
178
179
        if ($opts['save_close']) {
180
            $this->addSaveAndClose($actions, $record);
181
        }
182
183
        if ($opts['save_prev_next']) {
184
            $this->addSaveNextAndPrevious($actions, $record);
185
        }
186
187
        if ($opts['delete_right']) {
188
            $this->moveCancelAndDelete($actions, $record);
189
        }
190
191
        // Add extension hook
192
        $this->extend('onAfterUpdateCMSActions', $actions, $record);
193
        $record->extend('onAfterUpdateCMSActions', $actions);
194
    }
195
196
    /**
197
     * Collect all Drop-Up actions into a menu.
198
     * @param FieldList $actions
199
     * @return void
200
     */
201
    protected function processDropUpMenu($actions)
202
    {
203
        // The Drop-up container may already exist
204
        $dropUpContainer = $actions->fieldByName('ActionMenus.MoreOptions');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dropUpContainer is correct as $actions->fieldByName('ActionMenus.MoreOptions') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
205
        foreach ($actions as $action) {
206
            if ($action->hasMethod('getDropUp') && $action->getDropUp()) {
207
                if (!$dropUpContainer) {
208
                    $dropUpContainer = $this->createDropUpContainer($actions);
209
                }
210
                $action->getContainerFieldList()->removeByName($action->getName());
211
                $dropUpContainer->push($action);
212
            }
213
        }
214
    }
215
216
    /**
217
     * Prepares a Drop-Up menu
218
     * @param FieldList $actions
219
     * @return Tab
220
     */
221
    protected function createDropUpContainer($actions)
222
    {
223
        $rootTabSet = new TabSet('ActionMenus');
224
        $dropUpContainer = new Tab(
225
            'MoreOptions',
226
            _t(__CLASS__ . '.MoreOptions', 'More options', 'Expands a view for more buttons')
227
        );
228
        $dropUpContainer->addExtraClass('popover-actions-simulate');
229
        $rootTabSet->push($dropUpContainer);
230
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
231
232
        $actions->insertBefore('RightGroup', $rootTabSet);
233
234
        return $dropUpContainer;
235
    }
236
237
    /**
238
     * Check if a record can be edited/created/exists
239
     * @param DataObject $record
240
     * @return bool
241
     */
242
    protected function checkCan($record)
243
    {
244
        if (!$record->canEdit() || (!$record->ID && !$record->canCreate())) {
245
            return false;
246
        }
247
248
        return true;
249
    }
250
251
    /**
252
     * @param FieldList $actions
253
     * @param DataObject $record
254
     * @return void
255
     */
256
    public function moveCancelAndDelete(FieldList $actions, DataObject $record)
257
    {
258
        // We have a 4.4 setup, before that there was no RightGroup
259
        $RightGroup = $actions->fieldByName('RightGroup');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $RightGroup is correct as $actions->fieldByName('RightGroup') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
260
261
        // Move delete at the end
262
        $deleteAction = $actions->fieldByName('action_doDelete');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $deleteAction is correct as $actions->fieldByName('action_doDelete') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
263
        if ($deleteAction) {
0 ignored issues
show
introduced by
$deleteAction is of type null, thus it always evaluated to false.
Loading history...
264
            // Move at the end of the stack
265
            $actions->remove($deleteAction);
266
            $actions->push($deleteAction);
267
268
            if (!$RightGroup) {
269
                // Only necessary pre 4.4
270
                $deleteAction->addExtraClass('align-right');
271
            }
272
            // Set custom title
273
            if ($record->hasMethod('getDeleteButtonTitle')) {
274
                $deleteAction->setTitle($record->getDeleteButtonTitle());
275
            }
276
        }
277
        // Move cancel at the end
278
        $cancelButton = $actions->fieldByName('cancelbutton');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $cancelButton is correct as $actions->fieldByName('cancelbutton') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
279
        if ($cancelButton) {
0 ignored issues
show
introduced by
$cancelButton is of type null, thus it always evaluated to false.
Loading history...
280
            // Move at the end of the stack
281
            $actions->remove($cancelButton);
282
            $actions->push($cancelButton);
283
            if (!$RightGroup) {
284
                // Only necessary pre 4.4
285
                $cancelButton->addExtraClass('align-right');
286
            }
287
            // Set custom titlte
288
            if ($record->hasMethod('getCancelButtonTitle')) {
289
                $cancelButton->setTitle($record->getCancelButtonTitle());
290
            }
291
        }
292
    }
293
294
    /**
295
     * @param DataObject $record
296
     * @return int
297
     */
298
    public function getCustomPreviousRecordID(DataObject $record)
299
    {
300
        // This will overwrite state provided record
301
        if (self::config()->enable_custom_prevnext && $record->hasMethod('PrevRecord')) {
302
            return $record->PrevRecord()->ID ?? 0;
303
        }
304
        return $this->owner->getPreviousRecordID();
0 ignored issues
show
Bug introduced by
The method getPreviousRecordID() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

304
        return $this->owner->/** @scrutinizer ignore-call */ getPreviousRecordID();
Loading history...
305
    }
306
307
    /**
308
     * @param DataObject $record
309
     * @return int
310
     */
311
    public function getCustomNextRecordID(DataObject $record)
312
    {
313
314
        // This will overwrite state provided record
315
        if (self::config()->enable_custom_prevnext && $record->hasMethod('NextRecord')) {
316
            return $record->NextRecord()->ID ?? 0;
317
        }
318
        return $this->owner->getNextRecordID();
0 ignored issues
show
Bug introduced by
The method getNextRecordID() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

318
        return $this->owner->/** @scrutinizer ignore-call */ getNextRecordID();
Loading history...
319
    }
320
321
    /**
322
     * @param FieldList $actions
323
     * @param DataObject $record
324
     * @return void
325
     */
326
    public function addSaveNextAndPrevious(FieldList $actions, DataObject $record)
327
    {
328
        if (!$record->canEdit() || !$record->ID) {
329
            return;
330
        }
331
332
        $MajorActions = $actions->fieldByName('MajorActions');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $MajorActions is correct as $actions->fieldByName('MajorActions') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
333
334
        // If it doesn't exist, push to default group
335
        if (!$MajorActions) {
0 ignored issues
show
introduced by
$MajorActions is of type null, thus it always evaluated to false.
Loading history...
336
            $MajorActions = $actions;
337
        }
338
339
        // TODO: state is having a hard time on post
340
        // @link https://github.com/silverstripe/silverstripe-framework/issues/10742
341
        $getPreviousRecordID = $this->getCustomPreviousRecordID($record);
342
        $getNextRecordID = $this->getCustomNextRecordID($record);
343
344
        // Somehow grid state is sometimes lost, therefore we store prev/next ourselves
345
        // TODO: this is a really ugly hack, but at least it works :-) find a better solution later
346
        $class = get_class($record);
347
        $actions->push(new HiddenField("_cmsactions[prev][$class]", null, $getPreviousRecordID));
348
        $actions->push(new HiddenField("_cmsactions[next][$class]", null, $getNextRecordID));
349
350
        // Coupling for HasPrevNextUtils
351
        if (Controller::has_curr()) {
352
            /** @var HTTPRequest $request */
353
            $request = Controller::curr()->getRequest();
354
            $routeParams = $request->routeParams();
355
            $routeParams['PreviousRecordID'] = $getPreviousRecordID;
356
            $routeParams['NextRecordID'] = $getNextRecordID;
357
            $request->setRouteParams($routeParams);
358
        }
359
360
        if ($getPreviousRecordID) {
361
            $doSaveAndPrev = new FormAction('doSaveAndPrev', _t('ActionsGridFieldItemRequest.SAVEANDPREVIOUS', 'Save and Previous'));
362
            $doSaveAndPrev->addExtraClass($this->getBtnClassForRecord($record));
363
            $doSaveAndPrev->addExtraClass('font-icon-angle-double-left btn-mobile-collapse');
364
            $doSaveAndPrev->setUseButtonTag(true);
365
            $MajorActions->push($doSaveAndPrev);
366
        }
367
        if ($getNextRecordID) {
368
            $doSaveAndNext = new FormAction('doSaveAndNext', _t('ActionsGridFieldItemRequest.SAVEANDNEXT', 'Save and Next'));
369
            $doSaveAndNext->addExtraClass($this->getBtnClassForRecord($record));
370
            $doSaveAndNext->addExtraClass('font-icon-angle-double-right btn-mobile-collapse');
371
            $doSaveAndNext->setUseButtonTag(true);
372
            $MajorActions->push($doSaveAndNext);
373
        }
374
    }
375
376
    /**
377
     * @param FieldList $actions
378
     * @param DataObject $record
379
     * @return void
380
     */
381
    public function addSaveAndClose(FieldList $actions, DataObject $record)
382
    {
383
        if (!$this->checkCan($record)) {
384
            return;
385
        }
386
387
        $MajorActions = $actions->fieldByName('MajorActions');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $MajorActions is correct as $actions->fieldByName('MajorActions') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
388
389
        // If it doesn't exist, push to default group
390
        if (!$MajorActions) {
0 ignored issues
show
introduced by
$MajorActions is of type null, thus it always evaluated to false.
Loading history...
391
            $MajorActions = $actions;
392
        }
393
394
        if ($record->ID) {
395
            $label = _t('ActionsGridFieldItemRequest.SAVEANDCLOSE', 'Save and Close');
396
        } else {
397
            $label = _t('ActionsGridFieldItemRequest.CREATEANDCLOSE', 'Create and Close');
398
        }
399
        $saveAndClose = new FormAction('doSaveAndClose', $label);
400
        $saveAndClose->addExtraClass($this->getBtnClassForRecord($record));
401
        $saveAndClose->setAttribute('data-text-alternate', $label);
402
        if ($record->ID) {
403
            $saveAndClose->setAttribute('data-btn-alternate-add', 'btn-primary');
404
            $saveAndClose->setAttribute('data-btn-alternate-remove', 'btn-outline-primary');
405
        }
406
        $saveAndClose->addExtraClass('font-icon-level-up btn-mobile-collapse');
407
        $saveAndClose->setUseButtonTag(true);
408
        $MajorActions->push($saveAndClose);
409
    }
410
411
    /**
412
     * New and existing records have different classes
413
     *
414
     * @param DataObject $record
415
     * @return string
416
     */
417
    protected function getBtnClassForRecord(DataObject $record)
418
    {
419
        if ($record->ID) {
420
            return 'btn-outline-primary';
421
        }
422
423
        return 'btn-primary';
424
    }
425
426
    /**
427
     * @param $action
428
     * @param $definedActions
429
     * @return mixed|CompositeField|null
430
     */
431
    protected static function findAction($action, $definedActions)
432
    {
433
        $result = null;
434
435
        foreach ($definedActions as $definedAction) {
436
            if (is_a($definedAction, CompositeField::class)) {
437
                $result = self::findAction($action, $definedAction->FieldList());
438
                if ($result) {
439
                    break;
440
                }
441
            }
442
443
            $definedActionName = $definedAction->getName();
444
445
            if ($definedAction->hasMethod('actionName')) {
446
                $definedActionName = $definedAction->actionName();
447
            }
448
            if ($definedActionName === $action) {
449
                $result = $definedAction;
450
                break;
451
            }
452
        }
453
454
        return $result;
455
    }
456
457
    /**
458
     * Forward a given action to a DataObject
459
     *
460
     * Action must be declared in getCMSActions to be called
461
     *
462
     * @param string $action
463
     * @param array $data
464
     * @param Form $form
465
     * @return HTTPResponse|DBHTMLText|string
466
     * @throws HTTPResponse_Exception
467
     */
468
    protected function forwardActionToRecord($action, $data = [], $form = null)
469
    {
470
        $controller = $this->getToplevelController();
471
472
        // We have an item request or a controller that can provide a record
473
        $record = null;
474
        if ($this->owner->hasMethod('ItemEditForm')) {
475
            // It's a request handler. Don't check for a specific class as it may be subclassed
476
            $record = $this->owner->record;
0 ignored issues
show
Bug Best Practice introduced by
The property record does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Did you maybe forget to declare it?
Loading history...
477
        } elseif ($controller->hasMethod('save_siteconfig')) {
478
            // Check for any type of siteconfig controller
479
            $record = SiteConfig::current_site_config();
480
        } elseif (!empty($data['ClassName']) && !empty($data['ID'])) {
481
            $record = DataObject::get_by_id($data['ClassName'], $data['ID']);
482
        } elseif ($controller->hasMethod("getRecord")) {
483
            $record = $controller->getRecord();
484
        }
485
486
        if (!$record) {
487
            throw new Exception("No record to handle the action $action on " . get_class($controller));
488
        }
489
        $definedActions = $record->getCMSActions();
490
        // Check if the action is indeed available
491
        $clickedAction = null;
492
        if (!empty($definedActions)) {
493
            $clickedAction = self::findAction($action, $definedActions);
494
        }
495
        if (!$clickedAction) {
0 ignored issues
show
introduced by
$clickedAction is of type mixed|null, thus it always evaluated to false.
Loading history...
496
            $class = get_class($record);
497
            $availableActions = implode(',', $this->getAvailableActions($definedActions));
498
            if (!$availableActions) {
499
                $availableActions = "(no available actions, please check getCMSActions)";
500
            }
501
502
            return $this->owner->httpError(403, sprintf(
0 ignored issues
show
Bug introduced by
The method httpError() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

502
            return $this->owner->/** @scrutinizer ignore-call */ httpError(403, sprintf(
Loading history...
503
                'Action not available on %s. It must be one of : %s',
504
                $class,
505
                $availableActions
506
            ));
507
        }
508
        $message = null;
509
        $error = false;
510
511
        // Check record BEFORE the action
512
        // It can be deleted by the action, and it will return to the list
513
        $isNewRecord = $record->ID === 0;
514
515
        try {
516
            $result = $record->$action($data, $form, $controller);
517
518
            // We have a response
519
            if ($result instanceof HTTPResponse) {
520
                return $result;
521
            }
522
523
            if ($result === false) {
524
                // Result returned an error (false)
525
                $error = true;
526
                $message = _t(
527
                    'ActionsGridFieldItemRequest.FAILED',
528
                    'Action {action} failed on {name}',
529
                    ['action' => $clickedAction->getTitle(), 'name' => $record->i18n_singular_name()]
530
                );
531
            } elseif (is_string($result)) {
532
                // Result is a message
533
                $message = $result;
534
            }
535
        } catch (Exception $ex) {
536
            $error = true;
537
            $message = $ex->getMessage();
538
        }
539
540
        // Build default message
541
        if (!$message) {
542
            $message = _t(
543
                'ActionsGridFieldItemRequest.DONE',
544
                'Action {action} was done on {name}',
545
                ['action' => $clickedAction->getTitle(), 'name' => $record->i18n_singular_name()]
546
            );
547
        }
548
        $status = 'good';
549
        if ($error) {
550
            $status = 'bad';
551
        }
552
553
        // Progressive actions return array with json data
554
        if (method_exists($clickedAction, 'getProgressive') && $clickedAction->getProgressive()) {
555
            $response = $controller->getResponse();
556
            $response->addHeader('Content-Type', 'application/json');
557
            if ($result) {
558
                $response->setBody(json_encode($result));
559
            }
560
561
            return $response;
562
        }
563
564
        // We don't have a form, simply return the result
565
        if (!$form) {
566
            if ($error) {
567
                return $this->owner->httpError(403, $message);
568
            }
569
570
            return $message;
571
        }
572
573
        if (Director::is_ajax()) {
574
            $controller->getResponse()->addHeader('X-Status', rawurlencode($message));
575
            if (method_exists($clickedAction, 'getShouldRefresh') && $clickedAction->getShouldRefresh()) {
576
                $controller->getResponse()->addHeader('X-Reload', "true");
577
            }
578
            // 4xx status makes a red box
579
            if ($error) {
580
                $controller->getResponse()->setStatusCode(400);
581
            }
582
        } else {
583
            // If the controller support sessionMessage, use it instead of form
584
            if ($controller->hasMethod('sessionMessage')) {
585
                $controller->sessionMessage($message, $status, ValidationResult::CAST_HTML);
586
            } else {
587
                $form->sessionMessage($message, $status, ValidationResult::CAST_HTML);
588
            }
589
        }
590
591
        // Custom redirect
592
        if (method_exists($clickedAction, 'getRedirectURL') && $clickedAction->getRedirectURL()) {
593
            $controller->getResponse()->addHeader('X-Reload', "true"); // we probably need a full ui refresh
594
            return $controller->redirect($clickedAction->getRedirectURL());
595
        }
596
597
        // Redirect after action
598
        return $this->redirectAfterAction($isNewRecord, $record);
599
    }
600
601
    /**
602
     * Handles custom links
603
     *
604
     * Use CustomLink with default behaviour to trigger this
605
     *
606
     * See:
607
     * DefaultLink::getModelLink
608
     * GridFieldCustomLink::getLink
609
     *
610
     * @param HTTPRequest $request
611
     * @return HTTPResponse|DBHTMLText|string
612
     * @throws Exception
613
     */
614
    public function doCustomLink(HTTPRequest $request)
615
    {
616
        $action = $request->getVar('CustomLink');
617
        return $this->forwardActionToRecord($action);
618
    }
619
620
    /**
621
     * Handles custom actions
622
     *
623
     * Use CustomAction class to trigger this
624
     *
625
     * Nested actions are submitted like this
626
     * [action_doCustomAction] => Array
627
     * (
628
     *   [doTestAction] => 1
629
     * )
630
     *
631
     * @param array $data The form data
632
     * @param Form $form The form object
633
     * @return HTTPResponse|DBHTMLText|string
634
     * @throws Exception
635
     */
636
    public function doCustomAction($data, $form)
637
    {
638
        $action = key($data['action_doCustomAction']);
639
640
        return $this->forwardActionToRecord($action, $data, $form);
641
    }
642
643
    /**
644
     * Saves the form and goes back to list view
645
     *
646
     * @param array $data The form data
647
     * @param Form $form The form object
648
     */
649
    public function doSaveAndClose($data, $form)
650
    {
651
        $this->owner->doSave($data, $form);
0 ignored issues
show
Bug introduced by
The method doSave() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

651
        $this->owner->/** @scrutinizer ignore-call */ 
652
                      doSave($data, $form);
Loading history...
652
        // Redirect after save
653
        $controller = $this->getToplevelController();
654
655
        $link = $this->getBackLink();
656
        $link = $this->addGridState($link, $data);
657
658
        $controller->getResponse()->addHeader("X-Pjax", "Content");
659
660
        // Prevent Already directed to errors
661
        // $controller->getResponse()->addHeader("Location", $link);
662
663
        return $controller->redirect($link);
664
    }
665
666
    /**
667
     * Saves the form and goes back to the next item
668
     *
669
     * @param array $data The form data
670
     * @param Form $form The form object
671
     */
672
    public function doSaveAndNext($data, $form)
673
    {
674
        $record = $this->owner->record;
0 ignored issues
show
Bug Best Practice introduced by
The property record does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Did you maybe forget to declare it?
Loading history...
675
        $this->owner->doSave($data, $form);
676
        // Redirect after save
677
        $controller = $this->getToplevelController();
678
        $controller->getResponse()->addHeader("X-Pjax", "Content");
679
680
        $class = get_class($record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

680
        $class = get_class(/** @scrutinizer ignore-type */ $record);
Loading history...
681
        $getNextRecordID = $data['_cmsactions']['next'][$class] ?? $this->getCustomNextRecordID($record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type null; however, parameter $record of LeKoala\CmsActions\Actio...getCustomNextRecordID() does only seem to accept SilverStripe\ORM\DataObject, maybe add an additional type check? ( Ignorable by Annotation )

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

681
        $getNextRecordID = $data['_cmsactions']['next'][$class] ?? $this->getCustomNextRecordID(/** @scrutinizer ignore-type */ $record);
Loading history...
682
        $class = get_class($record);
683
        $next = $class::get()->byID($getNextRecordID);
684
685
        $link = $this->owner->getEditLink($getNextRecordID);
0 ignored issues
show
Bug introduced by
The method getEditLink() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

685
        /** @scrutinizer ignore-call */ 
686
        $link = $this->owner->getEditLink($getNextRecordID);
Loading history...
686
        $link = $this->addGridState($link, $data);
687
688
        // Link to a specific tab if set, see cms-actions.js
689
        if ($next && !empty($data['_activetab'])) {
690
            $link .= sprintf('#%s', $data['_activetab']);
691
        }
692
693
        // Prevent Already directed to errors
694
        // $controller->getResponse()->addHeader("Location", $link);
695
696
        return $controller->redirect($link);
697
    }
698
699
    /**
700
     * Saves the form and goes to the previous item
701
     *
702
     * @param array $data The form data
703
     * @param Form $form The form object
704
     */
705
    public function doSaveAndPrev($data, $form)
706
    {
707
        $record = $this->owner->record;
0 ignored issues
show
Bug Best Practice introduced by
The property record does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Did you maybe forget to declare it?
Loading history...
708
        $this->owner->doSave($data, $form);
709
        // Redirect after save
710
        $controller = $this->getToplevelController();
711
        $controller->getResponse()->addHeader("X-Pjax", "Content");
712
713
        $class = get_class($record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

713
        $class = get_class(/** @scrutinizer ignore-type */ $record);
Loading history...
714
        $getPreviousRecordID = $data['_cmsactions']['prev'][$class] ?? $this->getPreviousRecordID($record);
715
        $class = get_class($record);
716
        $prev = $class::get()->byID($getPreviousRecordID);
717
718
        $link = $this->owner->getEditLink($getPreviousRecordID);
719
        $link = $this->addGridState($link, $data);
720
721
        // Link to a specific tab if set, see cms-actions.js
722
        if ($prev && !empty($data['_activetab'])) {
723
            $link .= sprintf('#%s', $data['_activetab']);
724
        }
725
726
        // Prevent Already directed to errors
727
        // $controller->getResponse()->addHeader("Location", $link);
728
729
        return $controller->redirect($link);
730
    }
731
732
    protected function addGridState($url, $data)
733
    {
734
        // This should not be necessary at all if the state is correctly passed along
735
        $BackURL = $data['BackURL'] ?? null;
736
        if ($BackURL) {
737
            $query = parse_url($BackURL, PHP_URL_QUERY);
738
            if ($query) {
739
                $url = strtok($url, '?');
740
                $url .= '?' . $query;
741
            }
742
        }
743
        return $url;
744
    }
745
746
    /**
747
     * Gets the top level controller.
748
     *
749
     * @return Controller
750
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
751
     * because it is a protected method and not visible to a decorator!
752
     */
753
    protected function getToplevelController()
754
    {
755
        if ($this->isLeftAndMain($this->owner)) {
756
            return $this->owner;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->owner also could return the type LeKoala\CmsActions\Actio...dDetailForm_ItemRequest which is incompatible with the documented return type SilverStripe\Control\Controller.
Loading history...
757
        }
758
        if (!$this->owner->hasMethod("getController")) {
759
            return Controller::curr();
760
        }
761
        $controller = $this->owner->getController();
0 ignored issues
show
Bug introduced by
The method getController() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

761
        /** @scrutinizer ignore-call */ 
762
        $controller = $this->owner->getController();
Loading history...
762
        while ($controller instanceof GridFieldDetailForm_ItemRequest) {
763
            $controller = $controller->getController();
764
        }
765
766
        return $controller;
767
    }
768
769
    protected function isLeftAndMain($controller)
770
    {
771
        return is_subclass_of($controller, LeftAndMain::class);
772
    }
773
774
    /**
775
     * Gets the back link
776
     *
777
     * @return string
778
     * @todo This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
779
     * because it is a protected method and not visible to a decorator!
780
     */
781
    public function getBackLink()
782
    {
783
        $backlink = '';
784
        $toplevelController = $this->getToplevelController();
785
        // Check for LeftAndMain and alike controllers with a Backlink or Breadcrumbs methods
786
        if ($toplevelController->hasMethod('Backlink')) {
787
            $backlink = $toplevelController->Backlink();
788
        } elseif ($this->owner->getController()->hasMethod('Breadcrumbs')) {
789
            $parents = $this->owner->getController()->Breadcrumbs(false)->items;
790
            $backlink = array_pop($parents)->Link;
791
        }
792
        if (!$backlink) {
793
            $backlink = $toplevelController->Link();
794
        }
795
796
        return $backlink;
797
    }
798
799
    /**
800
     * Response object for this request after a successful save
801
     *
802
     * @param bool $isNewRecord True if this record was just created
803
     * @param DataObject $record
804
     * @return HTTPResponse|DBHTMLText|string
805
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
806
     * because it is a protected method and not visible to a decorator!
807
     */
808
    protected function redirectAfterAction($isNewRecord, $record = null)
809
    {
810
        $controller = $this->getToplevelController();
811
812
        if ($this->isLeftAndMain($controller)) {
813
            // CMSMain => redirect to show
814
            if ($this->owner->hasMethod("LinkPageEdit")) {
815
                return $controller->redirect($this->owner->LinkPageEdit($record->ID));
0 ignored issues
show
Bug introduced by
The method LinkPageEdit() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

815
                return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ LinkPageEdit($record->ID));
Loading history...
816
            }
817
        }
818
819
        if ($isNewRecord) {
820
            return $controller->redirect($this->owner->Link());
0 ignored issues
show
Bug introduced by
The method Link() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

820
            return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ Link());
Loading history...
821
        }
822
        if ($this->owner->gridField && $this->owner->gridField->getList()->byID($this->owner->record->ID)) {
0 ignored issues
show
Bug introduced by
The method byID() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Limitable. Are you sure you never get one of those? ( Ignorable by Annotation )

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

822
        if ($this->owner->gridField && $this->owner->gridField->getList()->/** @scrutinizer ignore-call */ byID($this->owner->record->ID)) {
Loading history...
Bug Best Practice introduced by
The property gridField does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Did you maybe forget to declare it?
Loading history...
Bug Best Practice introduced by
The property record does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Did you maybe forget to declare it?
Loading history...
823
            // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
824
            // to the same URL (it assumes that its content is already current, and doesn't reload)
825
            return $this->owner->edit($controller->getRequest());
0 ignored issues
show
Bug introduced by
The method edit() does not exist on LeKoala\CmsActions\ActionsGridFieldItemRequest. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

825
            return $this->owner->/** @scrutinizer ignore-call */ edit($controller->getRequest());
Loading history...
826
        }
827
        // Changes to the record properties might've excluded the record from
828
        // a filtered list, so return back to the main view if it can't be found
829
        $noActionURL = $url = $controller->getRequest()->getURL();
830
831
        // The controller may not have these
832
        if ($controller->hasMethod('getAction')) {
833
            $action = $controller->getAction();
834
            // Handle GridField detail form editing
835
            if (strpos($url, 'ItemEditForm') !== false) {
836
                $action = 'ItemEditForm';
837
            }
838
            if ($action) {
839
                $noActionURL = $controller->removeAction($url, $action);
840
            }
841
        } else {
842
            // Simple fallback (last index of)
843
            $pos = strrpos($url ?? '', 'ItemEditForm');
844
            $noActionURL = substr($url ?? '', 0, $pos);
845
        }
846
847
        $controller->getRequest()->addHeader('X-Pjax', 'Content');
848
849
        return $controller->redirect($noActionURL, 302);
850
    }
851
}
852