Passed
Push — master ( 0e1598...5733a8 )
by Thomas
11:52 queued 12s
created

getCustomNextRecordID()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

126
        /** @scrutinizer ignore-call */ 
127
        $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...
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

126
        /** @scrutinizer ignore-call */ 
127
        $record = $this->owner->getRecord();
Loading history...
127
128
        // We get the actions as defined on our record
129
        /** @var FieldList $CMSActions */
130
        $CMSActions = $record->getCMSActions();
131
132
        // The default button group that contains the Save or Create action
133
        // @link https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface/#extending-the-cms-actions
134
        $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...
135
136
        // If it doesn't exist, push to default group
137
        if (!$MajorActions) {
0 ignored issues
show
introduced by
$MajorActions is of type null, thus it always evaluated to false.
Loading history...
138
            $MajorActions = $actions;
0 ignored issues
show
Unused Code introduced by
The assignment to $MajorActions is dead and can be removed.
Loading history...
139
        }
140
141
        // Push our actions that are otherwise ignored by SilverStripe
142
        foreach ($CMSActions as $action) {
143
            // Avoid duplicated actions (eg: when added by SilverStripe\Versioned\VersionedGridFieldItemRequest)
144
            if ($actions->fieldByName($action->getName())) {
145
                continue;
146
            }
147
            $actions->push($action);
148
        }
149
150
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
151
        // and we don't want to duplicate it
152
        $this->processDropUpMenu($actions);
153
154
        // Add extension hook
155
        $this->extend('onBeforeUpdateCMSActions', $actions, $record);
156
        $record->extend('onBeforeUpdateCMSActions', $actions);
157
158
        $ActionMenus = $actions->fieldByName('ActionMenus');
159
        // Re-insert ActionMenus to make sure they always follow the buttons
160
        if ($ActionMenus) {
161
            $actions->remove($ActionMenus);
162
            $actions->push($ActionMenus);
163
        }
164
165
        // We have a 4.4 setup, before that there was no RightGroup
166
        $RightGroup = $actions->fieldByName('RightGroup');
167
168
        // Insert again to make sure our actions are properly placed after apply changes
169
        if ($RightGroup) {
170
            $actions->remove($RightGroup);
171
            $actions->push($RightGroup);
172
        }
173
174
        $opts = [
175
            'save_close'     => self::config()->enable_save_close,
176
            'save_prev_next' => self::config()->enable_save_prev_next,
177
            'delete_right'   => self::config()->enable_delete_right,
178
        ];
179
        if ($record->hasMethod('getCMSActionsOptions')) {
180
            $opts = array_merge($opts, $record->getCMSActionsOptions());
181
        }
182
183
        if ($opts['save_close']) {
184
            $this->addSaveAndClose($actions, $record);
185
        }
186
187
        if ($opts['save_prev_next']) {
188
            $this->addSaveNextAndPrevious($actions, $record);
189
        }
190
191
        if ($opts['delete_right']) {
192
            $this->moveCancelAndDelete($actions, $record);
193
        }
194
195
        // Add extension hook
196
        $this->extend('onAfterUpdateCMSActions', $actions, $record);
197
        $record->extend('onAfterUpdateCMSActions', $actions);
198
    }
199
200
    /**
201
     * Collect all Drop-Up actions into a menu.
202
     * @param FieldList $actions
203
     * @return void
204
     */
205
    protected function processDropUpMenu($actions)
206
    {
207
        // The Drop-up container may already exist
208
        /** @var ?Tab $dropUpContainer */
209
        $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...
210
        foreach ($actions as $action) {
211
            //@phpstan-ignore-next-line
212
            if ($action->hasMethod('getDropUp') && $action->getDropUp()) {
213
                if (!$dropUpContainer) {
214
                    $dropUpContainer = $this->createDropUpContainer($actions);
215
                }
216
                $action->getContainerFieldList()->removeByName($action->getName());
217
                $dropUpContainer->push($action);
218
            }
219
        }
220
    }
