Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

lidationResponse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 2
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use SilverStripe\Admin\LeftAndMain;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\RequestHandler;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Forms\FieldList;
11
use SilverStripe\Forms\Form;
12
use SilverStripe\Forms\FormAction;
13
use SilverStripe\Forms\LiteralField;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\ORM\FieldType\DBHTMLText;
17
use SilverStripe\ORM\HasManyList;
18
use SilverStripe\ORM\ManyManyList;
19
use SilverStripe\ORM\SS_List;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\ORM\ValidationResult;
22
use SilverStripe\View\ArrayData;
23
use SilverStripe\View\SSViewer;
24
25
class GridFieldDetailForm_ItemRequest extends RequestHandler
26
{
27
28
    private static $allowed_actions = array(
29
        'edit',
30
        'view',
31
        'ItemEditForm'
32
    );
33
34
    /**
35
     *
36
     * @var GridField
37
     */
38
    protected $gridField;
39
40
    /**
41
     *
42
     * @var GridFieldDetailForm
43
     */
44
    protected $component;
45
46
    /**
47
     *
48
     * @var DataObject
49
     */
50
    protected $record;
51
52
    /**
53
     * This represents the current parent RequestHandler (which does not necessarily need to be a Controller).
54
     * It allows us to traverse the RequestHandler chain upwards to reach the Controller stack.
55
     *
56
     * @var RequestHandler
57
     */
58
    protected $popupController;
59
60
    /**
61
     *
62
     * @var string
63
     */
64
    protected $popupFormName;
65
66
    /**
67
     * @var String
68
     */
69
    protected $template = null;
70
71
    private static $url_handlers = array(
72
        '$Action!' => '$Action',
73
        '' => 'edit',
74
    );
75
76
    /**
77
     *
78
     * @param GridField $gridField
79
     * @param GridFieldDetailForm $component
80
     * @param DataObject $record
81
     * @param RequestHandler $requestHandler
82
     * @param string $popupFormName
83
     */
84
    public function __construct($gridField, $component, $record, $requestHandler, $popupFormName)
85
    {
86
        $this->gridField = $gridField;
87
        $this->component = $component;
88
        $this->record = $record;
89
        $this->popupController = $requestHandler;
90
        $this->popupFormName = $popupFormName;
91
        parent::__construct();
92
    }
93
94
    public function Link($action = null)
95
    {
96
        return Controller::join_links(
97
            $this->gridField->Link('item'),
98
            $this->record->ID ? $this->record->ID : 'new',
99
            $action
100
        );
101
    }
102
103
    /**
104
     * @param HTTPRequest $request
105
     * @return mixed
106
     */
107
    public function view($request)
108
    {
109
        if (!$this->record->canView()) {
110
            $this->httpError(403);
111
        }
112
113
        $controller = $this->getToplevelController();
114
115
        $form = $this->ItemEditForm();
116
        $form->makeReadonly();
117
118
        $data = new ArrayData(array(
119
            'Backlink'     => $controller->Link(),
120
            'ItemEditForm' => $form
121
        ));
122
        $return = $data->renderWith($this->getTemplates());
123
124
        if ($request->isAjax()) {
125
            return $return;
126
        } else {
127
            return $controller->customise(array('Content' => $return));
128
        }
129
    }
130
131
    /**
132
     * @param HTTPRequest $request
133
     * @return mixed
134
     */
135
    public function edit($request)
136
    {
137
        $controller = $this->getToplevelController();
138
        $form = $this->ItemEditForm();
139
140
        $return = $this->customise(array(
141
            'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
142
            'ItemEditForm' => $form,
143
        ))->renderWith($this->getTemplates());
144
145
        if ($request->isAjax()) {
146
            return $return;
147
        } else {
148
            // If not requested by ajax, we need to render it within the controller context+template
149
            return $controller->customise(array(
150
                // TODO CMS coupling
151
                'Content' => $return,
152
            ));
153
        }
154
    }
155
156
    /**
157
     * Builds an item edit form.  The arguments to getCMSFields() are the popupController and
158
     * popupFormName, however this is an experimental API and may change.
159
     *
160
     * @todo In the future, we will probably need to come up with a tigher object representing a partially
161
     * complete controller with gaps for extra functionality.  This, for example, would be a better way
162
     * of letting Security/login put its log-in form inside a UI specified elsewhere.
163
     *
164
     * @return Form
165
     */
166
    public function ItemEditForm()
167
    {
168
        $list = $this->gridField->getList();
169
170
        if (empty($this->record)) {
171
            $controller = $this->getToplevelController();
172
            $url = $controller->getRequest()->getURL();
173
            $noActionURL = $controller->removeAction($url);
174
            $controller->getResponse()->removeHeader('Location');   //clear the existing redirect
175
            return $controller->redirect($noActionURL, 302);
176
        }
177
178
        $canView = $this->record->canView();
179
        $canEdit = $this->record->canEdit();
180
        $canDelete = $this->record->canDelete();
181
        $canCreate = $this->record->canCreate();
182
183
        if (!$canView) {
184
            $controller = $this->getToplevelController();
185
            // TODO More friendly error
186
            return $controller->httpError(403);
187
        }
188
189
        // Build actions
190
        $actions = $this->getFormActions();
191
192
        // If we are creating a new record in a has-many list, then
193
        // pre-populate the record's foreign key.
194
        if ($list instanceof HasManyList && !$this->record->isInDB()) {
195
            $key = $list->getForeignKey();
196
            $id = $list->getForeignID();
197
            $this->record->$key = $id;
198
        }
199
200
        $fields = $this->component->getFields();
201
        if (!$fields) {
202
            $fields = $this->record->getCMSFields();
203
        }
204
205
        // If we are creating a new record in a has-many list, then
206
        // Disable the form field as it has no effect.
207
        if ($list instanceof HasManyList) {
208
            $key = $list->getForeignKey();
209
210
            if ($field = $fields->dataFieldByName($key)) {
211
                $fields->makeFieldReadonly($field);
212
            }
213
        }
214
215
216
        // Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead.
217
        // Thanks to this however, we are able to nest GridFields, and also access the initial Controller by
218
        // dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController
219
        // below.
220
        $form = new Form(
221
            $this,
222
            'ItemEditForm',
223
            $fields,
224
            $actions,
225
            $this->component->getValidator()
226
        );
227
228
        $form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
229
230
        if ($this->record->ID && !$canEdit) {
231
            // Restrict editing of existing records
232
            $form->makeReadonly();
233
            // Hack to re-enable delete button if user can delete
234
            if ($canDelete) {
235
                $form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
236
            }
237
        } elseif (!$this->record->ID && !$canCreate) {
238
            // Restrict creation of new records
239
            $form->makeReadonly();
240
        }
241
242
        // Load many_many extraData for record.
243
        // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
244
        if ($list instanceof ManyManyList) {
245
            $extraData = $list->getExtraData('', $this->record->ID);
246
            $form->loadDataFrom(array('ManyMany' => $extraData));
247
        }
248
249
        // TODO Coupling with CMS
250
        $toplevelController = $this->getToplevelController();
251
        if ($toplevelController && $toplevelController instanceof LeftAndMain) {
252
            // Always show with base template (full width, no other panels),
253
            // regardless of overloaded CMS controller templates.
254
            // TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
255
            $form->setTemplate([
256
                'type' => 'Includes',
257
                'SilverStripe\\Admin\\LeftAndMain_EditForm',
258
            ]);
259
            $form->addExtraClass('cms-content cms-edit-form center fill-height flexbox-area-grow');
260
            $form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
261
            if ($form->Fields()->hasTabSet()) {
262
                $form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
263
                $form->addExtraClass('cms-tabset');
264
            }
265
266
            $form->Backlink = $this->getBackLink();
0 ignored issues
show
Documentation introduced by
The property Backlink does not exist on object<SilverStripe\Forms\Form>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
267
        }
268
269
        $cb = $this->component->getItemEditFormCallback();
270
        if ($cb) {
271
            $cb($form, $this);
272
        }
273
        $this->extend("updateItemEditForm", $form);
274
        return $form;
275
    }
276
277
    /**
278
     * Build the set of form field actions for this DataObject
279
     *
280
     * @return FieldList
281
     */
282
    protected function getFormActions()
283
    {
284
        $canEdit = $this->record->canEdit();
285
        $canDelete = $this->record->canDelete();
286
        $actions = new FieldList();
287
        if ($this->record->ID !== 0) {
288
            if ($canEdit) {
289
                $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
290
                    ->setUseButtonTag(true)
291
                    ->addExtraClass('ss-ui-action-constructive')
292
                    ->setAttribute('data-icon', 'accept'));
293
            }
294
295
            if ($canDelete) {
296
                $actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
297
                    ->setUseButtonTag(true)
298
                    ->addExtraClass('ss-ui-action-destructive action-delete'));
299
            }
300
        } else { // adding new record
301
            //Change the Save label to 'Create'
302
            $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
303
                ->setUseButtonTag(true)
304
                ->addExtraClass('ss-ui-action-constructive')
305
                ->setAttribute('data-icon', 'add'));
