Passed
Push — master ( 130c0f...fc4684 )
by Thomas
02:42
created

ActionsGridFieldItemRequest::getMajorActions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 10
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
     * This module does not interact with the /schema/SearchForm endpoint
102
     * and therefore all requests for these urls don't need any special treatement
103
     *
104
     * @return bool
105
     */
106
    protected function isSearchFormRequest(): bool
107
    {
108
        if (!Controller::has_curr()) {
109
            return false;
110
        }
111
        $curr =  Controller::curr();
112
        if ($curr) {
0 ignored issues
show
introduced by
$curr is of type SilverStripe\Control\Controller, thus it always evaluated to true.
Loading history...
113
            return str_contains($curr->getRequest()->getURL(), '/schema/SearchForm');
114
        }
115
        return false;
116
    }
117
118
    /**
119
     * Called by CMSMain, typically in the CMS or in the SiteConfig admin
120
     * CMSMain already uses getCMSActions so we are good to go with anything defined there
121
     *
122
     * @param Form $form
123
     * @return void
124
     */
125
    public function updateEditForm(Form $form)
126
    {
127
        // Ignore search form requests
128
        if ($this->isSearchFormRequest()) {
129
            return;
130
        }
131
132
        $actions = $form->Actions();
133
134
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
135
        // and we don't want to duplicate it
136
        $this->processDropUpMenu($actions);
137
    }
138
139
    /**
140
     * Called by GridField_ItemRequest
141
     * GridField_ItemRequest defines its own set of actions so we need to add ours
142
     * We add our custom save&close, save&next and other tweaks
143
     * Actions can be made readonly after this extension point
144
     * @param FieldList $actions
145
     * @return void
146
     */
147
    public function updateFormActions($actions)
148
    {
149
        // Ignore search form requests
150
        if ($this->isSearchFormRequest()) {
151
            return;
152
        }
153
154
        $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

154
        /** @scrutinizer ignore-call */ 
155
        $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

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

341
        return $this->owner->/** @scrutinizer ignore-call */ getPreviousRecordID();
Loading history...
342
    }
343
344
    /**
345
     * @param DataObject $record
346
     * @return int
347
     */
348
    public function getCustomNextRecordID(DataObject $record)
349
    {
350
351
        // This will overwrite state provided record
352
        if (self::config()->enable_custom_prevnext && $record->hasMethod('NextRecord')) {
353
            //@phpstan-ignore-next-line
354
            return $record->NextRecord()->ID ?? 0;
355
        }
356
        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

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

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

710
        $this->owner->/** @scrutinizer ignore-call */ 
711
                      doSave($data, $form);
Loading history...
711
        // Redirect after save
712
        $controller = $this->getToplevelController();
713
714
        $link = $this->getBackLink();
715
        $link = $this->addGridState($link, $data);
716
717
        $controller->getResponse()->addHeader("X-Pjax", "Content");
718
719
        // Prevent Already directed to errors
720
        // $controller->getResponse()->addHeader("Location", $link);
721
722
        return $controller->redirect($link);
723
    }
724
725
    /**
726
     * Saves the form and goes back to the next item
727
     *
728
     * @param array<string,mixed> $data The form data
729
     * @param Form $form The form object
730
     * @return HTTPResponse
731
     */
732
    public function doSaveAndNext($data, $form)
733
    {
734
        $record = $this->owner->record;
735
        $this->owner->doSave($data, $form);
736
        // Redirect after save
737
        $controller = $this->getToplevelController();
738
        $controller->getResponse()->addHeader("X-Pjax", "Content");
739
740
        $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

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

741
        $getNextRecordID = $data['_cmsactions']['next'][$class] ?? $this->getCustomNextRecordID(/** @scrutinizer ignore-type */ $record);
Loading history...
742
        $class = get_class($record);
743
        if (!$class) {
744
            throw new Exception("Could not get class");
745
        }
746
        /** @var ?DataObject $next */
747
        $next = $class::get()->byID($getNextRecordID);
748
749
        $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

749
        /** @scrutinizer ignore-call */ 
750
        $link = $this->owner->getEditLink($getNextRecordID);
Loading history...
750
        $link = $this->addGridState($link, $data);
751
752
        // Link to a specific tab if set, see cms-actions.js
753
        if ($next && !empty($data['_activetab'])) {
754
            $link .= sprintf('#%s', $data['_activetab']);
755
        }
756
757
        // Prevent Already directed to errors
758
        // $controller->getResponse()->addHeader("Location", $link);
759
760
        return $controller->redirect($link);
761
    }
762
763
    /**
764
     * Saves the form and goes to the previous item
765
     *
766
     * @param array<string,mixed> $data The form data
767
     * @param Form $form The form object
768
     * @return HTTPResponse
769
     */
770
    public function doSaveAndPrev($data, $form)
