Passed
Push — master ( 115177...9352c7 )
by Thomas
02:26
created

getCustomPreviousRecordID()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 3 Features 0
Metric Value
cc 2
eloc 3
c 4
b 3
f 0
nc 2
nop 1
dl 0
loc 8
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
use ReflectionObject;
29
use SilverStripe\Admin\ModelAdmin;
30
31
/**
32
 * Decorates GridDetailForm_ItemRequest to use new form actions and buttons.
33
 *
34
 * This is also applied to LeftAndMain to allow actions on pages
35
 * Warning: LeftAndMain doesn't call updateItemEditForm
36
 *
37
 * This is a lightweight version of BetterButtons that use default getCMSActions functionnality
38
 * on DataObjects
39
 *
40
 * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons
41
 * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons/blob/master/src/Extensions/GridFieldBetterButtonsItemRequest.php
42
 * @property LeftAndMain&GridFieldDetailForm_ItemRequest&ActionsGridFieldItemRequest $owner
43
 */
44
class ActionsGridFieldItemRequest extends DataExtension
45
{
46
    use Configurable;
47
    use Extensible;
48
49
    /**
50
     * @config
51
     * @var boolean
52
     */
53
    private static $enable_save_prev_next = true;
54
55
    /**
56
     * @config
57
     * @var boolean
58
     */
59
    private static $enable_save_close = true;
60
61
    /**
62
     * @config
63
     * @var boolean
64
     */
65
    private static $enable_delete_right = true;
66
67
    /**
68
     * @config
69
     * @var boolean
70
     */
71
    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...
72
73
    /**
74
     * @var array<string> Allowed controller actions
75
     */
76
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
77
        'doSaveAndClose',
78
        'doSaveAndNext',
79
        'doSaveAndPrev',
80
        'doCustomAction', // For CustomAction
81
        'doCustomLink', // For CustomLink
82
    ];
83
84
    /**
85
     * @param FieldList $actions
86
     * @return array<string>
87
     */
88
    protected function getAvailableActions($actions)
89
    {
90
        $list = [];
91
        foreach ($actions as $action) {
92
            if (is_a($action, CompositeField::class)) {
93
                $list = array_merge($list, $this->getAvailableActions($action->FieldList()));
94
            } else {
95
                $list[] = $action->getName();
96
            }
97
        }
98
        return $list;
99
    }
100
101
    /**
102
     * This module does not interact with the /schema/SearchForm endpoint
103
     * and therefore all requests for these urls don't need any special treatement
104
     *
105
     * @return bool
106
     */
107
    protected function isSearchFormRequest(): bool
108
    {
109
        if (!Controller::has_curr()) {
110
            return false;
111
        }
112
        $curr =  Controller::curr();
113
        if ($curr) {
0 ignored issues
show
introduced by
$curr is of type SilverStripe\Control\Controller, thus it always evaluated to true.
Loading history...
114
            return str_contains($curr->getRequest()->getURL(), '/schema/SearchForm');
115
        }
116
        return false;
117
    }
118
119
    /**
120
     * Called by CMSMain, typically in the CMS or in the SiteConfig admin
121
     * CMSMain already uses getCMSActions so we are good to go with anything defined there
122
     *
123
     * @param Form $form
124
     * @return void
125
     */
126
    public function updateEditForm(Form $form)
127
    {
128
        // Ignore search form requests
129
        if ($this->isSearchFormRequest()) {
130
            return;
131
        }
132
133
        $actions = $form->Actions();
134
135
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
136
        // and we don't want to duplicate it
137
        $this->processDropUpMenu($actions);
138
    }
139
140
    /**
141
     * @return FieldList|false
142
     */
143
    public function recordCmsUtils()
144
    {
145
        $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

145
        /** @scrutinizer ignore-call */ 
146
        $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

145
        /** @scrutinizer ignore-call */ 
146
        $record = $this->owner->getRecord();
Loading history...
146
        if ($record->hasMethod('getCMSUtils')) {
147
            $utils = $record->getCMSUtils();
148
            $this->extend('onCMSUtils', $utils, $record);
149
            $record->extend('onCMSUtils', $utils);
150
            return $utils;
151
        }
152
        return false;
153
    }
154
155
    /**
156
     * Called by GridField_ItemRequest
157
     * GridField_ItemRequest defines its own set of actions so we need to add ours
158
     * We add our custom save&close, save&next and other tweaks
159
     * Actions can be made readonly after this extension point
160
     * @param FieldList $actions
161
     * @return void
162
     */
163
    public function updateFormActions($actions)
