Passed
Push — master ( e3472f...a5929e )
by Thomas
02:39
created

TabulatorGrid_ItemRequest::getTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
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
use Symfony\Component\Translation\Dumper\QtFileDumper;
26
27
/**
28
 * Endpoint for actions related to a specific record
29
 *
30
 * It also allows to display a form to edit this record
31
 */
32
class TabulatorGrid_ItemRequest extends RequestHandler
33
{
34
35
    private static $allowed_actions = [
36
        'edit',
37
        'ajaxEdit',
38
        'ajaxMove',
39
        'view',
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 $template = '';
65
66
    private static $url_handlers = [
67
        'customAction/$CustomAction' => 'customAction',
68
        '$Action!' => '$Action',
69
        '' => 'edit',
70
    ];
71
72
    /**
73
     *
74
     * @param TabulatorGrid $tabulatorGrid
75
     * @param DataObject $record
76
     * @param RequestHandler $requestHandler
77
     */
78
    public function __construct($tabulatorGrid, $record, $requestHandler)
79
    {
80
        $this->tabulatorGrid = $tabulatorGrid;
81
        $this->record = $record;
82
        $this->popupController = $requestHandler;
83
        parent::__construct();
84
    }
85
86
    public function Link($action = null)
87
    {
88
        return Controller::join_links(
89
            $this->tabulatorGrid->Link('item'),
90
            $this->record->ID ? $this->record->ID : 'new',
91
            $action
92
        );
93
    }
94
95
    public function AbsoluteLink($action = null)
96
    {
97
        return Director::absoluteURL($this->Link($action));
98
    }
99
100
    protected function getManipulatedData(): array
101
    {
102
        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...
103
            return $this->manipulatedData;
104
        }
105
        $grid = $this->getTabulatorGrid();
106
107
        $state = $grid->getState($this->popupController->getRequest());
108
109
        $currentPage = $state['page'];
110
        $itemsPerPage = $state['limit'];
111
112
        $limit = $itemsPerPage + 2;
113
        $offset = max(0, $itemsPerPage * ($currentPage - 1) - 1);
114
115
        $list = $grid->getManipulatedData($limit, $offset, $state['sort'], $state['filter']);
116
117
        $this->manipulatedData = $list;
118
        return $list;
119
    }
120
121
    public function index(HTTPRequest $request)
122
    {
123
        $controller = $this->getToplevelController();
124
        return $controller->redirect($this->Link('edit'));
125
    }
126
127
    protected function returnWithinContext(HTTPRequest $request, RequestHandler $controller, Form $form)
128
    {
129
        $data = $this->customise([
130
            'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
131
            'ItemEditForm' => $form,
132
        ]);
133
134
        $return = $data->renderWith('LeKoala\\Tabulator\\TabulatorGrid_ItemEditForm');
135
        if ($request->isAjax()) {
136
            return $return;
137
        }
138
        // If not requested by ajax, we need to render it within the controller context+template
139
        return $controller->customise([
140
            'Content' => $return,
141
        ]);
142
    }
143
144
    /**
145
     * This is responsible to display an edit form, like GridFieldDetailForm, but much simpler
146
     *
147
     * @return mixed
148
     */
149
    public function edit(HTTPRequest $request)
150
    {
151
        if (!$this->record->canEdit()) {
152
            return $this->httpError(403, _t(
153
                __CLASS__ . '.EditPermissionsFailure',
154
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
155
                ['ObjectTitle' => $this->record->singular_name()]
156
            ));
157
        }
158
        $controller = $this->getToplevelController();
159
160
        $form = $this->ItemEditForm();
161
162
        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

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

194
        if (strpos(/** @scrutinizer ignore-type */ $Column, ".") !== false) {
Loading history...
195
            $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

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

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

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

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

375
        $this->sessionMessage($controller, /** @scrutinizer ignore-type */ $form, $result, $error);
Loading history...
376
377
        $url = $this->getBackURL()
378
            ?: $this->getReturnReferer()
379
            ?: $this->AbsoluteLink();
380
381
        $url = $this->appendHash($url);
382
383
        return $controller->redirect($url);
384
    }
385
386
    protected function sessionMessage(Controller $controller, Form $form, $message, $error = false)
387
    {
388
        if ($controller->hasMethod('sessionMessage')) {
389
            $controller->sessionMessage($message, $error ? "bad" : "good");
390
        } elseif ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
391
            $form->sessionMessage($message, $error ? "bad" : "good");
392
        }
393
    }
394
395
    protected function appendHash($url): string
396
    {
397
        $hash = Cookie::get('hash');
398
        if ($hash) {
399
            $url .= '#' . ltrim($hash, '#');
400
        }
401
        return $url;
402
    }
403
404
    /**
405
     * Builds an item edit form
406
     *
407
     * @return Form|HTTPResponse
408
     */
409
    public function ItemEditForm()
410
    {
411
        $list = $this->tabulatorGrid->getList();
412
        $controller = $this->getToplevelController();
413
414
        try {
415
            $record = $this->getRecord();
416
        } catch (Exception $e) {
417
            $url = $controller->getRequest()->getURL();
418
            $noActionURL = $controller->removeAction($url);
419
            //clear the existing redirect
420
            $controller->getResponse()->removeHeader('Location');
421
            return $controller->redirect($noActionURL, 302);
422
        }
423
424
        // If we are creating a new record in a has-many list, then
425
        // pre-populate the record's foreign key.
426
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
427
            $key = $list->getForeignKey();
428
            $id = $list->getForeignID();
429
            $record->$key = $id;
430
        }
431
432
        if (!$record->canView()) {
433
            return $controller->httpError(403, _t(
434
                __CLASS__ . '.ViewPermissionsFailure',
435
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
436
                ['ObjectTitle' => $this->record->singular_name()]
437
            ));
438
        }
439
440
        if ($record->hasMethod("tabulatorCMSFields")) {
441
            $fields = $record->tabulatorCMSFields();
442
        } else {
443
            $fields = $record->getCMSFields();
444
        }
445
446
        // If we are creating a new record in a has-many list, then
447
        // Disable the form field as it has no effect.
448
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
449
            $key = $list->getForeignKey();
450
451
            if ($field = $fields->dataFieldByName($key)) {
452
                $fields->makeFieldReadonly($field);
453
            }
454
        }
