Completed
Push — master ( 5021cc...96c1cc )
by Daniel
02:44
created

EditableFormField::getDisplayRuleFields()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 71
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 71
rs 9.1369
c 0
b 0
f 0
cc 2
eloc 46
nc 2
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\UserForms\Model;
4
5
use SilverStripe\CMS\Controllers\CMSMain;
6
use SilverStripe\CMS\Controllers\CMSPageEditController;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Manifest\ModuleLoader;
12
use SilverStripe\Forms\CheckboxField;
13
use SilverStripe\Forms\DropdownField;
14
use SilverStripe\Forms\FieldList;
15
use SilverStripe\Forms\GridField\GridField;
16
use SilverStripe\Forms\GridField\GridFieldButtonRow;
17
use SilverStripe\Forms\GridField\GridFieldConfig;
18
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
19
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
20
use SilverStripe\Forms\LabelField;
21
use SilverStripe\Forms\LiteralField;
22
use SilverStripe\Forms\ReadonlyField;
23
use SilverStripe\Forms\SegmentField;
24
use SilverStripe\Forms\TabSet;
25
use SilverStripe\Forms\TextField;
26
use SilverStripe\ORM\ArrayList;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\FieldType\DBField;
29
use SilverStripe\ORM\ValidationException;
30
use SilverStripe\UserForms\Extension\UserFormFieldEditorExtension;
31
use SilverStripe\UserForms\Model\UserDefinedForm;
32
use SilverStripe\UserForms\Model\EditableCustomRule;
33
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
34
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
35
use SilverStripe\UserForms\Model\EditableFormField\EditableFormStep;
36
use SilverStripe\UserForms\Model\Submission\SubmittedFormField;
37
use SilverStripe\UserForms\Modifier\DisambiguationSegmentFieldModifier;
38
use SilverStripe\UserForms\Modifier\UnderscoreSegmentFieldModifier;
39
use SilverStripe\Versioned\Versioned;
40
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
41
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
42
43
/**
44
 * Represents the base class of a editable form field
45
 * object like {@link EditableTextField}.
46
 *
47
 * @package userforms
48
 *
49
 * @property string $Name
50
 * @property string $Title
51
 * @property string $Default
52
 * @property int $Sort
53
 * @property bool $Required
54
 * @property string $CustomErrorMessage
55
 * @property boolean $ShowOnLoad
56
 * @property string $DisplayRulesConjunction
57
 * @method UserDefinedForm Parent() Parent page
58
 * @method DataList DisplayRules() List of EditableCustomRule objects
59
 * @mixin Versioned
60
 */
61
class EditableFormField extends DataObject
62
{
63
    /**
64
     * Set to true to hide from class selector
65
     *
66
     * @config
67
     * @var bool
68
     */
69
    private static $hidden = false;
0 ignored issues
show
Unused Code introduced by
The property $hidden is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
70
71
    /**
72
     * Define this field as abstract (not inherited)
73
     *
74
     * @config
75
     * @var bool
76
     */
77
    private static $abstract = true;
0 ignored issues
show
Unused Code introduced by
The property $abstract is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
78
79
    /**
80
     * Flag this field type as non-data (e.g. literal, header, html)
81
     *
82
     * @config
83
     * @var bool
84
     */
85
    private static $literal = false;
0 ignored issues
show
Unused Code introduced by
The property $literal is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
86
87
    /**
88
     * Default sort order
89
     *
90
     * @config
91
     * @var string
92
     */
93
    private static $default_sort = '"Sort"';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $default_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
94
95
    /**
96
     * A list of CSS classes that can be added
97
     *
98
     * @var array
99
     */
100
    public static $allowed_css = [];
101
102
    /**
103
     * Set this to true to enable placeholder field for any given class
104
     * @config
105
     * @var bool
106
     */
107
    private static $has_placeholder = false;
0 ignored issues
show
Unused Code introduced by
The property $has_placeholder is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
108
109
    /**
110
     * @config
111
     * @var array
112
     */
113
    private static $summary_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $summary_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
114
        'Title'
115
    ];
116
117
    /**
118
     * @config
119
     * @var array
120
     */
121
    private static $db = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
122
        'Name' => 'Varchar',
123
        'Title' => 'Varchar(255)',
124
        'Default' => 'Varchar(255)',
125
        'Sort' => 'Int',
126
        'Required' => 'Boolean',
