Passed
Push — master ( 47e01d...760836 )
by Thomas
12:05
created

TabulatorGrid_ItemRequest::index()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use SilverStripe\Forms\Form;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\View\SSViewer;
9
use SilverStripe\Control\Cookie;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\View\ArrayData;
12
use SilverStripe\ORM\HasManyList;
13
use SilverStripe\Control\Director;
14
use SilverStripe\ORM\ManyManyList;
15
use SilverStripe\ORM\RelationList;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\HTTPRequest;
18
use SilverStripe\Control\HTTPResponse;
19
use SilverStripe\ORM\ValidationResult;
20
use SilverStripe\Control\RequestHandler;
21
use SilverStripe\Security\SecurityToken;
22
use SilverStripe\ORM\ValidationException;
23
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
24
use SilverStripe\ORM\DB;
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
        'customAction',
40
        'ItemEditForm',
41
    ];
42
43
    protected TabulatorGrid $tabulatorGrid;
44
45
    /**
46
     * @var DataObject
47
     */
48
    protected $record;
49
50
    /**
51
     * @var array
52
     */
53
    protected $manipulatedData = null;
54
55
    /**
56
     * This represents the current parent RequestHandler (which does not necessarily need to be a Controller).
57
     * It allows us to traverse the RequestHandler chain upwards to reach the Controller stack.
58
     *
59
     * @var RequestHandler
60
     */
61
    protected $popupController;
62
63
    protected string $template = '';
64
65
    private static $url_handlers = [
66
        'customAction/$CustomAction' => 'customAction',
67
        '$Action!' => '$Action',
68
        '' => 'edit',
69
    ];
70
71
    /**
72
     *
73
     * @param TabulatorGrid $tabulatorGrid
74
     * @param DataObject $record
75
     * @param RequestHandler $requestHandler
76
     */
77
    public function __construct($tabulatorGrid, $record, $requestHandler)
78
    {
79
        $this->tabulatorGrid = $tabulatorGrid;
80
        $this->record = $record;
81
        $this->popupController = $requestHandler;
82
        parent::__construct();
83
    }
84
85
    public function Link($action = null)
86
    {
87
        return Controller::join_links(
88
            $this->tabulatorGrid->Link('item'),
89
            $this->record->ID ? $this->record->ID : 'new',
90
            $action
91
        );
92
    }
93
94
    public function AbsoluteLink($action = null)
95
    {
96
        return Director::absoluteURL($this->Link($action));
97
    }
98
99
    protected function getManipulatedData(): array
100
    {
101
        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...
102
            return $this->manipulatedData;
103
        }
104
        $grid = $this->getTabulatorGrid();
105
106
        $state = $grid->getState($this->popupController->getRequest());
107
108
        $currentPage = $state['page'];
109
        $itemsPerPage = $state['limit'];
110
111
        $limit = $itemsPerPage + 2;
112
        $offset = max(0, $itemsPerPage * ($currentPage - 1) - 1);
113
114
        $list = $grid->getManipulatedData($limit, $offset, $state['sort'], $state['filter']);
115
116
        $this->manipulatedData = $list;
117
        return $list;
118
    }
119
120
    public function index(HTTPRequest $request)
121
    {
122
        $controller = $this->getToplevelController();
123
        return $controller->redirect($this->Link('edit'));
124
    }
125
126
    protected function returnWithinContext(HTTPRequest $request, RequestHandler $controller, Form $form)
127
    {
128
        $data = $this->customise([
129
            'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
130
            'ItemEditForm' => $form,
131
        ]);
132
133
        $return = $data->renderWith('LeKoala\\Tabulator\\TabulatorGrid_ItemEditForm');
134
        if ($request->isAjax()) {
135
            return $return;
136
        }
137
        // If not requested by ajax, we need to render it within the controller context+template
138
        return $controller->customise([
139
            'Content' => $return,
140
        ]);
141
    }
142
143
    /**
144
     * This is responsible to display an edit form, like GridFieldDetailForm, but much simpler
145
     *
146
     * @return mixed
147
     */
148
    public function edit(HTTPRequest $request)