221
222
    /**
223
     * Prepares a Drop-Up menu
224
     * @param FieldList $actions
225
     * @return Tab
226
     */
227
    protected function createDropUpContainer($actions)
228
    {
229
        $rootTabSet = new TabSet('ActionMenus');
230
        $dropUpContainer = new Tab(
231
            'MoreOptions',
232
            _t(__CLASS__ . '.MoreOptions', 'More options', 'Expands a view for more buttons')
233
        );
234
        $dropUpContainer->addExtraClass('popover-actions-simulate');
235
        $rootTabSet->push($dropUpContainer);
236
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
237
238
        $actions->insertBefore('RightGroup', $rootTabSet);
239
240
        return $dropUpContainer;
241
    }
242
243
    /**
244
     * Check if a record can be edited/created/exists
245
     * @param DataObject $record
246
     * @return bool
247
     */
248
    protected function checkCan($record)
249
    {
250
        if (!$record->canEdit() || (!$record->ID && !$record->canCreate())) {
251
            return false;
252
        }
253
254
        return true;
255
    }
256
257
    /**
258
     * @param FieldList $actions
259
     * @param DataObject $record
260
     * @return void
261
     */
262
    public function moveCancelAndDelete(FieldList $actions, DataObject $record)
263
    {
264
        // We have a 4.4 setup, before that there was no RightGroup
265
        $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...
266
267
        // Move delete at the end
268
        $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...
269
        if ($deleteAction) {
0 ignored issues
show
introduced by
$deleteAction is of type null, thus it always evaluated to false.
Loading history...
270
            // Move at the end of the stack
271
            $actions->remove($deleteAction);
272
            $actions->push($deleteAction);
273
274
            if (!$RightGroup) {
275
                // Only necessary pre 4.4
276
                $deleteAction->addExtraClass('align-right');
277
            }
278
            // Set custom title
279
            if ($record->hasMethod('getDeleteButtonTitle')) {
280
                //@phpstan-ignore-next-line
281
                $deleteAction->setTitle($record->getDeleteButtonTitle());
282
            }
283
        }
284
        // Move cancel at the end
285
        $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...
286
        if ($cancelButton) {
0 ignored issues
show
introduced by
$cancelButton is of type null, thus it always evaluated to false.
Loading history...
287
            // Move at the end of the stack
288
            $actions->remove($cancelButton);
289
            $actions->push($cancelButton);
290
            if (!$RightGroup) {
291
                // Only necessary pre 4.4
292
                $cancelButton->addExtraClass('align-right');
293
            }
294
            // Set custom titlte
295
            if ($record->hasMethod('getCancelButtonTitle')) {
296
                //@phpstan-ignore-next-line
297
                $cancelButton->setTitle($record->getCancelButtonTitle());
298
            }
299
        }
300
    }
301
302
    /**
303
     * @param DataObject $record
304
     * @return int
305
     */
306
    public function getCustomPreviousRecordID(DataObject $record)
307
    {
308
        // This will overwrite state provided record
309
        if (self::config()->enable_custom_prevnext && $record->hasMethod('PrevRecord')) {
310
            //@phpstan-ignore-next-line
311
            return $record->PrevRecord()->ID ?? 0;
312
        }
313
        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

313
        return $this->owner->/** @scrutinizer ignore-call */ getPreviousRecordID();
Loading history...
314
    }
315
316
    /**
317
     * @param DataObject $record
318
     * @return int
319
     */
320
    public function getCustomNextRecordID(DataObject $record)
321
    {
322
323
        // This will overwrite state provided record
324
        if (self::config()->enable_custom_prevnext && $record->hasMethod('NextRecord')) {
325
            //@phpstan-ignore-next-line
326
            return $record->NextRecord()->ID ?? 0;
327
        }
328
        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

328
        return $this->owner->/** @scrutinizer ignore-call */ getNextRecordID();
Loading history...
329
    }
330
331
    /**
332
     * @param FieldList $actions
333
     * @return CompositeField|FieldList
334
     */
335
    protected function getMajorActions(FieldList $actions)