127
        'CustomErrorMessage' => 'Varchar(255)',
128
        'ExtraClass' => 'Text',
129
        'RightTitle' => 'Varchar(255)',
130
        'ShowOnLoad' => 'Boolean(1)',
131
        'ShowInSummary' => 'Boolean',
132
        'Placeholder' => 'Varchar(255)',
133
        'DisplayRulesConjunction' => 'Enum("And,Or","Or")',
134
    ];
135
136
    private static $table_name = 'EditableFormField';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $table_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
137
138
139
    private static $defaults = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $defaults is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
140
        'ShowOnLoad' => true,
141
    ];
142
143
144
    /**
145
     * @config
146
     * @var array
147
     */
148
    private static $has_one = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
149
        'Parent' => UserDefinedForm::class,
150
    ];
151
152
    /**
153
     * Built in extensions required
154
     *
155
     * @config
156
     * @var array
157
     */
158
    private static $extensions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $extensions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
159
        Versioned::class . "('Stage', 'Live')"
160
    ];
161
162
    /**
163
     * @config
164
     * @var array
165
     */
166
    private static $has_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
167
        'DisplayRules' => EditableCustomRule::class . '.Parent'
168
    ];
169
170
    private static $owns = [
0 ignored issues
show
Unused Code introduced by
The property $owns is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
171
        'DisplayRules',
172
    ];
173
174
    private static $cascade_deletes = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $cascade_deletes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
175
        'DisplayRules',
176
    ];
177
178
    /**
179
     * @var bool
180
     */
181
    protected $readonly;
182
183
    /**
184
     * Property holds the JS event which gets fired for this type of element
185
     *
186
     * @var string
187
     */
188
    protected $jsEventHandler = 'change';
189
190
    /**
191
     * Returns the jsEventHandler property for the current object. Bearing in mind it could've been overridden.
192
     * @return string
193
     */
194
    public function getJsEventHandler()
195
    {
196
        return $this->jsEventHandler;
197
    }
198
199
    /**
200
     * Set the visibility of an individual form field
201
     *
202
     * @param bool
203
     * @return $this
204
     */
205
    public function setReadonly($readonly = true)
206
    {
207
        $this->readonly = $readonly;
208
        return $this;
209
    }
210
211
    /**
212
     * Returns whether this field is readonly
213
     *
214
     * @return bool
215
     */
216
    private function isReadonly()
217
    {
218
        return $this->readonly;
219
    }
220
221
    /**
222
     * @return FieldList
223
     */
224
    public function getCMSFields()
