Passed
Push — master ( febce6...7a83de )
by Thomas
12:12
created

TabulatorGrid_ItemRequest::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

196
            $parts = explode(".", /** @scrutinizer ignore-type */ $Column);
Loading history...
197
            $rel = $parts[0];
198
            $relField = $parts[1];
199
            $field = $rel . "ID";
200
            if (!is_numeric($Value)) {
201
                return $this->httpError(400, "ID must have a numerical value");
202
            }
203
        }
204
        if (!$field) {
205
            return $this->httpError(400, "Field must not be empty");
206
        }
207
208
        $this->record->$field = $Value;
209
210
        $error = null;
211
        try {
212
            $this->record->write();
213
            $updatedValue = $this->record->$field;
214
            if ($rel) {
215
                /** @var DataObject $relObject */
216
                $relObject = $this->record->$rel();
217
                $updatedValue = $relObject->relField($relField);
218
            }
219
        } catch (Exception $e) {
220
            $error = $e->getMessage();
221
        }
222
223
        if ($error) {
224
            return $this->httpError(400, $error);
225
        }
226
227
        $response = new HTTPResponse(json_encode([
228
            'success' => true,
229
            'message' => _t(__CLASS__ . '.RecordEdited', 'Record edited'),
230
            '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...
231
        ]));
232
        $response->addHeader('Content-Type', 'application/json');
233
        return $response;
234
    }
235
236
    public function ajaxMove(HTTPRequest $request)
237
    {
238
        $SecurityID = $request->postVar('SecurityID');
239
        if (!SecurityToken::inst()->check($SecurityID)) {
240
            return $this->httpError(404, "Invalid SecurityID");
241
        }
242
        if (!$this->record->canEdit()) {
243
            return $this->httpError(403, _t(
244
                __CLASS__ . '.EditPermissionsFailure',
245
                'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
246
                ['ObjectTitle' => $this->record->singular_name()]
247
            ));
248
        }
249
250
        $table = DataObject::getSchema()->baseDataTable(get_class($this->record));
251
        $sortField = 'Sort';
252
253
        $Data = $request->postVar("Data");
254
        if (is_string($Data)) {
255
            $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

255
            $Data = json_decode($Data, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
256
        }
257
        $Sort = $request->postVar("Sort");
258
259
        if (!isset($Data[$sortField])) {
260
            return $this->httpError(403, _t(
261
                __CLASS__ . '.UnableToResolveSort',
262
                'Unable to resolve previous sort order'
263
            ));
264
        }
265
266
        $prevSort = $Data[$sortField];
267
268
        $error = null;
269
        try {
270
            // Just make sure you don't have 0 (except first record) or equal sorts
271
            if ($prevSort < $Sort) {
272
                $set = "$sortField = $sortField - 1";
273
                $where = "$sortField > $prevSort and $sortField <= $Sort";
274
            } else {
275
                $set = "$sortField = $sortField + 1";
276
                $where = "$sortField < $prevSort and $sortField >= $Sort";
277
            }
278
            DB::query("UPDATE `$table` SET $set WHERE $where");
279
            $this->record->$sortField = $Sort;
280
            $this->record->write();
281
        } catch (Exception $e) {
282
            $error = $e->getMessage();
283
        }
284
285
        if ($error) {
286
            return $this->httpError(400, $error);
287
        }
288
289
        $response = new HTTPResponse(json_encode([
290
            'success' => true,
291
            'message' => _t(__CLASS__ . '.RecordMove', 'Record moved'),
292
        ]));
293
        $response->addHeader('Content-Type', 'application/json');
294
        return $response;
295
    }
296
297
    /**
298
     * @return mixed
299
     */
300
    public function view(HTTPRequest $request)
301
    {
302
        if (!$this->record->canView()) {
303
            return $this->httpError(403, _t(
304
                __CLASS__ . '.ViewPermissionsFailure',
305
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
306
                ['ObjectTitle' => $this->record->singular_name()]
307
            ));
308
        }
309
310
        $controller = $this->getToplevelController();
311
312
        $form = $this->ItemEditForm();
313
        $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

313
        $form->/** @scrutinizer ignore-call */ 
314
               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...
314
315
        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

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

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

781
        $list->/** @scrutinizer ignore-call */ 
782
               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...
782
783
        return $this->record;
784
    }
785
786
    /**
787
     * @param array $data
788
     * @param Form $form
789
     * @return HTTPResponse
790
     * @throws ValidationException
791
     */
792
    public function doDelete($data, $form)
793
    {
794
        $title = $this->record->Title;
795
        if (!$this->record->canDelete()) {
796
            throw new ValidationException(
797
                _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
798
            );
799
        }
800
801
        $this->record->delete();
802
803
        $message = _t(
804
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
805
            'Deleted {type} {name}',
806
            [
807
                'type' => $this->record->i18n_singular_name(),
808
                'name' => htmlspecialchars($title, ENT_QUOTES)
809
            ]
810
        );
811
812
813
        $backForm = $form;
0 ignored issues
show
Unused Code introduced by
The assignment to $backForm is dead and can be removed.
Loading history...
814
        $toplevelController = $this->getToplevelController();
815
        if ($this->isSilverStripeAdmin($toplevelController)) {
816
            $backForm = $toplevelController->getEditForm();
817
        }
818
        $this->sessionMessage($message, "good");
819
820
        //when an item is deleted, redirect to the parent controller
821
        $controller = $this->getToplevelController();
822
823
        if ($this->isSilverStripeAdmin($toplevelController)) {
824
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
825
        }
826
        return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
827
    }
828
829
    public function isSilverStripeAdmin($controller)
830
    {
831
        if ($controller) {
832
            return is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class);
833
        }
834
        return false;
835
    }
