EditableFormField   F
last analyzed

Complexity

Total Complexity 120

Size/Duplication

Total Lines 1016
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 346
dl 0
loc 1016
rs 2
c 3
b 0
f 1
wmc 120

45 Methods

Rating   Name   Duplication   Size   Complexity  
A getCMSTitle() 0 3 1
A getEscapedTitle() 0 3 1
A getFormField() 0 3 1
A getSetsOwnError() 0 3 1
A canDelete() 0 3 1
A showExtraOptions() 0 3 1
A getHasAddableOptions() 0 3 1
A showInReports() 0 3 1
A getSelectorOnly() 0 3 1
A getSubmittedFormField() 0 3 1
A getSelectorHolder() 0 3 1
A getSelectorFieldOnly() 0 3 1
A getSelectorField() 0 3 1
A getInlineClassnameField() 0 3 1
A getCMSValidator() 0 4 1
A generateName() 0 13 2
A getIcon() 0 13 2
B getFieldNumber() 0 30 11
A setAllowedCss() 0 5 4
A getErrorMessage() 0 9 3
A onBeforeWrite() 0 17 5
A getCanCreateContext() 0 13 4
A getFieldValidationOptions() 0 11 1
B getEditableFieldClasses() 0 28 7
A getInlineTitleField() 0 5 1
A canCreate() 0 10 2
A doUpdateFormField() 0 5 1
B canEdit() 0 23 10
A canView() 0 8 3
A isNew() 0 11 3
A EffectiveDisplayRules() 0 6 2
A getJsEventHandler() 0 3 1
A isReadonly() 0 3 1
A setReadonly() 0 4 1
C getCMSFields() 0 108 11
A requireDefaultRecords() 0 8 1
A isRadioField() 0 3 1
A DisplayRulesConjunctionNice() 0 3 2
A ShowOnLoadNice() 0 3 2
B formatDisplayRules() 0 45 6
B isDisplayed() 0 34 8
B updateFormField() 0 37 7
A isCheckBoxGroupField() 0 3 1
A isCheckBoxField() 0 3 1
A getDisplayRuleFields() 0 55 1

How to fix   Complexity   

Complex Class

Complex classes like EditableFormField often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EditableFormField, and based on these observations, apply Extract Interface, too.

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\FormField;
16
use SilverStripe\Forms\GridField\GridField;
17
use SilverStripe\Forms\GridField\GridFieldButtonRow;
18
use SilverStripe\Forms\GridField\GridFieldConfig;
19
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
20
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
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\DataList;
28
use SilverStripe\ORM\DataObject;
29
use SilverStripe\ORM\DB;
30
use SilverStripe\ORM\FieldType\DBField;
31
use SilverStripe\ORM\FieldType\DBVarchar;
32
use SilverStripe\ORM\SS_List;
33
use SilverStripe\ORM\ValidationException;
34
use SilverStripe\UserForms\Extension\UserFormFieldEditorExtension;
35
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
36
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
37
use SilverStripe\UserForms\Model\EditableFormField\EditableFormStep;
38
use SilverStripe\UserForms\Model\Submission\SubmittedFormField;
39
use SilverStripe\UserForms\Modifier\DisambiguationSegmentFieldModifier;
40
use SilverStripe\UserForms\Modifier\UnderscoreSegmentFieldModifier;
41
use SilverStripe\Versioned\Versioned;
42
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
43
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
44
45
/**
46
 * Represents the base class of a editable form field
47
 * object like {@link EditableTextField}.
48
 *
49
 * @package userforms
50
 *
51
 * @property string $Name
52
 * @property string $Title
53
 * @property string $Default
54
 * @property int $Sort
55
 * @property bool $Required
56
 * @property string $CustomErrorMessage
57
 * @property boolean $ShowOnLoad
58
 * @property string $DisplayRulesConjunction
59
 * @method UserDefinedForm Parent() Parent page
60
 * @method DataList|EditableCustomRule[] DisplayRules() List of EditableCustomRule objects
61
 * @mixin Versioned
62
 */
63
class EditableFormField extends DataObject
64
{
65
    /**
66
     * Set to true to hide from class selector
67
     *
68
     * @config
69
     * @var bool
70
     */
71
    private static $hidden = false;
0 ignored issues
show
introduced by
The private property $hidden is not used, and could be removed.
Loading history...
72
73
    /**
74
     * Define this field as abstract (not inherited)
75
     *
76
     * @config
77
     * @var bool
78
     */
79
    private static $abstract = true;
0 ignored issues
show
introduced by
The private property $abstract is not used, and could be removed.
Loading history...
80
81
    /**
82
     * Flag this field type as non-data (e.g. literal, header, html)
83
     *
84
     * @config
85
     * @var bool
86
     */
87
    private static $literal = false;
0 ignored issues
show
introduced by
The private property $literal is not used, and could be removed.
Loading history...
88
89
    /**
90
     * Default sort order
91
     *
92
     * @config
93
     * @var string
94
     */
95
    private static $default_sort = '"Sort"';
0 ignored issues
show
introduced by
The private property $default_sort is not used, and could be removed.
Loading history...
96
97
    /**
98
     * A list of CSS classes that can be added
99
     *
100
     * @var array
101
     */
102
    public static $allowed_css = [];
103
104
    /**
105
     * Set this to true to enable placeholder field for any given class
106
     * @config
107
     * @var bool
108
     */
109
    private static $has_placeholder = false;
110
111
    /**
112
     * @config
113
     * @var array
114
     */
115
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
116
        'Title'
117
    ];