225
    {
226
        $fields = FieldList::create(TabSet::create('Root'));
227
228
        // Main tab
229
        $fields->addFieldsToTab(
230
            'Root.Main',
231
            [
232
                ReadonlyField::create(
233
                    'Type',
234
                    _t(__CLASS__.'.TYPE', 'Type'),
235
                    $this->i18n_singular_name()
236
                ),
237
                CheckboxField::create('ShowInSummary', _t(__CLASS__.'.SHOWINSUMMARY', 'Show in summary gridfield')),
238
                LiteralField::create(
239
                    'MergeField',
240
                    _t(
241
                        __CLASS__.'.MERGEFIELDNAME',
242
                        '<div class="field readonly">' .
243
                            '<label class="left">' . _t(__CLASS__.'.MERGEFIELDNAME', 'Merge field') . '</label>' .
244
                            '<div class="middleColumn">' .
245
                                '<span class="readonly">$' . $this->Name . '</span>' .
246
                            '</div>' .
247
                        '</div>'
248
                    )
249
                ),
250
                TextField::create('Title', _t(__CLASS__.'.TITLE', 'Title')),
251
                TextField::create('Default', _t(__CLASS__.'.DEFAULT', 'Default value')),
252
                TextField::create('RightTitle', _t(__CLASS__.'.RIGHTTITLE', 'Right title')),
253
                SegmentField::create('Name', _t(__CLASS__.'.NAME', 'Name'))->setModifiers([
254
                    UnderscoreSegmentFieldModifier::create()->setDefault('FieldName'),
255
                    DisambiguationSegmentFieldModifier::create(),
256
                ])->setPreview($this->Name)
257
            ]
258
        );
259
        $fields->fieldByName('Root.Main')->setTitle(_t('SilverStripe\\CMS\\Model\\SiteTree.TABMAIN', 'Main'));
260
261
        // Custom settings
262
        if (!empty(self::$allowed_css)) {
263
            $cssList = [];
264
            foreach (self::$allowed_css as $k => $v) {
265
                if (!is_array($v)) {
266
                    $cssList[$k]=$v;
267
                } elseif ($k === $this->ClassName) {
268
                    $cssList = array_merge($cssList, $v);
269
                }
270
            }
271
272
            $fields->addFieldToTab(
273
                'Root.Main',
274
                DropdownField::create(
275
                    'ExtraClass',
276
                    _t(__CLASS__.'.EXTRACLASS_TITLE', 'Extra Styling/Layout'),
277
                    $cssList
278
                )->setDescription(_t(
279
                    __CLASS__.'.EXTRACLASS_SELECT',
280
                    'Select from the list of allowed styles'
281
                ))
282
            );
283
        } else {
284
            $fields->addFieldToTab(
285
                'Root.Main',
286
                TextField::create(
287
                    'ExtraClass',
288
                    _t(__CLASS__.'.EXTRACLASS_Title', 'Extra CSS classes')
289
                )->setDescription(_t(
290
                    __CLASS__.'.EXTRACLASS_MULTIPLE',
291
                    'Separate each CSS class with a single space'
292
                ))
293
            );
294
        }
295
296
        // Validation
297
        $validationFields = $this->getFieldValidationOptions();
298
        if ($validationFields && $validationFields->count()) {
299
            $fields->addFieldsToTab('Root.Validation', $validationFields);
0 ignored issues
show
Documentation introduced by
$validationFields is of type object<SilverStripe\Forms\FieldList>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
300
            $fields->fieldByName('Root.Validation')->setTitle(_t(__CLASS__.'.VALIDATION', 'Validation'));
301
        }
302
303
        // Add display rule fields
304
        $displayFields = $this->getDisplayRuleFields();
305
        if ($displayFields && $displayFields->count()) {
306
            $fields->addFieldsToTab('Root.DisplayRules', $displayFields);
0 ignored issues
show
Documentation introduced by
$displayFields is of type object<SilverStripe\Forms\FieldList>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
307
        }
308
309
        // Placeholder
310
        if ($this->config()->has_placeholder) {
311
            $fields->addFieldToTab(
312
                'Root.Main',
313
                TextField::create(
314
                    'Placeholder',
315
                    _t(__CLASS__.'.PLACEHOLDER', 'Placeholder')
316
                )
317
            );
318
        }
319
320
        $this->extend('updateCMSFields', $fields);
321
322
        return $fields;
323
    }
324
325
    /**
326
     * Return fields to display on the 'Display Rules' tab
327
     *
328
     * @return FieldList
329
     */
330
    protected function getDisplayRuleFields()
331
    {
332
        // Check display rules
333
        if ($this->Required) {
334
            return new FieldList(
335
                LabelField::create(
336
                    _t(
337
                        __CLASS__.'.DISPLAY_RULES_DISABLED',
338
                        'Display rules are not enabled for required fields. Please uncheck "Is this field Required?" under "Validation" to re-enable.'
339
                    )
340
                )
341
                ->addExtraClass('message warning')
342
            );
343
        }
344
345
        $allowedClasses = array_keys($this->getEditableFieldClasses(false));
346
        $editableColumns = new GridFieldEditableColumns();
347
        $editableColumns->setDisplayFields([
348
            'ConditionFieldID' => function ($record, $column, $grid) use ($allowedClasses) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid 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...
349
                    return DropdownField::create($column, '', EditableFormField::get()->filter([
350
                            'ParentID' => $this->ParentID,
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
351
                            'ClassName' => $allowedClasses,
352
                        ])->exclude([
353
                            'ID' => $this->ID,
354
                        ])->map('ID', 'Title'));
355
            },
356
            'ConditionOption' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid 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...
357
                $options = Config::inst()->get(EditableCustomRule::class, 'condition_options');
358
359
                return DropdownField::create($column, '', $options);
360
            },
361
            'FieldValue' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid 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...
362
                return TextField::create($column);
363
            }
364
        ]);
365
366
        // Custom rules
367
        $customRulesConfig = GridFieldConfig::create()
368
            ->addComponents(
369
                $editableColumns,
370
                new GridFieldButtonRow(),
371
                new GridFieldToolbarHeader(),
372
                new GridFieldAddNewInlineButton(),
373
                new GridFieldDeleteAction()
374
            );