336
    {
337
        /** @var ?CompositeField $MajorActions */
338
        $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...
339
340
        // If it doesn't exist, push to default group
341
        if (!$MajorActions) {
0 ignored issues
show
introduced by
$MajorActions is of type null, thus it always evaluated to false.
Loading history...
342
            $MajorActions = $actions;
343
        }
344
        return $MajorActions;
345
    }
346
347
    /**
348
     * @param FieldList $actions
349
     * @param DataObject $record
350
     * @return void
351
     */
352
    public function addSaveNextAndPrevious(FieldList $actions, DataObject $record)
353
    {
354
        if (!$record->canEdit() || !$record->ID) {
355
            return;
356
        }
357
358
        $MajorActions = $this->getMajorActions($actions);
359
360
        // TODO: state is having a hard time on post
361
        // @link https://github.com/silverstripe/silverstripe-framework/issues/10742
362
        $getPreviousRecordID = $this->getCustomPreviousRecordID($record);
363
        $getNextRecordID = $this->getCustomNextRecordID($record);
364
365
        // Somehow grid state is sometimes lost, therefore we store prev/next ourselves
366
        // TODO: this is a really ugly hack, but at least it works :-) find a better solution later
367
        $class = get_class($record);
368
        $actions->push(new HiddenField("_cmsactions[prev][$class]", null, $getPreviousRecordID));
369
        $actions->push(new HiddenField("_cmsactions[next][$class]", null, $getNextRecordID));
370
371
        // Coupling for HasPrevNextUtils
372
        if (Controller::has_curr()) {
373
            /** @var HTTPRequest $request */
374
            $request = Controller::curr()->getRequest();
375
            $routeParams = $request->routeParams();
376
            $routeParams['PreviousRecordID'] = $getPreviousRecordID;
377
            $routeParams['NextRecordID'] = $getNextRecordID;
378
            $request->setRouteParams($routeParams);
379
        }
380
381
        if ($getPreviousRecordID) {
382
            $doSaveAndPrev = new FormAction('doSaveAndPrev', _t('ActionsGridFieldItemRequest.SAVEANDPREVIOUS', 'Save and Previous'));
383
            $doSaveAndPrev->addExtraClass($this->getBtnClassForRecord($record));
384
            $doSaveAndPrev->addExtraClass('font-icon-angle-double-left btn-mobile-collapse');
385
            $doSaveAndPrev->setUseButtonTag(true);
386
            $MajorActions->push($doSaveAndPrev);
387
        }
388
        if ($getNextRecordID) {
389
            $doSaveAndNext = new FormAction('doSaveAndNext', _t('ActionsGridFieldItemRequest.SAVEANDNEXT', 'Save and Next'));
390
            $doSaveAndNext->addExtraClass($this->getBtnClassForRecord($record));
391
            $doSaveAndNext->addExtraClass('font-icon-angle-double-right btn-mobile-collapse');
392
            $doSaveAndNext->setUseButtonTag(true);
393
            $MajorActions->push($doSaveAndNext);
394
        }
395
    }
396
397
    /**
398
     * @param FieldList $actions
399
     * @param DataObject $record
400
     * @return void
401
     */
402
    public function addSaveAndClose(FieldList $actions, DataObject $record)
403
    {
404
        if (!$this->checkCan($record)) {
405
            return;
406
        }
407
408
        $MajorActions = $this->getMajorActions($actions);
409
410
        if ($record->ID) {
411
            $label = _t('ActionsGridFieldItemRequest.SAVEANDCLOSE', 'Save and Close');
412
        } else {
413
            $label = _t('ActionsGridFieldItemRequest.CREATEANDCLOSE', 'Create and Close');
414
        }
415
        $saveAndClose = new FormAction('doSaveAndClose', $label);
416
        $saveAndClose->addExtraClass($this->getBtnClassForRecord($record));
417
        $saveAndClose->setAttribute('data-text-alternate', $label);
418
        if ($record->ID) {
419
            $saveAndClose->setAttribute('data-btn-alternate-add', 'btn-primary');
420
            $saveAndClose->setAttribute('data-btn-alternate-remove', 'btn-outline-primary');
421
        }
422
        $saveAndClose->addExtraClass('font-icon-level-up btn-mobile-collapse');
423
        $saveAndClose->setUseButtonTag(true);
424
        $MajorActions->push($saveAndClose);
425
    }
