Passed
Push — master ( 98de9f...355f40 )
by Thomas
02:42
created

TabulatorGrid_ItemRequest::unlink()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 37
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

392
        /** @scrutinizer ignore-call */ 
393
        d($controller);
Loading history...
393
394
        if ($this->isSilverStripeAdmin($controller)) {
395
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
396
        }
397
398
        //redirect back to admin section
399
        $response = $controller->redirect($this->getBackLink(), 302);
400
401
        $this->sessionMessage($message, "good");
402
403
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
404
    }
405
406
    /**
407
     * @return mixed
408
     */
409
    public function view(HTTPRequest $request)
410
    {
411
        if (!$this->record->canView()) {
412
            return $this->httpError(403, _t(
413
                __CLASS__ . '.ViewPermissionsFailure',
414
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
415
                ['ObjectTitle' => $this->record->singular_name()]
416
            ));
417
        }
418
419
        $controller = $this->getToplevelController();
420
421
        $form = $this->ItemEditForm();
422
        $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

422
        $form->/** @scrutinizer ignore-call */ 
423
               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...
423
424
        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

424
        return $this->returnWithinContext($request, $controller, /** @scrutinizer ignore-type */ $form);
Loading history...
425
    }
426
427
    /**
428
     * This is responsible to forward actions to the model if necessary
429
     * @param HTTPRequest $request
430
     * @return HTTPResponse
431
     */
432
    public function customAction(HTTPRequest $request)
433
    {
434
        // This gets populated thanks to our updated URL handler
435
        $params = $request->params();
436
        $customAction = $params['CustomAction'] ?? null;
437
        $ID = $params['ID'] ?? 0;
438
439
        $dataClass = $this->tabulatorGrid->getModelClass();
440
        $record = DataObject::get_by_id($dataClass, $ID);
441
        $rowActions = $record->tabulatorRowActions();
442
        $validActions = array_keys($rowActions);
443
        if (!$customAction || !in_array($customAction, $validActions)) {
444
            return $this->httpError(403, _t(
445
                __CLASS__ . '.CustomActionPermissionsFailure',
446
                'It seems you don\'t have the necessary permissions to {ActionName} "{ObjectTitle}"',
447
                ['ActionName' => $customAction, 'ObjectTitle' => $this->record->singular_name()]
448
            ));
449
        }
450
451
        $clickedAction = $rowActions[$customAction];
452
453
        $error = false;
454
        try {
455
            $result = $record->$customAction();
456
        } catch (Exception $e) {
457
            $error = true;
458
            $result = $e->getMessage();
459
        }
460
461
        // Maybe it's a custom redirect or a file ?
462
        if ($result && $result instanceof HTTPResponse) {
463
            return $result;
464
        }
465
466
        // Show message on controller or in form
467
        $controller = $this->getToplevelController();
468
        $response = $controller->getResponse();
469
        if (Director::is_ajax()) {
470
            $responseData = [
471
                'message' => $result,
472
                'status' => $error ? 'error' : 'success',
473
            ];
474
            if (!empty($clickedAction['reload'])) {
475
                $responseData['reload'] = true;
476
            }
477
            if (!empty($clickedAction['refresh'])) {
478
                $responseData['refresh'] = true;
479
            }
480
            $response->setBody(json_encode($responseData));
481
            // 4xx status makes a red box
482
            if ($error) {
483
                $response->setStatusCode(400);
484
            }
485
            return $response;
486
        }
487
488
        $url = $this->getDefaultBackLink();
489
        $response = $this->redirect($url);
490
491
        $this->sessionMessage($result, $error ? "error" : "good", "html");
492
493
        return $response;
494
    }
495
496
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
497
    {
498
        $controller = $this->getToplevelController();
499
        if ($controller->hasMethod('sessionMessage')) {
500
            $controller->sessionMessage($message, $type, $cast);
501
        } else {
502
            $form = $this->ItemEditForm();
503
            if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Control\HTTPResponse, thus it always evaluated to true.
Loading history...
504
                $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

504
                $form->/** @scrutinizer ignore-call */ 
505
                       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...
505
            }
506
        }
