Passed
Push — master ( ed64cb...98de9f )
by Thomas
02:50
created

TabulatorGrid_ItemRequest::executeEdit()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 46
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 46
rs 8.5546
cc 7
nc 13
nop 2
1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\Forms\Form;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\View\SSViewer;
10
use SilverStripe\Control\Cookie;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\View\ArrayData;
13
use SilverStripe\ORM\HasManyList;
14
use SilverStripe\Control\Director;
15
use SilverStripe\ORM\ManyManyList;
16
use SilverStripe\ORM\RelationList;
17
use SilverStripe\Control\Controller;
18
use SilverStripe\Control\HTTPRequest;
19
use SilverStripe\Control\HTTPResponse;
20
use SilverStripe\ORM\ValidationResult;
21
use SilverStripe\Control\RequestHandler;
22
use SilverStripe\Security\SecurityToken;
23
use SilverStripe\ORM\ValidationException;
24
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
25
26
/**
27
 * Endpoint for actions related to a specific record
28
 *
29
 * It also allows to display a form to edit this record
30
 */
31
class TabulatorGrid_ItemRequest extends RequestHandler
32
{
33
34
    private static $allowed_actions = [
35
        'edit',
36
        'ajaxEdit',
37
        'ajaxMove',
38
        'view',
39
        'delete',
40
        'customAction',
41
        'ItemEditForm',
42
    ];
43
44
    protected TabulatorGrid $tabulatorGrid;
45
46
    /**
47
     * @var DataObject
48
     */
49
    protected $record;
50
51
    /**
52
     * @var array
53
     */
54
    protected $manipulatedData = null;
55
56
    /**
57
     * This represents the current parent RequestHandler (which does not necessarily need to be a Controller).
58
     * It allows us to traverse the RequestHandler chain upwards to reach the Controller stack.
59
     *
60
     * @var RequestHandler
61
     */
62
    protected $popupController;
63
64
    protected string $hash = '';
65
66
    protected string $template = '';
67
68
    private static $url_handlers = [
69
        'customAction/$CustomAction' => 'customAction',
70
        '$Action!' => '$Action',
71
        '' => 'edit',
72
    ];
73
74
    /**
75
     *
76
     * @param TabulatorGrid $tabulatorGrid
77
     * @param DataObject $record
78
     * @param RequestHandler $requestHandler
79
     */
80
    public function __construct($tabulatorGrid, $record, $requestHandler)
81
    {
82
        $this->tabulatorGrid = $tabulatorGrid;
83
        $this->record = $record;
84
        $this->popupController = $requestHandler;
85
        parent::__construct();
86
    }
87
88
    public function Link($action = null)
89
    {
90
        return Controller::join_links(
91
            $this->tabulatorGrid->Link('item'),
92
            $this->record->ID ? $this->record->ID : 'new',
93
            $action
94
        );
95
    }
96
97
    public function AbsoluteLink($action = null)
98
    {
99
        return Director::absoluteURL($this->Link($action));
100
    }
101
102
    protected function getManipulatedData(): array
103
    {
104
        if ($this->manipulatedData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->manipulatedData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
105
            return $this->manipulatedData;
106
        }
107
        $grid = $this->getTabulatorGrid();
108
109
        $state = $grid->getState($this->popupController->getRequest());
110
111
        $currentPage = $state['page'];
112
        $itemsPerPage = $state['limit'];
113
114
        $limit = $itemsPerPage + 2;
115
        $offset = max(0, $itemsPerPage * ($currentPage - 1) - 1);
116
117
        $list = $grid->getManipulatedData($limit, $offset, $state['sort'], $state['filter']);
118
119
        $this->manipulatedData = $list;
120
        return $list;
121
    }
122
123
    public function index(HTTPRequest $request)
124
    {
125
        $controller = $this->getToplevelController();
126
        return $controller->redirect($this->Link('edit'));
127
    }
128
129
    protected function returnWithinContext(HTTPRequest $request, RequestHandler $controller, Form $form)
130
    {
131
        $data = $this->customise([
132
            'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
133
            'ItemEditForm' => $form,
134
        ]);
135
136
        $return = $data->renderWith('LeKoala\\Tabulator\\TabulatorGrid_ItemEditForm');
137
        if ($request->isAjax()) {
138
            return $return;
139
        }
140
        // If not requested by ajax, we need to render it within the controller context+template
141
        return $controller->customise([
142
            'Content' => $return,
143
        ]);
144
    }
145
146
    protected function editFailure(): HTTPResponse
147
    {
148
        return $this->httpError(403, _t(
149
            __CLASS__ . '.EditPermissionsFailure',
150
            'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
151
            ['ObjectTitle' => $this->record->singular_name()]
152
        ));
153
    }
154
155
    /**
156
     * This is responsible to display an edit form, like GridFieldDetailForm, but much simpler
157
     *
158
     * @return mixed
159
     */
160
    public function edit(HTTPRequest $request)
161
    {
162
        if (!$this->record->canEdit()) {
163
            return $this->editFailure();
164
        }
165
        $controller = $this->getToplevelController();
166
167
        $form = $this->ItemEditForm();
168
169
        return $this->returnWithinContext($request, $controller, $form);
0 ignored issues
show
Bug introduced by
$form of type SilverStripe\Control\HTTPResponse is incompatible with the type SilverStripe\Forms\Form expected by parameter $form of LeKoala\Tabulator\Tabula...::returnWithinContext(). ( Ignorable by Annotation )

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

169
        return $this->returnWithinContext($request, $controller, /** @scrutinizer ignore-type */ $form);
Loading history...
170
    }
171
172
    public function ajaxEdit(HTTPRequest $request)
173
    {
174
        $SecurityID = $request->postVar('SecurityID');
175
        if (!SecurityToken::inst()->check($SecurityID)) {
176
            return $this->httpError(404, "Invalid SecurityID");
177
        }
178
        if (!$this->record->canEdit()) {
179
            return $this->editFailure();
180
        }
181
182
        $preventEmpty = [];
183
        if ($this->record->hasMethod('tabulatorPreventEmpty')) {
184
            $preventEmpty = $this->record->tabulatorPreventEmpty();
185
        }
186
187
        $Data = $request->postVar("Data");
0 ignored issues
show
Unused Code introduced by
The assignment to $Data is dead and can be removed.
Loading history...
188
        $Column = $request->postVar("Column");
189
        $Value = $request->postVar("Value");
190
191
        if (!$Value && in_array($Column, $preventEmpty)) {
192
            return $this->httpError(400, _t(__CLASS__ . '.ValueCannotBeEmpty', 'Value cannot be empty'));
193
        }
194
195
        try {
196
            $updatedValue = $this->executeEdit($Column, $Value);
0 ignored issues
show
Bug introduced by
It seems like $Column can also be of type null; however, parameter $Column of LeKoala\Tabulator\Tabula...mRequest::executeEdit() does only seem to accept string, 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

196
            $updatedValue = $this->executeEdit(/** @scrutinizer ignore-type */ $Column, $Value);
Loading history...
197
        } catch (Exception $e) {
198
            return $this->httpError(400, $e->getMessage());
199
        }
200
201
        $response = new HTTPResponse(json_encode([
202
            'success' => true,
203
            'message' => _t(__CLASS__ . '.RecordEdited', 'Record edited'),
204
            'value' => $updatedValue,
205
        ]));
206
        $response->addHeader('Content-Type', 'application/json');
207
        return $response;
208
    }
209
210
    public function executeEdit(string $Column, $Value)
211
    {
212
        $field = $Column;
213
        $rel = $relField = null;
214
        if (strpos($Column, ".") !== false) {
215
            $parts = explode(".", $Column);
216
            $rel = $parts[0];
217
            $relField = $parts[1];
218
            $field = $rel . "ID";
219
            if (!is_numeric($Value)) {
220
                return $this->httpError(400, "ID must have a numerical value");
221
            }
222
        }
223
        if (!$field) {
224
            return $this->httpError(400, "Field must not be empty");
225
        }
226
227
        $grid = $this->tabulatorGrid;
228
229
        $list = $grid->getDataList();
230
231
        if ($list instanceof ManyManyList) {
232
            $extraData = $list->getExtraData($grid->getName(), $this->record->ID);
233
234
            // Is it a many many field
235
            // This is a bit basic but it works
236
            if (isset($extraData[$Column])) {
237
                $extra = [
238
                    $Column => $Value
239
                ];
240
                $list->add($this->record, $extra);
241
                return $Value;
242
            }
243
        }
244
245
246
        // Its on the object itself
247
        $this->record->$field = $Value;
248
        $this->record->write();
249
        $updatedValue = $this->record->$field;
250
        if ($rel) {
251
            /** @var DataObject $relObject */
252
            $relObject = $this->record->$rel();
253
            $updatedValue = $relObject->relField($relField);
254
        }
255
        return $updatedValue;
256
    }
257
258
    public function ajaxMove(HTTPRequest $request)
259
    {
260
        $SecurityID = $request->postVar('SecurityID');
261
        if (!SecurityToken::inst()->check($SecurityID)) {
262
            return $this->httpError(404, "Invalid SecurityID");
263
        }
264
        if (!$this->record->canEdit()) {
265
            return $this->editFailure();
266
        }
267
        $Data = $request->postVar("Data");
268
        if (is_string($Data)) {
269
            $Data = json_decode($Data, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Bug introduced by
LeKoala\Tabulator\JSON_OBJECT_AS_ARRAY of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

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

269
            $Data = json_decode($Data, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
270
        }
271
        $Sort = $request->postVar("Sort");
272
273
        try {
274
            $updatedSort = $this->executeSort($Data, $Sort);
0 ignored issues
show
Bug introduced by
It seems like $Sort can also be of type null; however, parameter $Sort of LeKoala\Tabulator\Tabula...mRequest::executeSort() does only seem to accept integer, 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

274
            $updatedSort = $this->executeSort($Data, /** @scrutinizer ignore-type */ $Sort);
Loading history...
275
        } catch (Exception $e) {
276
            return $this->httpError(400, $e->getMessage());
277
        }
278
279
        $response = new HTTPResponse(json_encode([
280
            'success' => true,
281
            'message' => _t(__CLASS__ . '.RecordMove', 'Record moved'),
282
            'value' => $updatedSort,
283
        ]));
284
        $response->addHeader('Content-Type', 'application/json');
285
        return $response;
286
    }
287
288
    public function executeSort(array $Data, int $Sort, string $sortField = 'Sort'): int
289
    {
290
        $table = DataObject::getSchema()->baseDataTable(get_class($this->record));
291
292
        if (!isset($Data[$sortField])) {
293
            return $this->httpError(403, _t(
294
                __CLASS__ . '.UnableToResolveSort',
295
                'Unable to resolve previous sort order'
296
            ));
297
        }
298
299
        $prevSort = $Data[$sortField];
300
301
        // Just make sure you don't have 0 (except first record) or equal sorts
302
        if ($prevSort < $Sort) {
303
            $set = "$sortField = $sortField - 1";
304
            $where = "$sortField > $prevSort and $sortField <= $Sort";
305
        } else {
306
            $set = "$sortField = $sortField + 1";
307
            $where = "$sortField < $prevSort and $sortField >= $Sort";
308
        }
309
        DB::query("UPDATE `$table` SET $set WHERE $where");
310
        $this->record->$sortField = $Sort;
311
        $this->record->write();
312
313
        return $this->record->Sort;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->record->Sort could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
314
    }
315
316
317
    /**
318
     * Delete from the row level
319
     *
320
     * @param HTTPRequest $request
321
     * @return void
322
     */
323
    public function delete(HTTPRequest $request)
324
    {
325
        if (!$this->record->canDelete()) {
326
            return $this->httpError(403, _t(
327
                __CLASS__ . '.DeletePermissionsFailure',
328
                'It seems you don\'t have the necessary permissions to delete "{ObjectTitle}"',
329
                ['ObjectTitle' => $this->record->singular_name()]
330
            ));
331
        }
332
333
        $title = $this->record->getTitle();
334
        $this->record->delete();
335
336
        $message = _t(
337
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
338
            'Deleted {type} {name}',
339
            [
340
                'type' => $this->record->i18n_singular_name(),
341
                'name' => htmlspecialchars($title, ENT_QUOTES)
342
            ]
343
        );
344
345
        //when an item is deleted, redirect to the parent controller
346
        $controller = $this->getToplevelController();
347
348
        if ($this->isSilverStripeAdmin($controller)) {
349
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
350
        }
351
352
        //redirect back to admin section
353
        $response = $controller->redirect($this->getBackLink(), 302);
354
355
        $this->sessionMessage($message, "good");
356
357
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
358
    }
359
360
    /**
361
     * @return mixed
362
     */
363
    public function view(HTTPRequest $request)
364
    {
365
        if (!$this->record->canView()) {
366
            return $this->httpError(403, _t(
367
                __CLASS__ . '.ViewPermissionsFailure',
368
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
369
                ['ObjectTitle' => $this->record->singular_name()]
370
            ));
371
        }
372
373
        $controller = $this->getToplevelController();
374
375
        $form = $this->ItemEditForm();
376
        $form->makeReadonly();
0 ignored issues
show
Bug introduced by
The method makeReadonly() does not exist on SilverStripe\Control\HTTPResponse. ( Ignorable by Annotation )

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

376
        $form->/** @scrutinizer ignore-call */ 
377
               makeReadonly();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
377
378
        return $this->returnWithinContext($request, $controller, $form);
0 ignored issues
show
Bug introduced by
$form of type SilverStripe\Control\HTTPResponse is incompatible with the type SilverStripe\Forms\Form expected by parameter $form of LeKoala\Tabulator\Tabula...::returnWithinContext(). ( Ignorable by Annotation )

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

378
        return $this->returnWithinContext($request, $controller, /** @scrutinizer ignore-type */ $form);
Loading history...
379
    }
380
381
    /**
382
     * This is responsible to forward actions to the model if necessary
383
     * @param HTTPRequest $request
384
     * @return HTTPResponse
385
     */
386
    public function customAction(HTTPRequest $request)
387
    {
388
        // This gets populated thanks to our updated URL handler
389
        $params = $request->params();
390
        $customAction = $params['CustomAction'] ?? null;
391
        $ID = $params['ID'] ?? 0;
392
393
        $dataClass = $this->tabulatorGrid->getModelClass();
394
        $record = DataObject::get_by_id($dataClass, $ID);
395
        $rowActions = $record->tabulatorRowActions();
396
        $validActions = array_keys($rowActions);
397
        if (!$customAction || !in_array($customAction, $validActions)) {
398
            return $this->httpError(403, _t(
399
                __CLASS__ . '.CustomActionPermissionsFailure',
400
                'It seems you don\'t have the necessary permissions to {ActionName} "{ObjectTitle}"',
401
                ['ActionName' => $customAction, 'ObjectTitle' => $this->record->singular_name()]
402
            ));
403
        }
404
405
        $clickedAction = $rowActions[$customAction];
406
407
        $error = false;
408
        try {
409
            $result = $record->$customAction();
410
        } catch (Exception $e) {
411
            $error = true;
412
            $result = $e->getMessage();
413
        }
414
415
        // Maybe it's a custom redirect or a file ?
416
        if ($result && $result instanceof HTTPResponse) {
417
            return $result;
418
        }
419
420
        // Show message on controller or in form
421
        $controller = $this->getToplevelController();
422
        $response = $controller->getResponse();
423
        if (Director::is_ajax()) {
424
            $responseData = [
425
                'message' => $result,
426
                'status' => $error ? 'error' : 'success',
427
            ];
428
            if (!empty($clickedAction['reload'])) {
429
                $responseData['reload'] = true;
430
            }
431
            if (!empty($clickedAction['refresh'])) {
432
                $responseData['refresh'] = true;
433
            }
434
            $response->setBody(json_encode($responseData));
435
            // 4xx status makes a red box
436
            if ($error) {
437
                $response->setStatusCode(400);
438
            }
439
            return $response;
440
        }
441
442
        $url = $this->getDefaultBackLink();
443
        $response = $this->redirect($url);
444
445
        $this->sessionMessage($result, $error ? "error" : "good", "html");
446
447
        return $response;
448
    }
449
450
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
451
    {
452
        $controller = $this->getToplevelController();
453
        if ($controller->hasMethod('sessionMessage')) {
454
            $controller->sessionMessage($message, $type, $cast);
455
        } else {
456
            $form = $this->ItemEditForm();
457
            if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Control\HTTPResponse, thus it always evaluated to true.
Loading history...
458
                $form->sessionMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
The method sessionMessage() does not exist on SilverStripe\Control\HTTPResponse. ( Ignorable by Annotation )

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

458
                $form->/** @scrutinizer ignore-call */ 
459
                       sessionMessage($message, $type, $cast);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
459
            }
460
        }
461
    }
462
463
    /**
464
     * Builds an item edit form
465
     *
466
     * @return Form|HTTPResponse
467
     */
468
    public function ItemEditForm()
469
    {
470
        $list = $this->tabulatorGrid->getList();
471
        $controller = $this->getToplevelController();
472
473
        try {
474
            $record = $this->getRecord();
475
        } catch (Exception $e) {
476
            $url = $controller->getRequest()->getURL();
477
            $noActionURL = $controller->removeAction($url);
478
            //clear the existing redirect
479
            $controller->getResponse()->removeHeader('Location');
480
            return $controller->redirect($noActionURL, 302);
481
        }
482
483
        // If we are creating a new record in a has-many list, then
484
        // pre-populate the record's foreign key.
485
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
486
            $key = $list->getForeignKey();
487
            $id = $list->getForeignID();
488
            $record->$key = $id;
489
        }
490
491
        if (!$record->canView()) {
492
            return $controller->httpError(403, _t(
493
                __CLASS__ . '.ViewPermissionsFailure',
494
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
495
                ['ObjectTitle' => $this->record->singular_name()]
496
            ));
497
        }
498
499
        if ($record->hasMethod("tabulatorCMSFields")) {
500
            $fields = $record->tabulatorCMSFields();
501
        } else {
502
            $fields = $record->getCMSFields();
503
        }
504
505
        // If we are creating a new record in a has-many list, then
506
        // Disable the form field as it has no effect.
507
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
508
            $key = $list->getForeignKey();
509
510
            if ($field = $fields->dataFieldByName($key)) {
511
                $fields->makeFieldReadonly($field);
512
            }
513
        }
514
515
        $compatLayer = $this->tabulatorGrid->getCompatLayer($controller);
516
517
        $actions = $compatLayer->getFormActions($this);
518
        $this->extend('updateFormActions', $actions);
519
520
        $validator = null;
521
522
        $form = new Form(
523
            $this,
524
            'ItemEditForm',
525
            $fields,
526
            $actions,
527
            $validator
528
        );
529
530
        $form->loadDataFrom($record, $record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
531
532
        if ($record->ID && !$record->canEdit()) {
533
            // Restrict editing of existing records
534
            $form->makeReadonly();
535
            // Hack to re-enable delete button if user can delete
536
            if ($record->canDelete()) {
537
                $form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $form->Actions()->fieldByName('action_doDelete') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is 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...
538
            }
539
        }
540
        $cannotCreate = !$record->ID && !$record->canCreate(null, $this->getCreateContext());
541
        if ($cannotCreate || $this->tabulatorGrid->isViewOnly()) {
542
            // Restrict creation of new records
543
            $form->makeReadonly();
544
        }
545
546
        // Load many_many extraData for record.
547
        // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
548
        if ($list instanceof ManyManyList) {
549
            $extraData = $list->getExtraData('', $this->record->ID);
550
            $form->loadDataFrom(['ManyMany' => $extraData]);
551
        }
552
553
        // Coupling with CMS
554
        $compatLayer->adjustItemEditForm($this, $form);
555
556
        $this->extend("updateItemEditForm", $form);
557
558
        return $form;
559
    }
560
561
    /**
562
     * Build context for verifying canCreate
563
     *
564
     * @return array
565
     */
566
    protected function getCreateContext()
567
    {
568
        $grid = $this->tabulatorGrid;
569
        $context = [];
570
        if ($grid->getList() instanceof RelationList) {
571
            $record = $grid->getForm()->getRecord();
572
            if ($record && $record instanceof DataObject) {
0 ignored issues
show
introduced by
$record is always a sub-type of SilverStripe\ORM\DataObject.
Loading history...
573
                $context['Parent'] = $record;
574
            }
575
        }
576
        return $context;
577
    }
578
579
    /**
580
     * @return \SilverStripe\Control\Controller|\SilverStripe\Admin\LeftAndMain|TabulatorGrid_ItemRequest
0 ignored issues
show
Bug introduced by
The type SilverStripe\Admin\LeftAndMain 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...
581
     */
582
    public function getToplevelController(): RequestHandler
583
    {
584
        $c = $this->popupController;
585
        // Maybe our field is included in a GridField or in a TabulatorGrid?
586
        while ($c && ($c instanceof GridFieldDetailForm_ItemRequest || $c instanceof TabulatorGrid_ItemRequest)) {
587
            $c = $c->getController();
588
        }
589
        return $c;
590
    }
591
592
    public function getDefaultBackLink(): string
593
    {
594
        $url = $this->getBackURL()
595
            ?: $this->getReturnReferer()
596
            ?: $this->AbsoluteLink();
597
        return $url;
598
    }
599
600
    public function getBackLink(): string
601
    {
602
        $backlink = '';
603
        $toplevelController = $this->getToplevelController();
604
        if ($this->popupController->hasMethod('Breadcrumbs')) {
605
            $parents = $this->popupController->Breadcrumbs(false);
606
            if ($parents && $parents = $parents->items) {
607
                $backlink = array_pop($parents)->Link;
608
            }
609
        }
610
        if ($toplevelController && $toplevelController->hasMethod('Backlink')) {
611
            $backlink = $toplevelController->Backlink();
612
        }
613
        if (!$backlink) {
614
            $backlink = $toplevelController->Link();
615
        }
616
        return $backlink;
617
    }
618
619
    /**
620
     * Get the list of extra data from the $record as saved into it by
621
     * {@see Form::saveInto()}
622
     *
623
     * Handles detection of falsey values explicitly saved into the
624
     * DataObject by formfields
625
     *
626
     * @param DataObject $record
627
     * @param SS_List $list
0 ignored issues
show
Bug introduced by
The type LeKoala\Tabulator\SS_List 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...
628
     * @return array List of data to write to the relation
629
     */
630
    protected function getExtraSavedData($record, $list)
631
    {
632
        // Skip extra data if not ManyManyList
633
        if (!($list instanceof ManyManyList)) {
634
            return null;
635
        }
636
637
        $data = [];
638
        foreach ($list->getExtraFields() as $field => $dbSpec) {
639
            $savedField = "ManyMany[{$field}]";
640
            if ($record->hasField($savedField)) {
641
                $data[$field] = $record->getField($savedField);
642
            }
643
        }
644
        return $data;
645
    }
646
647
    public function doSave($data, $form)
648
    {
649
        $isNewRecord = $this->record->ID == 0;
650
651
        // Check permission
652
        if (!$this->record->canEdit()) {
653
            return $this->editFailure();
654
        }
655
656
        // _activetab is used in cms-action
657
        $this->hash = $data['_hash'] ?? $data['_activetab'] ?? '';
658
659
        // Save from form data
660
        $error = false;
661
662
        // Leave it to FormRequestHandler::getAjaxErrorResponse so that we don't lose our data on error
663
        // try {
664
        $this->saveFormIntoRecord($data, $form);
665
666
        $title = $this->record->Title ?? '';
667
        $link = '<a href="' . $this->Link('edit') . '">"'
668
            . htmlspecialchars($title, ENT_QUOTES)
669
            . '"</a>';
670
        $message = _t(
671
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved',
672
            'Saved {name} {link}',
673
            [
674
                'name' => $this->record->i18n_singular_name(),
675
                'link' => $link
676
            ]
677
        );
678
        // } catch (Exception $e) {
679
        //     $message = $e->getMessage();
680
        //     $error = true;
681
        // }
682
683
        // Redirect after save
684
        $response = $this->redirectAfterSave($isNewRecord);
685
686
        // Session message may add stuff to the response, so create the redirect before
687
        $this->sessionMessage($message, $error ? "error" : "good", 'html');
0 ignored issues
show
introduced by
The condition $error is always false.
Loading history...
688
689
        return $response;
690
    }
691
692
    /**
693
     * Gets the edit link for a record
694
     *
695
     * @param  int $id The ID of the record in the GridField
696
     * @return string
697
     */
698
    public function getEditLink($id)
699
    {
700
        $link = Controller::join_links(
701
            $this->tabulatorGrid->Link(),
702
            'item',
703
            $id
704
        );
705
706
        return $link;
707
    }
708
709
    /**
710
     * @param int $offset The offset from the current record
711
     * @return int|bool
712
     */
713
    private function getAdjacentRecordID($offset)
714
    {
715
        $list = $this->getManipulatedData();
716
        $map = array_column($list['data'], "ID");
717
        $index = array_search($this->record->ID, $map);
718
        return isset($map[$index + $offset]) ? $map[$index + $offset] : false;
719
    }
720
721
    /**
722
     * Gets the ID of the previous record in the list.
723
     */
724
    public function getPreviousRecordID(): int
725
    {
726
        return $this->getAdjacentRecordID(-1);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getAdjacentRecordID(-1) could return the type boolean which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
727
    }
728
729
    /**
730
     * Gets the ID of the next record in the list.
731
     */
732
    public function getNextRecordID(): int
733
    {
734
        return $this->getAdjacentRecordID(1);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getAdjacentRecordID(1) could return the type boolean which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
735
    }
736
737
    /**
738
     * This is expected in lekoala/silverstripe-cms-actions ActionsGridFieldItemRequest
739
     * @return HTTPResponse
740
     */
741
    public function getResponse()
742
    {
743
        return $this->getToplevelController()->getResponse();
744
    }
745
746
    /**
747
     * Response object for this request after a successful save
748
     *
749
     * @param bool $isNewRecord True if this record was just created
750
     * @return HTTPResponse|DBHTMLText
0 ignored issues
show
Bug introduced by
The type LeKoala\Tabulator\DBHTMLText 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...
751
     */
752
    protected function redirectAfterSave($isNewRecord)
753
    {
754
        $controller = $this->getToplevelController();
755
        if ($isNewRecord) {
756
            return $this->redirect($this->Link());
757
        } elseif ($this->tabulatorGrid->hasByIDList() && $this->tabulatorGrid->getByIDList()->byID($this->record->ID)) {
758
            return $this->redirect($this->getDefaultBackLink());
759
        } else {
760
            // Changes to the record properties might've excluded the record from
761
            // a filtered list, so return back to the main view if it can't be found
762
            $url = $controller->getRequest()->getURL();
763
            $noActionURL = $controller->removeAction($url);
764
            if ($this->isSilverStripeAdmin($controller)) {
765
                $controller->getRequest()->addHeader('X-Pjax', 'Content');
766
            }
767
            return $controller->redirect($noActionURL, 302);
768
        }
769
    }
770
771
    protected function getHashValue()
772
    {
773
        if ($this->hash) {
774
            $hash = $this->hash;
775
        } else {
776
            $hash = Cookie::get('hash');
777
        }
778
        if ($hash) {
779
            $hash = '#' . ltrim($hash, '#');
780
        }
781
        return $hash;
782
    }
783
784
    /**
785
     * Redirect to the given URL.
786
     *
787
     * @param string $url
788
     * @param int $code
789
     * @return HTTPResponse
790
     */
791
    public function redirect($url, $code = 302): HTTPResponse
792
    {
793
        $hash = $this->getHashValue();
794
        if ($hash) {
795
            $url .= $hash;
796
        }
797
798
        $controller = $this->getToplevelController();
799
        $response = $controller->redirect($url, $code);
800
801
        // if ($hash) {
802
        // We also pass it as a hash
803
        // @link https://github.com/whatwg/fetch/issues/1167
804
        // $response = $response->addHeader('X-Hash', $hash);
805
        // }
806
807
        return $response;
808
    }
809
810
    public function httpError($errorCode, $errorMessage = null)
811
    {
812
        $controller = $this->getToplevelController();
813
        return $controller->httpError($errorCode, $errorMessage);
814
    }
815
816
    /**
817
     * Loads the given form data into the underlying dataobject and relation
818
     *
819
     * @param array $data
820
     * @param Form $form
821
     * @throws ValidationException On error
822
     * @return DataObject Saved record
823
     */
824
    protected function saveFormIntoRecord($data, $form)
825
    {
826
        $list = $this->tabulatorGrid->getList();
827
828
        // Check object matches the correct classname
829
        if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
830
            $newClassName = $data['ClassName'];
831
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
832
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
833
            // on the ClassName attribute
834
            $this->record->setClassName($this->record->ClassName);
835
            // Replace $record with a new instance
836
            $this->record = $this->record->newClassInstance($newClassName);
837
        }
838
839
        // Save form and any extra saved data into this dataobject.
840
        // Set writeComponents = true to write has-one relations / join records
841
        $form->saveInto($this->record);
842
        // https://github.com/silverstripe/silverstripe-assets/issues/365
843
        $this->record->write();
844
        $this->extend('onAfterSave', $this->record);
845
846
        $extraData = $this->getExtraSavedData($this->record, $list);
847
        $list->add($this->record, $extraData);
0 ignored issues
show
Unused Code introduced by
The call to SilverStripe\ORM\SS_List::add() has too many arguments starting with $extraData. ( Ignorable by Annotation )

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

847
        $list->/** @scrutinizer ignore-call */ 
848
               add($this->record, $extraData);

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.

Loading history...
848
849
        return $this->record;
850
    }