375
376
        return new FieldList(
377
            DropdownField::create(
378
                'ShowOnLoad',
379
                _t(__CLASS__.'.INITIALVISIBILITY', 'Initial visibility'),
380
                [
381
                    1 => 'Show',
382
                    0 => 'Hide',
383
                ]
384
            ),
385
            DropdownField::create(
386
                'DisplayRulesConjunction',
387
                _t(__CLASS__.'.DISPLAYIF', 'Toggle visibility when'),
388
                [
389
                    'Or'  => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR', 'Any conditions are true'),
390
                    'And' => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND', 'All conditions are true'),
391
                ]
392
            ),
393
            GridField::create(
394
                'DisplayRules',
395
                _t(__CLASS__.'.CUSTOMRULES', 'Custom Rules'),
396
                $this->DisplayRules(),
397
                $customRulesConfig
398
            )
399
        );
400
    }
401
402
    public function onBeforeWrite()
403
    {
404
        parent::onBeforeWrite();
405
406
        // Set a field name.
407
        if (!$this->Name) {
408
            // New random name
409
            $this->Name = $this->generateName();
410
        } elseif ($this->Name === 'Field') {
411
            throw new ValidationException('Field name cannot be "Field"');
412
        }
413
414
        if (!$this->Sort && $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
415
            $parentID = $this->ParentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. 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...
416
            $this->Sort = EditableFormField::get()
417
                ->filter('ParentID', $parentID)
418
                ->max('Sort') + 1;
419
        }
420
    }
421
422
    /**
423
     * Generate a new non-conflicting Name value
424
     *
425
     * @return string
426
     */
427
    protected function generateName()
428
    {
429
        do {
430
            // Generate a new random name after this class (handles namespaces)
431
            $classNamePieces = explode('\\', static::class);
432
            $class = array_pop($classNamePieces);
433
            $entropy = substr(sha1(uniqid()), 0, 5);
434
            $name = "{$class}_{$entropy}";
435
436
            // Check if it conflicts
437
            $exists = EditableFormField::get()->filter('Name', $name)->count() > 0;
438
        } while ($exists);
439
        return $name;
440
    }
441
442
    /**
443
     * Flag indicating that this field will set its own error message via data-msg='' attributes
444
     *
445
     * @return bool
446
     */
447
    public function getSetsOwnError()
448
    {
449
        return false;
450
    }
451
452
    /**
453
     * Return whether a user can delete this form field
454
     * based on whether they can edit the page
455
     *
456
     * @param Member $member
457
     * @return bool
458
     */
459
    public function canDelete($member = null)
460
    {
461
        return $this->canEdit($member);
462
    }
463
464
    /**
465
     * Return whether a user can edit this form field
466
     * based on whether they can edit the page
467
     *
468
     * @param Member $member
469
     * @return bool
470
     */
471
    public function canEdit($member = null)
472
    {
473
        $parent = $this->Parent();
474
        if ($parent && $parent->exists()) {
475
            return $parent->canEdit($member) && !$this->isReadonly();
476
        } elseif (!$this->exists() && Controller::has_curr()) {
477
            // This is for GridFieldOrderableRows support as it checks edit permissions on
478
            // singleton of the class. Allows editing of User Defined Form pages by
479
            // 'Content Authors' and those with permission to edit the UDF page. (ie. CanEditType/EditorGroups)
480
            // This is to restore User Forms 2.x backwards compatibility.
481
            $controller = Controller::curr();
482
            if ($controller && $controller instanceof CMSPageEditController) {
483
                $parent = $controller->getRecord($controller->currentPageID());
484
                // Only allow this behaviour on pages using UserFormFieldEditorExtension, such
485
                // as UserDefinedForm page type.
486
                if ($parent && $parent->hasExtension(UserFormFieldEditorExtension::class)) {
487
                    return $parent->canEdit($member);
488
                }
489
            }
490
        }
491
492
        // Fallback to secure admin permissions
493
        return parent::canEdit($member);
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::canEdit($member); of type boolean|string adds the type string to the return on line 493 which is incompatible with the return type documented by SilverStripe\UserForms\M...tableFormField::canEdit of type boolean.
Loading history...
494
    }
495
496
    /**
497
     * Return whether a user can view this form field
498
     * based on whether they can view the page, regardless of the ReadOnly status of the field
499
     *
500
     * @param Member $member
501
     * @return bool
502
     */
503
    public function canView($member = null)