426
427
    /**
428
     * New and existing records have different classes
429
     *
430
     * @param DataObject $record
431
     * @return string
432
     */
433
    protected function getBtnClassForRecord(DataObject $record)
434
    {
435
        if ($record->ID) {
436
            return 'btn-outline-primary';
437
        }
438
        return 'btn-primary';
439
    }
440
441
    /**
442
     * @param string $action
443
     * @param array<FormField>|FieldList $definedActions
444
     * @return mixed|CompositeField|null
445
     */
446
    protected static function findAction($action, $definedActions)
447
    {
448
        $result = null;
449
450
        foreach ($definedActions as $definedAction) {
451
            if (is_a($definedAction, CompositeField::class)) {
452
                $result = self::findAction($action, $definedAction->FieldList());
453
                if ($result) {
454
                    break;
455
                }
456
            }
457
458
            $definedActionName = $definedAction->getName();
459
460
            if ($definedAction->hasMethod('actionName')) {
461
                //@phpstan-ignore-next-line
462
                $definedActionName = $definedAction->actionName();
463
            }
464
            if ($definedActionName === $action) {
465
                $result = $definedAction;
466
                break;
467
            }
468
        }
469
470
        return $result;
471
    }
472
473
    /**
474
     * Forward a given action to a DataObject
475
     *
476
     * Action must be declared in getCMSActions to be called
477
     *
478
     * @param string $action
479
     * @param array<string,mixed> $data
480
     * @param Form $form
481
     * @return HTTPResponse|DBHTMLText|string
482
     * @throws HTTPResponse_Exception
483
     */
484
    protected function forwardActionToRecord($action, $data = [], $form = null)