149
    {
150
        if (!$this->record->canEdit()) {
151
            return $this->httpError(403, _t(
152
                __CLASS__ . '.EditPermissionsFailure',
153
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
154
                ['ObjectTitle' => $this->record->singular_name()]
155
            ));
156
        }
157
        $controller = $this->getToplevelController();
158
159
        $form = $this->ItemEditForm();
160
161
        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

161
        return $this->returnWithinContext($request, $controller, /** @scrutinizer ignore-type */ $form);
Loading history...
162
    }
163
164
    public function ajaxEdit(HTTPRequest $request)
165
    {
166
        $SecurityID = $request->postVar('SecurityID');
167
        if (!SecurityToken::inst()->check($SecurityID)) {
168
            return $this->httpError(404, "Invalid SecurityID: $SecurityID");
169
        }
170
        if (!$this->record->canEdit()) {
171
            return $this->httpError(403, _t(
172
                __CLASS__ . '.EditPermissionsFailure',
173
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
174
                ['ObjectTitle' => $this->record->singular_name()]
175
            ));
176
        }
177
178
        $preventEmpty = [];
179
        if ($this->record->hasMethod('tabulatorPreventEmpty')) {
180
            $preventEmpty = $this->record->tabulatorPreventEmpty();
181
        }
182
183
        $Data = $request->postVar("Data");
0 ignored issues
show
Unused Code introduced by
The assignment to $Data is dead and can be removed.
Loading history...
184
        $Column = $request->postVar("Column");
185
        $Value = $request->postVar("Value");
186
187
        if (!$Value && in_array($Column, $preventEmpty)) {
188
            return $this->httpError(400, _t(__CLASS__ . '.ValueCannotBeEmpty', 'Value cannot be empty'));
189
        }
190
191
        $field = $Column;
192
        $rel = $relField = null;
193
        if (strpos($Column, ".") !== false) {
0 ignored issues
show
Bug introduced by
It seems like $Column can also be of type null; however, parameter $haystack of strpos() 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

193
        if (strpos(/** @scrutinizer ignore-type */ $Column, ".") !== false) {
Loading history...
194
            $parts = explode(".", $Column);
0 ignored issues
show
Bug introduced by
It seems like $Column can also be of type null; however, parameter $string of explode() 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

194
            $parts = explode(".", /** @scrutinizer ignore-type */ $Column);
Loading history...
195
            $rel = $parts[0];
196
            $relField = $parts[1];
197
            $field = $rel . "ID";
198
            if (!is_numeric($Value)) {
199
                return $this->httpError(400, "ID must have a numerical value");
200
            }
201
        }
202
        if (!$field) {
203
            return $this->httpError(400, "Field must not be empty");
204
        }
205
206
        $this->record->$field = $Value;
207
208
        $error = null;
209
        try {
210
            $this->record->write();
211
            $updatedValue = $this->record->$field;
212
            if ($rel) {
213
                /** @var DataObject $relObject */
214
                $relObject = $this->record->$rel();
215
                $updatedValue = $relObject->relField($relField);
216
            }
217
        } catch (Exception $e) {
218
            $error = $e->getMessage();
219
        }
220
221
        if ($error) {
222
            return $this->httpError(400, $error);
223
        }
224
225
        $response = new HTTPResponse(json_encode([
226
            'success' => true,
227
            'message' => _t(__CLASS__ . '.RecordEdited', 'Record edited'),
228
            'value' => $updatedValue,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $updatedValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
229
        ]));
230
        $response->addHeader('Content-Type', 'application/json');
231
        return $response;
232
    }
233
234
    public function ajaxMove(HTTPRequest $request)
235
    {
236
        $SecurityID = $request->postVar('SecurityID');
237
        if (!SecurityToken::inst()->check($SecurityID)) {
238
            return $this->httpError(404, "Invalid SecurityID: $SecurityID");
239
        }
240
        if (!$this->record->canEdit()) {
241
            return $this->httpError(403, _t(
242
                __CLASS__ . '.EditPermissionsFailure',
243
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
244
                ['ObjectTitle' => $this->record->singular_name()]
245
            ));
246
        }
247
248
        $table = DataObject::getSchema()->baseDataTable(get_class($this->record));
249
        $sortField = 'Sort';
250
251
        $Data = $request->postVar("Data");
0 ignored issues
show
Unused Code introduced by
The assignment to $Data is dead and can be removed.
Loading history...
252
        $Sort = $request->postVar("Sort");
253
254
        if (!isset($data[$sortField])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data seems to never exist and therefore isset should always be false.
Loading history...
255
            return $this->httpError(403, _t(
256
                __CLASS__ . '.UnableToResolveSort',
257
                'Unable to resolve previous sort order'
258
            ));
259
        }
260
261
        $prevSort = $data[$sortField];
262
263
        $error = null;
264
        try {
265
            if ($prevSort < $Sort) {
266
                $set = "$sortField = $sortField - 1";
267
                $where = "$sortField > $prevSort and $sortField <= $Sort";
268
            } else {
269
                $set = "$sortField = $sortField + 1";
270
                $where = "$sortField < $prevSort and $sortField >= $Sort";
271
            }
272
            DB::query("UPDATE $table SET $set WHERE $where");
273
            $this->record->$sortField = $Sort;
274
            $this->record->write();
275
        } catch (Exception $e) {
276
            $error = $e->getMessage();
277
        }
278
279
        if ($error) {
280
            return $this->httpError(400, $error);
281
        }
282
283
        $response = new HTTPResponse(json_encode([
284
            'success' => true,
285
            'message' => _t(__CLASS__ . '.RecordMove', 'Record moved'),
286
        ]));
287
        $response->addHeader('Content-Type', 'application/json');
288
        return $response;
289
    }
290
291
    /**
292
     * @return mixed
293
     */
294
    public function view(HTTPRequest $request)
295
    {
296
        if (!$this->record->canView()) {
297
            return $this->httpError(403, _t(
298
                __CLASS__ . '.ViewPermissionsFailure',
299
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
300
                ['ObjectTitle' => $this->record->singular_name()]
301
            ));
302
        }
303
304
        $controller = $this->getToplevelController();
305
306
        $form = $this->ItemEditForm();
307
        $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

307
        $form->/** @scrutinizer ignore-call */ 
308
               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...
308
309
        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

309
        return $this->returnWithinContext($request, $controller, /** @scrutinizer ignore-type */ $form);
Loading history...
310
    }
311
312
    /**
313
     * This is responsible to forward actions to the model if necessary
314
     * @param HTTPRequest $request
315
     * @return HTTPResponse
316
     */
317
    public function customAction(HTTPRequest $request)
318
    {
319
        // This gets populated thanks to our updated URL handler
320
        $params = $request->params();
321
        $customAction = $params['CustomAction'] ?? null;
322
        $ID = $params['ID'] ?? 0;
323
324
        $dataClass = $this->tabulatorGrid->getModelClass();
325
        $record = DataObject::get_by_id($dataClass, $ID);
326
        $rowActions = $record->tabulatorRowActions();
327
        $validActions = array_keys($rowActions);
328
        if (!$customAction || !in_array($customAction, $validActions)) {
329
            return $this->httpError(403, _t(
330
                __CLASS__ . '.CustomActionPermissionsFailure',
331
                'It seems you don\'t have the necessary permissions to {ActionName} "{ObjectTitle}"',
332
                ['ActionName' => $customAction, 'ObjectTitle' => $this->record->singular_name()]
333
            ));
334
        }
335
336
        $clickedAction = $rowActions[$customAction];
337
338
        $error = false;
339
        try {
340
            $result = $record->$customAction();
341
        } catch (Exception $e) {
342
            $error = true;
343
            $result = $e->getMessage();
344
        }
345
346
        // Maybe it's a custom redirect or a file ?
347
        if ($result && $result instanceof HTTPResponse) {
348
            return $result;
349
        }
350
351
        // Show message on controller or in form
352
        $controller = $this->getToplevelController();
353
        $response = $controller->getResponse();
354
        if (Director::is_ajax()) {
355
            $responseData = [
356
                'message' => $result,
357
                'status' => $error ? 'error' : 'success',
358
            ];
359
            if (!empty($clickedAction['reload'])) {
360
                $responseData['reload'] = true;
361
            }
362
            if (!empty($clickedAction['refresh'])) {
363
                $responseData['refresh'] = true;
364
            }
365
            $response->setBody(json_encode($responseData));
366
            // 4xx status makes a red box
367
            if ($error) {
368
                $response->setStatusCode(400);
369
            }
370
            return $response;
371
        }
372
373
        $target = $this->ItemEditForm();
374
        if ($controller->hasMethod('sessionMessage')) {
375
            $target = $controller;
376
        }
377
        if ($target) {
378
            $target->sessionMessage($result, $error ? "bad" : "good");
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

378
            $target->/** @scrutinizer ignore-call */ 
379
                     sessionMessage($result, $error ? "bad" : "good");

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...
379
        }
380
381
        $url = $this->getBackURL()
382
            ?: $this->getReturnReferer()
383
            ?: $this->AbsoluteLink();
384
385
        $url = $this->appendHash($url);
386
387
        return $controller->redirect($url);
388
    }
389
390
    protected function appendHash($url): string
391
    {
392
        $hash = Cookie::get('hash');
393
        if ($hash) {
394
            $url .= '#' . ltrim($hash, '#');
395
        }
396
        return $url;
397
    }
398
399
    /**
400
     * Builds an item edit form
401
     *
402
     * @return Form|HTTPResponse
403
     */
404
    public function ItemEditForm()
405
    {
406
        $list = $this->tabulatorGrid->getList();
407
        $controller = $this->getToplevelController();
408
409
        try {
410
            $record = $this->getRecord();
411
        } catch (Exception $e) {
412
            $url = $controller->getRequest()->getURL();
413
            $noActionURL = $controller->removeAction($url);
414
            //clear the existing redirect
415
            $controller->getResponse()->removeHeader('Location');
416
            return $controller->redirect($noActionURL, 302);
417
        }
418
419
        // If we are creating a new record in a has-many list, then
420
        // pre-populate the record's foreign key.
421
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
422
            $key = $list->getForeignKey();
423
            $id = $list->getForeignID();
424
            $record->$key = $id;
425
        }
426
427
        if (!$record->canView()) {
428
            return $controller->httpError(403, _t(
429
                __CLASS__ . '.ViewPermissionsFailure',
430
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
431
                ['ObjectTitle' => $this->record->singular_name()]
432
            ));
433
        }
434
435
        if ($record->hasMethod("tabulatorCMSFields")) {
436
            $fields = $record->tabulatorCMSFields();
437
        } else {
438
            $fields = $record->getCMSFields();
439
        }
440
441
        // If we are creating a new record in a has-many list, then
442
        // Disable the form field as it has no effect.
443
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
444
            $key = $list->getForeignKey();
445
446
            if ($field = $fields->dataFieldByName($key)) {
447
                $fields->makeFieldReadonly($field);
448
            }
449
        }