504
    {
505
        $parent = $this->Parent();
506
        if ($parent && $parent->exists()) {
507
            return $parent->canView($member);
508
        }
509
510
        return true;
511
    }
512
513
    /**
514
     * Return whether a user can create an object of this type
515
     *
516
     * @param Member $member
517
     * @param array $context Virtual parameter to allow context to be passed in to check
518
     * @return bool
519
     */
520 View Code Duplication
    public function canCreate($member = null, $context = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
521
    {
522
        // Check parent page
523
        $parent = $this->getCanCreateContext(func_get_args());
524
        if ($parent) {
525
            return $parent->canEdit($member);
526
        }
527
528
        // Fall back to secure admin permissions
529
        return parent::canCreate($member);
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::canCreate($member); of type boolean|string adds the type string to the return on line 529 which is incompatible with the return type documented by SilverStripe\UserForms\M...bleFormField::canCreate of type boolean.
Loading history...
530
    }
531
532
    /**
533
     * Helper method to check the parent for this object
534
     *
535
     * @param array $args List of arguments passed to canCreate
536
     * @return SiteTree Parent page instance
537
     */
538 View Code Duplication
    protected function getCanCreateContext($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
539
    {
540
        // Inspect second parameter to canCreate for a 'Parent' context
541
        if (isset($args[1]['Parent'])) {
542
            return $args[1]['Parent'];
543
        }
544
        // Hack in currently edited page if context is missing
545
        if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
546
            return Controller::curr()->currentPage();
547
        }
548
549
        // No page being edited
550
        return null;
551
    }
552
553
    /**
554
     * checks whether record is new, copied from SiteTree
555
     */
556
    public function isNew()
557
    {
558
        if (empty($this->ID)) {
559
            return true;
560
        }
561
562
        if (is_numeric($this->ID)) {
563
            return false;
564
        }
565
566
        return stripos($this->ID, 'new') === 0;
567
    }
568
569
    /**
570
     * Set the allowed css classes for the extraClass custom setting
571
     *
572
     * @param array $allowed The permissible CSS classes to add
573
     */
574
    public function setAllowedCss(array $allowed)
575
    {
576
        if (is_array($allowed)) {
577
            foreach ($allowed as $k => $v) {
578
                self::$allowed_css[$k] = (!is_null($v)) ? $v : $k;
579
            }
580
        }
581
    }
582
583
    /**
584
     * Get the path to the icon for this field type, relative to the site root.
585
     *
586
     * @return string
587
     */
588
    public function getIcon()
589
    {
590
        return ModuleLoader::getModule('silverstripe/userforms')
591
            ->getRelativeResourcePath('images/' . strtolower($this->class) . '.png');
0 ignored issues
show
Documentation introduced by
The property class does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
592
    }
593
594
    /**
595
     * Return whether or not this field has addable options
596
     * such as a dropdown field or radio set
597
     *
598
     * @return bool
599
     */
600
    public function getHasAddableOptions()
601
    {
602
        return false;
603
    }
604
605
    /**
606
     * Return whether or not this field needs to show the extra
607
     * options dropdown list
608
     *
609
     * @return bool
610
     */
611
    public function showExtraOptions()
612
    {
613
        return true;
614
    }
615
616
    /**
617
     * Returns the Title for rendering in the front-end (with XML values escaped)
618
     *
619
     * @return string
620
     */
621
    public function getEscapedTitle()
622
    {
623
        return Convert::raw2xml($this->Title);
624
    }
625
626
    /**
627
     * Find the numeric indicator (1.1.2) that represents it's nesting value
628
     *
629
     * Only useful for fields attached to a current page, and that contain other fields such as pages
630
     * or groups
631
     *
632
     * @return string
633
     */
634
    public function getFieldNumber()
635
    {
636
        // Check if exists
637
        if (!$this->exists()) {
638
            return null;
639
        }
640
        // Check parent
641
        $form = $this->Parent();
642
        if (!$form || !$form->exists() || !($fields = $form->Fields())) {
643
            return null;
644
        }
645
646
        $prior = 0; // Number of prior group at this level
647
        $stack = []; // Current stack of nested groups, where the top level = the page
648
        foreach ($fields->map('ID', 'ClassName') as $id => $className) {
649
            if ($className === EditableFormStep::class) {
650
                $priorPage = empty($stack) ? $prior : $stack[0];
651
                $stack = array($priorPage + 1);
652
                $prior = 0;
653
            } elseif ($className === EditableFieldGroup::class) {
654
                $stack[] = $prior + 1;
655
                $prior = 0;
656
            } elseif ($className === EditableFieldGroupEnd::class) {
657
                $prior = array_pop($stack);
658
            }
659
            if ($id == $this->ID) {
660
                return implode('.', $stack);
661
            }
662
        }
663
        return null;
664
    }
665
666
    public function getCMSTitle()
667
    {
668
        return $this->i18n_singular_name() . ' (' . $this->Title . ')';
669
    }
670
671
    /**
672
     * Append custom validation fields to the default 'Validation'
673
     * section in the editable options view
674
     *
675
     * @return FieldList
676
     */
677
    public function getFieldValidationOptions()
678
    {
679
        $fields = new FieldList(
680
            CheckboxField::create('Required', _t(__CLASS__.'.REQUIRED', 'Is this field Required?'))
681
                ->setDescription(_t(__CLASS__.'.REQUIRED_DESCRIPTION', 'Please note that conditional fields can\'t be required')),
682
            TextField::create('CustomErrorMessage', _t(__CLASS__.'.CUSTOMERROR', 'Custom Error Message'))
683
        );
684
685
        $this->extend('updateFieldValidationOptions', $fields);
686
687
        return $fields;
688
    }
689
690
    /**
691
     * Return a FormField to appear on the front end. Implement on
692
     * your subclass.
693
     *
694
     * @return FormField
695
     */
696
    public function getFormField()
697
    {
698
        user_error("Please implement a getFormField() on your EditableFormClass ". $this->ClassName, E_USER_ERROR);
699
    }
700
701
    /**
702
     * Updates a formfield with extensions
703
     *
704
     * @param FormField $field
705
     */
706
    public function doUpdateFormField($field)
707
    {
708
        $this->extend('beforeUpdateFormField', $field);
709
        $this->updateFormField($field);
710
        $this->extend('afterUpdateFormField', $field);
711
    }
712
713
    /**
714
     * Updates a formfield with the additional metadata specified by this field
715
     *
716
     * @param FormField $field
717
     */
718
    protected function updateFormField($field)
719
    {
720
        // set the error / formatting messages
721
        $field->setCustomValidationMessage($this->getErrorMessage()->RAW());
722
723
        // set the right title on this field
724
        if ($this->RightTitle) {
0 ignored issues
show
Documentation introduced by
The property RightTitle does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
725
            // Since this field expects raw html, safely escape the user data prior
726
            $field->setRightTitle(Convert::raw2xml($this->RightTitle));
0 ignored issues
show
Documentation introduced by
The property RightTitle does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
727
        }
728
729
        // if this field is required add some
730
        if ($this->Required) {
731
            // Required validation can conflict so add the Required validation messages as input attributes
732
            $errorMessage = $this->getErrorMessage()->HTML();
733
            $field->addExtraClass('requiredField');
734
            $field->setAttribute('data-rule-required', 'true');
735
            $field->setAttribute('data-msg-required', $errorMessage);
736
737
            if ($identifier = UserDefinedForm::config()->required_identifier) {
738
                $title = $field->Title() . " <span class='required-identifier'>". $identifier . "</span>";
739
                $field->setTitle($title);
740
            }
741
        }
742
743
        // if this field has an extra class
744
        if ($this->ExtraClass) {
0 ignored issues
show
Documentation introduced by
The property ExtraClass does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
745
            $field->addExtraClass($this->ExtraClass);
0 ignored issues
show
Documentation introduced by
The property ExtraClass does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
746
        }
747
748
        // if ShowOnLoad is false hide the field
749
        if (!$this->ShowOnLoad) {
750
            $field->addExtraClass($this->ShowOnLoadNice());
751
        }
752
753
        // if this field has a placeholder
754
        if ($this->Placeholder) {
0 ignored issues
show
Bug introduced by
The property Placeholder does not seem to exist. Did you mean has_placeholder?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
755
            $field->setAttribute('placeholder', $this->Placeholder);
0 ignored issues
show
Bug introduced by
The property Placeholder does not seem to exist. Did you mean has_placeholder?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
756
        }
757
    }
758
759
    /**
760
     * Return the instance of the submission field class
761
     *
762
     * @return SubmittedFormField
763
     */
764
    public function getSubmittedFormField()
765
    {
766
        return SubmittedFormField::create();
767
    }
768
769
770
    /**
771
     * Show this form field (and its related value) in the reports and in emails.
772
     *
773
     * @return bool
774
     */
775
    public function showInReports()
776
    {
777
        return true;
778
    }
779
780
    /**
781
     * Return the error message for this field. Either uses the custom
782
     * one (if provided) or the default SilverStripe message
783
     *
784
     * @return Varchar
785
     */
786
    public function getErrorMessage()
787
    {
788
        $title = strip_tags("'". ($this->Title ? $this->Title : $this->Name) . "'");
789
        $standard = _t('SilverStripe\\Forms\\Form.FIELDISREQUIRED', '{field} is required.', ['field' => $title]);
790
791
        // only use CustomErrorMessage if it has a non empty value
792
        $errorMessage = (!empty($this->CustomErrorMessage)) ? $this->CustomErrorMessage : $standard;
793
794
        return DBField::create_field('Varchar', $errorMessage);
795
    }
796
797
    /**
798
     * Invoked by UserFormUpgradeService to migrate settings specific to this field from CustomSettings
799
     * to the field proper
800
     *
801
     * @param array $data Unserialised data
802
     */
803
    public function migrateSettings($data)
804
    {
805
        // Map 'Show' / 'Hide' to boolean
806
        if (isset($data['ShowOnLoad'])) {
807
            $this->ShowOnLoad = $data['ShowOnLoad'] === '' || ($data['ShowOnLoad'] && $data['ShowOnLoad'] !== 'Hide');
808
            unset($data['ShowOnLoad']);
809
        }
810
811
        // Migrate all other settings
812
        foreach ($data as $key => $value) {
813
            if ($this->hasField($key)) {
814
                $this->setField($key, $value);
815
            }
816
        }
817
    }
818
819
    /**
820
     * Get the formfield to use when editing this inline in gridfield
821
     *
822
     * @param string $column name of column
823
     * @param array $fieldClasses List of allowed classnames if this formfield has a selectable class
824
     * @return FormField
825
     */
826
    public function getInlineClassnameField($column, $fieldClasses)
827
    {
828
        return DropdownField::create($column, false, $fieldClasses);
829
    }
830
831
    /**
832
     * Get the formfield to use when editing the title inline
833
     *
834
     * @param string $column
835
     * @return FormField
836
     */
837
    public function getInlineTitleField($column)
838
    {
839
        return TextField::create($column, false)
840
            ->setAttribute('placeholder', _t(__CLASS__.'.TITLE', 'Title'))
841
            ->setAttribute('data-placeholder', _t(__CLASS__.'.TITLE', 'Title'));
842
    }
843
844
    /**
845
     * Get the JS expression for selecting the holder for this field
846
     *
847
     * @return string
848
     */
849
    public function getSelectorHolder()
850
    {
851
        return sprintf('$("%s")', $this->getSelectorOnly());
852
    }
853
854
    /**
855
     * Returns only the JS identifier of a string, less the $(), which can be inserted elsewhere, for example when you
856
     * want to perform selections on multiple selectors
857
     * @return string
858
     */
859
    public function getSelectorOnly()
860
    {
861
        return "#{$this->Name}";
862
    }
863
864
    /**
865
     * Gets the JS expression for selecting the value for this field
866
     *
867
     * @param EditableCustomRule $rule Custom rule this selector will be used with
868
     * @param bool $forOnLoad Set to true if this will be invoked on load
869
     *
870
     * @return string
871
     */
872
    public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false)