507
    }
508
509
    /**
510
     * Builds an item edit form
511
     *
512
     * @return Form|HTTPResponse
513
     */
514
    public function ItemEditForm()
515
    {
516
        $list = $this->tabulatorGrid->getList();
517
        $controller = $this->getToplevelController();
518
519
        try {
520
            $record = $this->getRecord();
521
        } catch (Exception $e) {
522
            $url = $controller->getRequest()->getURL();
523
            $noActionURL = $controller->removeAction($url);
524
            //clear the existing redirect
525
            $controller->getResponse()->removeHeader('Location');
526
            return $controller->redirect($noActionURL, 302);
527
        }
528
529
        // If we are creating a new record in a has-many list, then
530
        // pre-populate the record's foreign key.
531
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
532
            $key = $list->getForeignKey();
533
            $id = $list->getForeignID();
534
            $record->$key = $id;
535
        }
536
537
        if (!$record->canView()) {
538
            return $controller->httpError(403, _t(
539
                __CLASS__ . '.ViewPermissionsFailure',
540
                'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
541
                ['ObjectTitle' => $this->record->singular_name()]
542
            ));
543
        }
544
545
        if ($record->hasMethod("tabulatorCMSFields")) {
546
            $fields = $record->tabulatorCMSFields();
547
        } else {
548
            $fields = $record->getCMSFields();
549
        }
550
551
        // If we are creating a new record in a has-many list, then
552
        // Disable the form field as it has no effect.
553
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
554
            $key = $list->getForeignKey();
555
556
            if ($field = $fields->dataFieldByName($key)) {
557
                $fields->makeFieldReadonly($field);
558
            }
559
        }
560
561
        $compatLayer = $this->tabulatorGrid->getCompatLayer($controller);
562
563
        $actions = $compatLayer->getFormActions($this);
564
        $this->extend('updateFormActions', $actions);
565
566
        $validator = null;
567
568
        $form = new Form(
569
            $this,
570
            'ItemEditForm',
571
            $fields,
572
            $actions,
573
            $validator
574
        );
575
576
        $form->loadDataFrom($record, $record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
577
578
        if ($record->ID && !$record->canEdit()) {
579
            // Restrict editing of existing records
580
            $form->makeReadonly();
581
            // Hack to re-enable delete button if user can delete
582
            if ($record->canDelete()) {
583
                $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...
584
            }
585
        }
586
        $cannotCreate = !$record->ID && !$record->canCreate(null, $this->getCreateContext());
587
        if ($cannotCreate || $this->tabulatorGrid->isViewOnly()) {
588
            // Restrict creation of new records
589
            $form->makeReadonly();
590
        }
591
592
        // Load many_many extraData for record.
593
        // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
594
        if ($list instanceof ManyManyList) {
595
            $extraData = $list->getExtraData('', $this->record->ID);
596
            $form->loadDataFrom(['ManyMany' => $extraData]);
597
        }
598
599
        // Coupling with CMS
600
        $compatLayer->adjustItemEditForm($this, $form);
601
602
        $this->extend("updateItemEditForm", $form);
603
604
        return $form;
605
    }
606
607
    /**
608
     * Build context for verifying canCreate
609
     *
610
     * @return array
611
     */
612
    protected function getCreateContext()
613
    {
614
        $grid = $this->tabulatorGrid;
615
        $context = [];
616
        if ($grid->getList() instanceof RelationList) {
617
            $record = $grid->getForm()->getRecord();
618
            if ($record && $record instanceof DataObject) {
0 ignored issues
show
introduced by
$record is always a sub-type of SilverStripe\ORM\DataObject.
Loading history...
619
                $context['Parent'] = $record;
620
            }
621
        }
622
        return $context;
623
    }
624
625
    /**
626
     * @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...
627
     */
628
    public function getToplevelController(): RequestHandler
629
    {
630
        $c = $this->popupController;
631
        // Maybe our field is included in a GridField or in a TabulatorGrid?
632
        while ($c && ($c instanceof GridFieldDetailForm_ItemRequest || $c instanceof TabulatorGrid_ItemRequest)) {
633
            $c = $c->getController();
634
        }
635
        return $c;
636
    }