118
119
    /**
120
     * @config
121
     * @var array
122
     */
123
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
124
        'Name' => 'Varchar',
125
        'Title' => 'Varchar(255)',
126
        'Default' => 'Varchar(255)',
127
        'Sort' => 'Int',
128
        'Required' => 'Boolean',
129
        'CustomErrorMessage' => 'Varchar(255)',
130
        'ExtraClass' => 'Text',
131
        'RightTitle' => 'Varchar(255)',
132
        'ShowOnLoad' => 'Boolean(1)',
133
        'ShowInSummary' => 'Boolean',
134
        'Placeholder' => 'Varchar(255)',
135
        'DisplayRulesConjunction' => 'Enum("And,Or","Or")',
136
    ];
137
138
    private static $table_name = 'EditableFormField';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
139
140
    private static $defaults = [
0 ignored issues
show
introduced by
The private property $defaults is not used, and could be removed.
Loading history...
141
        'ShowOnLoad' => true,
142
    ];
143
144
145
    /**
146
     * @config
147
     * @var array
148
     */
149
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
150
        'Parent' => DataObject::class,
151
    ];
152
153
    /**
154
     * Built in extensions required
155
     *
156
     * @config
157
     * @var array
158
     */
159
    private static $extensions = [
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
160
        Versioned::class . "('Stage', 'Live')"
161
    ];
162
163
    /**
164
     * @config
165
     * @var array
166
     */
167
    private static $has_many = [
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
168
        'DisplayRules' => EditableCustomRule::class . '.Parent'
169
    ];
170
171
    private static $owns = [
0 ignored issues
show
introduced by
The private property $owns is not used, and could be removed.
Loading history...
172
        'DisplayRules',
173
    ];
174
175
    private static $cascade_deletes = [
0 ignored issues
show
introduced by
The private property $cascade_deletes is not used, and could be removed.
Loading history...
176
        'DisplayRules',
177
    ];
178
179
    private static $cascade_duplicates = false;
0 ignored issues
show
introduced by
The private property $cascade_duplicates is not used, and could be removed.
Loading history...
180
181
    /**
182
     * @var bool
183
     */
184
    protected $readonly;
185
186
    /**
187
     * Property holds the JS event which gets fired for this type of element
188
     *
189
     * @var string
190
     */
191
    protected $jsEventHandler = 'change';
192
193
    /**
194
     * Returns the jsEventHandler property for the current object. Bearing in mind it could've been overridden.
195
     * @return string
196
     */
197
    public function getJsEventHandler()
198
    {
199
        return $this->jsEventHandler;
200
    }
201
202
    /**
203
     * Set the visibility of an individual form field
204
     *
205
     * @param bool
206
     * @return $this
207
     */
208
    public function setReadonly($readonly = true)
209
    {
210
        $this->readonly = $readonly;
211
        return $this;
212
    }
213
214
    /**
215
     * Returns whether this field is readonly
216
     *
217
     * @return bool
218
     */
219
    private function isReadonly()
220
    {
221
        return $this->readonly;
222
    }
223
224
    /**
225
     * @return FieldList
226
     */
227
    public function getCMSFields()
