1 | <?php |
||||
2 | |||||
3 | namespace LeKoala\CmsActions; |
||||
4 | |||||
5 | use Exception; |
||||
6 | use ReflectionMethod; |
||||
7 | use ReflectionObject; |
||||
8 | use SilverStripe\Admin\LeftAndMain; |
||||
9 | use SilverStripe\Admin\ModelAdmin; |
||||
10 | use SilverStripe\Control\Controller; |
||||
11 | use SilverStripe\Control\Director; |
||||
12 | use SilverStripe\Control\HTTPRequest; |
||||
13 | use SilverStripe\Control\HTTPResponse; |
||||
14 | use SilverStripe\Control\HTTPResponse_Exception; |
||||
15 | use SilverStripe\Core\Config\Configurable; |
||||
16 | use SilverStripe\Core\Extensible; |
||||
17 | use SilverStripe\Core\Extension; |
||||
18 | use SilverStripe\Core\Validation\ValidationResult; |
||||
19 | use SilverStripe\Forms\CompositeField; |
||||
20 | use SilverStripe\Forms\FieldList; |
||||
21 | use SilverStripe\Forms\Form; |
||||
22 | use SilverStripe\Forms\FormAction; |
||||
23 | use SilverStripe\Forms\FormField; |
||||
24 | use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; |
||||
25 | use SilverStripe\Forms\HiddenField; |
||||
26 | use SilverStripe\Forms\Tab; |
||||
27 | use SilverStripe\Forms\TabSet; |
||||
28 | use SilverStripe\Model\ModelData; |
||||
29 | use SilverStripe\ORM\DataObject; |
||||
30 | use SilverStripe\ORM\FieldType\DBHTMLText; |
||||
31 | use SilverStripe\SiteConfig\SiteConfig; |
||||
32 | use SilverStripe\Versioned\VersionedGridFieldItemRequest; |
||||
33 | use SilverStripe\Control\RequestHandler; |
||||
34 | use SilverStripe\View\Requirements; |
||||
35 | |||||
36 | /** |
||||
37 | * Decorates GridDetailForm_ItemRequest to use new form actions and buttons. |
||||
38 | * |
||||
39 | * This is also applied to LeftAndMain to allow actions on pages |
||||
40 | * Warning: LeftAndMain doesn't call updateItemEditForm |
||||
41 | * |
||||
42 | * This is a lightweight version of BetterButtons that use default getCMSActions functionnality |
||||
43 | * on DataObjects |
||||
44 | * |
||||
45 | * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons |
||||
46 | * @link https://github.com/unclecheese/silverstripe-gridfield-betterbuttons/blob/master/src/Extensions/GridFieldBetterButtonsItemRequest.php |
||||
47 | * @property LeftAndMain|GridFieldDetailForm_ItemRequest|ActionsGridFieldItemRequest $owner |
||||
48 | * @extends Extension<object> |
||||
49 | */ |
||||
50 | class ActionsGridFieldItemRequest extends Extension |
||||
51 | { |
||||
52 | use Configurable; |
||||
53 | use Extensible; |
||||
54 | |||||
55 | /** |
||||
56 | * @config |
||||
57 | * @var boolean |
||||
58 | */ |
||||
59 | private static $enable_save_prev_next = true; |
||||
60 | |||||
61 | /** |
||||
62 | * @config |
||||
63 | * @var boolean |
||||
64 | */ |
||||
65 | private static $enable_save_close = true; |
||||
66 | |||||
67 | /** |
||||
68 | * @config |
||||
69 | * @var boolean |
||||
70 | */ |
||||
71 | private static $enable_delete_right = true; |
||||
72 | |||||
73 | /** |
||||
74 | * @config |
||||
75 | * @var boolean |
||||
76 | */ |
||||
77 | private static $enable_utils_prev_next = false; |
||||
78 | |||||
79 | /** |
||||
80 | * @var array<string> Allowed controller actions |
||||
81 | */ |
||||
82 | private static $allowed_actions = [ |
||||
83 | 'doSaveAndClose', |
||||
84 | 'doSaveAndNext', |
||||
85 | 'doSaveAndPrev', |
||||
86 | 'doCustomAction', // For CustomAction |
||||
87 | 'doCustomLink', // For CustomLink |
||||
88 | ]; |
||||
89 | |||||
90 | /** |
||||
91 | * @param FieldList $actions |
||||
92 | * @return array<string> |
||||
93 | */ |
||||
94 | protected function getAvailableActions($actions) |
||||
95 | { |
||||
96 | $list = []; |
||||
97 | foreach ($actions as $action) { |
||||
98 | if (is_a($action, CompositeField::class)) { |
||||
99 | $list = array_merge($list, $this->getAvailableActions($action->FieldList())); |
||||
100 | } else { |
||||
101 | $list[] = $action->getName(); |
||||
102 | } |
||||
103 | } |
||||
104 | return $list; |
||||
105 | } |
||||
106 | |||||
107 | /** |
||||
108 | * This module does not interact with the /schema/SearchForm endpoint |
||||
109 | * and therefore all requests for these urls don't need any special treatement |
||||
110 | * |
||||
111 | * @return bool |
||||
112 | */ |
||||
113 | protected function isSearchFormRequest(): bool |
||||
114 | { |
||||
115 | $curr = Controller::curr(); |
||||
116 | if ($curr === null) { |
||||
117 | return false; |
||||
118 | } |
||||
119 | return str_contains($curr->getRequest()->getURL(), '/schema/SearchForm'); |
||||
120 | } |
||||
121 | |||||
122 | /** |
||||
123 | * Called by CMSMain, typically in the CMS or in the SiteConfig admin |
||||
124 | * CMSMain already uses getCMSActions so we are good to go with anything defined there |
||||
125 | * |
||||
126 | * @param Form $form |
||||
127 | * @return void |
||||
128 | */ |
||||
129 | public function updateEditForm(Form $form) |
||||
130 | { |
||||
131 | // Ignore search form requests |
||||
132 | if ($this->isSearchFormRequest()) { |
||||
133 | return; |
||||
134 | } |
||||
135 | |||||
136 | $actions = $form->Actions(); |
||||
137 | |||||
138 | // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions |
||||
139 | // and we don't want to duplicate it |
||||
140 | $this->processDropUpMenu($actions); |
||||
141 | } |
||||
142 | |||||
143 | /** |
||||
144 | * @return FieldList|false |
||||
145 | */ |
||||
146 | public function recordCmsUtils() |
||||
147 | { |
||||
148 | /** @var VersionedGridFieldItemRequest|LeftAndMain $owner */ |
||||
149 | $owner = $this->getOwner(); |
||||
150 | |||||
151 | // At this stage, the get record could be from a gridfield item request, or from a more general left and main which requires an id |
||||
152 | // maybe we could simply do: |
||||
153 | // $record = DataObject::singleton($controller->getModelClass()); |
||||
154 | $reflectionMethod = new ReflectionMethod($owner, 'getRecord'); |
||||
155 | $record = count($reflectionMethod->getParameters()) > 0 ? $owner->getRecord(0) : $owner->getRecord(); |
||||
0 ignored issues
–
show
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
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. ![]() |
|||||
156 | if ($record && $record->hasMethod('getCMSUtils')) { |
||||
157 | //@phpstan-ignore-next-line |
||||
158 | $utils = $record->getCMSUtils(); |
||||
159 | $this->extend('onCMSUtils', $utils, $record); |
||||
160 | $record->extend('onCMSUtils', $utils); |
||||
161 | return $utils; |
||||
162 | } |
||||
163 | return false; |
||||
164 | } |
||||
165 | |||||
166 | /** |
||||
167 | * @param Form $form |
||||
168 | * @return void |
||||
169 | */ |
||||
170 | public function updateItemEditForm($form) |
||||
171 | { |
||||
172 | /** @var ?DataObject $record */ |
||||
173 | $record = $this->getOwner()->getRecord(); |
||||
174 | if (!$record) { |
||||
175 | return; |
||||
176 | } |
||||
177 | |||||
178 | // Display pending message after a X-Reload |
||||
179 | $curr = Controller::curr(); |
||||
180 | if ($curr && !Director::is_ajax() && $pendingMessage = $curr->getRequest()->getSession()->get('CmsActionsPendingMessage')) { |
||||
181 | $curr->getRequest()->getSession()->clear('CmsActionsPendingMessage'); |
||||
182 | $text = addslashes($pendingMessage['message'] ?? ''); |
||||
183 | $type = addslashes($pendingMessage['status'] ?? 'good'); |
||||
184 | Requirements::customScript("jQuery.noticeAdd({text: '$text', type: '$type', stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});"); |
||||
185 | } |
||||
186 | |||||
187 | // We get the actions as defined on our record |
||||
188 | $CMSActions = $this->getCmsActionsFromRecord($record); |
||||
189 | |||||
190 | $FormActions = $form->Actions(); |
||||
191 | |||||
192 | // Push our actions that are otherwise ignored by SilverStripe |
||||
193 | if ($CMSActions) { |
||||
194 | foreach ($CMSActions as $CMSAction) { |
||||
195 | $action = $FormActions->fieldByName($CMSAction->getName()); |
||||
196 | |||||
197 | if ($action) { |
||||
198 | // If it has been made readonly, revert |
||||
199 | if ($CMSAction->isReadonly() != $action->isReadonly()) { |
||||
200 | $FormActions->replaceField($action->getName(), $action->setReadonly($CMSAction->isReadonly())); |
||||
201 | } |
||||
202 | } |
||||
203 | } |
||||
204 | } |
||||
205 | } |
||||
206 | |||||
207 | /** |
||||
208 | * Called by GridField_ItemRequest |
||||
209 | * We add our custom save&close, save&next and other tweaks |
||||
210 | * Actions can be made readonly after this extension point |
||||
211 | * @param FieldList $actions |
||||
212 | * @return void |
||||
213 | */ |
||||
214 | public function updateFormActions($actions) |
||||
215 | { |
||||
216 | // Ignore search form requests |
||||
217 | if ($this->isSearchFormRequest()) { |
||||
218 | return; |
||||
219 | } |
||||
220 | |||||
221 | /** @var DataObject|ModelData|null $record */ |
||||
222 | $record = $this->getOwner()->getRecord(); |
||||
223 | if (!$record) { |
||||
224 | return; |
||||
225 | } |
||||
226 | |||||
227 | // We get the actions as defined on our record |
||||
228 | $CMSActions = $this->getCmsActionsFromRecord($record); |
||||
229 | |||||
230 | // The default button group that contains the Save or Create action |
||||
231 | // @link https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface/#extending-the-cms-actions |
||||
232 | $MajorActions = $actions->fieldByName('MajorActions'); |
||||
233 | |||||
234 | // If it doesn't exist, push to default group |
||||
235 | if (!$MajorActions) { |
||||
236 | $MajorActions = $actions; |
||||
0 ignored issues
–
show
|
|||||
237 | } |
||||
238 | |||||
239 | // Push our actions that are otherwise ignored by SilverStripe |
||||
240 | if ($CMSActions) { |
||||
241 | foreach ($CMSActions as $action) { |
||||
242 | // Avoid duplicated actions (eg: when added by SilverStripe\Versioned\VersionedGridFieldItemRequest) |
||||
243 | if ($actions->fieldByName($action->getName())) { |
||||
244 | continue; |
||||
245 | } |
||||
246 | $actions->push($action); |
||||
247 | } |
||||
248 | } |
||||
249 | |||||
250 | // We create a Drop-Up menu afterwards because it may already exist in the $CMSActions |
||||
251 | // and we don't want to duplicate it |
||||
252 | $this->processDropUpMenu($actions); |
||||
253 | |||||
254 | // Add extension hook |
||||
255 | $this->extend('onBeforeUpdateCMSActions', $actions, $record); |
||||
256 | $record->extend('onBeforeUpdateCMSActions', $actions); |
||||
257 | |||||
258 | $ActionMenus = $actions->fieldByName('ActionMenus'); |
||||
259 | // Re-insert ActionMenus to make sure they always follow the buttons |
||||
260 | if ($ActionMenus) { |
||||
261 | $actions->remove($ActionMenus); |
||||
262 | $actions->push($ActionMenus); |
||||
263 | } |
||||
264 | |||||
265 | // We have a 4.4 setup, before that there was no RightGroup |
||||
266 | $RightGroup = $this->getRightGroupActions($actions); |
||||
267 | |||||
268 | // Insert again to make sure our actions are properly placed after apply changes |
||||
269 | if ($RightGroup) { |
||||
270 | $actions->remove($RightGroup); |
||||
271 | $actions->push($RightGroup); |
||||
272 | } |
||||
273 | |||||
274 | $opts = [ |
||||
275 | 'save_close' => self::config()->enable_save_close, |
||||
276 | 'save_prev_next' => self::config()->enable_save_prev_next, |
||||
277 | 'delete_right' => self::config()->enable_delete_right, |
||||
278 | ]; |
||||
279 | if ($record->hasMethod('getCMSActionsOptions')) { |
||||
280 | $opts = array_merge($opts, $record->getCMSActionsOptions()); |
||||
281 | } |
||||
282 | |||||
283 | if ($opts['save_close']) { |
||||
284 | $this->addSaveAndClose($actions, $record); |
||||
285 | } |
||||
286 | |||||
287 | if ($opts['save_prev_next']) { |
||||
288 | $this->addSaveNextAndPrevious($actions, $record); |
||||
289 | } |
||||
290 | |||||
291 | if ($opts['delete_right']) { |
||||
292 | $this->moveCancelAndDelete($actions, $record); |
||||
293 | } |
||||
294 | |||||
295 | // Fix gridstate being lost when running custom actions |
||||
296 | if (method_exists($this->getOwner(), 'getStateManager')) { |
||||
297 | $request = $this->getOwner()->getRequest(); |
||||
298 | $stateManager = $this->getOwner()->getStateManager(); |
||||
299 | $gridField = $this->getOwner()->getGridField(); |
||||
300 | $state = $stateManager->getStateFromRequest($gridField, $request); |
||||
301 | $actions->push(HiddenField::create($stateManager->getStateKey($gridField), null, $state)); |
||||
302 | } |
||||
303 | |||||
304 | // Add extension hook |
||||
305 | $this->extend('onAfterUpdateCMSActions', $actions, $record); |
||||
306 | $record->extend('onAfterUpdateCMSActions', $actions); |
||||
307 | } |
||||
308 | |||||
309 | /** |
||||
310 | * Collect all Drop-Up actions into a menu. |
||||
311 | * @param FieldList $actions |
||||
312 | * @return void |
||||
313 | */ |
||||
314 | protected function processDropUpMenu($actions) |
||||
315 | { |
||||
316 | // The Drop-up container may already exist |
||||
317 | /** @var ?Tab $dropUpContainer */ |
||||
318 | $dropUpContainer = $actions->fieldByName('ActionMenus.MoreOptions'); |
||||
319 | foreach ($actions as $action) { |
||||
320 | //@phpstan-ignore-next-line |
||||
321 | if ($action->hasMethod('getDropUp') && $action->getDropUp()) { |
||||
322 | if (!$dropUpContainer) { |
||||
323 | $dropUpContainer = $this->createDropUpContainer($actions); |
||||
324 | } |
||||
325 | $action->getContainerFieldList()->removeByName($action->getName()); |
||||
326 | $dropUpContainer->push($action); |
||||
0 ignored issues
–
show
It seems like
$action can also be of type SilverStripe\Model\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
![]() |
|||||
327 | } |
||||
328 | } |
||||
329 | } |
||||
330 | |||||
331 | /** |
||||
332 | * Prepares a Drop-Up menu |
||||
333 | * @param FieldList $actions |
||||
334 | * @return Tab |
||||
335 | */ |
||||
336 | protected function createDropUpContainer($actions) |
||||
337 | { |
||||
338 | $rootTabSet = TabSet::create('ActionMenus'); |
||||
339 | $dropUpContainer = Tab::create( |
||||
340 | 'MoreOptions', |
||||
341 | _t(__CLASS__ . '.MoreOptions', 'More options', 'Expands a view for more buttons') |
||||
342 | ); |
||||
343 | $dropUpContainer->addExtraClass('popover-actions-simulate'); |
||||
344 | $rootTabSet->push($dropUpContainer); |
||||
345 | $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder'); |
||||
346 | |||||
347 | $actions->insertBefore('RightGroup', $rootTabSet); |
||||
348 | |||||
349 | return $dropUpContainer; |
||||
350 | } |
||||
351 | |||||
352 | /** |
||||
353 | * Check if a record can be edited/created/exists |
||||
354 | * @param ModelData $record |
||||
355 | * @param bool $editOnly |
||||
356 | * @return bool |
||||
357 | */ |
||||
358 | protected function checkCan($record, $editOnly = false) |
||||
359 | { |
||||
360 | // For ViewableData, we assume all methods should be implemented |
||||
361 | // @link https://docs.silverstripe.org/en/5/developer_guides/forms/using_gridfield_with_arbitrary_data/#custom-edit |
||||
362 | if (!method_exists($record, 'canEdit') || !method_exists($record, 'canCreate')) { |
||||
363 | return false; |
||||
364 | } |
||||
365 | //@phpstan-ignore-next-line |
||||
366 | if (!$record->ID && ($editOnly || !$record->canCreate())) { |
||||
367 | return false; |
||||
368 | } |
||||
369 | if (!$record->canEdit()) { |
||||
370 | return false; |
||||
371 | } |
||||
372 | |||||
373 | return true; |
||||
374 | } |
||||
375 | |||||
376 | /** |
||||
377 | * @param ModelData $record |
||||
378 | * @return ?FieldList |
||||
379 | */ |
||||
380 | protected function getCmsActionsFromRecord(ModelData $record) |
||||
381 | { |
||||
382 | if ($record instanceof DataObject) { |
||||
383 | return $record->getCMSActions(); |
||||
384 | } |
||||
385 | if (method_exists($record, 'getCMSActions')) { |
||||
386 | return $record->getCMSActions(); |
||||
387 | } |
||||
388 | return null; |
||||
389 | } |
||||
390 | |||||
391 | /** |
||||
392 | * @param FieldList $actions |
||||
393 | * @param ModelData $record |
||||
394 | * @return void |
||||
395 | */ |
||||
396 | public function moveCancelAndDelete(FieldList $actions, ModelData $record) |
||||
397 | { |
||||
398 | // We have a 4.4 setup, before that there was no RightGroup |
||||
399 | $RightGroup = $actions->fieldByName('RightGroup'); |
||||
400 | |||||
401 | // Move delete at the end |
||||
402 | $deleteAction = $actions->fieldByName('action_doDelete'); |
||||
403 | if ($deleteAction) { |
||||
404 | // Move at the end of the stack |
||||
405 | $actions->remove($deleteAction); |
||||
406 | $actions->push($deleteAction); |
||||
407 | |||||
408 | $deleteAction->addExtraClass('btn-group-spacer'); |
||||
409 | if (!$RightGroup) { |
||||
410 | // Only necessary pre 4.4 |
||||
411 | $deleteAction->addExtraClass('align-right'); |
||||
412 | } |
||||
413 | // Set custom title |
||||
414 | if ($record->hasMethod('getDeleteButtonTitle')) { |
||||
415 | //@phpstan-ignore-next-line |
||||
416 | $deleteAction->setTitle($record->getDeleteButtonTitle()); |
||||
417 | } |
||||
418 | } else { |
||||
419 | // Add disabled delete action to avoid clicking by mistake on delete |
||||
420 | // if it was not there when navigating |
||||
421 | $actions->push($fakeDelete = new FormAction('doDelete', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Delete', 'Delete'))); |
||||
422 | $fakeDelete |
||||
423 | ->setUseButtonTag(true) |
||||
424 | ->addExtraClass('btn-group-spacer btn-hide-outline font-icon-trash-bin') |
||||
425 | ->setDisabled(true); |
||||
426 | } |
||||
427 | // Move cancel at the end |
||||
428 | $cancelButton = $actions->fieldByName('cancelbutton'); |
||||
429 | if ($cancelButton) { |
||||
430 | // Move at the end of the stack |
||||
431 | $actions->remove($cancelButton); |
||||
432 | $actions->push($cancelButton); |
||||
433 | |||||
434 | $cancelButton->addExtraClass('btn-group-spacer'); |
||||
435 | if (!$RightGroup) { |
||||
436 | // Only necessary pre 4.4 |
||||
437 | $cancelButton->addExtraClass('align-right'); |
||||
438 | } |
||||
439 | // Set custom titlte |
||||
440 | if ($record->hasMethod('getCancelButtonTitle')) { |
||||
441 | //@phpstan-ignore-next-line |
||||
442 | $cancelButton->setTitle($record->getCancelButtonTitle()); |
||||
443 | } |
||||
444 | } |
||||
445 | } |
||||
446 | |||||
447 | /** |
||||
448 | * @param ModelData $record |
||||
449 | * @return bool |
||||
450 | */ |
||||
451 | public function useCustomPrevNext(ModelData $record): bool |
||||
452 | { |
||||
453 | if (self::config()->enable_custom_prevnext) { |
||||
454 | return $record->hasMethod('PrevRecord') && $record->hasMethod('NextRecord'); |
||||
455 | } |
||||
456 | return false; |
||||
457 | } |
||||
458 | |||||
459 | /** |
||||
460 | * @param ModelData $record |
||||
461 | * @return int |
||||
462 | */ |
||||
463 | public function getCustomPreviousRecordID(ModelData $record) |
||||
464 | { |
||||
465 | // This will overwrite state provided record |
||||
466 | if ($this->useCustomPrevNext($record)) { |
||||
467 | //@phpstan-ignore-next-line |
||||
468 | return $record->PrevRecord()->ID ?? 0; |
||||
469 | } |
||||
470 | return $this->getOwner()->getPreviousRecordID(); |
||||
471 | } |
||||
472 | |||||
473 | /** |
||||
474 | * @param ModelData $record |
||||
475 | * @return int |
||||
476 | */ |
||||
477 | public function getCustomNextRecordID(ModelData $record) |
||||
478 | { |
||||
479 | // This will overwrite state provided record |
||||
480 | if ($this->useCustomPrevNext($record)) { |
||||
481 | //@phpstan-ignore-next-line |
||||
482 | return $record->NextRecord()->ID ?? 0; |
||||
483 | } |
||||
484 | return $this->getOwner()->getNextRecordID(); |
||||
485 | } |
||||
486 | |||||
487 | /** |
||||
488 | * @param FieldList $actions |
||||
489 | * @return CompositeField|FieldList |
||||
490 | */ |
||||
491 | protected function getMajorActions(FieldList $actions) |
||||
492 | { |
||||
493 | /** @var ?CompositeField $MajorActions */ |
||||
494 | $MajorActions = $actions->fieldByName('MajorActions'); |
||||
495 | |||||
496 | // If it doesn't exist, push to default group |
||||
497 | if (!$MajorActions) { |
||||
498 | $MajorActions = $actions; |
||||
499 | } |
||||
500 | return $MajorActions; |
||||
501 | } |
||||
502 | |||||
503 | /** |
||||
504 | * @param FieldList $actions |
||||
505 | * @return CompositeField |
||||
506 | */ |
||||
507 | protected function getRightGroupActions(FieldList $actions) |
||||
508 | { |
||||
509 | /** @var ?CompositeField $RightGroup */ |
||||
510 | $RightGroup = $actions->fieldByName('RightGroup'); |
||||
511 | return $RightGroup; |
||||
512 | } |
||||
513 | |||||
514 | /** |
||||
515 | * @param FieldList $actions |
||||
516 | * @param ModelData $record |
||||
517 | * @return void |
||||
518 | */ |
||||
519 | public function addSaveNextAndPrevious(FieldList $actions, ModelData $record) |
||||
520 | { |
||||
521 | if (!$this->checkCan($record, true)) { |
||||
522 | return; |
||||
523 | } |
||||
524 | |||||
525 | $MajorActions = $this->getMajorActions($actions); |
||||
526 | |||||
527 | // @link https://github.com/silverstripe/silverstripe-framework/issues/10742 |
||||
528 | $getPreviousRecordID = $this->getCustomPreviousRecordID($record); |
||||
529 | $getNextRecordID = $this->getCustomNextRecordID($record); |
||||
530 | $isCustom = $this->useCustomPrevNext($record); |
||||
531 | |||||
532 | // Coupling for HasPrevNextUtils |
||||
533 | if (Controller::curr() instanceof Controller) { |
||||
534 | $prevLink = $nextLink = null; |
||||
535 | if (!$isCustom && $this->getOwner() instanceof GridFieldDetailForm_ItemRequest) { |
||||
536 | if ($getPreviousRecordID) { |
||||
537 | $prevLink = $this->getPublicEditLinkForAdjacentRecord(-1); |
||||
538 | } |
||||
539 | if ($getNextRecordID) { |
||||
540 | $nextLink = $this->getPublicEditLinkForAdjacentRecord(+1); |
||||
541 | } |
||||
542 | } |
||||
543 | |||||
544 | /** @var HTTPRequest $request */ |
||||
545 | $request = Controller::curr()->getRequest(); |
||||
546 | $routeParams = $request->routeParams(); |
||||
547 | $recordClass = get_class($record); |
||||
548 | $routeParams['cmsactions'][$recordClass]['PreviousRecordID'] = $getPreviousRecordID; |
||||
549 | $routeParams['cmsactions'][$recordClass]['NextRecordID'] = $getNextRecordID; |
||||
550 | $routeParams['cmsactions'][$recordClass]['PrevRecordLink'] = $prevLink; |
||||
551 | $routeParams['cmsactions'][$recordClass]['NextRecordLink'] = $nextLink; |
||||
552 | $request->setRouteParams($routeParams); |
||||
553 | } |
||||
554 | |||||
555 | if ($getPreviousRecordID) { |
||||
556 | $doSaveAndPrev = FormAction::create( |
||||
557 | 'doSaveAndPrev', |
||||
558 | _t('ActionsGridFieldItemRequest.SAVEANDPREVIOUS', 'Save and Previous') |
||||
559 | ); |
||||
560 | $doSaveAndPrev->addExtraClass($this->getBtnClassForRecord($record)); |
||||
561 | $doSaveAndPrev->addExtraClass('font-icon-angle-double-left btn-mobile-collapse'); |
||||
562 | $doSaveAndPrev->setUseButtonTag(true); |
||||
563 | $MajorActions->push($doSaveAndPrev); |
||||
564 | } |
||||
565 | if ($getNextRecordID) { |
||||
566 | $doSaveAndNext = FormAction::create( |
||||
567 | 'doSaveAndNext', |
||||
568 | _t('ActionsGridFieldItemRequest.SAVEANDNEXT', 'Save and Next') |
||||
569 | ); |
||||
570 | $doSaveAndNext->addExtraClass($this->getBtnClassForRecord($record)); |
||||
571 | $doSaveAndNext->addExtraClass('font-icon-angle-double-right btn-mobile-collapse'); |
||||
572 | $doSaveAndNext->setUseButtonTag(true); |
||||
573 | $MajorActions->push($doSaveAndNext); |
||||
574 | } |
||||
575 | } |
||||
576 | |||||
577 | public function getPublicEditLinkForAdjacentRecord(int $offset): ?string |
||||
578 | { |
||||
579 | $this->getOwner()->getStateManager(); |
||||
580 | $reflObject = new ReflectionObject($this->getOwner()); |
||||
581 | $reflMethod = $reflObject->getMethod('getEditLinkForAdjacentRecord'); |
||||
582 | $reflMethod->setAccessible(true); |
||||
583 | |||||
584 | try { |
||||
585 | return $reflMethod->invoke($this->getOwner(), $offset); |
||||
586 | } catch (Exception $e) { |
||||
587 | return null; |
||||
588 | } |
||||
589 | } |
||||
590 | |||||
591 | /** |
||||
592 | * @param FieldList $actions |
||||
593 | * @param ModelData $record |
||||
594 | * @return void |
||||
595 | */ |
||||
596 | public function addSaveAndClose(FieldList $actions, ModelData $record) |
||||
597 | { |
||||
598 | if (!$this->checkCan($record)) { |
||||
599 | return; |
||||
600 | } |
||||
601 | |||||
602 | $MajorActions = $this->getMajorActions($actions); |
||||
603 | |||||
604 | //@phpstan-ignore-next-line |
||||
605 | if ($record->ID) { |
||||
606 | $label = _t('ActionsGridFieldItemRequest.SAVEANDCLOSE', 'Save and Close'); |
||||
607 | } else { |
||||
608 | $label = _t('ActionsGridFieldItemRequest.CREATEANDCLOSE', 'Create and Close'); |
||||
609 | } |
||||
610 | $saveAndClose = FormAction::create('doSaveAndClose', $label); |
||||
611 | $saveAndClose->addExtraClass($this->getBtnClassForRecord($record)); |
||||
612 | $saveAndClose->setAttribute('data-text-alternate', $label); |
||||
613 | |||||
614 | if ($record->ID) { |
||||
615 | $saveAndClose->setAttribute('data-btn-alternate-add', 'btn-primary'); |
||||
616 | $saveAndClose->setAttribute('data-btn-alternate-remove', 'btn-outline-primary'); |
||||
617 | } |
||||
618 | $saveAndClose->addExtraClass('font-icon-level-up btn-mobile-collapse'); |
||||
619 | $saveAndClose->setUseButtonTag(true); |
||||
620 | $MajorActions->push($saveAndClose); |
||||
621 | } |
||||
622 | |||||
623 | /** |
||||
624 | * New and existing records have different classes |
||||
625 | * |
||||
626 | * @param ModelData $record |
||||
627 | * @return string |
||||
628 | */ |
||||
629 | protected function getBtnClassForRecord(ModelData $record) |
||||
630 | { |
||||
631 | //@phpstan-ignore-next-line |
||||
632 | if ($record->ID) { |
||||
633 | return 'btn-outline-primary'; |
||||
634 | } |
||||
635 | return 'btn-primary'; |
||||
636 | } |
||||
637 | |||||
638 | /** |
||||
639 | * @param string $action |
||||
640 | * @param array<FormField>|FieldList $definedActions |
||||
641 | * @return FormField|null |
||||
642 | */ |
||||
643 | protected static function findAction($action, $definedActions) |
||||
644 | { |
||||
645 | $result = null; |
||||
646 | |||||
647 | foreach ($definedActions as $definedAction) { |
||||
648 | if (is_a($definedAction, CompositeField::class)) { |
||||
649 | $result = self::findAction($action, $definedAction->FieldList()); |
||||
650 | if ($result) { |
||||
651 | break; |
||||
652 | } |
||||
653 | } |
||||
654 | |||||
655 | $definedActionName = $definedAction->getName(); |
||||
656 | |||||
657 | if ($definedAction->hasMethod('actionName')) { |
||||
658 | //@phpstan-ignore-next-line |
||||
659 | $definedActionName = $definedAction->actionName(); |
||||
660 | } |
||||
661 | if ($definedActionName === $action) { |
||||
662 | $result = $definedAction; |
||||
663 | break; |
||||
664 | } |
||||
665 | } |
||||
666 | |||||
667 | return $result; |
||||
668 | } |
||||
669 | |||||
670 | /** |
||||
671 | * Forward a given action to a DataObject |
||||
672 | * |
||||
673 | * Action must be declared in getCMSActions to be called |
||||
674 | * |
||||
675 | * @param string $action |
||||
676 | * @param array<string,mixed> $data |
||||
677 | * @param Form $form |
||||
678 | * @return HTTPResponse|DBHTMLText|string |
||||
679 | * @throws HTTPResponse_Exception |
||||
680 | */ |
||||
681 | protected function forwardActionToRecord($action, $data = [], $form = null) |
||||
682 | { |
||||
683 | $controller = $this->getToplevelController(); |
||||
684 | |||||
685 | // We have an item request or a controller that can provide a record |
||||
686 | $record = null; |
||||
687 | if ($this->getOwner()->hasMethod('ItemEditForm')) { |
||||
688 | // It's a request handler. Don't check for a specific class as it may be subclassed |
||||
689 | //@phpstan-ignore-next-line |
||||
690 | $record = $this->getOwner()->record; |
||||
691 | } elseif ($controller->hasMethod('save_siteconfig')) { |
||||
692 | // Check for any type of siteconfig controller |
||||
693 | $record = SiteConfig::current_site_config(); |
||||
694 | } elseif (!empty($data['ClassName']) && !empty($data['ID'])) { |
||||
695 | $record = DataObject::get_by_id($data['ClassName'], $data['ID']); |
||||
696 | } elseif ($controller->hasMethod("getRecord")) { |
||||
697 | // LeftAndMain requires an id |
||||
698 | if ($controller instanceof LeftAndMain && !empty($data['ID'])) { |
||||
699 | $record = $controller->getRecord($data['ID']); |
||||
700 | } elseif ($controller instanceof ModelAdmin) { |
||||
701 | // Otherwise fallback to singleton |
||||
702 | $record = DataObject::singleton($controller->getModelClass()); |
||||
703 | } |
||||
704 | } |
||||
705 | |||||
706 | if (!$record) { |
||||
707 | throw new Exception("No record to handle the action $action on " . get_class($controller)); |
||||
708 | } |
||||
709 | $CMSActions = $this->getCmsActionsFromRecord($record); |
||||
710 | |||||
711 | // Check if the action is indeed available |
||||
712 | $clickedAction = null; |
||||
713 | if (!empty($CMSActions)) { |
||||
714 | $clickedAction = self::findAction($action, $CMSActions); |
||||
715 | } |
||||
716 | if (!$clickedAction) { |
||||
717 | $class = get_class($record); |
||||
718 | $availableActions = null; |
||||
719 | if ($CMSActions) { |
||||
720 | $availableActions = implode(',', $this->getAvailableActions($CMSActions)); |
||||
721 | } |
||||
722 | if (!$availableActions) { |
||||
723 | $availableActions = "(no available actions, please check getCMSActions)"; |
||||
724 | } |
||||
725 | |||||
726 | return $this->getOwner()->httpError(403, sprintf( |
||||
727 | 'Action not available on %s. It must be one of : %s', |
||||
728 | $class, |
||||
729 | $availableActions |
||||
730 | )); |
||||
731 | } |
||||
732 | |||||
733 | if ($clickedAction->isReadonly() || $clickedAction->isDisabled()) { |
||||
734 | return $this->getOwner()->httpError(403, sprintf( |
||||
735 | 'Action %s is disabled', |
||||
736 | $clickedAction->getName(), |
||||
737 | )); |
||||
738 | } |
||||
739 | |||||
740 | $message = null; |
||||
741 | $error = false; |
||||
742 | |||||
743 | // Check record BEFORE the action |
||||
744 | // It can be deleted by the action, and it will return to the list |
||||
745 | $isNewRecord = isset($record->ID) && $record->ID === 0; |
||||
746 | |||||
747 | $actionTitle = $clickedAction->getName(); |
||||
748 | if (method_exists($clickedAction, 'getTitle')) { |
||||
749 | $actionTitle = $clickedAction->getTitle(); |
||||
750 | } |
||||
751 | |||||
752 | $recordName = $record instanceof DataObject ? $record->i18n_singular_name() : _t( |
||||
753 | 'ActionsGridFieldItemRequest.record', |
||||
754 | 'record' |
||||
755 | ); |
||||
756 | |||||
757 | try { |
||||
758 | $result = $record->$action($data, $form, $controller); |
||||
759 | |||||
760 | // We have a response |
||||
761 | if ($result instanceof HTTPResponse) { |
||||
762 | return $result; |
||||
763 | } |
||||
764 | |||||
765 | if ($result === false) { |
||||
766 | // Result returned an error (false) |
||||
767 | $error = true; |
||||
768 | $message = _t( |
||||
769 | 'ActionsGridFieldItemRequest.FAILED', |
||||
770 | 'Action {action} failed on {name}', |
||||
771 | [ |
||||
772 | 'action' => $actionTitle, |
||||
773 | 'name' => $recordName, |
||||
774 | ] |
||||
775 | ); |
||||
776 | } elseif (is_string($result)) { |
||||
777 | // Result is a message |
||||
778 | $message = $result; |
||||
779 | } |
||||
780 | } catch (Exception $ex) { |
||||
781 | $result = null; |
||||
782 | $error = true; |
||||
783 | $message = $ex->getMessage(); |
||||
784 | } |
||||
785 | |||||
786 | // Build default message |
||||
787 | if (!$message) { |
||||
788 | $message = _t( |
||||
789 | 'ActionsGridFieldItemRequest.DONE', |
||||
790 | 'Action {action} was done on {name}', |
||||
791 | [ |
||||
792 | 'action' => $actionTitle, |
||||
793 | 'name' => $recordName, |
||||
794 | ] |
||||
795 | ); |
||||
796 | } |
||||
797 | $status = 'good'; |
||||
798 | if ($error) { |
||||
799 | $status = 'bad'; |
||||
800 | } |
||||
801 | |||||
802 | // Progressive actions return array with json data |
||||
803 | if (method_exists($clickedAction, 'getProgressive') && $clickedAction->getProgressive()) { |
||||
804 | $response = $controller->getResponse(); |
||||
805 | $response->addHeader('Content-Type', 'application/json'); |
||||
806 | if ($result) { |
||||
807 | $encodedResult = json_encode($result); |
||||
808 | if (!$encodedResult) { |
||||
809 | $encodedResult = json_last_error_msg(); |
||||
810 | } |
||||
811 | $response->setBody($encodedResult); |
||||
812 | } |
||||
813 | |||||
814 | return $response; |
||||
815 | } |
||||
816 | |||||
817 | // We don't have a form, simply return the result |
||||
818 | if (!$form) { |
||||
819 | if ($error) { |
||||
820 | return $this->getOwner()->httpError(403, $message); |
||||
821 | } |
||||
822 | |||||
823 | return $message; |
||||
824 | } |
||||
825 | |||||
826 | $shouldRefresh = method_exists($clickedAction, 'getShouldRefresh') && $clickedAction->getShouldRefresh(); |
||||
827 | |||||
828 | // When loading using pjax, we can show toasts through X-Status |
||||
829 | if (Director::is_ajax()) { |
||||
830 | $controller->getResponse()->addHeader('X-Status', rawurlencode($message)); |
||||
831 | // 4xx status makes a red box |
||||
832 | if ($error) { |
||||
833 | $controller->getResponse()->setStatusCode(400); |
||||
834 | } |
||||
835 | |||||
836 | if ($shouldRefresh) { |
||||
837 | self::addXReload($controller); |
||||
838 | |||||
839 | // Store a pending session message to display after reload |
||||
840 | $controller->getRequest()->getSession()->set('CmsActionsPendingMessage', [ |
||||
841 | 'message' => $message, |
||||
842 | 'status' => $status, |
||||
843 | ]); |
||||
844 | } |
||||
845 | } else { |
||||
846 | // If the controller support sessionMessage, use it instead of form |
||||
847 | if ($controller->hasMethod('sessionMessage')) { |
||||
848 | //@phpstan-ignore-next-line |
||||
849 | $controller->sessionMessage($message, $status, ValidationResult::CAST_HTML); |
||||
850 | } else { |
||||
851 | $form->sessionMessage($message, $status, ValidationResult::CAST_HTML); |
||||
852 | } |
||||
853 | } |
||||
854 | |||||
855 | // Custom redirect |
||||
856 | /** @var CustomAction $clickedAction */ |
||||
857 | if (method_exists($clickedAction, 'getRedirectURL') && $clickedAction->getRedirectURL()) { |
||||
858 | // we probably need a full ui refresh |
||||
859 | self::addXReload($controller, $clickedAction->getRedirectURL()); |
||||
860 | return $controller->redirect($clickedAction->getRedirectURL()); |
||||
861 | } |
||||
862 | |||||
863 | // Redirect after action |
||||
864 | return $this->redirectAfterAction($isNewRecord, $record); |
||||
865 | } |
||||
866 | |||||
867 | /** |
||||
868 | * Adds X-Reload headers |
||||
869 | * |
||||
870 | * @param Controller $controller |
||||
871 | * @param string|null $url |
||||
872 | * @return bool Returns true if will reload |
||||
873 | */ |
||||
874 | public static function addXReload(Controller $controller, ?string $url = null): bool |
||||
875 | { |
||||
876 | if (!$url) { |
||||
877 | $url = $controller->getReferer(); |
||||
878 | } |
||||
879 | if (!$url) { |
||||
880 | $url = $controller->getBackURL(); |
||||
881 | } |
||||
882 | if (!$url) { |
||||
883 | return false; |
||||
884 | } |
||||
885 | // Triggers a full reload. Needs both headers to work |
||||
886 | // @link https://github.com/silverstripe/silverstripe-admin/blob/3/client/src/legacy/LeftAndMain.js#L819 |
||||
887 | $controller->getResponse()->addHeader('X-ControllerURL', $url); |
||||
888 | $controller->getResponse()->addHeader('X-Reload', "true"); |
||||
889 | return true; |
||||
890 | } |
||||
891 | |||||
892 | /** |
||||
893 | * Handles custom links |
||||
894 | * |
||||
895 | * Use CustomLink with default behaviour to trigger this |
||||
896 | * |
||||
897 | * See: |
||||
898 | * DefaultLink::getModelLink |
||||
899 | * GridFieldCustomLink::getLink |
||||
900 | * |
||||
901 | * @param HTTPRequest $request |
||||
902 | * @return HTTPResponse|DBHTMLText|string |
||||
903 | * @throws Exception |
||||
904 | */ |
||||
905 | public function doCustomLink(HTTPRequest $request) |
||||
906 | { |
||||
907 | $action = $request->getVar('CustomLink'); |
||||
908 | return $this->forwardActionToRecord($action); |
||||
909 | } |
||||
910 | |||||
911 | /** |
||||
912 | * Handles custom actions |
||||
913 | * |
||||
914 | * Use CustomAction class to trigger this |
||||
915 | * |
||||
916 | * Nested actions are submitted like this |
||||
917 | * [action_doCustomAction] => Array |
||||
918 | * ( |
||||
919 | * [doTestAction] => 1 |
||||
920 | * ) |
||||
921 | * |
||||
922 | * @param array<string,mixed> $data The form data |
||||
923 | * @param Form $form The form object |
||||
924 | * @return HTTPResponse|DBHTMLText|string |
||||
925 | * @throws Exception |
||||
926 | */ |
||||
927 | public function doCustomAction($data, $form) |
||||
928 | { |
||||
929 | $action = key($data['action_doCustomAction']); |
||||
930 | return $this->forwardActionToRecord($action, $data, $form); |
||||
931 | } |
||||
932 | |||||
933 | /** |
||||
934 | * Saves the form and goes back to list view |
||||
935 | * |
||||
936 | * @param array<string,mixed> $data The form data |
||||
937 | * @param Form $form The form object |
||||
938 | * @return HTTPResponse |
||||
939 | */ |
||||
940 | public function doSaveAndClose($data, $form) |
||||
941 | { |
||||
942 | $this->getOwner()->doSave($data, $form); |
||||
943 | // Redirect after save |
||||
944 | $controller = $this->getToplevelController(); |
||||
945 | |||||
946 | $link = $this->getBackLink(); |
||||
947 | |||||
948 | // Doesn't seem to be needed anymore |
||||
949 | // $link = $this->addGridState($link, $data); |
||||
950 | |||||
951 | $controller->getResponse()->addHeader("X-Pjax", "Content"); |
||||
952 | |||||
953 | return $controller->redirect($link); |
||||
954 | } |
||||
955 | |||||
956 | /** |
||||
957 | * @param string $dir prev|next |
||||
958 | * @param array<string,mixed> $data The form data |
||||
959 | * @param Form|null $form |
||||
960 | * @return HTTPResponse |
||||
961 | */ |
||||
962 | protected function doSaveAndAdjacent(string $dir, array $data, ?Form $form) |
||||
963 | { |
||||
964 | //@phpstan-ignore-next-line |
||||
965 | $record = $this->getOwner()->record; |
||||
966 | $this->getOwner()->doSave($data, $form); |
||||
967 | // Redirect after save |
||||
968 | $controller = $this->getToplevelController(); |
||||
969 | $controller->getResponse()->addHeader("X-Pjax", "Content"); |
||||
970 | |||||
971 | if (!($record instanceof DataObject)) { |
||||
972 | throw new Exception("Works only with DataObject"); |
||||
973 | } |
||||
974 | |||||
975 | $class = get_class($record); |
||||
976 | if (!$class) { |
||||
977 | throw new Exception("Could not get class"); |
||||
978 | } |
||||
979 | |||||
980 | if (!in_array($dir, ['prev', 'next'])) { |
||||
981 | throw new Exception("Invalid dir $dir"); |
||||
982 | } |
||||
983 | |||||
984 | $method = match ($dir) { |
||||
985 | 'prev' => 'getCustomPreviousRecordID', |
||||
986 | 'next' => 'getCustomNextRecordID', |
||||
987 | }; |
||||
988 | |||||
989 | $offset = match ($dir) { |
||||
990 | 'prev' => -1, |
||||
991 | 'next' => +1, |
||||
992 | }; |
||||
993 | |||||
994 | $adjRecordID = $this->$method($record); |
||||
995 | |||||
996 | /** @var ?DataObject $adj */ |
||||
997 | $adj = $class::get()->byID($adjRecordID); |
||||
998 | |||||
999 | $useCustom = $this->useCustomPrevNext($record); |
||||
1000 | $link = $this->getPublicEditLinkForAdjacentRecord($offset); |
||||
1001 | if (!$link || $useCustom) { |
||||
1002 | $link = $this->getOwner()->getEditLink($adjRecordID); |
||||
1003 | $link = $this->addGridState($link, $data); |
||||
0 ignored issues
–
show
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
![]() |
|||||
1004 | } |
||||
1005 | |||||
1006 | // Link to a specific tab if set, see cms-actions.js |
||||
1007 | if ($adj && !empty($data['_activetab'])) { |
||||
1008 | $link .= sprintf('#%s', $data['_activetab']); |
||||
1009 | } |
||||
1010 | |||||
1011 | return $controller->redirect($link); |
||||
1012 | } |
||||
1013 | |||||
1014 | /** |
||||
1015 | * Saves the form and goes back to the next item |
||||
1016 | * |
||||
1017 | * @param array<string,mixed> $data The form data |
||||
1018 | * @param Form $form The form object |
||||
1019 | * @return HTTPResponse |
||||
1020 | */ |
||||
1021 | public function doSaveAndNext($data, $form) |
||||
1022 | { |
||||
1023 | return $this->doSaveAndAdjacent('next', $data, $form); |
||||
1024 | } |
||||
1025 | |||||
1026 | /** |
||||
1027 | * Saves the form and goes to the previous item |
||||
1028 | * |
||||
1029 | * @param array<string,mixed> $data The form data |
||||
1030 | * @param Form $form The form object |
||||
1031 | * @return HTTPResponse |
||||
1032 | */ |
||||
1033 | public function doSaveAndPrev($data, $form) |
||||
1034 | { |
||||
1035 | return $this->doSaveAndAdjacent('prev', $data, $form); |
||||
1036 | } |
||||
1037 | |||||
1038 | /** |
||||
1039 | * Check if we can remove this safely |
||||
1040 | * @param string $url |
||||
1041 | * @param array<mixed> $data |
||||
1042 | * @return string |
||||
1043 | * @deprecated |
||||
1044 | */ |
||||
1045 | protected function addGridState($url, $data) |
||||
1046 | { |
||||
1047 | // This should not be necessary at all if the state is correctly passed along |
||||
1048 | $BackURL = $data['BackURL'] ?? null; |
||||
1049 | if ($BackURL) { |
||||
1050 | $query = parse_url($BackURL, PHP_URL_QUERY); |
||||
1051 | if ($query) { |
||||
1052 | $url = strtok($url, '?'); |
||||
1053 | $url .= '?' . $query; |
||||
1054 | } |
||||
1055 | } |
||||
1056 | return $url; |
||||
1057 | } |
||||
1058 | |||||
1059 | /** |
||||
1060 | * Gets the top level controller. |
||||
1061 | * |
||||
1062 | * @return Controller|RequestHandler |
||||
1063 | */ |
||||
1064 | protected function getToplevelController() |
||||
1065 | { |
||||
1066 | if ($this->isLeftAndMain($this->getOwner())) { |
||||
1067 | return $this->getOwner(); |
||||
1068 | } |
||||
1069 | if (!$this->getOwner()->hasMethod("getController")) { |
||||
1070 | return Controller::curr(); |
||||
1071 | } |
||||
1072 | $controller = $this->getOwner()->getController(); |
||||
1073 | while ($controller instanceof GridFieldDetailForm_ItemRequest) { |
||||
1074 | $controller = $controller->getController(); |
||||
1075 | } |
||||
1076 | |||||
1077 | return $controller; |
||||
1078 | } |
||||
1079 | |||||
1080 | /** |
||||
1081 | * @param Controller $controller |
||||
1082 | * @return boolean |
||||
1083 | */ |
||||
1084 | protected function isLeftAndMain($controller) |
||||
1085 | { |
||||
1086 | return is_subclass_of($controller, LeftAndMain::class); |
||||
1087 | } |
||||
1088 | |||||
1089 | /** |
||||
1090 | * Gets the back link |
||||
1091 | * |
||||
1092 | * @return string |
||||
1093 | */ |
||||
1094 | public function getBackLink() |
||||
1095 | { |
||||
1096 | $backlink = ''; |
||||
1097 | $toplevelController = $this->getToplevelController(); |
||||
1098 | // Check for LeftAndMain and alike controllers with a Backlink or Breadcrumbs methods |
||||
1099 | if ($toplevelController->hasMethod('Backlink')) { |
||||
1100 | //@phpstan-ignore-next-line |
||||
1101 | $backlink = $toplevelController->Backlink(); |
||||
1102 | } elseif ($this->getOwner()->getController()->hasMethod('Breadcrumbs')) { |
||||
1103 | //@phpstan-ignore-next-line |
||||
1104 | $parents = $this->getOwner()->getController()->Breadcrumbs(false)->items; |
||||
1105 | $backlink = array_pop($parents)->Link; |
||||
1106 | } |
||||
1107 | if (!$backlink) { |
||||
1108 | $backlink = $toplevelController->Link(); |
||||
0 ignored issues
–
show
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 The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. ![]() |
|||||
1109 | } |
||||
1110 | |||||
1111 | return $backlink; |
||||
1112 | } |
||||
1113 | |||||
1114 | /** |
||||
1115 | * Response object for this request after a successful save |
||||
1116 | * |
||||
1117 | * @param bool $isNewRecord True if this record was just created |
||||
1118 | * @param ModelData $record |
||||
1119 | * @return HTTPResponse|DBHTMLText|string |
||||
1120 | */ |
||||
1121 | protected function redirectAfterAction($isNewRecord, $record = null) |
||||
1122 | { |
||||
1123 | $controller = $this->getToplevelController(); |
||||
1124 | |||||
1125 | if ($this->isLeftAndMain($controller)) { |
||||
1126 | // CMSMain => redirect to show |
||||
1127 | if ($this->getOwner()->hasMethod("LinkPageEdit")) { |
||||
1128 | //@phpstan-ignore-next-line |
||||
1129 | return $controller->redirect($this->getOwner()->LinkPageEdit($record->ID)); |
||||
1130 | } |
||||
1131 | } |
||||
1132 | |||||
1133 | if ($isNewRecord) { |
||||
1134 | return $controller->redirect($this->getOwner()->Link()); |
||||
1135 | } |
||||
1136 | //@phpstan-ignore-next-line |
||||
1137 | if ($this->getOwner()->gridField && $this->getOwner()->gridField->getList()->byID($this->getOwner()->record->ID)) { |
||||
1138 | // Return new view, as we can't do a "virtual redirect" via the CMS Ajax |
||||
1139 | // to the same URL (it assumes that its content is already current, and doesn't reload) |
||||
1140 | return $this->getOwner()->edit($controller->getRequest()); |
||||
1141 | } |
||||
1142 | // Changes to the record properties might've excluded the record from |
||||
1143 | // a filtered list, so return back to the main view if it can't be found |
||||
1144 | $noActionURL = $url = $controller->getRequest()->getURL(); |
||||
1145 | if (!$url) { |
||||
1146 | $url = ''; |
||||
1147 | } |
||||
1148 | |||||
1149 | // The controller may not have these |
||||
1150 | if ($controller->hasMethod('getAction')) { |
||||
1151 | $action = $controller->getAction(); |
||||
1152 | // Handle GridField detail form editing |
||||
1153 | if (strpos($url, 'ItemEditForm') !== false) { |
||||
1154 | $action = 'ItemEditForm'; |
||||
1155 | } |
||||
1156 | if ($action) { |
||||
1157 | $noActionURL = $controller->removeAction($url, $action); |
||||
1158 | } |
||||
1159 | } else { |
||||
1160 | // Simple fallback (last index of) |
||||
1161 | $pos = strrpos($url, 'ItemEditForm'); |
||||
1162 | if (is_int($pos)) { |
||||
1163 | $noActionURL = substr($url, 0, $pos); |
||||
1164 | } |
||||
1165 | } |
||||
1166 | |||||
1167 | $controller->getRequest()->addHeader('X-Pjax', 'Content'); |
||||
1168 | return $controller->redirect($noActionURL, 302); |
||||
1169 | } |
||||
1170 | } |
||||
1171 |
This check compares calls to functions or methods with their respective definitions. If the call has more 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.