450
451
        $compatLayer = $this->tabulatorGrid->getCompatLayer($controller);
452
453
        $actions = $compatLayer->getFormActions($this);
454
        $this->extend('updateFormActions', $actions);
455
456
        $validator = null;
457
458
        $form = new Form(
459
            $this,
460
            'ItemEditForm',
461
            $fields,
462
            $actions,
463
            $validator
464
        );
465
466
        $form->loadDataFrom($record, $record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
467
468
        if ($record->ID && !$record->canEdit()) {
469
            // Restrict editing of existing records
470
            $form->makeReadonly();
471
            // Hack to re-enable delete button if user can delete
472
            if ($record->canDelete()) {
473
                $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...
474
            }
475
        }
476
        $cannotCreate = !$record->ID && !$record->canCreate(null, $this->getCreateContext());
477
        if ($cannotCreate) {
478
            // Restrict creation of new records
479
            $form->makeReadonly();
480
        }
481
482
        // Load many_many extraData for record.
483
        // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
484
        if ($list instanceof ManyManyList) {
485
            $extraData = $list->getExtraData('', $this->record->ID);
486
            $form->loadDataFrom(['ManyMany' => $extraData]);
487
        }
488
489
        // Coupling with CMS
490
        $compatLayer->adjustItemEditForm($this, $form);
491
492
        $this->extend("updateItemEditForm", $form);
493
494
        return $form;
495
    }