306
307
            // Add a Cancel link which is a button-like link and link back to one level up.
308
            $crumbs = $this->Breadcrumbs();
309
            if ($crumbs && $crumbs->count() >= 2) {
310
                $oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2);
311
                $text = sprintf(
312
                    "<a class=\"%s\" href=\"%s\">%s</a>",
313
                    "crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
314
                    $oneLevelUp->Link, // url
315
                    _t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
316
                );
317
                $actions->push(new LiteralField('cancelbutton', $text));
318
            }
319
        }
320
        $this->extend('updateFormActions', $actions);
321
        return $actions;
322
    }
323
324
    /**
325
     * Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest.
326
     * This allows us to access the Controller responsible for invoking the top-level GridField.
327
     * This should be equivalent to getting the controller off the top of the controller stack via Controller::curr(),
328
     * but allows us to avoid accessing the global state.
329
     *
330
     * GridFieldDetailForm_ItemRequests are RequestHandlers, and as such they are not part of the controller stack.
331
     *
332
     * @return Controller
333
     */
334
    protected function getToplevelController()
335
    {
336
        $c = $this->popupController;
337
        while ($c && $c instanceof GridFieldDetailForm_ItemRequest) {
338
            $c = $c->getController();
339
        }
340
        return $c;
341
    }