851
852
    /**
853
     * Delete from ItemRequest action
854
     *
855
     * @param array $data
856
     * @param Form $form
857
     * @return HTTPResponse
858
     * @throws ValidationException
859
     */
860
    public function doDelete($data, $form)
861
    {
862
        $title = $this->record->Title;
863
        if (!$this->record->canDelete()) {
864
            throw new ValidationException(
865
                _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
866
            );
867
        }
868
869
        $this->record->delete();
870
871
        $message = _t(
872
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
873
            'Deleted {type} {name}',
874
            [
875
                'type' => $this->record->i18n_singular_name(),
876
                'name' => htmlspecialchars($title, ENT_QUOTES)
877
            ]
878
        );
879
880
881
        $backForm = $form;
0 ignored issues
show
Unused Code introduced by
The assignment to $backForm is dead and can be removed.
Loading history...
882
        $toplevelController = $this->getToplevelController();
883
        if ($this->isSilverStripeAdmin($toplevelController)) {
884
            $backForm = $toplevelController->getEditForm();
885
        }
886
        //when an item is deleted, redirect to the parent controller
887
        $controller = $this->getToplevelController();
888
889
        if ($this->isSilverStripeAdmin($toplevelController)) {
890
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
891
        }
892
        $response = $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
893
        $this->sessionMessage($message, "good");
894
895
        return $response;
896
    }