873
    {
874
        return sprintf("$(%s)", $this->getSelectorFieldOnly());
875
    }
876
877
    /**
878
     * @return string
879
     */
880
    public function getSelectorFieldOnly()
881
    {
882
        return "[name='{$this->Name}']";
883
    }
884
885
886
    /**
887
     * Get the list of classes that can be selected and used as data-values
888
     *
889
     * @param $includeLiterals Set to false to exclude non-data fields
890
     * @return array
891
     */
892
    public function getEditableFieldClasses($includeLiterals = true)
893
    {
894
        $classes = ClassInfo::getValidSubClasses(EditableFormField::class);
895
896
        // Remove classes we don't want to display in the dropdown.
897
        $editableFieldClasses = [];
898
        foreach ($classes as $class) {
899
            // Skip abstract / hidden classes
900
            if (Config::inst()->get($class, 'abstract', Config::UNINHERITED)
901
                || Config::inst()->get($class, 'hidden')
902
            ) {
903
                continue;
904
            }
905
906
            if (!$includeLiterals && Config::inst()->get($class, 'literal')) {
907
                continue;
908
            }
909
910
            $singleton = singleton($class);
911
            if (!$singleton->canCreate()) {
912
                continue;
913
            }
914
915
            $editableFieldClasses[$class] = $singleton->i18n_singular_name();
916
        }
917
918
        asort($editableFieldClasses);
919
        return $editableFieldClasses;
920
    }