228
    {
229
        $fields = FieldList::create(TabSet::create('Root'));
230
231
        // If created with (+) button
232
        if ($this->ClassName === EditableFormField::class) {
233
            $fieldClasses = $this->getEditableFieldClasses();
234
            $fields->addFieldsToTab('Root.Main', [
235
                DropdownField::create('ClassName', _t(__CLASS__.'.TYPE', 'Type'), $fieldClasses)
236
                    ->setEmptyString(_t(__CLASS__ . 'TYPE_EMPTY', 'Select field type'))
237
            ]);
238
            return $fields;
239
        }
240
241
        // Main tab
242
        $fields->addFieldsToTab(
243
            'Root.Main',
244
            [
245
                ReadonlyField::create(
246
                    'Type',
247
                    _t(__CLASS__.'.TYPE', 'Type'),
248
                    $this->i18n_singular_name()
249
                ),
250
                CheckboxField::create('ShowInSummary', _t(__CLASS__.'.SHOWINSUMMARY', 'Show in summary gridfield')),
251
                LiteralField::create(
252
                    'MergeField',
253
                    '<div class="form-group field readonly">' .
254
                        '<label class="left form__field-label" for="Form_ItemEditForm_MergeField">'
255
                            . _t(__CLASS__.'.MERGEFIELDNAME', 'Merge field')
256
                        . '</label>'
257
                        . '<div class="form__field-holder">'
258
                            . '<span class="readonly" id="Form_ItemEditForm_MergeField">$' . $this->Name . '</span>'
259
                        . '</div>'
260
                    . '</div>'
261
                ),
262
                TextField::create('Title', _t(__CLASS__.'.TITLE', 'Title')),
263
                TextField::create('Default', _t(__CLASS__.'.DEFAULT', 'Default value')),
264
                TextField::create('RightTitle', _t(__CLASS__.'.RIGHTTITLE', 'Right title')),
265
                SegmentField::create('Name', _t(__CLASS__.'.NAME', 'Name'))->setModifiers([
266
                    UnderscoreSegmentFieldModifier::create()->setDefault('FieldName'),
267
                    DisambiguationSegmentFieldModifier::create(),
268
                ])->setPreview($this->Name)
269
            ]
270
        );
271
        $fields->fieldByName('Root.Main')->setTitle(_t('SilverStripe\\CMS\\Model\\SiteTree.TABMAIN', 'Main'));
272
273
        // Custom settings
274
        if (!empty(self::$allowed_css)) {
275
            $cssList = [];
276
            foreach (self::$allowed_css as $k => $v) {
277
                if (!is_array($v)) {
278
                    $cssList[$k]=$v;
279
                } elseif ($k === $this->ClassName) {
280
                    $cssList = array_merge($cssList, $v);
281
                }
282
            }
283
284
            $fields->addFieldToTab(
285
                'Root.Main',
286
                DropdownField::create(
287
                    'ExtraClass',
288
                    _t(__CLASS__.'.EXTRACLASS_TITLE', 'Extra Styling/Layout'),
289
                    $cssList
290
                )->setDescription(_t(
291
                    __CLASS__.'.EXTRACLASS_SELECT',
292
                    'Select from the list of allowed styles'
293
                ))
294
            );
295
        } else {
296
            $fields->addFieldToTab(
297
                'Root.Main',
298
                TextField::create(
299
                    'ExtraClass',
300
                    _t(__CLASS__.'.EXTRACLASS_Title', 'Extra CSS classes')
301
                )->setDescription(_t(
302
                    __CLASS__.'.EXTRACLASS_MULTIPLE',
303
                    'Separate each CSS class with a single space'
304
                ))
305
            );
306
        }
307
308
        // Validation
309
        $validationFields = $this->getFieldValidationOptions();
310
        if ($validationFields && $validationFields->count()) {
311
            $fields->addFieldsToTab('Root.Validation', $validationFields);
312
            $fields->fieldByName('Root.Validation')->setTitle(_t(__CLASS__.'.VALIDATION', 'Validation'));
313
        }
314
315
        // Add display rule fields
316
        $displayFields = $this->getDisplayRuleFields();
317
        if ($displayFields && $displayFields->count()) {
318
            $fields->addFieldsToTab('Root.DisplayRules', $displayFields);
319
        }
320
321
        // Placeholder
322
        if ($this->config()->has_placeholder) {
323
            $fields->addFieldToTab(
324
                'Root.Main',
325
                TextField::create(
326
                    'Placeholder',
327
                    _t(__CLASS__.'.PLACEHOLDER', 'Placeholder')
328
                )
329
            );
330
        }
331
332
        $this->extend('updateCMSFields', $fields);
333
334
        return $fields;
335
    }
336
337
338
    public function requireDefaultRecords()
339
    {
340
        parent::requireDefaultRecords();
341
342
        // make sure to migrate the class across (prior to v5.x)
343
        DB::query("UPDATE \"EditableFormField\" SET \"ParentClass\" = 'Page' WHERE \"ParentClass\" IS NULL");
344
        DB::query("UPDATE \"EditableFormField_Live\" SET \"ParentClass\" = 'Page' WHERE \"ParentClass\" IS NULL");
345
        DB::query("UPDATE \"EditableFormField_Versions\" SET \"ParentClass\" = 'Page' WHERE \"ParentClass\" IS NULL");
346
    }
347
348
    /**
349
     * Return fields to display on the 'Display Rules' tab
350
     *
351
     * @return FieldList
352
     */
353
    protected function getDisplayRuleFields()