164
    {
165
        // Ignore search form requests
166
        if ($this->isSearchFormRequest()) {
167
            return;
168
        }
169
170
        $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

170
        /** @scrutinizer ignore-call */ 
171
        $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...
171
172
        // We get the actions as defined on our record
173
        /** @var FieldList $CMSActions */
174
        $CMSActions = $record->getCMSActions();
175
176
        // The default button group that contains the Save or Create action
177
        // @link https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface/#extending-the-cms-actions
178
        $MajorActions = $actions->fieldByName('MajorActions');
179
180
        // If it doesn't exist, push to default group
181
        if (!$MajorActions) {
182
            $MajorActions = $actions;
0 ignored issues
show
Unused Code introduced by
The assignment to $MajorActions is dead and can be removed.
Loading history...
183
        }
184
185
        // Push our actions that are otherwise ignored by SilverStripe
186
        foreach ($CMSActions as $action) {
187
            // Avoid duplicated actions (eg: when added by SilverStripe\Versioned\VersionedGridFieldItemRequest)
188
            if ($actions->fieldByName($action->getName())) {
189
                continue;
190
            }
191
            $actions->push($action);
192
        }
193
194
        // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions
195
        // and we don't want to duplicate it
196
        $this->processDropUpMenu($actions);
197
198
        // Add extension hook
199
        $this->extend('onBeforeUpdateCMSActions', $actions, $record);
200
        $record->extend('onBeforeUpdateCMSActions', $actions);
201
202
        $ActionMenus = $actions->fieldByName('ActionMenus');
203
        // Re-insert ActionMenus to make sure they always follow the buttons
204
        if ($ActionMenus) {
205
            $actions->remove($ActionMenus);
206
            $actions->push($ActionMenus);
207
        }
208
209
        // We have a 4.4 setup, before that there was no RightGroup
210
        $RightGroup = $this->getRightGroupActions($actions);
211
212
        // Insert again to make sure our actions are properly placed after apply changes
213
        if ($RightGroup) {
0 ignored issues
show
introduced by
$RightGroup is of type SilverStripe\Forms\CompositeField, thus it always evaluated to true.
Loading history...
214
            $actions->remove($RightGroup);
215
            $actions->push($RightGroup);
216
        }
217
218
        $opts = [
219
            'save_close'     => self::config()->enable_save_close,
220
            'save_prev_next' => self::config()->enable_save_prev_next,
221
            'delete_right'   => self::config()->enable_delete_right,
222
        ];
223
        if ($record->hasMethod('getCMSActionsOptions')) {
224
            $opts = array_merge($opts, $record->getCMSActionsOptions());
225
        }
226
227
        if ($opts['save_close']) {
228
            $this->addSaveAndClose($actions, $record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type integer and null; however, parameter $record of LeKoala\CmsActions\Actio...uest::addSaveAndClose() 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

228
            $this->addSaveAndClose($actions, /** @scrutinizer ignore-type */ $record);
Loading history...
229
        }
230
231
        if ($opts['save_prev_next']) {
232
            $this->addSaveNextAndPrevious($actions, $record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type integer and null; however, parameter $record of LeKoala\CmsActions\Actio...ddSaveNextAndPrevious() 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

232
            $this->addSaveNextAndPrevious($actions, /** @scrutinizer ignore-type */ $record);
Loading history...
233
        }
234
235
        if ($opts['delete_right']) {
236
            $this->moveCancelAndDelete($actions, $record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type integer and null; however, parameter $record of LeKoala\CmsActions\Actio...::moveCancelAndDelete() 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

236
            $this->moveCancelAndDelete($actions, /** @scrutinizer ignore-type */ $record);
Loading history...
237
        }
238
239
        // Fix gridstate being lost when running custom actions
240
        if (method_exists($this->owner, 'getStateManager')) {
241
            $request = $this->owner->getRequest();
0 ignored issues
show
Bug introduced by
The method getRequest() 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

241
            /** @scrutinizer ignore-call */ 
242
            $request = $this->owner->getRequest();
Loading history...
242
            $stateManager = $this->owner->getStateManager();
0 ignored issues
show
Bug introduced by
The method getStateManager() 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

242
            /** @scrutinizer ignore-call */ 
243
            $stateManager = $this->owner->getStateManager();
Loading history...
243
            $gridField = $this->owner->getGridField();
0 ignored issues
show
Bug introduced by
The method getGridField() 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

243
            /** @scrutinizer ignore-call */ 
244
            $gridField = $this->owner->getGridField();
Loading history...
244
            $state = $stateManager->getStateFromRequest($gridField, $request);
245
            $actions->push(new HiddenField($stateManager->getStateKey($gridField), null, $state));
246
        }
247
248
        // Add extension hook
249
        $this->extend('onAfterUpdateCMSActions', $actions, $record);
250
        $record->extend('onAfterUpdateCMSActions', $actions);
251
    }
252
253
    /**
254
     * Collect all Drop-Up actions into a menu.
255
     * @param FieldList $actions
256
     * @return void
257
     */
258
    protected function processDropUpMenu($actions)
259
    {
260
        // The Drop-up container may already exist
261
        /** @var ?Tab $dropUpContainer */
262
        $dropUpContainer = $actions->fieldByName('ActionMenus.MoreOptions');
263
        foreach ($actions as $action) {
264
            //@phpstan-ignore-next-line
265
            if ($action->hasMethod('getDropUp') && $action->getDropUp()) {
266
                if (!$dropUpContainer) {
267
                    $dropUpContainer = $this->createDropUpContainer($actions);
268
                }
269
                $action->getContainerFieldList()->removeByName($action->getName());
270
                $dropUpContainer->push($action);
0 ignored issues
show
Bug introduced by
It seems like $action can also be of type SilverStripe\View\ArrayData; however, parameter $field of SilverStripe\Forms\CompositeField::push() does only seem to accept SilverStripe\Forms\FormField, 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

270
                $dropUpContainer->push(/** @scrutinizer ignore-type */ $action);
Loading history...
271
            }
272
        }
273
    }
274
275
    /**
276
     * Prepares a Drop-Up menu
277
     * @param FieldList $actions
278
     * @return Tab
279
     */
280
    protected function createDropUpContainer($actions)
281
    {
282
        $rootTabSet = new TabSet('ActionMenus');
283
        $dropUpContainer = new Tab(
284
            'MoreOptions',
285
            _t(__CLASS__ . '.MoreOptions', 'More options', 'Expands a view for more buttons')
286
        );
287
        $dropUpContainer->addExtraClass('popover-actions-simulate');
288
        $rootTabSet->push($dropUpContainer);
289
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
290
291
        $actions->insertBefore('RightGroup', $rootTabSet);
292
293
        return $dropUpContainer;
294
    }
295
296
    /**
297
     * Check if a record can be edited/created/exists
298
     * @param DataObject $record
299
     * @return bool
300
     */
301
    protected function checkCan($record)
302
    {
303
        if (!$record->canEdit() || (!$record->ID && !$record->canCreate())) {
304
            return false;
305
        }
306
307
        return true;
308
    }
309
310
    /**
311
     * @param FieldList $actions
312
     * @param DataObject $record
313
     * @return void
314
     */
315
    public function moveCancelAndDelete(FieldList $actions, DataObject $record)
316
    {
317
        // We have a 4.4 setup, before that there was no RightGroup
318
        $RightGroup = $actions->fieldByName('RightGroup');
319
320
        // Move delete at the end
321
        $deleteAction = $actions->fieldByName('action_doDelete');
322
        if ($deleteAction) {
323
            // Move at the end of the stack
324
            $actions->remove($deleteAction);
325
            $actions->push($deleteAction);
326
327
            if (!$RightGroup) {
328
                // Only necessary pre 4.4
329
                $deleteAction->addExtraClass('align-right');
330
            }
331
            // Set custom title
332
            if ($record->hasMethod('getDeleteButtonTitle')) {
333
                //@phpstan-ignore-next-line
334
                $deleteAction->setTitle($record->getDeleteButtonTitle());
335
            }
336
        }
337
        // Move cancel at the end
338
        $cancelButton = $actions->fieldByName('cancelbutton');
339
        if ($cancelButton) {
340
            // Move at the end of the stack
341
            $actions->remove($cancelButton);
342
            $actions->push($cancelButton);
343
            if (!$RightGroup) {
344
                // Only necessary pre 4.4
345
                $cancelButton->addExtraClass('align-right');
346
            }
347
            // Set custom titlte
348
            if ($record->hasMethod('getCancelButtonTitle')) {
349
                //@phpstan-ignore-next-line
350
                $cancelButton->setTitle($record->getCancelButtonTitle());
351
            }
352
        }
353
    }
354
355
    /**
356
     * @param DataObject $record
357
     * @return bool
358
     */
359
    public function useCustomPrevNext(DataObject $record): bool
360
    {
361
        if (self::config()->enable_custom_prevnext) {
362
            return $record->hasMethod('PrevRecord') && $record->hasMethod('NextRecord');
363
        }
364
        return false;
365
    }
366
367
    /**
368
     * @param DataObject $record
369
     * @return int
370
     */
371
    public function getCustomPreviousRecordID(DataObject $record)
372
    {
373
        // This will overwrite state provided record
374
        if ($this->useCustomPrevNext($record)) {
375
            //@phpstan-ignore-next-line
376
            return $record->PrevRecord()->ID ?? 0;
377
        }
378
        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

378
        return $this->owner->/** @scrutinizer ignore-call */ getPreviousRecordID();
Loading history...
379
    }
380
381
    /**
382
     * @param DataObject $record
383
     * @return int
384
     */
385
    public function getCustomNextRecordID(DataObject $record)
386
    {
387
388
        // This will overwrite state provided record
389
        if ($this->useCustomPrevNext($record)) {
390
            //@phpstan-ignore-next-line
391
            return $record->NextRecord()->ID ?? 0;
392
        }
393
        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

393
        return $this->owner->/** @scrutinizer ignore-call */ getNextRecordID();
Loading history...
394
    }
395
396
    /**
397
     * @param FieldList $actions
398
     * @return CompositeField|FieldList
399
     */
400
    protected function getMajorActions(FieldList $actions)
401
    {
402
        /** @var ?CompositeField $MajorActions */
403
        $MajorActions = $actions->fieldByName('MajorActions');
404
405
        // If it doesn't exist, push to default group
406
        if (!$MajorActions) {
0 ignored issues
show
introduced by
$MajorActions is of type SilverStripe\Forms\CompositeField, thus it always evaluated to true.
Loading history...
407
            $MajorActions = $actions;
408
        }
409
        return $MajorActions;
410
    }
411
412
    /**
413
     * @param FieldList $actions
414
     * @return CompositeField
415
     */
416
    protected function getRightGroupActions(FieldList $actions)
417
    {
418
        /** @var ?CompositeField $RightGroup */
419
        $RightGroup = $actions->fieldByName('RightGroup');
420
        return $RightGroup;
421
    }
422
423
    /**
424
     * @param FieldList $actions
425
     * @param DataObject $record
426
     * @return void
427
     */
428
    public function addSaveNextAndPrevious(FieldList $actions, DataObject $record)
429
    {
430
        if (!$record->canEdit() || !$record->ID) {
431
            return;
432
        }
433
434
        $MajorActions = $this->getMajorActions($actions);
435
436
        // @link https://github.com/silverstripe/silverstripe-framework/issues/10742
437
        $getPreviousRecordID = $this->getCustomPreviousRecordID($record);
438
        $getNextRecordID = $this->getCustomNextRecordID($record);
439
        $isCustom  = $this->useCustomPrevNext($record);
440
441
        // Coupling for HasPrevNextUtils
442
        if (Controller::has_curr()) {
443
            $prevLink = $nextLink = null;
444
            if (!$isCustom && $this->owner instanceof GridFieldDetailForm_ItemRequest) {
445
                if ($getPreviousRecordID) {
446
                    $prevLink = $this->getPublicEditLinkForAdjacentRecord(-1);
447
                }
448
                if ($getNextRecordID) {
449
                    $nextLink = $this->getPublicEditLinkForAdjacentRecord(+1);
450
                }
451
            }
452
453
            /** @var HTTPRequest $request */
454
            $request = Controller::curr()->getRequest();
455
            $routeParams = $request->routeParams();
456
            $recordClass = get_class($record);
457
            $routeParams['cmsactions'][$recordClass]['PreviousRecordID'] = $getPreviousRecordID;
458
            $routeParams['cmsactions'][$recordClass]['NextRecordID'] = $getNextRecordID;
459
            $routeParams['cmsactions'][$recordClass]['PrevRecordLink'] = $prevLink;
460
            $routeParams['cmsactions'][$recordClass]['NextRecordLink'] = $nextLink;
461
            $request->setRouteParams($routeParams);
462
        }
463
464
        if ($getPreviousRecordID) {
465
            $doSaveAndPrev = new FormAction('doSaveAndPrev', _t('ActionsGridFieldItemRequest.SAVEANDPREVIOUS', 'Save and Previous'));
466
            $doSaveAndPrev->addExtraClass($this->getBtnClassForRecord($record));
467
            $doSaveAndPrev->addExtraClass('font-icon-angle-double-left btn-mobile-collapse');
468
            $doSaveAndPrev->setUseButtonTag(true);
469
            $MajorActions->push($doSaveAndPrev);
470
        }
471
        if ($getNextRecordID) {
472
            $doSaveAndNext = new FormAction('doSaveAndNext', _t('ActionsGridFieldItemRequest.SAVEANDNEXT', 'Save and Next'));
473
            $doSaveAndNext->addExtraClass($this->getBtnClassForRecord($record));
474
            $doSaveAndNext->addExtraClass('font-icon-angle-double-right btn-mobile-collapse');
475
            $doSaveAndNext->setUseButtonTag(true);
476
            $MajorActions->push($doSaveAndNext);
477
        }
478
    }
479
480
    public function getPublicEditLinkForAdjacentRecord(int $offset): ?string
481
    {
482
        $this->owner->getStateManager();
483
        $reflObject = new ReflectionObject($this->owner);
484
        $reflMethod = $reflObject->getMethod('getEditLinkForAdjacentRecord');
485
        $reflMethod->setAccessible(true);
486
487
        try {
488
            return $reflMethod->invoke($this->owner, $offset);
489
        } catch (Exception $e) {
490
            return null;
491
        }
492
    }
493
494
    /**
495
     * @param FieldList $actions
496
     * @param DataObject $record
497
     * @return void
498
     */
499
    public function addSaveAndClose(FieldList $actions, DataObject $record)
500
    {
501
        if (!$this->checkCan($record)) {
502
            return;
503
        }
504
505
        $MajorActions = $this->getMajorActions($actions);
506
507
        if ($record->ID) {
508
            $label = _t('ActionsGridFieldItemRequest.SAVEANDCLOSE', 'Save and Close');
509
        } else {
510
            $label = _t('ActionsGridFieldItemRequest.CREATEANDCLOSE', 'Create and Close');
511
        }
512
        $saveAndClose = new FormAction('doSaveAndClose', $label);
513
        $saveAndClose->addExtraClass($this->getBtnClassForRecord($record));
514
        $saveAndClose->setAttribute('data-text-alternate', $label);
515
        if ($record->ID) {
516
            $saveAndClose->setAttribute('data-btn-alternate-add', 'btn-primary');
517
            $saveAndClose->setAttribute('data-btn-alternate-remove', 'btn-outline-primary');
518
        }
519
        $saveAndClose->addExtraClass('font-icon-level-up btn-mobile-collapse');
520
        $saveAndClose->setUseButtonTag(true);
521
        $MajorActions->push($saveAndClose);
522
    }
523
524
    /**
525
     * New and existing records have different classes
526
     *
527
     * @param DataObject $record
528
     * @return string
529
     */
530
    protected function getBtnClassForRecord(DataObject $record)
531
    {
532
        if ($record->ID) {
533
            return 'btn-outline-primary';
534
        }
535
        return 'btn-primary';
536
    }
537
538
    /**
539
     * @param string $action
540
     * @param array<FormField>|FieldList $definedActions
541
     * @return mixed|CompositeField|null
542
     */
543
    protected static function findAction($action, $definedActions)
544
    {
545
        $result = null;
546
547
        foreach ($definedActions as $definedAction) {
548
            if (is_a($definedAction, CompositeField::class)) {
549
                $result = self::findAction($action, $definedAction->FieldList());
550
                if ($result) {
551
                    break;
552
                }
553
            }
554
555
            $definedActionName = $definedAction->getName();
556
557
            if ($definedAction->hasMethod('actionName')) {
558
                //@phpstan-ignore-next-line
559
                $definedActionName = $definedAction->actionName();
560
            }
561
            if ($definedActionName === $action) {
562
                $result = $definedAction;
563
                break;
564
            }
565
        }
566
567
        return $result;
568
    }
569
570
    /**
571
     * Forward a given action to a DataObject
572
     *
573
     * Action must be declared in getCMSActions to be called
574
     *
575
     * @param string $action
576
     * @param array<string,mixed> $data
577
     * @param Form $form
578
     * @return HTTPResponse|DBHTMLText|string
579
     * @throws HTTPResponse_Exception
580
     */
581
    protected function forwardActionToRecord($action, $data = [], $form = null)
582
    {
583
        $controller = $this->getToplevelController();
584
585
        // We have an item request or a controller that can provide a record
586
        $record = null;
587
        if ($this->owner->hasMethod('ItemEditForm')) {
588
            // It's a request handler. Don't check for a specific class as it may be subclassed
589
            $record = $this->owner->record;
590
        } elseif ($controller->hasMethod('save_siteconfig')) {
591
            // Check for any type of siteconfig controller
592
            $record = SiteConfig::current_site_config();
593
        } elseif (!empty($data['ClassName']) && !empty($data['ID'])) {
594
            $record = DataObject::get_by_id($data['ClassName'], $data['ID']);
595
        } elseif ($controller->hasMethod("getRecord")) {
596
            // LeftAndMain requires an id
597
            if ($controller instanceof LeftAndMain && !empty($data['ID'])) {
598
                $record = $controller->getRecord($data['ID']);
599
            } elseif ($controller instanceof ModelAdmin) {
600
                // Otherwise fallback to singleton
601
                $record = DataObject::singleton($controller->getModelClass());
602
            }
603
        }
604
605
        if (!$record) {
606
            throw new Exception("No record to handle the action $action on " . get_class($controller));
607
        }
608
        $definedActions = $record->getCMSActions();
609
        // Check if the action is indeed available
610
        $clickedAction = null;
611
        if (!empty($definedActions)) {
612
            $clickedAction = self::findAction($action, $definedActions);
613
        }
614
        if (!$clickedAction) {
615
            $class = get_class($record);
616
            $availableActions = implode(',', $this->getAvailableActions($definedActions));
617
            if (!$availableActions) {
618
                $availableActions = "(no available actions, please check getCMSActions)";
619
            }
620
621
            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

621
            return $this->owner->/** @scrutinizer ignore-call */ httpError(403, sprintf(
Loading history...
622
                'Action not available on %s. It must be one of : %s',
623
                $class,
624
                $availableActions
625
            ));
626
        }
627
        $message = null;
628
        $error = false;
629
630
        // Check record BEFORE the action
631
        // It can be deleted by the action, and it will return to the list
632
        $isNewRecord = $record->ID === 0;
633
634
        try {
635
            $result = $record->$action($data, $form, $controller);
636
637
            // We have a response
638
            if ($result instanceof HTTPResponse) {
639
                return $result;
640
            }
641
642
            if ($result === false) {
643
                // Result returned an error (false)
644
                $error = true;
645
                $message = _t(
646
                    'ActionsGridFieldItemRequest.FAILED',
647
                    'Action {action} failed on {name}',
648
                    ['action' => $clickedAction->getTitle(), 'name' => $record->i18n_singular_name()]
649
                );
650
            } elseif (is_string($result)) {
651
                // Result is a message
652
                $message = $result;
653
            }
654
        } catch (Exception $ex) {
655
            $result = null;
656
            $error = true;
657
            $message = $ex->getMessage();
658
        }
659
660
        // Build default message
661
        if (!$message) {
662
            $message = _t(
663
                'ActionsGridFieldItemRequest.DONE',
664
                'Action {action} was done on {name}',
665
                ['action' => $clickedAction->getTitle(), 'name' => $record->i18n_singular_name()]
666
            );
667
        }
668
        $status = 'good';
669
        if ($error) {
670
            $status = 'bad';
671
        }
672
673
        // Progressive actions return array with json data
674
        //@phpstan-ignore-next-line
675
        if (method_exists($clickedAction, 'getProgressive') && $clickedAction->getProgressive()) {
676
            $response = $controller->getResponse();
677
            $response->addHeader('Content-Type', 'application/json');
678
            if ($result) {
679
                $encodedResult = json_encode($result);
680
                if (!$encodedResult) {
681
                    $encodedResult = json_last_error_msg();
682
                }
683
                $response->setBody($encodedResult);
684
            }
685
686
            return $response;
687
        }
688
689
        // We don't have a form, simply return the result
690
        if (!$form) {
691
            if ($error) {
692
                return $this->owner->httpError(403, $message);
693
            }
694
695
            return $message;
696
        }
697
698
        if (Director::is_ajax()) {
699
            $controller->getResponse()->addHeader('X-Status', rawurlencode($message));
700
701
            //@phpstan-ignore-next-line
702
            if (method_exists($clickedAction, 'getShouldRefresh') && $clickedAction->getShouldRefresh()) {
703
                self::addXReload($controller);
704
            }
705
            // 4xx status makes a red box
706
            if ($error) {
707
                $controller->getResponse()->setStatusCode(400);
708
            }
709
        } else {
710
            // If the controller support sessionMessage, use it instead of form
711
            if ($controller->hasMethod('sessionMessage')) {
712
                //@phpstan-ignore-next-line
713
                $controller->sessionMessage($message, $status, ValidationResult::CAST_HTML);
714
            } else {
715
                $form->sessionMessage($message, $status, ValidationResult::CAST_HTML);
716
            }
717
        }
718
719
        // Custom redirect
720
        //@phpstan-ignore-next-line
721
        if (method_exists($clickedAction, 'getRedirectURL') && $clickedAction->getRedirectURL()) {
722
            // we probably need a full ui refresh
723
            self::addXReload($clickedAction->getRedirectURL());
724
            //@phpstan-ignore-next-line
725
            return $controller->redirect($clickedAction->getRedirectURL());
726
        }
727
728
        // Redirect after action
729
        return $this->redirectAfterAction($isNewRecord, $record);
730
    }
731
732
    /**
733
     * Requires a ControllerURL as well, see
734
     * https://github.com/silverstripe/silverstripe-admin/blob/a3aa41cea4c4df82050eef65ad5efcfae7bfde69/client/src/legacy/LeftAndMain.js#L773-L780
735
     *
736
     * @param Controller $controller
737
     * @param string|null $url
738
     * @return void
739
     */
740
    public static function addXReload(Controller $controller, ?string $url = null): void
741
    {
742
        if (!$url) {
743
            $url = $controller->getReferer();
744
        }
745
        $controller->getResponse()->addHeader('X-ControllerURL', $url);
746
        $controller->getResponse()->addHeader('X-Reload', "true");
747
    }
748
749
    /**
750
     * Handles custom links
751
     *
752
     * Use CustomLink with default behaviour to trigger this
753
     *
754
     * See:
755
     * DefaultLink::getModelLink
756
     * GridFieldCustomLink::getLink
757
     *
758
     * @param HTTPRequest $request
759
     * @return HTTPResponse|DBHTMLText|string
760
     * @throws Exception
761
     */
762
    public function doCustomLink(HTTPRequest $request)
763
    {
764
        $action = $request->getVar('CustomLink');
765
        return $this->forwardActionToRecord($action);
766
    }
767
768
    /**
769
     * Handles custom actions
770
     *
771
     * Use CustomAction class to trigger this
772
     *
773
     * Nested actions are submitted like this
774
     * [action_doCustomAction] => Array
775
     * (
776
     *   [doTestAction] => 1
777
     * )
778
     *
779
     * @param array<string,mixed> $data The form data
780
     * @param Form $form The form object
781
     * @return HTTPResponse|DBHTMLText|string
782
     * @throws Exception
783
     */
784
    public function doCustomAction($data, $form)
785
    {
786
        $action = key($data['action_doCustomAction']);
787
        return $this->forwardActionToRecord($action, $data, $form);
788
    }
789
790
    /**
791
     * Saves the form and goes back to list view
792
     *
793
     * @param array<string,mixed> $data The form data
794
     * @param Form $form The form object
795
     * @return HTTPResponse
796
     */
797
    public function doSaveAndClose($data, $form)
798
    {
799
        $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

799
        $this->owner->/** @scrutinizer ignore-call */ 
800
                      doSave($data, $form);
Loading history...
800
        // Redirect after save
801
        $controller = $this->getToplevelController();
802
803
        $link = $this->getBackLink();
804
805
        // Doesn't seem to be needed anymore
806
        // $link = $this->addGridState($link, $data);
807
808
        $controller->getResponse()->addHeader("X-Pjax", "Content");
809
810
        return $controller->redirect($link);
811
    }
812
813
    /**
814
     * @param string $dir prev|next
815
     * @param array<string,mixed> $data The form data
816
     * @param Form|null $form
817
     * @return void
818
     */
819
    protected function doSaveAndAdjacent(string $dir, array $data, ?Form $form)
820
    {
821
        $record = $this->owner->record;
822
        $this->owner->doSave($data, $form);
823
        // Redirect after save
824
        $controller = $this->getToplevelController();
825
        $controller->getResponse()->addHeader("X-Pjax", "Content");
826
827
        $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

827
        $class = get_class(/** @scrutinizer ignore-type */ $record);
Loading history...
828
        if (!$class) {
829
            throw new Exception("Could not get class");
830
        }
831
832
        $method = match ($dir) {
833
            'prev' => 'getCustomPreviousRecordID',
834
            'next' => 'getCustomNextRecordID',
835
        };
836
837
        $offset = match ($dir) {
838
            'prev' => -1,
839
            'next' => +1,
840
        };
841
842
        $adjRecordID = $this->$method($record);
843
844
        /** @var ?DataObject $adj */
845
        $adj = $class::get()->byID($adjRecordID);
846
847
        $useCustom = $this->useCustomPrevNext($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...st::useCustomPrevNext() 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

847
        $useCustom = $this->useCustomPrevNext(/** @scrutinizer ignore-type */ $record);
Loading history...
848
        $link = $this->getPublicEditLinkForAdjacentRecord($offset);
849
        if (!$link || $useCustom) {
850
            $link = $this->owner->getEditLink($adjRecordID);
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

850
            /** @scrutinizer ignore-call */ 
851
            $link = $this->owner->getEditLink($adjRecordID);
Loading history...
851
            $link = $this->addGridState($link, $data);
0 ignored issues
show
Deprecated Code introduced by
The function LeKoala\CmsActions\Actio...Request::addGridState() has been deprecated. ( Ignorable by Annotation )

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

851
            $link = /** @scrutinizer ignore-deprecated */ $this->addGridState($link, $data);
Loading history...
852
        }
853
854
        // Link to a specific tab if set, see cms-actions.js
855
        if ($adj && !empty($data['_activetab'])) {
856
            $link .= sprintf('#%s', $data['_activetab']);
857
        }
858
859
        return $controller->redirect($link);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $controller->redirect($link) returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
860
    }
861
862
    /**
863
     * Saves the form and goes back to the next item
864
     *
865
     * @param array<string,mixed> $data The form data
866
     * @param Form $form The form object
867
     * @return HTTPResponse
868
     */
869
    public function doSaveAndNext($data, $form)
870
    {
871
        return $this->doSaveAndAdjacent('next', $data, $form);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->doSaveAndAdjacent('next', $data, $form) targeting LeKoala\CmsActions\Actio...st::doSaveAndAdjacent() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
Bug Best Practice introduced by
The expression return $this->doSaveAndA...t('next', $data, $form) returns the type void which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
872
    }
873
874
    /**
875
     * Saves the form and goes to the previous item
876
     *
877
     * @param array<string,mixed> $data The form data
878
     * @param Form $form The form object
879
     * @return HTTPResponse
880
     */
881
    public function doSaveAndPrev($data, $form)
882
    {
883
        return $this->doSaveAndAdjacent('prev', $data, $form);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->doSaveAndA...t('prev', $data, $form) returns the type void which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
Bug introduced by
Are you sure the usage of $this->doSaveAndAdjacent('prev', $data, $form) targeting LeKoala\CmsActions\Actio...st::doSaveAndAdjacent() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
884
    }
885
886
    /**
887
     * Check if we can remove this safely
888
     * @deprecated
889
     * @param string $url
890
     * @param array<mixed> $data
891
     * @return string
892
     */
893
    protected function addGridState($url, $data)
894
    {
895
        // This should not be necessary at all if the state is correctly passed along
896
        $BackURL = $data['BackURL'] ?? null;
897
        if ($BackURL) {
898
            $query = parse_url($BackURL, PHP_URL_QUERY);
899
            if ($query) {
900
                $url = strtok($url, '?');
901
                $url .= '?' . $query;
902
            }
903
        }
904
        return $url;
905
    }
906
907
    /**
908
     * Gets the top level controller.
909
     *
910
     * @return Controller
911
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
912
     * because it is a protected method and not visible to a decorator!
913
     */
914
    protected function getToplevelController()
915
    {
916
        if ($this->isLeftAndMain($this->owner)) {
917
            return $this->owner;
918
        }
919
        if (!$this->owner->hasMethod("getController")) {
920
            return Controller::curr();
921
        }
922
        $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

922
        /** @scrutinizer ignore-call */ 
923
        $controller = $this->owner->getController();
Loading history...
923
        while ($controller instanceof GridFieldDetailForm_ItemRequest) {
924
            $controller = $controller->getController();
925
        }
926
927
        return $controller;
928
    }
929
930
    /**
931
     * @param Controller $controller
932
     * @return boolean
933
     */
934
    protected function isLeftAndMain($controller)
935
    {
936
        return is_subclass_of($controller, LeftAndMain::class);
937
    }
938
939
    /**
940
     * Gets the back link
941
     *
942
     * @return string
943
     */
944
    public function getBackLink()
945
    {
946
        $backlink = '';
947
        $toplevelController = $this->getToplevelController();
948
        // Check for LeftAndMain and alike controllers with a Backlink or Breadcrumbs methods
949
        if ($toplevelController->hasMethod('Backlink')) {
950
            //@phpstan-ignore-next-line
951
            $backlink = $toplevelController->Backlink();
952
        } elseif ($this->owner->getController()->hasMethod('Breadcrumbs')) {
953
            $parents = $this->owner->getController()->Breadcrumbs(false)->items;
954
            $backlink = array_pop($parents)->Link;
955
        }
956
        if (!$backlink) {
957
            $backlink = $toplevelController->Link();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $backlink is correct as $toplevelController->Link() targeting SilverStripe\Control\RequestHandler::Link() 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...
958
        }
959
960
        return $backlink;
961
    }
962
963
    /**
964
     * Response object for this request after a successful save
965
     *
966
     * @param bool $isNewRecord True if this record was just created
967
     * @param DataObject $record
968
     * @return HTTPResponse|DBHTMLText|string
969
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
970
     * because it is a protected method and not visible to a decorator!
971
     */
972
    protected function redirectAfterAction($isNewRecord, $record = null)
973
    {
974
        $controller = $this->getToplevelController();
975
976
        if ($this->isLeftAndMain($controller)) {
977
            // CMSMain => redirect to show
978
            if ($this->owner->hasMethod("LinkPageEdit")) {
979
                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

979
                return $controller->redirect($this->owner->/** @scrutinizer ignore-call */ LinkPageEdit($record->ID));
Loading history...
980
            }
981
        }
982
983
        if ($isNewRecord) {
984
            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

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

989
            return $this->owner->/** @scrutinizer ignore-call */ edit($controller->getRequest());
Loading history...
990
        }
991
        // Changes to the record properties might've excluded the record from
992
        // a filtered list, so return back to the main view if it can't be found
993
        $noActionURL = $url = $controller->getRequest()->getURL();
994
        if (!$url) {
995
            $url = '';
996
        }
997
998
        // The controller may not have these
999
        if ($controller->hasMethod('getAction')) {
1000
            $action = $controller->getAction();
1001
            // Handle GridField detail form editing
1002
            if (strpos($url, 'ItemEditForm') !== false) {
1003
                $action = 'ItemEditForm';
1004
            }
1005
            if ($action) {
1006
                $noActionURL = $controller->removeAction($url, $action);
1007
            }
1008
        } else {
1009
            // Simple fallback (last index of)
1010
            $pos = strrpos($url, 'ItemEditForm');
1011
            if (is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
1012
                $noActionURL = substr($url, 0, $pos);
1013
            }
1014
        }
1015
1016
        $controller->getRequest()->addHeader('X-Pjax', 'Content');
1017
        return $controller->redirect($noActionURL, 302);
1018
    }
1019
}
1020