496
497
    /**
498
     * Build context for verifying canCreate
499
     *
500
     * @return array
501
     */
502
    protected function getCreateContext()
503
    {
504
        $grid = $this->tabulatorGrid;
505
        $context = [];
506
        if ($grid->getList() instanceof RelationList) {
507
            $record = $grid->getForm()->getRecord();
508
            if ($record && $record instanceof DataObject) {
0 ignored issues
show
introduced by
$record is always a sub-type of SilverStripe\ORM\DataObject.
Loading history...
509
                $context['Parent'] = $record;
510
            }
511
        }
512
        return $context;
513
    }
514
515
    /**
516
     * @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...
517
     */
518
    public function getToplevelController(): RequestHandler
519
    {
520
        $c = $this->popupController;
521
        // Maybe our field is included in a GridField or in a TabulatorGrid?
522
        while ($c && ($c instanceof GridFieldDetailForm_ItemRequest || $c instanceof TabulatorGrid_ItemRequest)) {
523
            $c = $c->getController();
524
        }
525
        return $c;
526
    }
527
528
    public function getBackLink(): string
529
    {
530
        $backlink = '';
531
        $toplevelController = $this->getToplevelController();
532
        if ($this->popupController->hasMethod('Breadcrumbs')) {
533
            $parents = $this->popupController->Breadcrumbs(false);
534
            if ($parents && $parents = $parents->items) {
535
                $backlink = array_pop($parents)->Link;
536
            }
537
        }
538
        if ($toplevelController && $toplevelController->hasMethod('Backlink')) {
539
            $backlink = $toplevelController->Backlink();
540
        }
541
        if (!$backlink) {
542
            $backlink = $toplevelController->Link();
543
        }
544
        return $backlink;
545
    }