354
    {
355
        $allowedClasses = array_keys($this->getEditableFieldClasses(false));
356
        $editableColumns = new GridFieldEditableColumns();
357
        $editableColumns->setDisplayFields([
358
            '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. ( Ignorable by Annotation )

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

358
            'ConditionFieldID' => function ($record, $column, /** @scrutinizer ignore-unused */ $grid) use ($allowedClasses) {

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

Loading history...
359
                    return DropdownField::create($column, '', EditableFormField::get()->filter([
360
                            'ParentID' => $this->ParentID,
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on SilverStripe\UserForms\Model\EditableFormField. Since you implemented __get, consider adding a @property annotation.
Loading history...
361
                            'ClassName' => $allowedClasses,
362
                        ])->exclude([
363
                            'ID' => $this->ID,
364
                        ])->map('ID', 'Title'));
365
            },
366
            'ConditionOption' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid is not used and could be removed. ( Ignorable by Annotation )

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

366
            'ConditionOption' => function ($record, $column, /** @scrutinizer ignore-unused */ $grid) {

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

Loading history...
367
                $options = Config::inst()->get(EditableCustomRule::class, 'condition_options');
368
369
                return DropdownField::create($column, '', $options);
370
            },
371
            'FieldValue' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid is not used and could be removed. ( Ignorable by Annotation )

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

371
            'FieldValue' => function ($record, $column, /** @scrutinizer ignore-unused */ $grid) {

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

Loading history...
372
                return TextField::create($column);
373
            }
374
        ]);
375
376
        // Custom rules
377
        $customRulesConfig = GridFieldConfig::create()
378
            ->addComponents(
379
                $editableColumns,
380
                new GridFieldButtonRow(),
381
                new GridFieldToolbarHeader(),
382
                new GridFieldAddNewInlineButton(),
383
                new GridFieldDeleteAction()
384
            );
385
386
        return new FieldList(
387
            DropdownField::create(
388
                'ShowOnLoad',
389
                _t(__CLASS__.'.INITIALVISIBILITY', 'Initial visibility'),
390
                [
391
                    1 => 'Show',
392
                    0 => 'Hide',
393
                ]
394
            ),
395
            DropdownField::create(
396
                'DisplayRulesConjunction',
397
                _t(__CLASS__.'.DISPLAYIF', 'Toggle visibility when'),
398
                [
399
                    'Or'  => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR', 'Any conditions are true'),
400
                    'And' => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND', 'All conditions are true'),
401
                ]
402
            ),
403
            GridField::create(
404
                'DisplayRules',
405
                _t(__CLASS__.'.CUSTOMRULES', 'Custom Rules'),
406
                $this->DisplayRules(),
407
                $customRulesConfig
408
            )
409
        );
410
    }
411
412
    public function onBeforeWrite()
413
    {
414
        parent::onBeforeWrite();
415
416
        // Set a field name.
417
        if (!$this->Name) {
418
            // New random name
419
            $this->Name = $this->generateName();
420
        } elseif ($this->Name === 'Field') {
421
            throw new ValidationException('Field name cannot be "Field"');
422
        }
423
424
        if (!$this->Sort && $this->ParentID) {
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on SilverStripe\UserForms\Model\EditableFormField. Since you implemented __get, consider adding a @property annotation.
Loading history...
425
            $parentID = $this->ParentID;
426
            $this->Sort = EditableFormField::get()
427
                ->filter('ParentID', $parentID)
428
                ->max('Sort') + 1;
429
        }
430
    }
431
432
    /**
433
     * Generate a new non-conflicting Name value
434
     *
435
     * @return string
436
     */
437
    protected function generateName()
438
    {
439
        do {
440
            // Generate a new random name after this class (handles namespaces)
441
            $classNamePieces = explode('\\', static::class);
442
            $class = array_pop($classNamePieces);
443
            $entropy = substr(sha1(uniqid()), 0, 5);
444
            $name = "{$class}_{$entropy}";
445
446
            // Check if it conflicts
447
            $exists = EditableFormField::get()->filter('Name', $name)->count() > 0;
448
        } while ($exists);
449
        return $name;
450
    }
451
452
    /**
453
     * Flag indicating that this field will set its own error message via data-msg='' attributes
454
     *
455
     * @return bool
456
     */
457
    public function getSetsOwnError()
458
    {
459
        return false;
460
    }
461
462
    /**
463
     * Return whether a user can delete this form field
464
     * based on whether they can edit the page
465
     *
466
     * @param Member $member
0 ignored issues
show
Bug introduced by
The type SilverStripe\UserForms\Model\Member 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...
467
     * @return bool
468
     */
469
    public function canDelete($member = null)
470
    {
471
        return $this->canEdit($member);
472
    }
473
474
    /**
475
     * Return whether a user can edit this form field
476
     * based on whether they can edit the page
477
     *
478
     * @param Member $member
479
     * @return bool
480
     */
481
    public function canEdit($member = null)
482
    {
483
        $parent = $this->Parent();
484
        if ($parent && $parent->exists()) {
485
            return $parent->canEdit($member) && !$this->isReadonly();
486
        } elseif (!$this->exists() && Controller::has_curr()) {
487
            // This is for GridFieldOrderableRows support as it checks edit permissions on
488
            // singleton of the class. Allows editing of User Defined Form pages by
489
            // 'Content Authors' and those with permission to edit the UDF page. (ie. CanEditType/EditorGroups)
490
            // This is to restore User Forms 2.x backwards compatibility.
491
            $controller = Controller::curr();
492
            if ($controller && $controller instanceof CMSPageEditController) {
493
                $parent = $controller->getRecord($controller->currentPageID());
494
                // Only allow this behaviour on pages using UserFormFieldEditorExtension, such
495
                // as UserDefinedForm page type.
496
                if ($parent && $parent->hasExtension(UserFormFieldEditorExtension::class)) {
497
                    return $parent->canEdit($member);
498
                }
499
            }
500
        }
501
502
        // Fallback to secure admin permissions
503
        return parent::canEdit($member);
504
    }