836
837
    /**
838
     * @param string $template
839
     * @return $this
840
     */
841
    public function setTemplate($template)
842
    {
843
        $this->template = $template;
844
        return $this;
845
    }
846
847
    /**
848
     * @return string
849
     */
850
    public function getTemplate()
851
    {
852
        return $this->template;
853
    }
854
855
    /**
856
     * Get list of templates to use
857
     *
858
     * @return array
859
     */
860
    public function getTemplates()
861
    {
862
        $templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
863
        // Prefer any custom template
864
        if ($this->getTemplate()) {
865
            array_unshift($templates, $this->getTemplate());
866
        }
867
        return $templates;
868
    }
869
870
    /**
871
     * @return Controller
872
     */
873
    public function getController()
874
    {
875
        return $this->popupController;
876
    }
877
878
    /**
879
     * @return TabulatorGrid
880
     */
881
    public function getTabulatorGrid()
882
    {
883
        return $this->tabulatorGrid;
884
    }
885
886
    /**
887
     * @return DataObject
888
     */
889
    public function getRecord()
890
    {
891
        return $this->record;
892
    }
893
894
    /**
895
     * CMS-specific functionality: Passes through navigation breadcrumbs
896
     * to the template, and includes the currently edited record (if any).
897
     * see {@link LeftAndMain->Breadcrumbs()} for details.
898
     *
899
     * @param boolean $unlinked
900
     * @return ArrayList
901
     */
902
    public function Breadcrumbs($unlinked = false)
903
    {
904
        if (!$this->popupController->hasMethod('Breadcrumbs')) {
905
            return null;
906
        }
907
908
        /** @var ArrayList $items */
909
        $items = $this->popupController->Breadcrumbs($unlinked);
910
911
        if (!$items) {
0 ignored issues
show
introduced by
$items is of type SilverStripe\ORM\ArrayList, thus it always evaluated to true.
Loading history...
912
            $items = new ArrayList();
913
        }
914
915
        if ($this->record && $this->record->ID) {
916
            $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
917
            $items->push(new ArrayData([
918
                'Title' => $title,
919
                'Link' => $this->Link()
920
            ]));
921
        } else {
922
            $items->push(new ArrayData([
923
                'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]),
924
                'Link' => false
925
            ]));
926
        }
927
928
        $this->extend('updateBreadcrumbs', $items);
929
        return $items;
930
    }
931
}
932