342
343
    protected function getBackLink()
344
    {
345
        // TODO Coupling with CMS
346
        $backlink = '';
347
        $toplevelController = $this->getToplevelController();
348
        if ($toplevelController && $toplevelController instanceof LeftAndMain) {
349
            if ($toplevelController->hasMethod('Backlink')) {
350
                $backlink = $toplevelController->Backlink();
351
            } elseif ($this->popupController->hasMethod('Breadcrumbs')) {
352
                $parents = $this->popupController->Breadcrumbs(false)->items;
353
                $backlink = array_pop($parents)->Link;
354
            }
355
        }
356
        if (!$backlink) {
357
            $backlink = $toplevelController->Link();
358
        }
359
360
        return $backlink;
361
    }
362
363
    /**
364
     * Get the list of extra data from the $record as saved into it by
365
     * {@see Form::saveInto()}
366
     *
367
     * Handles detection of falsey values explicitly saved into the
368
     * DataObject by formfields
369
     *
370
     * @param DataObject $record
371
     * @param SS_List $list
372
     * @return array List of data to write to the relation
373
     */
374
    protected function getExtraSavedData($record, $list)
375
    {
376
        // Skip extra data if not ManyManyList
377
        if (!($list instanceof ManyManyList)) {
378
            return null;
379
        }
380
381
        $data = array();
382
        foreach ($list->getExtraFields() as $field => $dbSpec) {
383
            $savedField = "ManyMany[{$field}]";
384
            if ($record->hasField($savedField)) {
385
                $data[$field] = $record->getField($savedField);
386
            }
387
        }
388
        return $data;
389
    }
390
391
    public function doSave($data, $form)
392
    {
393
        $isNewRecord = $this->record->ID == 0;
394
395
        // Check permission
396
        if (!$this->record->canEdit()) {
397
            return $this->httpError(403);
398
        }
399
400
        // Save from form data
401
        $this->saveFormIntoRecord($data, $form);
402
403
        $link = '<a href="' . $this->Link('edit') . '">"'
404
            . htmlspecialchars($this->record->Title, ENT_QUOTES)
405
            . '"</a>';
406
        $message = _t(
407
            'GridFieldDetailForm.Saved',
408
            'Saved {name} {link}',
409
            array(
410
                'name' => $this->record->i18n_singular_name(),
411
                'link' => $link
412
            )
413
        );
414
415
        $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
416
417
        // Redirect after save
418
        return $this->redirectAfterSave($isNewRecord);
419
    }
420
421
    /**
422
     * Response object for this request after a successful save
423
     *
424
     * @param bool $isNewRecord True if this record was just created
425
     * @return HTTPResponse|DBHTMLText
426
     */
427
    protected function redirectAfterSave($isNewRecord)
428
    {
429
        $controller = $this->getToplevelController();
430
        if ($isNewRecord) {
431
            return $controller->redirect($this->Link());
432
        } elseif ($this->gridField->getList()->byID($this->record->ID)) {
433
            // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
434
            // to the same URL (it assumes that its content is already current, and doesn't reload)
435
            return $this->edit($controller->getRequest());
436
        } else {
437
            // Changes to the record properties might've excluded the record from
438
            // a filtered list, so return back to the main view if it can't be found
439
            $url = $controller->getRequest()->getURL();
440
            $noActionURL = $controller->removeAction($url);
441
            $controller->getRequest()->addHeader('X-Pjax', 'Content');
442
            return $controller->redirect($noActionURL, 302);
443
        }
444
    }