546
547
    /**
548
     * Get the list of extra data from the $record as saved into it by
549
     * {@see Form::saveInto()}
550
     *
551
     * Handles detection of falsey values explicitly saved into the
552
     * DataObject by formfields
553
     *
554
     * @param DataObject $record
555
     * @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...
556
     * @return array List of data to write to the relation
557
     */
558
    protected function getExtraSavedData($record, $list)
559
    {
560
        // Skip extra data if not ManyManyList
561
        if (!($list instanceof ManyManyList)) {
562
            return null;
563
        }
564
565
        $data = [];
566
        foreach ($list->getExtraFields() as $field => $dbSpec) {
567
            $savedField = "ManyMany[{$field}]";
568
            if ($record->hasField($savedField)) {
569
                $data[$field] = $record->getField($savedField);
570
            }
571
        }
572
        return $data;
573
    }
574
575
    public function doSave($data, $form)
576
    {
577
        $isNewRecord = $this->record->ID == 0;
578
579
        // Check permission
580
        if (!$this->record->canEdit()) {
581
            $this->httpError(403, _t(
582
                __CLASS__ . '.EditPermissionsFailure',
583
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
584
                ['ObjectTitle' => $this->record->singular_name()]
585
            ));
586
            return null;
587
        }
588
589
        // Save from form data
590
        $this->saveFormIntoRecord($data, $form);
591
592
        $link = '<a href="' . $this->Link('edit') . '">"'
593
            . htmlspecialchars($this->record->Title, ENT_QUOTES)
594
            . '"</a>';
595
        $message = _t(
596
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved',
597
            'Saved {name} {link}',
598
            [
599
                'name' => $this->record->i18n_singular_name(),
600
                'link' => $link
601
            ]
602
        );
603
604
        $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
605
606
        // Redirect after save
607
        return $this->redirectAfterSave($isNewRecord);
608
    }
609
610
    /**
611
     * Gets the edit link for a record
612
     *
613
     * @param  int $id The ID of the record in the GridField
614
     * @return string
615
     */
616
    public function getEditLink($id)