505
506
    /**
507
     * Return whether a user can view this form field
508
     * based on whether they can view the page, regardless of the ReadOnly status of the field
509
     *
510
     * @param Member $member
511
     * @return bool
512
     */
513
    public function canView($member = null)
514
    {
515
        $parent = $this->Parent();
516
        if ($parent && $parent->exists()) {
517
            return $parent->canView($member);
518
        }
519
520
        return true;
521
    }
522
523
    /**
524
     * Return whether a user can create an object of this type
525
     *
526
     * @param Member $member
527
     * @param array $context Virtual parameter to allow context to be passed in to check
528
     * @return bool
529
     */
530
    public function canCreate($member = null, $context = [])
531
    {
532
        // Check parent page
533
        $parent = $this->getCanCreateContext(func_get_args());
534
        if ($parent) {
0 ignored issues
show
introduced by
$parent is of type SilverStripe\UserForms\Model\SiteTree, thus it always evaluated to true.
Loading history...
535
            return $parent->canEdit($member);
536
        }
537
538
        // Fall back to secure admin permissions
539
        return parent::canCreate($member);
540
    }
541
542
    /**
543
     * Helper method to check the parent for this object
544
     *
545
     * @param array $args List of arguments passed to canCreate
546
     * @return SiteTree Parent page instance
0 ignored issues
show
Bug introduced by
The type SilverStripe\UserForms\Model\SiteTree 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...
547
     */
548
    protected function getCanCreateContext($args)
549
    {
550
        // Inspect second parameter to canCreate for a 'Parent' context
551
        if (isset($args[1]['Parent'])) {
552
            return $args[1]['Parent'];
553
        }
554
        // Hack in currently edited page if context is missing
555
        if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
556
            return Controller::curr()->currentPage();
557
        }
558
559
        // No page being edited
560
        return null;
561
    }
562
563
    /**
564
     * checks whether record is new, copied from SiteTree
565
     */
566
    public function isNew()
567
    {
568
        if (empty($this->ID)) {
569
            return true;
570
        }
571
572
        if (is_numeric($this->ID)) {
0 ignored issues
show
introduced by
The condition is_numeric($this->ID) is always true.
Loading history...
573
            return false;
574
        }
575
576
        return stripos($this->ID, 'new') === 0;
577
    }
578
579
    /**
580
     * Set the allowed css classes for the extraClass custom setting
581
     *
582
     * @param array $allowed The permissible CSS classes to add
583
     */
584
    public function setAllowedCss(array $allowed)
585
    {
586
        if (is_array($allowed)) {
0 ignored issues
show
introduced by
The condition is_array($allowed) is always true.
Loading history...
587
            foreach ($allowed as $k => $v) {
588
                self::$allowed_css[$k] = (!is_null($v)) ? $v : $k;
589
            }
590
        }
591
    }
592
593
    /**
594
     * Get the path to the icon for this field type, relative to the site root.
595
     *
596
     * @return string
597
     */
598
    public function getIcon()
599
    {
600
        $classNamespaces = explode("\\", static::class);
601
        $shortClass = end($classNamespaces);
602
603
        $resource = ModuleLoader::getModule('silverstripe/userforms')
604
            ->getResource('images/' . strtolower($shortClass) . '.png');
605
606
        if (!$resource->exists()) {
607
            return '';
608
        }
609
610
        return $resource->getURL();
611
    }
612
613
    /**
614
     * Return whether or not this field has addable options
615
     * such as a dropdown field or radio set
616
     *
617
     * @return bool
618
     */
619
    public function getHasAddableOptions()
620
    {
621
        return false;
622
    }
623
624
    /**
625
     * Return whether or not this field needs to show the extra
626
     * options dropdown list
627
     *
628
     * @return bool
629
     */
630
    public function showExtraOptions()
631
    {
632
        return true;
633
    }
634
635
    /**
636
     * Returns the Title for rendering in the front-end (with XML values escaped)
637
     *
638
     * @deprecated 5.0..6.0 XML is automatically escaped in templates from SS 4 onwards. Please use $Title directly.
639
     *
640
     * @return string
641
     */
642
    public function getEscapedTitle()
643
    {
644
        return Convert::raw2xml($this->Title);
645
    }
646
647
    /**
648
     * Find the numeric indicator (1.1.2) that represents it's nesting value
649
     *
650
     * Only useful for fields attached to a current page, and that contain other fields such as pages
651
     * or groups
652
     *
653
     * @return string
654
     */
655
    public function getFieldNumber()
656
    {
657
        // Check if exists
658
        if (!$this->exists()) {
659
            return null;
660
        }
661
        // Check parent
662
        $form = $this->Parent();
663
        if (!$form || !$form->exists() || !($fields = $form->Fields())) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\UserForms\Model\UserDefinedForm, thus it always evaluated to true.
Loading history...
664
            return null;
665
        }