445
446
    public function httpError($errorCode, $errorMessage = null)
447
    {
448
        $controller = $this->getToplevelController();
449
        return $controller->httpError($errorCode, $errorMessage);
450
    }
451
452
    /**
453
     * Loads the given form data into the underlying dataobject and relation
454
     *
455
     * @param array $data
456
     * @param Form $form
457
     * @throws ValidationException On error
458
     * @return DataObject Saved record
459
     */
460
    protected function saveFormIntoRecord($data, $form)
461
    {
462
        $list = $this->gridField->getList();
463
464
        // Check object matches the correct classname
465
        if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
466
            $newClassName = $data['ClassName'];
467
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
468
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
469
            // on the ClassName attribute
470
            $this->record->setClassName($this->record->ClassName);
471
            // Replace $record with a new instance
472
            $this->record = $this->record->newClassInstance($newClassName);
473
        }
474
475
        // Save form and any extra saved data into this dataobject
476
            $form->saveInto($this->record);
477
            $this->record->write();
478
        $extraData = $this->getExtraSavedData($this->record, $list);
479
            $list->add($this->record, $extraData);
0 ignored issues
show
Unused Code introduced by
The call to SS_List::add() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
480
481
        return $this->record;
482
    }
483
484
    /**
485
     * @param array $data
486
     * @param Form $form
487
     * @return HTTPResponse
488
     * @throws ValidationException
489
     */
490
    public function doDelete($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
491
    {
492
        $title = $this->record->Title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
493
        if (!$this->record->canDelete()) {
494
            throw new ValidationException(
495
                _t('GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
496
            );
497
        }
498
        $this->record->delete();
499
500
        $message = sprintf(
501
            _t('GridFieldDetailForm.Deleted', 'Deleted %s %s'),
502
            $this->record->i18n_singular_name(),
503
            htmlspecialchars($title, ENT_QUOTES)
504
        );
505
506
        $toplevelController = $this->getToplevelController();
507
        if ($toplevelController && $toplevelController instanceof LeftAndMain) {
508
            $backForm = $toplevelController->getEditForm();
509
            $backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
510
        } else {
511
            $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
512
        }
513
514
        //when an item is deleted, redirect to the parent controller
515
        $controller = $this->getToplevelController();
516
        $controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
517
518
        return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
519
    }
520
521
    /**
522
     * @param string $template
523
     * @return $this
524
     */
525
    public function setTemplate($template)
526
    {
527
        $this->template = $template;
528
        return $this;
529
    }
530
531
    /**
532
     * @return string
533
     */
534
    public function getTemplate()
535
    {
536
        return $this->template;
537
    }
538
539
    /**
540
     * Get list of templates to use
541
     *
542
     * @return array
543
     */
544
    public function getTemplates()
545
    {
546
        $templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
547
        // Prefer any custom template
548
        if ($this->getTemplate()) {
549
            array_unshift($templates, $this->getTemplate());
550
        }
551
        return $templates;
552
    }
553
554
    /**
555
     * @return Controller
556
     */
557
    public function getController()
558
    {
559
        return $this->popupController;
560
    }
561
562
    /**
563
     * @return GridField
564
     */
565
    public function getGridField()
566
    {
567
        return $this->gridField;
568
    }
569
570
    /**
571
     * @return DataObject
572
     */
573
    public function getRecord()
574
    {
575
        return $this->record;
576
    }
577
578
    /**
579
     * CMS-specific functionality: Passes through navigation breadcrumbs
580
     * to the template, and includes the currently edited record (if any).
581
     * see {@link LeftAndMain->Breadcrumbs()} for details.
582
     *
583
     * @param boolean $unlinked
584
     * @return ArrayList
585
     */
586
    public function Breadcrumbs($unlinked = false)
587
    {
588
        if (!$this->popupController->hasMethod('Breadcrumbs')) {
589
            return null;
590
        }
591
592
        /** @var ArrayList $items */
593
        $items = $this->popupController->Breadcrumbs($unlinked);
594
        if ($this->record && $this->record->ID) {
595
            $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
596
            $items->push(new ArrayData(array(
597
                'Title' => $title,
598
                'Link' => $this->Link()
599
            )));
600
        } else {
601
            $items->push(new ArrayData(array(
602
                'Title' => sprintf(_t('GridField.NewRecord', 'New %s'), $this->record->i18n_singular_name()),
603
                'Link' => false
604
            )));
605
        }
606
607
        return $items;
608
    }
609
}
610