921
922
    /**
923
     * @return EditableFormField\Validator
924
     */
925
    public function getCMSValidator()
926
    {
927
        return EditableFormField\Validator::create()
928
            ->setRecord($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<SilverStripe\UserFo...odel\EditableFormField>, but the function expects a object<SilverStripe\User...ield\EditableFormField>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
929
    }
930
931
    /**
932
     * Determine effective display rules for this field.
933
     *
934
     * @return SS_List
935
     */
936
    public function EffectiveDisplayRules()
937
    {
938
        if ($this->Required) {
939
            return ArrayList::create();
940
        }
941
        return $this->DisplayRules();
942
    }
943
944
    /**
945
     * Extracts info from DisplayRules into array so UserDefinedForm->buildWatchJS can run through it.
946
     * @return array|null
947
     */
948
    public function formatDisplayRules()
949
    {
950
        $holderSelector = $this->getSelectorOnly();
951
        $result = [
952
            'targetFieldID' => $holderSelector,
953
            'conjunction'   => $this->DisplayRulesConjunctionNice(),
954
            'selectors'     => [],
955
            'events'        => [],
956
            'operations'    => [],
957
            'initialState'  => $this->ShowOnLoadNice(),
958
            'view'          => [],
959
            'opposite'      => [],
960
        ];
961
962
        // Check for field dependencies / default
963
        /** @var EditableCustomRule $rule */
964
        foreach ($this->EffectiveDisplayRules() as $rule) {
965
            // Get the field which is effected
966
            /** @var EditableFormField $formFieldWatch */
967
            $formFieldWatch = DataObject::get_by_id(EditableFormField::class, $rule->ConditionFieldID);
968
            // Skip deleted fields
969
            if (! $formFieldWatch) {
970
                continue;
971
            }
972
            $fieldToWatch = $formFieldWatch->getSelectorFieldOnly();
973
974
            $expression = $rule->buildExpression();
975
            if (!in_array($fieldToWatch, $result['selectors'])) {
976
                $result['selectors'][] = $fieldToWatch;
977
            }
978
            if (!in_array($expression['event'], $result['events'])) {
979
                $result['events'][] = $expression['event'];
980
            }
981
            $result['operations'][] = $expression['operation'];
982
983
            // View/Show should read
984
            $opposite = ($result['initialState'] === 'hide') ? 'show' : 'hide';
985
            $result['view'] = $rule->toggleDisplayText($result['initialState']);
986
            $result['opposite'] = $rule->toggleDisplayText($opposite);
987
        }
988
989
        return (count($result['selectors'])) ? $result : null;
990
    }
991
992
    /**
993
     * Replaces the set DisplayRulesConjunction with their JS logical operators
994
     * @return string
995
     */
996
    public function DisplayRulesConjunctionNice()
997
    {
998
        return (strtolower($this->DisplayRulesConjunction) === 'or') ? '||' : '&&';
999
    }
1000
1001
    /**
1002
     * Replaces boolean ShowOnLoad with its JS string equivalent
1003
     * @return string
1004
     */
1005
    public function ShowOnLoadNice()
1006
    {
1007
        return ($this->ShowOnLoad) ? 'show' : 'hide';
1008
    }
1009
1010
    /**
1011
     * Returns whether this is of type EditableCheckBoxField
1012
     * @return bool
1013
     */
1014
    public function isCheckBoxField()
1015
    {
1016
        return false;
1017
    }
1018
1019
    /**
1020
     * Returns whether this is of type EditableRadioField
1021
     * @return bool
1022
     */
1023
    public function isRadioField()
1024
    {
1025
        return false;
1026
    }
1027
1028
    /**
1029
     * Determined is this is of type EditableCheckboxGroupField
1030
     * @return bool
1031
     */
1032
    public function isCheckBoxGroupField()
1033
    {
1034
        return false;
1035
    }
1036
}
1037