666
667
        $prior = 0; // Number of prior group at this level
668
        $stack = []; // Current stack of nested groups, where the top level = the page
669
        foreach ($fields->map('ID', 'ClassName') as $id => $className) {
670
            if ($className === EditableFormStep::class) {
671
                $priorPage = empty($stack) ? $prior : $stack[0];
672
                $stack = array($priorPage + 1);
673
                $prior = 0;
674
            } elseif ($className === EditableFieldGroup::class) {
675
                $stack[] = $prior + 1;
676
                $prior = 0;
677
            } elseif ($className === EditableFieldGroupEnd::class) {
678
                $prior = array_pop($stack);
679
            }
680
            if ($id == $this->ID) {
681
                return implode('.', $stack);
682
            }
683
        }
684
        return null;
685
    }
686
687
    public function getCMSTitle()
688
    {
689
        return $this->i18n_singular_name() . ' (' . $this->Title . ')';
690
    }
691
692
    /**
693
     * Append custom validation fields to the default 'Validation'
694
     * section in the editable options view
695
     *
696
     * @return FieldList
697
     */
698
    public function getFieldValidationOptions()
699
    {
700
        $fields = new FieldList(
701
            CheckboxField::create('Required', _t(__CLASS__.'.REQUIRED', 'Is this field Required?'))
702
                ->setDescription(_t(__CLASS__.'.REQUIRED_DESCRIPTION', 'Please note that conditional fields can\'t be required')),
703
            TextField::create('CustomErrorMessage', _t(__CLASS__.'.CUSTOMERROR', 'Custom Error Message'))
704
        );
705
706
        $this->extend('updateFieldValidationOptions', $fields);
707
708
        return $fields;
709
    }
710
711
    /**
712
     * Return a FormField to appear on the front end. Implement on
713
     * your subclass.
714
     *
715
     * @return FormField
716
     */
717
    public function getFormField()
718
    {
719
        user_error("Please implement a getFormField() on your EditableFormClass ". $this->ClassName, E_USER_ERROR);
720
    }
721
722
    /**
723
     * Updates a formfield with extensions
724
     *
725
     * @param FormField $field
726
     */
727
    public function doUpdateFormField($field)
728
    {
729
        $this->extend('beforeUpdateFormField', $field);
730
        $this->updateFormField($field);
731
        $this->extend('afterUpdateFormField', $field);
732
    }
733
734
    /**
735
     * Updates a formfield with the additional metadata specified by this field
736
     *
737
     * @param FormField $field
738
     */
739
    protected function updateFormField($field)
740
    {
741
        // set the error / formatting messages
742
        $field->setCustomValidationMessage($this->getErrorMessage()->RAW());
743
744
        // set the right title on this field
745
        if ($this->RightTitle) {
0 ignored issues
show
Bug Best Practice introduced by
The property RightTitle does not exist on SilverStripe\UserForms\Model\EditableFormField. Since you implemented __get, consider adding a @property annotation.
Loading history...
746
            $field->setRightTitle($this->RightTitle);
747
        }
748
749
        // if this field is required add some
750
        if ($this->Required) {
751
            // Required validation can conflict so add the Required validation messages as input attributes
752
            $errorMessage = $this->getErrorMessage()->HTML();
753
            $field->addExtraClass('requiredField');
754
            $field->setAttribute('data-rule-required', 'true');
755
            $field->setAttribute('data-msg-required', $errorMessage);
756
757
            if ($identifier = UserDefinedForm::config()->required_identifier) {
758
                $title = $field->Title() . " <span class='required-identifier'>". $identifier . "</span>";
759
                $field->setTitle(DBField::create_field('HTMLText', $title));
760
            }
761
        }
762
763
        // if this field has an extra class
764
        if ($this->ExtraClass) {
0 ignored issues
show
Bug Best Practice introduced by
The property ExtraClass does not exist on SilverStripe\UserForms\Model\EditableFormField. Since you implemented __get, consider adding a @property annotation.
Loading history...
765
            $field->addExtraClass($this->ExtraClass);
766
        }
767
768
        // if ShowOnLoad is false hide the field
769
        if (!$this->ShowOnLoad) {
770
            $field->addExtraClass($this->ShowOnLoadNice());
771
        }
772
773
        // if this field has a placeholder
774
        if (strlen($this->Placeholder) >= 0) {
0 ignored issues
show
Bug Best Practice introduced by
The property Placeholder does not exist on SilverStripe\UserForms\Model\EditableFormField. Since you implemented __get, consider adding a @property annotation.
Loading history...
775
            $field->setAttribute('placeholder', $this->Placeholder);
776
        }
777
    }
778
779
    /**
780
     * Return the instance of the submission field class
781
     *
782
     * @return SubmittedFormField
783
     */
784
    public function getSubmittedFormField()
785
    {
786
        return SubmittedFormField::create();
787
    }