455
456
        $compatLayer = $this->tabulatorGrid->getCompatLayer($controller);
457
458
        $actions = $compatLayer->getFormActions($this);
459
        $this->extend('updateFormActions', $actions);
460
461
        $validator = null;
462
463
        $form = new Form(
464
            $this,
465
            'ItemEditForm',
466
            $fields,
467
            $actions,
468
            $validator
469
        );
470
471
        $form->loadDataFrom($record, $record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
472
473
        if ($record->ID && !$record->canEdit()) {
474
            // Restrict editing of existing records
475
            $form->makeReadonly();
476
            // Hack to re-enable delete button if user can delete
477
            if ($record->canDelete()) {
478
                $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...
479
            }
480
        }
481
        $cannotCreate = !$record->ID && !$record->canCreate(null, $this->getCreateContext());
482
        if ($cannotCreate) {
483
            // Restrict creation of new records
484
            $form->makeReadonly();
485
        }
486
487
        // Load many_many extraData for record.
488
        // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
489
        if ($list instanceof ManyManyList) {
490
            $extraData = $list->getExtraData('', $this->record->ID);
491
            $form->loadDataFrom(['ManyMany' => $extraData]);
492
        }
493
494
        // Coupling with CMS
495
        $compatLayer->adjustItemEditForm($this, $form);
496
497
        $this->extend("updateItemEditForm", $form);
498
499
        return $form;
500
    }
501
502
    /**
503
     * Build context for verifying canCreate
504
     *
505
     * @return array
506
     */