771
    {
772
        $record = $this->owner->record;
773
        $this->owner->doSave($data, $form);
774
        // Redirect after save
775
        $controller = $this->getToplevelController();
776
        $controller->getResponse()->addHeader("X-Pjax", "Content");
777
778
        $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

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

779
        $getPreviousRecordID = $data['_cmsactions']['prev'][$class] ?? $this->getCustomPreviousRecordID(/** @scrutinizer ignore-type */ $record);
Loading history...
780
        $class = get_class($record);
781
        if (!$class) {
782
            throw new Exception("Could not get class");
783
        }
784
785
        /** @var ?DataObject $prev */
786
        $prev = $class::get()->byID($getPreviousRecordID);
787
788
        $link = $this->owner->getEditLink($getPreviousRecordID);
789
        $link = $this->addGridState($link, $data);
790
791
        // Link to a specific tab if set, see cms-actions.js
792
        if ($prev && !empty($data['_activetab'])) {
793
            $link .= sprintf('#%s', $data['_activetab']);
794
        }
795
796
        // Prevent Already directed to errors
797
        // $controller->getResponse()->addHeader("Location", $link);
798
799
        return $controller->redirect($link);
800
    }
801
802
    /**
803
     * @param string $url
804
     * @param array<mixed> $data
805
     * @return string
806
     */
807
    protected function addGridState($url, $data)
808
    {
809
        // This should not be necessary at all if the state is correctly passed along
810
        $BackURL = $data['BackURL'] ?? null;
811
        if ($BackURL) {
812
            $query = parse_url($BackURL, PHP_URL_QUERY);
813
            if ($query) {
814
                $url = strtok($url, '?');
815
                $url .= '?' . $query;
816
            }
817
        }
818
        return $url;
819
    }
820
821
    /**
822
     * Gets the top level controller.
823
     *
824
     * @return Controller
825
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
826
     * because it is a protected method and not visible to a decorator!
827
     */
828
    protected function getToplevelController()
829
    {
830
        if ($this->isLeftAndMain($this->owner)) {
831
            return $this->owner;
832
        }
833
        if (!$this->owner->hasMethod("getController")) {
834
            return Controller::curr();
835
        }
836
        $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

836
        /** @scrutinizer ignore-call */ 
837
        $controller = $this->owner->getController();
Loading history...
837
        while ($controller instanceof GridFieldDetailForm_ItemRequest) {
838
            $controller = $controller->getController();
839
        }
840
841
        return $controller;
842
    }
843
844
    /**
845
     * @param Controller $controller
846
     * @return boolean
847
     */
848
    protected function isLeftAndMain($controller)
849
    {
850
        return is_subclass_of($controller, LeftAndMain::class);
851
    }
852
853
    /**
854
     * Gets the back link
855
     *
856
     * @return string
857
     */
858
    public function getBackLink()
859
    {
860
        $backlink = '';
861
        $toplevelController = $this->getToplevelController();
862
        // Check for LeftAndMain and alike controllers with a Backlink or Breadcrumbs methods
863
        if ($toplevelController->hasMethod('Backlink')) {
864
            //@phpstan-ignore-next-line
865
            $backlink = $toplevelController->Backlink();
866
        } elseif ($this->owner->getController()->hasMethod('Breadcrumbs')) {
867
            $parents = $this->owner->getController()->Breadcrumbs(false)->items;
868
            $backlink = array_pop($parents)->Link;
869
        }
870
        if (!$backlink) {
871
            $backlink = $toplevelController->Link();
872
        }
873
874
        return $backlink;
875
    }
876
877
    /**
878
     * Response object for this request after a successful save
879
     *
880
     * @param bool $isNewRecord True if this record was just created
881
     * @param DataObject $record
882
     * @return HTTPResponse|DBHTMLText|string
883
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
884
     * because it is a protected method and not visible to a decorator!
885
     */
886
    protected function redirectAfterAction($isNewRecord, $record = null)
887
    {
888
        $controller = $this->getToplevelController();
889
890
        if ($this->isLeftAndMain($controller)) {
891
            // CMSMain => redirect to show
892
            if ($this->owner->hasMethod("LinkPageEdit")) {
893
                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

893
                return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ LinkPageEdit($record->ID));
Loading history...
894
            }
895
        }
896
897
        if ($isNewRecord) {
898
            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

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

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

903
            return $this->owner->/** @scrutinizer ignore-call */ edit($controller->getRequest());
Loading history...
904
        }
905
        // Changes to the record properties might've excluded the record from
906
        // a filtered list, so return back to the main view if it can't be found
907
        $noActionURL = $url = $controller->getRequest()->getURL();
908
        if (!$url) {
909
            $url = '';
910
        }
911
912
        // The controller may not have these
913
        if ($controller->hasMethod('getAction')) {
914
            $action = $controller->getAction();
915
            // Handle GridField detail form editing
916
            if (strpos($url, 'ItemEditForm') !== false) {
917
                $action = 'ItemEditForm';
918
            }
919
            if ($action) {
920
                $noActionURL = $controller->removeAction($url, $action);
921
            }
922
        } else {
923
            // Simple fallback (last index of)
924
            $pos = strrpos($url, 'ItemEditForm');
925
            if (is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
926
                $noActionURL = substr($url, 0, $pos);
927
            }
928
        }
929
930
        $controller->getRequest()->addHeader('X-Pjax', 'Content');
931
        return $controller->redirect($noActionURL, 302);
932
    }
933
}
934