788
789
790
    /**
791
     * Show this form field (and its related value) in the reports and in emails.
792
     *
793
     * @return bool
794
     */
795
    public function showInReports()
796
    {
797
        return true;
798
    }
799
800
    /**
801
     * Return the error message for this field. Either uses the custom
802
     * one (if provided) or the default SilverStripe message
803
     *
804
     * @return DBVarchar
805
     */
806
    public function getErrorMessage()
807
    {
808
        $title = strip_tags("'". ($this->Title ? $this->Title : $this->Name) . "'");
809
        $standard = _t(__CLASS__ . '.FIELDISREQUIRED', '{name} is required', ['name' => $title]);
810
811
        // only use CustomErrorMessage if it has a non empty value
812
        $errorMessage = (!empty($this->CustomErrorMessage)) ? $this->CustomErrorMessage : $standard;
813
814
        return DBField::create_field('Varchar', $errorMessage);
815
    }
816
817
    /**
818
     * Get the formfield to use when editing this inline in gridfield
819
     *
820
     * @param string $column name of column
821
     * @param array $fieldClasses List of allowed classnames if this formfield has a selectable class
822
     * @return FormField
823
     */
824
    public function getInlineClassnameField($column, $fieldClasses)
825
    {
826
        return DropdownField::create($column, false, $fieldClasses);
827
    }
828
829
    /**
830
     * Get the formfield to use when editing the title inline
831
     *
832
     * @param string $column
833
     * @return FormField
834
     */
835
    public function getInlineTitleField($column)
836
    {
837
        return TextField::create($column, false)
838
            ->setAttribute('placeholder', _t(__CLASS__.'.TITLE', 'Title'))
839
            ->setAttribute('data-placeholder', _t(__CLASS__.'.TITLE', 'Title'));
840
    }
841
842
    /**
843
     * Get the JS expression for selecting the holder for this field
844
     *
845
     * @return string
846
     */
847
    public function getSelectorHolder()
848
    {
849
        return sprintf('$("%s")', $this->getSelectorOnly());
850
    }
851
852
    /**
853
     * Returns only the JS identifier of a string, less the $(), which can be inserted elsewhere, for example when you
854
     * want to perform selections on multiple selectors
855
     * @return string
856
     */
857
    public function getSelectorOnly()
858
    {
859
        return "#{$this->Name}";
860
    }
861
862
    /**
863
     * Gets the JS expression for selecting the value for this field
864
     *
865
     * @param EditableCustomRule $rule Custom rule this selector will be used with
866
     * @param bool $forOnLoad Set to true if this will be invoked on load
867
     *
868
     * @return string
869
     */
870
    public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false)
0 ignored issues
show
Unused Code introduced by
The parameter $forOnLoad is not used and could be removed. ( Ignorable by Annotation )

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

870
    public function getSelectorField(EditableCustomRule $rule, /** @scrutinizer ignore-unused */ $forOnLoad = false)

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

Loading history...
Unused Code introduced by
The parameter $rule is not used and could be removed. ( Ignorable by Annotation )

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

870
    public function getSelectorField(/** @scrutinizer ignore-unused */ EditableCustomRule $rule, $forOnLoad = false)

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

Loading history...
871
    {
872
        return sprintf("$(%s)", $this->getSelectorFieldOnly());
873
    }
874
875
    /**
876
     * @return string
877
     */
878
    public function getSelectorFieldOnly()
879
    {
880
        return "[name='{$this->Name}']";
881
    }
882
883
884
    /**
885
     * Get the list of classes that can be selected and used as data-values
886
     *
887
     * @param $includeLiterals Set to false to exclude non-data fields
0 ignored issues
show
Bug introduced by
The type SilverStripe\UserForms\Model\Set 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...
888
     * @return array
889
     */
890
    public function getEditableFieldClasses($includeLiterals = true)
891
    {
892
        $classes = ClassInfo::getValidSubClasses(EditableFormField::class);
893
894
        // Remove classes we don't want to display in the dropdown.
895
        $editableFieldClasses = [];
896
        foreach ($classes as $class) {
897
            // Skip abstract / hidden classes
898
            if (Config::inst()->get($class, 'abstract', Config::UNINHERITED)
899
                || Config::inst()->get($class, 'hidden')
900
            ) {
901
                continue;
902
            }
903
904
            if (!$includeLiterals && Config::inst()->get($class, 'literal')) {
905
                continue;
906
            }
907
908
            $singleton = singleton($class);
909
            if (!$singleton->canCreate()) {
910
                continue;
911
            }
912
913
            $editableFieldClasses[$class] = $singleton->i18n_singular_name();
914
        }
915
916
        asort($editableFieldClasses);
917
        return $editableFieldClasses;
918
    }
919
920
    /**
921
     * @return EditableFormField\Validator
922
     */
923
    public function getCMSValidator()
924
    {
925
        return EditableFormField\Validator::create()
926
            ->setRecord($this);
927
    }
928
929
    /**
930
     * Determine effective display rules for this field.
931
     *
932
     * @return SS_List
933
     * @deprecated 5.6 No longer needed because of support for conditional required field.
934
     */