897
898
    public function isSilverStripeAdmin($controller)
899
    {
900
        if ($controller) {
901
            return is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class);
902
        }
903
        return false;
904
    }
905
906
    /**
907
     * @param string $template
908
     * @return $this
909
     */
910
    public function setTemplate($template)
911
    {
912
        $this->template = $template;
913
        return $this;
914
    }
915
916
    /**
917
     * @return string
918
     */
919
    public function getTemplate()
920
    {
921
        return $this->template;
922
    }
923
924
    /**
925
     * Get list of templates to use
926
     *
927
     * @return array
928
     */
929
    public function getTemplates()
930
    {
931
        $templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
932
        // Prefer any custom template
933
        if ($this->getTemplate()) {
934
            array_unshift($templates, $this->getTemplate());
935
        }
936
        return $templates;
937
    }
938
939
    /**
940
     * @return Controller
941
     */
942
    public function getController()
943
    {
944
        return $this->popupController;
945
    }
946
947
    /**
948
     * @return TabulatorGrid
949
     */
950
    public function getTabulatorGrid()
951
    {
952
        return $this->tabulatorGrid;
953
    }
954
955
    /**
956
     * @return DataObject
957
     */
958
    public function getRecord()
959
    {
960
        return $this->record;
961
    }
962
963
    /**
964
     * CMS-specific functionality: Passes through navigation breadcrumbs
965
     * to the template, and includes the currently edited record (if any).
966
     * see {@link LeftAndMain->Breadcrumbs()} for details.
967
     *
968
     * @param boolean $unlinked
969
     * @return ArrayList
970
     */
971
    public function Breadcrumbs($unlinked = false)
972
    {
973
        if (!$this->popupController->hasMethod('Breadcrumbs')) {
974
            return null;
975
        }
976
977
        /** @var ArrayList $items */
978
        $items = $this->popupController->Breadcrumbs($unlinked);
979
980
        if (!$items) {
0 ignored issues
show
introduced by
$items is of type SilverStripe\ORM\ArrayList, thus it always evaluated to true.
Loading history...
981
            $items = new ArrayList();
982
        }
983
984
        if ($this->record && $this->record->ID) {
985
            $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
986
            $items->push(new ArrayData([
987
                'Title' => $title,
988
                'Link' => $this->Link()
989
            ]));
990
        } else {
991
            $items->push(new ArrayData([
992
                'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]),
993
                'Link' => false
994
            ]));
995
        }
996
997
        $this->extend('updateBreadcrumbs', $items);
998
        return $items;
999
    }
1000
}
1001