637
638
    public function getDefaultBackLink(): string
639
    {
640
        $url = $this->getBackURL()
641
            ?: $this->getReturnReferer()
642
            ?: $this->AbsoluteLink();
643
        return $url;
644
    }
645
646
    public function getBackLink(): string
647
    {
648
        $backlink = '';
649
        $toplevelController = $this->getToplevelController();
650
        if ($this->popupController->hasMethod('Breadcrumbs')) {
651
            $parents = $this->popupController->Breadcrumbs(false);
652
            if ($parents && $parents = $parents->items) {
653
                $backlink = array_pop($parents)->Link;
654
            }
655
        }
656
        if ($toplevelController && $toplevelController->hasMethod('Backlink')) {
657
            $backlink = $toplevelController->Backlink();
658
        }
659
        if (!$backlink) {
660
            $backlink = $toplevelController->Link();
661
        }
662
        return $backlink;
663
    }
664
665
    /**
666
     * Get the list of extra data from the $record as saved into it by
667
     * {@see Form::saveInto()}
668
     *
669
     * Handles detection of falsey values explicitly saved into the
670
     * DataObject by formfields
671
     *
672
     * @param DataObject $record
673
     * @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...
674
     * @return array List of data to write to the relation
675
     */
676
    protected function getExtraSavedData($record, $list)
677
    {
678
        // Skip extra data if not ManyManyList
679
        if (!($list instanceof ManyManyList)) {
680
            return null;
681
        }
682
683
        $data = [];
684
        foreach ($list->getExtraFields() as $field => $dbSpec) {
685
            $savedField = "ManyMany[{$field}]";
686
            if ($record->hasField($savedField)) {
687
                $data[$field] = $record->getField($savedField);
688
            }
689
        }
690
        return $data;
691
    }
692
693
    public function doSave($data, $form)
694
    {
695
        $isNewRecord = $this->record->ID == 0;
696
697
        // Check permission
698
        if (!$this->record->canEdit()) {
699
            return $this->editFailure();
700
        }
701
702
        // _activetab is used in cms-action
703
        $this->hash = $data['_hash'] ?? $data['_activetab'] ?? '';
704
705
        // Save from form data
706
        $error = false;
707
708
        // Leave it to FormRequestHandler::getAjaxErrorResponse so that we don't lose our data on error
709
        // try {
710
        $this->saveFormIntoRecord($data, $form);
711
712
        $title = $this->record->Title ?? '';
713
        $link = '<a href="' . $this->Link('edit') . '">"'
714
            . htmlspecialchars($title, ENT_QUOTES)
715
            . '"</a>';
716
        $message = _t(
717
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved',
718
            'Saved {name} {link}',
719
            [
720
                'name' => $this->record->i18n_singular_name(),
721
                'link' => $link
722
            ]
723
        );
724
        // } catch (Exception $e) {
725
        //     $message = $e->getMessage();
726
        //     $error = true;
727
        // }
728
729
        // Redirect after save
730
        $response = $this->redirectAfterSave($isNewRecord);
731
732
        // Session message may add stuff to the response, so create the redirect before
733
        $this->sessionMessage($message, $error ? "error" : "good", 'html');
0 ignored issues
show
introduced by
The condition $error is always false.
Loading history...
734
735
        return $response;
736
    }
737
738
    /**
739
     * Gets the edit link for a record
740
     *
741
     * @param  int $id The ID of the record in the GridField
742
     * @return string
743
     */
744
    public function getEditLink($id)
745
    {
746
        $link = Controller::join_links(
747
            $this->tabulatorGrid->Link(),
748
            'item',
749
            $id
750
        );
751
752
        return $link;
753
    }
754
755
    /**
756
     * @param int $offset The offset from the current record
757
     * @return int|bool
758
     */
759
    private function getAdjacentRecordID($offset)
760
    {
761
        $list = $this->getManipulatedData();
762
        $map = array_column($list['data'], "ID");
763
        $index = array_search($this->record->ID, $map);
764
        return isset($map[$index + $offset]) ? $map[$index + $offset] : false;
765
    }