935
    public function EffectiveDisplayRules()
936
    {
937
        if ($this->Required) {
938
            return ArrayList::create();
939
        }
940
        return $this->DisplayRules();
941
    }
942
943
    /**
944
     * Extracts info from DisplayRules into array so UserDefinedForm->buildWatchJS can run through it.
945
     * @return array|null
946
     */
947
    public function formatDisplayRules()
948
    {
949
        $holderSelector = $this->getSelectorOnly();
950
        $result = [
951
            'targetFieldID' => $holderSelector,
952
            'conjunction'   => $this->DisplayRulesConjunctionNice(),
953
            'selectors'     => [],
954
            'events'        => [],
955
            'operations'    => [],
956
            'initialState'  => $this->ShowOnLoadNice(),
957
            'view'          => [],
958
            'opposite'      => [],
959
        ];
960
961
        // Check for field dependencies / default
962
        /** @var EditableCustomRule $rule */
963
        foreach ($this->DisplayRules() as $rule) {
964
            // Get the field which is effected
965
            /** @var EditableFormField $formFieldWatch */
966
            $formFieldWatch = DataObject::get_by_id(EditableFormField::class, $rule->ConditionFieldID);
0 ignored issues
show
Bug Best Practice introduced by
The property ConditionFieldID does not exist on SilverStripe\UserForms\Model\EditableCustomRule. Since you implemented __get, consider adding a @property annotation.
Loading history...
967
            // Skip deleted fields
968
            if (!$formFieldWatch) {
969
                continue;
970
            }
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
            $result['view'] = $rule->toggleDisplayText($result['initialState']);
985
            $result['opposite'] = $rule->toggleDisplayText($result['initialState'], true);
986
            $result['holder'] = $this->getSelectorHolder();
987
            $result['holder_event'] = $rule->toggleDisplayEvent($result['initialState']);
988
            $result['holder_event_opposite'] = $rule->toggleDisplayEvent($result['initialState'], true);
989
        }
990
991
        return (count($result['selectors'])) ? $result : null;
992
    }
993
994
    /**
995
     * Check if this EditableFormField is displayed based on its DisplayRules and the provided data.
996
     * @param array $data
997
     * @return bool
998
     */
999
    public function isDisplayed(array $data)
1000
    {
1001
        $displayRules = $this->DisplayRules();
1002
1003
        if ($displayRules->count() === 0) {
1004
            // If no display rule have been defined, isDisplayed equals the ShowOnLoad property
1005
            return $this->ShowOnLoad;
1006
        }
1007
1008
        $conjunction = $this->DisplayRulesConjunctionNice();
1009
1010
        // && start with true and find and condition that doesn't satisfy
1011
        // || start with false and find and condition that satisfies
1012
        $conditionsSatisfied = ($conjunction === '&&');
1013
1014
        foreach ($displayRules as $rule) {
1015
            $controllingField = $rule->ConditionField();
1016
1017
            // recursively check - if any of the dependant fields are hidden, assume the rule can not be satisfied
1018
            $ruleSatisfied = $controllingField->isDisplayed($data) && $rule->validateAgainstFormData($data);
1019
1020
            if ($conjunction === '||' && $ruleSatisfied) {
1021
                $conditionsSatisfied = true;
1022
                break;
1023
            }
1024
            if ($conjunction === '&&' && !$ruleSatisfied) {
1025
                $conditionsSatisfied = false;
1026
                break;
1027
            }
1028
        }
1029
1030
        // initially displayed - condition fails || initially hidden, condition passes
1031
        $startDisplayed = $this->ShowOnLoad;
1032
        return ($startDisplayed xor $conditionsSatisfied);
1033
    }
1034
1035
1036
    /**
1037
     * Replaces the set DisplayRulesConjunction with their JS logical operators
1038
     * @return string
1039
     */
1040
    public function DisplayRulesConjunctionNice()
1041
    {
1042
        return (strtolower($this->DisplayRulesConjunction) === 'or') ? '||' : '&&';
1043
    }
1044
1045
    /**
1046
     * Replaces boolean ShowOnLoad with its JS string equivalent
1047
     * @return string
1048
     */
1049
    public function ShowOnLoadNice()
1050
    {
1051
        return ($this->ShowOnLoad) ? 'show' : 'hide';
1052
    }
1053
1054
    /**
1055
     * Returns whether this is of type EditableCheckBoxField
1056
     * @return bool
1057
     */
1058
    public function isCheckBoxField()
1059
    {
1060
        return false;
1061
    }
1062
1063
    /**
1064
     * Returns whether this is of type EditableRadioField
1065
     * @return bool
1066
     */
1067
    public function isRadioField()
1068
    {
1069
        return false;
1070
    }
1071
1072
    /**
1073
     * Determined is this is of type EditableCheckboxGroupField
1074
     * @return bool
1075
     */
1076
    public function isCheckBoxGroupField()
1077
    {
1078
        return false;
1079
    }
1080
}
1081