507
    protected function getCreateContext()
508
    {
509
        $grid = $this->tabulatorGrid;
510
        $context = [];
511
        if ($grid->getList() instanceof RelationList) {
512
            $record = $grid->getForm()->getRecord();
513
            if ($record && $record instanceof DataObject) {
0 ignored issues
show
introduced by
$record is always a sub-type of SilverStripe\ORM\DataObject.
Loading history...
514
                $context['Parent'] = $record;
515
            }
516
        }
517
        return $context;
518
    }
519
520
    /**
521
     * @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...
522
     */
523
    public function getToplevelController(): RequestHandler
524
    {
525
        $c = $this->popupController;
526
        // Maybe our field is included in a GridField or in a TabulatorGrid?
527
        while ($c && ($c instanceof GridFieldDetailForm_ItemRequest || $c instanceof TabulatorGrid_ItemRequest)) {
528
            $c = $c->getController();
529
        }
530
        return $c;
531
    }
532
533
    public function getBackLink(): string
534
    {
535
        $backlink = '';
536
        $toplevelController = $this->getToplevelController();
537
        if ($this->popupController->hasMethod('Breadcrumbs')) {
538
            $parents = $this->popupController->Breadcrumbs(false);
539
            if ($parents && $parents = $parents->items) {
540
                $backlink = array_pop($parents)->Link;
541
            }
542
        }
543
        if ($toplevelController && $toplevelController->hasMethod('Backlink')) {
544
            $backlink = $toplevelController->Backlink();
545
        }
546
        if (!$backlink) {
547
            $backlink = $toplevelController->Link();
548
        }
549
        return $backlink;
550
    }
551
552
    /**
553
     * Get the list of extra data from the $record as saved into it by
554
     * {@see Form::saveInto()}
555
     *
556
     * Handles detection of falsey values explicitly saved into the
557
     * DataObject by formfields
558
     *
559
     * @param DataObject $record
560
     * @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...
561
     * @return array List of data to write to the relation
562
     */
563
    protected function getExtraSavedData($record, $list)
564
    {
565
        // Skip extra data if not ManyManyList
566
        if (!($list instanceof ManyManyList)) {
567
            return null;
568
        }
569
570
        $data = [];
571
        foreach ($list->getExtraFields() as $field => $dbSpec) {
572
            $savedField = "ManyMany[{$field}]";
573
            if ($record->hasField($savedField)) {
574
                $data[$field] = $record->getField($savedField);
575
            }
576
        }
577
        return $data;
578
    }
579
580
    public function doSave($data, $form)
581
    {
582
        $isNewRecord = $this->record->ID == 0;
583
584
        // Check permission
585
        if (!$this->record->canEdit()) {
586
            $this->httpError(403, _t(
587
                __CLASS__ . '.EditPermissionsFailure',
588
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
589
                ['ObjectTitle' => $this->record->singular_name()]
590
            ));
591
            return null;
592
        }
593
594
        // Save from form data
595
        $error = false;
596
        try {
597
            $this->saveFormIntoRecord($data, $form);
598
599
            $link = '<a href="' . $this->Link('edit') . '">"'
600
                . htmlspecialchars($this->record->Title, ENT_QUOTES)
601
                . '"</a>';
602
            $message = _t(
603
                'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved',
604
                'Saved {name} {link}',
605
                [
606
                    'name' => $this->record->i18n_singular_name(),
607
                    'link' => $link
608
                ]
609
            );
610
        } catch (Exception $e) {
611
            $message = $e->getMessage();
612
            $error = true;
613
        }
614
615
        $controller = $this->getToplevelController();
616
        $this->sessionMessage($controller, $form, $message, $error);
617
618
        // Redirect after save
619
        return $this->redirectAfterSave($isNewRecord);
620
    }
621
622
    /**
623
     * Gets the edit link for a record
624
     *
625
     * @param  int $id The ID of the record in the GridField
626
     * @return string
627
     */