485
    {
486
        $controller = $this->getToplevelController();
487
488
        // We have an item request or a controller that can provide a record
489
        $record = null;
490
        if ($this->owner->hasMethod('ItemEditForm')) {
491
            // It's a request handler. Don't check for a specific class as it may be subclassed
492
            $record = $this->owner->record;
493
        } elseif ($controller->hasMethod('save_siteconfig')) {
494
            // Check for any type of siteconfig controller
495
            $record = SiteConfig::current_site_config();
496
        } elseif (!empty($data['ClassName']) && !empty($data['ID'])) {
497
            $record = DataObject::get_by_id($data['ClassName'], $data['ID']);
498
        } elseif ($controller->hasMethod("getRecord")) {
499
            //@phpstan-ignore-next-line
500
            $record = $controller->getRecord();
501
        }
502
503
        if (!$record) {
504
            throw new Exception("No record to handle the action $action on " . get_class($controller));
505
        }
506
        $definedActions = $record->getCMSActions();
507
        // Check if the action is indeed available
508
        $clickedAction = null;
509
        if (!empty($definedActions)) {
510
            $clickedAction = self::findAction($action, $definedActions);
511
        }
512
        if (!$clickedAction) {
0 ignored issues
show
introduced by
$clickedAction is of type mixed|null, thus it always evaluated to false.
Loading history...
513
            $class = get_class($record);
514
            $availableActions = implode(',', $this->getAvailableActions($definedActions));
515
            if (!$availableActions) {
516
                $availableActions = "(no available actions, please check getCMSActions)";
517
            }
518
519
            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

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

678
        $this->owner->/** @scrutinizer ignore-call */ 
679
                      doSave($data, $form);
Loading history...
679
        // Redirect after save
680
        $controller = $this->getToplevelController();
681
682
        $link = $this->getBackLink();
683
        $link = $this->addGridState($link, $data);
684
685
        $controller->getResponse()->addHeader("X-Pjax", "Content");
686
687
        // Prevent Already directed to errors
688
        // $controller->getResponse()->addHeader("Location", $link);
689
690
        return $controller->redirect($link);
691
    }
692
693
    /**
694
     * Saves the form and goes back to the next item
695
     *
696
     * @param array<string,mixed> $data The form data
697
     * @param Form $form The form object
698
     * @return HTTPResponse
699
     */
700
    public function doSaveAndNext($data, $form)
701
    {
702
        $record = $this->owner->record;
703
        $this->owner->doSave($data, $form);
704
        // Redirect after save
705
        $controller = $this->getToplevelController();
706
        $controller->getResponse()->addHeader("X-Pjax", "Content");
707
708
        $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

708
        $class = get_class(/** @scrutinizer ignore-type */ $record);
Loading history...
709
        $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

709
        $getNextRecordID = $data['_cmsactions']['next'][$class] ?? $this->getCustomNextRecordID(/** @scrutinizer ignore-type */ $record);
Loading history...
710
        $class = get_class($record);
711
        if (!$class) {
712
            throw new Exception("Could not get class");
713
        }
714
        /** @var ?DataObject $next */
715
        $next = $class::get()->byID($getNextRecordID);
716
717
        $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

717
        /** @scrutinizer ignore-call */ 
718
        $link = $this->owner->getEditLink($getNextRecordID);
Loading history...
718
        $link = $this->addGridState($link, $data);
719
720
        // Link to a specific tab if set, see cms-actions.js
721
        if ($next && !empty($data['_activetab'])) {
722
            $link .= sprintf('#%s', $data['_activetab']);
723
        }
724
725
        // Prevent Already directed to errors
726
        // $controller->getResponse()->addHeader("Location", $link);
727
728
        return $controller->redirect($link);
729
    }
730
731
    /**
732
     * Saves the form and goes to the previous item
733
     *
734
     * @param array<string,mixed> $data The form data
735
     * @param Form $form The form object
736
     * @return HTTPResponse
737
     */
738
    public function doSaveAndPrev($data, $form)
739
    {
740
        $record = $this->owner->record;
741
        $this->owner->doSave($data, $form);
742
        // Redirect after save
743
        $controller = $this->getToplevelController();
744
        $controller->getResponse()->addHeader("X-Pjax", "Content");
745
746
        $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

746
        $class = get_class(/** @scrutinizer ignore-type */ $record);
Loading history...
747
        $getPreviousRecordID = $data['_cmsactions']['prev'][$class] ?? $this->getCustomPreviousRecordID($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...ustomPreviousRecordID() 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

747
        $getPreviousRecordID = $data['_cmsactions']['prev'][$class] ?? $this->getCustomPreviousRecordID(/** @scrutinizer ignore-type */ $record);
Loading history...
748
        $class = get_class($record);
749
        if (!$class) {
750
            throw new Exception("Could not get class");
751
        }
752
753
        /** @var ?DataObject $prev */
754
        $prev = $class::get()->byID($getPreviousRecordID);
755
756
        $link = $this->owner->getEditLink($getPreviousRecordID);
757
        $link = $this->addGridState($link, $data);
758
759
        // Link to a specific tab if set, see cms-actions.js
760
        if ($prev && !empty($data['_activetab'])) {
761
            $link .= sprintf('#%s', $data['_activetab']);
762
        }
763
764
        // Prevent Already directed to errors
765
        // $controller->getResponse()->addHeader("Location", $link);
766
767
        return $controller->redirect($link);
768
    }
769
770
    /**
771
     * @param string $url
772
     * @param array<mixed> $data
773
     * @return string
774
     */
775
    protected function addGridState($url, $data)
776
    {
777
        // This should not be necessary at all if the state is correctly passed along
778
        $BackURL = $data['BackURL'] ?? null;
779
        if ($BackURL) {
780
            $query = parse_url($BackURL, PHP_URL_QUERY);
781
            if ($query) {
782
                $url = strtok($url, '?');
783
                $url .= '?' . $query;
784
            }
785
        }
786
        return $url;
787
    }
788
789
    /**
790
     * Gets the top level controller.
791
     *
792
     * @return Controller
793
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
794
     * because it is a protected method and not visible to a decorator!
795
     */
796
    protected function getToplevelController()
797
    {
798
        if ($this->isLeftAndMain($this->owner)) {
799
            return $this->owner;
800
        }
801
        if (!$this->owner->hasMethod("getController")) {
802
            return Controller::curr();
803
        }
804
        $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

804
        /** @scrutinizer ignore-call */ 
805
        $controller = $this->owner->getController();
Loading history...
805
        while ($controller instanceof GridFieldDetailForm_ItemRequest) {
806
            $controller = $controller->getController();
807
        }
808
809
        return $controller;
810
    }
811
812
    /**
813
     * @param Controller $controller
814
     * @return boolean
815
     */
816
    protected function isLeftAndMain($controller)
817
    {
818
        return is_subclass_of($controller, LeftAndMain::class);
819
    }
820
821
    /**
822
     * Gets the back link
823
     *
824
     * @return string
825
     */
826
    public function getBackLink()
827
    {
828
        $backlink = '';
829
        $toplevelController = $this->getToplevelController();
830
        // Check for LeftAndMain and alike controllers with a Backlink or Breadcrumbs methods
831
        if ($toplevelController->hasMethod('Backlink')) {
832
            //@phpstan-ignore-next-line
833
            $backlink = $toplevelController->Backlink();
834
        } elseif ($this->owner->getController()->hasMethod('Breadcrumbs')) {
835
            $parents = $this->owner->getController()->Breadcrumbs(false)->items;
836
            $backlink = array_pop($parents)->Link;
837
        }
838
        if (!$backlink) {
839
            $backlink = $toplevelController->Link();
840
        }
841
842
        return $backlink;
843
    }
844
845
    /**
846
     * Response object for this request after a successful save
847
     *
848
     * @param bool $isNewRecord True if this record was just created
849
     * @param DataObject $record
850
     * @return HTTPResponse|DBHTMLText|string
851
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
852
     * because it is a protected method and not visible to a decorator!
853
     */
854
    protected function redirectAfterAction($isNewRecord, $record = null)
855
    {
856
        $controller = $this->getToplevelController();
857
858
        if ($this->isLeftAndMain($controller)) {
859
            // CMSMain => redirect to show
860
            if ($this->owner->hasMethod("LinkPageEdit")) {
861
                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

861
                return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ LinkPageEdit($record->ID));
Loading history...
862
            }
863
        }
864
865
        if ($isNewRecord) {
866
            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

866
            return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ Link());
Loading history...
867
        }
868
        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

868
        if ($this->owner->gridField && $this->owner->gridField->getList()->/** @scrutinizer ignore-call */ byID($this->owner->record->ID)) {
Loading history...
869
            // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
870
            // to the same URL (it assumes that its content is already current, and doesn't reload)
871
            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

871
            return $this->owner->/** @scrutinizer ignore-call */ edit($controller->getRequest());
Loading history...
872
        }
873
        // Changes to the record properties might've excluded the record from
874
        // a filtered list, so return back to the main view if it can't be found
875
        $noActionURL = $url = $controller->getRequest()->getURL();
876
        if (!$url) {
877
            $url = '';
878
        }
879
880
        // The controller may not have these
881
        if ($controller->hasMethod('getAction')) {
882
            $action = $controller->getAction();
883
            // Handle GridField detail form editing
884
            if (strpos($url, 'ItemEditForm') !== false) {
885
                $action = 'ItemEditForm';
886
            }
887
            if ($action) {
888
                $noActionURL = $controller->removeAction($url, $action);
889
            }
890
        } else {
891
            // Simple fallback (last index of)
892
            $pos = strrpos($url, 'ItemEditForm');
893
            if (is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
894
                $noActionURL = substr($url, 0, $pos);
895
            }
896
        }
897
898
        $controller->getRequest()->addHeader('X-Pjax', 'Content');
899
        return $controller->redirect($noActionURL, 302);
900
    }
901
}
902