766
767
    /**
768
     * Gets the ID of the previous record in the list.
769
     */
770
    public function getPreviousRecordID(): int
771
    {
772
        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...
773
    }
774
775
    /**
776
     * Gets the ID of the next record in the list.
777
     */
778
    public function getNextRecordID(): int
779
    {
780
        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...
781
    }
782
783
    /**
784
     * This is expected in lekoala/silverstripe-cms-actions ActionsGridFieldItemRequest
785
     * @return HTTPResponse
786
     */
787
    public function getResponse()
788
    {
789
        return $this->getToplevelController()->getResponse();
790
    }
791
792
    /**
793
     * Response object for this request after a successful save
794
     *
795
     * @param bool $isNewRecord True if this record was just created
796
     * @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...
797
     */
798
    protected function redirectAfterSave($isNewRecord)
799
    {
800
        $controller = $this->getToplevelController();
801
        if ($isNewRecord) {
802
            return $this->redirect($this->Link());
803
        } elseif ($this->tabulatorGrid->hasByIDList() && $this->tabulatorGrid->getByIDList()->byID($this->record->ID)) {
804
            return $this->redirect($this->getDefaultBackLink());
805
        } else {
806
            // Changes to the record properties might've excluded the record from
807
            // a filtered list, so return back to the main view if it can't be found
808
            $url = $controller->getRequest()->getURL();
809
            $noActionURL = $controller->removeAction($url);
810
            if ($this->isSilverStripeAdmin($controller)) {
811
                $controller->getRequest()->addHeader('X-Pjax', 'Content');
812
            }
813
            return $controller->redirect($noActionURL, 302);
814
        }
815
    }
816
817
    protected function getHashValue()
818
    {
819
        if ($this->hash) {
820
            $hash = $this->hash;
821
        } else {
822
            $hash = Cookie::get('hash');
823
        }
824
        if ($hash) {
825
            $hash = '#' . ltrim($hash, '#');
826
        }
827
        return $hash;
828
    }
829
830
    /**
831
     * Redirect to the given URL.
832
     *
833
     * @param string $url
834
     * @param int $code
835
     * @return HTTPResponse
836
     */
837
    public function redirect($url, $code = 302): HTTPResponse
838
    {
839
        $hash = $this->getHashValue();
840
        if ($hash) {
841
            $url .= $hash;
842
        }
843
844
        $controller = $this->getToplevelController();
845
        $response = $controller->redirect($url, $code);
846
847
        // if ($hash) {
848
        // We also pass it as a hash
849
        // @link https://github.com/whatwg/fetch/issues/1167
850
        // $response = $response->addHeader('X-Hash', $hash);
851
        // }
852
853
        return $response;
854
    }
855
856
    public function httpError($errorCode, $errorMessage = null)
857
    {
858
        $controller = $this->getToplevelController();
859
        return $controller->httpError($errorCode, $errorMessage);
860
    }
861
862
    /**
863
     * Loads the given form data into the underlying dataobject and relation
864
     *
865
     * @param array $data
866
     * @param Form $form
867
     * @throws ValidationException On error
868
     * @return DataObject Saved record
869
     */
870
    protected function saveFormIntoRecord($data, $form)
871
    {
872
        $list = $this->tabulatorGrid->getList();
873
874
        // Check object matches the correct classname
875
        if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
876
            $newClassName = $data['ClassName'];
877
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
878
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
879
            // on the ClassName attribute
880
            $this->record->setClassName($this->record->ClassName);
881
            // Replace $record with a new instance
882
            $this->record = $this->record->newClassInstance($newClassName);
883
        }
884
885
        // Save form and any extra saved data into this dataobject.
886
        // Set writeComponents = true to write has-one relations / join records
887
        $form->saveInto($this->record);
888
        // https://github.com/silverstripe/silverstripe-assets/issues/365
889
        $this->record->write();
890
        $this->extend('onAfterSave', $this->record);
891
892
        $extraData = $this->getExtraSavedData($this->record, $list);
893
        $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

893
        $list->/** @scrutinizer ignore-call */ 