628
    public function getEditLink($id)
629
    {
630
        $link = Controller::join_links(
631
            $this->tabulatorGrid->Link(),
632
            'item',
633
            $id
634
        );
635
636
        return $link;
637
    }
638
639
    /**
640
     * @param int $offset The offset from the current record
641
     * @return int|bool
642
     */
643
    private function getAdjacentRecordID($offset)
644
    {
645
        $list = $this->getManipulatedData();
646
        $map = array_column($list['data'], "ID");
647
        $index = array_search($this->record->ID, $map);
648
        return isset($map[$index + $offset]) ? $map[$index + $offset] : false;
649
    }
650
651
    /**
652
     * Gets the ID of the previous record in the list.
653
     */
654
    public function getPreviousRecordID(): int
655
    {
656
        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...
657
    }
658
659
    /**
660
     * Gets the ID of the next record in the list.
661
     */
662
    public function getNextRecordID(): int
663
    {
664
        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...
665
    }
666
667
    /**
668
     * This is expected in lekoala/silverstripe-cms-actions ActionsGridFieldItemRequest
669
     * @return HTTPResponse
670
     */
671
    public function getResponse()
672
    {
673
        return $this->getToplevelController()->getResponse();
674
    }
675
676
    /**
677
     * Response object for this request after a successful save
678
     *
679
     * @param bool $isNewRecord True if this record was just created
680
     * @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...
681
     */
682
    protected function redirectAfterSave($isNewRecord)
683
    {
684
        $controller = $this->getToplevelController();
685
        if ($isNewRecord) {
686
            $url = $this->appendHash($this->Link());
687
            // In Ajax, response content is discarded and hash is not used
688
            return $controller->redirect($url);
689
        } elseif ($this->tabulatorGrid->hasByIDList() && $this->tabulatorGrid->getByIDList()->byID($this->record->ID)) {
690
            if (Director::is_ajax()) {
691
                // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
692
                // to the same URL (it assumes that its content is already current, and doesn't reload)
693
                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...
694
            }
695
            return $controller->redirectBack();
696
        } else {
697
            // Changes to the record properties might've excluded the record from
698
            // a filtered list, so return back to the main view if it can't be found
699
            $url = $controller->getRequest()->getURL();
700
            $noActionURL = $controller->removeAction($url);
701
            if ($this->isSilverStripeAdmin($controller)) {
702
                $controller->getRequest()->addHeader('X-Pjax', 'Content');
703
            }
704
            return $controller->redirect($noActionURL, 302);
705
        }
706
    }
707
708
    public function httpError($errorCode, $errorMessage = null)
709
    {
710
        $controller = $this->getToplevelController();
711
        return $controller->httpError($errorCode, $errorMessage);
712
    }
713
714
    /**
715
     * Loads the given form data into the underlying dataobject and relation
716
     *
717
     * @param array $data
718
     * @param Form $form
719
     * @throws ValidationException On error
720
     * @return DataObject Saved record
721
     */
722
    protected function saveFormIntoRecord($data, $form)
723
    {
724
        $list = $this->tabulatorGrid->getList();
725
726
        // Check object matches the correct classname
727
        if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
728
            $newClassName = $data['ClassName'];
729
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
730
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
731
            // on the ClassName attribute
732
            $this->record->setClassName($this->record->ClassName);
733
            // Replace $record with a new instance
734
            $this->record = $this->record->newClassInstance($newClassName);
735
        }
736
737
        // Save form and any extra saved data into this dataobject.
738
        // Set writeComponents = true to write has-one relations / join records
739
        $form->saveInto($this->record);
740
        // https://github.com/silverstripe/silverstripe-assets/issues/365
741
        $this->record->write();
742
        $this->extend('onAfterSave', $this->record);
743
744
        $extraData = $this->getExtraSavedData($this->record, $list);
745
        $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

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