617
    {
618
        $link = Controller::join_links(
619
            $this->tabulatorGrid->Link(),
620
            'item',
621
            $id
622
        );
623
624
        return $link;
625
    }
626
627
    /**
628
     * @param int $offset The offset from the current record
629
     * @return int|bool
630
     */
631
    private function getAdjacentRecordID($offset)
632
    {
633
        $list = $this->getManipulatedData();
634
        $map = array_column($list['data'], "ID");
635
        $index = array_search($this->record->ID, $map);
636
        return isset($map[$index + $offset]) ? $map[$index + $offset] : false;
637
    }
638
639
    /**
640
     * Gets the ID of the previous record in the list.
641
     */
642
    public function getPreviousRecordID(): int
643
    {
644
        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...
645
    }
646
647
    /**
648
     * Gets the ID of the next record in the list.
649
     */
650
    public function getNextRecordID(): int
651
    {
652
        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...
653
    }
654
655
    /**
656
     * This is expected in lekoala/silverstripe-cms-actions ActionsGridFieldItemRequest
657
     * @return HTTPResponse
658
     */
659
    public function getResponse()
660
    {
661
        return $this->getToplevelController()->getResponse();
662
    }
663
664
    /**
665
     * Response object for this request after a successful save
666
     *
667
     * @param bool $isNewRecord True if this record was just created
668
     * @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...
669
     */
670
    protected function redirectAfterSave($isNewRecord)
671
    {
672
        $controller = $this->getToplevelController();
673
        if ($isNewRecord) {
674
            $url = $this->appendHash($this->Link());
675
            // In Ajax, response content is discarded and hash is not used
676
            return $controller->redirect($url);
677
        } elseif ($this->tabulatorGrid->hasArrayList() && $this->tabulatorGrid->getArrayList()->byID($this->record->ID)) {
678
            // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
679
            // to the same URL (it assumes that its content is already current, and doesn't reload)
680
            return $this->edit($controller->getRequest());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->edit($controller->getRequest()) returns the type SilverStripe\ORM\FieldTy...ViewableData_Customised which is incompatible with the documented return type LeKoala\Tabulator\DBHTML...pe\Control\HTTPResponse.
Loading history...
681
        } else {
682
            // We might be able to redirect to open the record in a different view
683
            if ($redirectDest = $this->component->getLostRecordRedirection($this->tabulatorGrid, $controller->getRequest(), $this->record->ID)) {
0 ignored issues
show
Bug introduced by
The method getLostRecordRedirection() does not exist on null. ( Ignorable by Annotation )

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

683
            if ($redirectDest = $this->component->/** @scrutinizer ignore-call */ getLostRecordRedirection($this->tabulatorGrid, $controller->getRequest(), $this->record->ID)) {

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...
Bug Best Practice introduced by
The property component does not exist on LeKoala\Tabulator\TabulatorGrid_ItemRequest. Since you implemented __get, consider adding a @property annotation.
Loading history...
684
                return $controller->redirect($redirectDest, 302);
685
            }
686
687
            // Changes to the record properties might've excluded the record from
688
            // a filtered list, so return back to the main view if it can't be found
689
            $url = $controller->getRequest()->getURL();
690
            $noActionURL = $controller->removeAction($url);
691
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
692
            return $controller->redirect($noActionURL, 302);
693
        }
694
    }
695
696
    public function httpError($errorCode, $errorMessage = null)
697
    {
698
        $controller = $this->getToplevelController();
699
        return $controller->httpError($errorCode, $errorMessage);
700
    }
701
702
    /**
703
     * Loads the given form data into the underlying dataobject and relation
704
     *
705
     * @param array $data
706
     * @param Form $form
707
     * @throws ValidationException On error
708
     * @return DataObject Saved record
709
     */
710
    protected function saveFormIntoRecord($data, $form)
711
    {
712
        $list = $this->tabulatorGrid->getList();
713
714
        // Check object matches the correct classname
715
        if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
716
            $newClassName = $data['ClassName'];
717
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
718
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
719
            // on the ClassName attribute
720
            $this->record->setClassName($this->record->ClassName);
721
            // Replace $record with a new instance
722
            $this->record = $this->record->newClassInstance($newClassName);
723
        }
724
725
        // Save form and any extra saved data into this dataobject.
726
        // Set writeComponents = true to write has-one relations / join records
727
        $form->saveInto($this->record);
728
        // https://github.com/silverstripe/silverstripe-assets/issues/365
729
        $this->record->write();
730
        $this->extend('onAfterSave', $this->record);
731
732
        $extraData = $this->getExtraSavedData($this->record, $list);
733
        $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

733
        $list->/** @scrutinizer ignore-call */ 
734
               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...
734
735
        return $this->record;
736
    }