894
               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...
894
895
        return $this->record;
896
    }
897
898
    /**
899
     * Delete from ItemRequest action
900
     *
901
     * @param array $data
902
     * @param Form $form
903
     * @return HTTPResponse
904
     * @throws ValidationException
905
     */
906
    public function doDelete($data, $form)
907
    {
908
        $title = $this->record->Title;
909
        if (!$this->record->canDelete()) {
910
            throw new ValidationException(
911
                _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
912
            );
913
        }
914
915
        $this->record->delete();
916
917
        $message = _t(
918
            'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
919
            'Deleted {type} {name}',
920
            [
921
                'type' => $this->record->i18n_singular_name(),
922
                'name' => htmlspecialchars($title, ENT_QUOTES)
923
            ]
924
        );
925
926
927
        $backForm = $form;
0 ignored issues
show
Unused Code introduced by
The assignment to $backForm is dead and can be removed.
Loading history...
928
        $toplevelController = $this->getToplevelController();
929
        if ($this->isSilverStripeAdmin($toplevelController)) {
930
            $backForm = $toplevelController->getEditForm();
931
        }
932
        //when an item is deleted, redirect to the parent controller
933
        $controller = $this->getToplevelController();
934
935
        if ($this->isSilverStripeAdmin($toplevelController)) {
936
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
937
        }
938
        $response = $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
939
        $this->sessionMessage($message, "good");
940
941
        return $response;
942
    }
943
944
    public function isSilverStripeAdmin($controller)
945
    {
946
        if ($controller) {
947
            return is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class);
948
        }
949
        return false;
950
    }
951
952
    /**
953
     * @param string $template
954
     * @return $this
955
     */
956
    public function setTemplate($template)
957
    {
958
        $this->template = $template;
959
        return $this;
960
    }
961
962
    /**
963
     * @return string
964
     */
965
    public function getTemplate()
966
    {
967
        return $this->template;
968
    }
969
970
    /**
971
     * Get list of templates to use
972
     *
973
     * @return array
974
     */
975
    public function getTemplates()
976
    {
977
        $templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
978
        // Prefer any custom template
979
        if ($this->getTemplate()) {
980
            array_unshift($templates, $this->getTemplate());
981
        }
982
        return $templates;
983
    }
984
985
    /**
986
     * @return Controller
987
     */
988
    public function getController()
989
    {
990
        return $this->popupController;
991
    }
992
993
    /**
994
     * @return TabulatorGrid
995
     */
996
    public function getTabulatorGrid()
997
    {
998
        return $this->tabulatorGrid;
999
    }
1000
1001
    /**
1002
     * @return DataObject
1003
     */
1004
    public function getRecord()
1005
    {
1006
        return $this->record;
1007
    }
1008
1009
    /**
1010
     * CMS-specific functionality: Passes through navigation breadcrumbs
1011
     * to the template, and includes the currently edited record (if any).
1012
     * see {@link LeftAndMain->Breadcrumbs()} for details.
1013
     *
1014
     * @param boolean $unlinked
1015
     * @return ArrayList
1016
     */
1017
    public function Breadcrumbs($unlinked = false)
1018
    {
1019
        if (!$this->popupController->hasMethod('Breadcrumbs')) {
1020
            return null;
1021
        }
1022
1023
        /** @var ArrayList $items */
1024
        $items = $this->popupController->Breadcrumbs($unlinked);
1025
1026
        if (!$items) {
0 ignored issues
show
introduced by
$items is of type SilverStripe\ORM\ArrayList, thus it always evaluated to true.
Loading history...
1027
            $items = new ArrayList();
1028
        }
1029
1030
        if ($this->record && $this->record->ID) {
1031
            $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
1032
            $items->push(new ArrayData([
1033
                'Title' => $title,
1034
                'Link' => $this->Link()
1035
            ]));
1036
        } else {
1037
            $items->push(new ArrayData([
1038
                'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]),
1039
                'Link' => false
1040
            ]));
1041
        }
1042
1043
        $this->extend('updateBreadcrumbs', $items);
1044
        return $items;
1045
    }
1046
}
1047