737
738
    /**
739
     * @param array $data
740
     * @param Form $form
741
     * @return HTTPResponse
742
     * @throws ValidationException
743
     */
744
    public function doDelete($data, $form)
745
    {
746
        $title = $this->record->Title;
747
        if (!$this->record->canDelete()) {
748
            throw new ValidationException(
749
                _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
750
            );
751
        }
752
        $this->record->delete();
753
754
        $message = _t(
755
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
756
            'Deleted {type} {name}',
757
            [
758
                'type' => $this->record->i18n_singular_name(),
759
                'name' => htmlspecialchars($title, ENT_QUOTES)
760
            ]
761
        );
762
763
        $toplevelController = $this->getToplevelController();
764
        if ($toplevelController && $toplevelController instanceof \SilverStripe\Admin\LeftAndMain) {
765
            $backForm = $toplevelController->getEditForm();
766
            $backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
767
        } else {
768
            $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
769
        }
770
771
        //when an item is deleted, redirect to the parent controller
772
        $controller = $this->getToplevelController();
773
        $controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
774
775
        return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
776
    }
777
778
    /**
779
     * @param string $template
780
     * @return $this
781
     */
782
    public function setTemplate($template)
783
    {
784
        $this->template = $template;
785
        return $this;
786
    }
787
788
    /**
789
     * @return string
790
     */
791
    public function getTemplate()
792
    {
793
        return $this->template;
794
    }
795
796
    /**
797
     * Get list of templates to use
798
     *
799
     * @return array
800
     */
801
    public function getTemplates()
802
    {
803
        $templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
804
        // Prefer any custom template
805
        if ($this->getTemplate()) {
806
            array_unshift($templates, $this->getTemplate());
807
        }
808
        return $templates;
809
    }
810
811
    /**
812
     * @return Controller
813
     */
814
    public function getController()
815
    {
816
        return $this->popupController;
817
    }
818
819
    /**
820
     * @return TabulatorGrid
821
     */
822
    public function getTabulatorGrid()
823
    {
824
        return $this->tabulatorGrid;
825
    }
826
827
    /**
828
     * @return DataObject
829
     */
830
    public function getRecord()
831
    {
832
        return $this->record;
833
    }
834
835
    /**
836
     * CMS-specific functionality: Passes through navigation breadcrumbs
837
     * to the template, and includes the currently edited record (if any).
838
     * see {@link LeftAndMain->Breadcrumbs()} for details.
839
     *
840
     * @param boolean $unlinked
841
     * @return ArrayList
842
     */
843
    public function Breadcrumbs($unlinked = false)
844
    {
845
        if (!$this->popupController->hasMethod('Breadcrumbs')) {
846
            return null;
847
        }
848
849
        /** @var ArrayList $items */
850
        $items = $this->popupController->Breadcrumbs($unlinked);
851
852
        if (!$items) {
0 ignored issues
show
introduced by
$items is of type SilverStripe\ORM\ArrayList, thus it always evaluated to true.
Loading history...
853
            $items = new ArrayList();
854
        }
855
856
        if ($this->record && $this->record->ID) {
857
            $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
858
            $items->push(new ArrayData([
859
                'Title' => $title,
860
                'Link' => $this->Link()
861
            ]));
862
        } else {
863
            $items->push(new ArrayData([
864
                'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]),
865
                'Link' => false
866
            ]));
867
        }
868
869
        $this->extend('updateBreadcrumbs', $items);
870
        return $items;
871
    }
872
}
873