Passed
Pull Request — master (#770)
by Jess
03:11
created

EditableFormField::EffectiveDisplayRules()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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\DataObject;
28
use SilverStripe\ORM\DB;
29
use SilverStripe\ORM\FieldType\DBField;
30
use SilverStripe\ORM\ValidationException;
31
use SilverStripe\UserForms\Extension\UserFormFieldEditorExtension;
32
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
33
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
34
use SilverStripe\UserForms\Model\EditableFormField\EditableFormStep;
35
use SilverStripe\UserForms\Model\Submission\SubmittedFormField;
36
use SilverStripe\UserForms\Modifier\DisambiguationSegmentFieldModifier;
37
use SilverStripe\UserForms\Modifier\UnderscoreSegmentFieldModifier;
38
use SilverStripe\Versioned\Versioned;
39
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
40
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
41
42
/**
43
 * Represents the base class of a editable form field
44
 * object like {@link EditableTextField}.
45
 *
46
 * @package userforms
47
 *
48
 * @property string $Name
49
 * @property string $Title
50
 * @property string $Default
51
 * @property int $Sort
52
 * @property bool $Required
53
 * @property string $CustomErrorMessage
54
 * @property boolean $ShowOnLoad
55
 * @property string $DisplayRulesConjunction
56
 * @method UserDefinedForm Parent() Parent page
57
 * @method DataList DisplayRules() List of EditableCustomRule objects
58
 * @mixin Versioned
59
 */
60
class EditableFormField extends DataObject
61
{
62
    /**
63
     * Set to true to hide from class selector
64
     *
65
     * @config
66
     * @var bool
67
     */
68
    private static $hidden = false;
0 ignored issues
show
introduced by
The private property $hidden is not used, and could be removed.
Loading history...
69
70
    /**
71
     * Define this field as abstract (not inherited)
72
     *
73
     * @config
74
     * @var bool
75
     */
76
    private static $abstract = true;
0 ignored issues
show
introduced by
The private property $abstract is not used, and could be removed.
Loading history...
77
78
    /**
79
     * Flag this field type as non-data (e.g. literal, header, html)
80
     *
81
     * @config
82
     * @var bool
83
     */
84
    private static $literal = false;
0 ignored issues
show
introduced by
The private property $literal is not used, and could be removed.
Loading history...
85
86
    /**
87
     * Default sort order
88
     *
89
     * @config
90
     * @var string
91
     */
92
    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...
93
94
    /**
95
     * A list of CSS classes that can be added
96
     *
97
     * @var array
98
     */
99
    public static $allowed_css = [];
100
101
    /**
102
     * Set this to true to enable placeholder field for any given class
103
     * @config
104
     * @var bool
105
     */
106
    private static $has_placeholder = false;
107
108
    /**
109
     * @config
110
     * @var array
111
     */
112
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
113
        'Title'
114
    ];
115
116
    /**
117
     * @config
118
     * @var array
119
     */
120
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
121
        'Name' => 'Varchar',
122
        'Title' => 'Varchar(255)',
123
        'Default' => 'Varchar(255)',
124
        'Sort' => 'Int',
125
        'Required' => 'Boolean',
126
        'CustomErrorMessage' => 'Varchar(255)',
127
        'ExtraClass' => 'Text',
128
        'RightTitle' => 'Varchar(255)',
129
        'ShowOnLoad' => 'Boolean(1)',
130
        'ShowInSummary' => 'Boolean',
131
        'Placeholder' => 'Varchar(255)',
132
        'DisplayRulesConjunction' => 'Enum("And,Or","Or")',
133
    ];
134
135
    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...
136
137
    private static $defaults = [
0 ignored issues
show
introduced by
The private property $defaults is not used, and could be removed.
Loading history...
138
        'ShowOnLoad' => true,
139
    ];
140
141
142
    /**
143
     * @config
144
     * @var array
145
     */
146
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
147
        'Parent' => DataObject::class,
148
    ];
149
150
    /**
151
     * Built in extensions required
152
     *
153
     * @config
154
     * @var array
155
     */
156
    private static $extensions = [
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
157
        Versioned::class . "('Stage', 'Live')"
158
    ];
159
160
    /**
161
     * @config
162
     * @var array
163
     */
164
    private static $has_many = [
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
165
        'DisplayRules' => EditableCustomRule::class . '.Parent'
166
    ];
167
168
    private static $owns = [
0 ignored issues
show
introduced by
The private property $owns is not used, and could be removed.
Loading history...
169
        'DisplayRules',
170
    ];
171
172
    private static $cascade_deletes = [
0 ignored issues
show
introduced by
The private property $cascade_deletes is not used, and could be removed.
Loading history...
173
        'DisplayRules',
174
    ];
175
176
    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...
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
                    '<div class="form-group field readonly">' .
241
                        '<label class="left form__field-label" for="Form_ItemEditForm_MergeField">'
242
                            . _t(__CLASS__.'.MERGEFIELDNAME', 'Merge field')
243
                        . '</label>'
244
                        . '<div class="form__field-holder">'
245
                            . '<span class="readonly" id="Form_ItemEditForm_MergeField">$' . $this->Name . '</span>'
246
                        . '</div>'
247
                    . '</div>'
248
                ),
249
                TextField::create('Title', _t(__CLASS__.'.TITLE', 'Title')),
250
                TextField::create('Default', _t(__CLASS__.'.DEFAULT', 'Default value')),
251
                TextField::create('RightTitle', _t(__CLASS__.'.RIGHTTITLE', 'Right title')),
252
                SegmentField::create('Name', _t(__CLASS__.'.NAME', 'Name'))->setModifiers([
253
                    UnderscoreSegmentFieldModifier::create()->setDefault('FieldName'),
254
                    DisambiguationSegmentFieldModifier::create(),
255
                ])->setPreview($this->Name)
256
            ]
257
        );
258
        $fields->fieldByName('Root.Main')->setTitle(_t('SilverStripe\\CMS\\Model\\SiteTree.TABMAIN', 'Main'));
259
260
        // Custom settings
261
        if (!empty(self::$allowed_css)) {
262
            $cssList = [];
263
            foreach (self::$allowed_css as $k => $v) {
264
                if (!is_array($v)) {
265
                    $cssList[$k]=$v;
266
                } elseif ($k === $this->ClassName) {
267
                    $cssList = array_merge($cssList, $v);
268
                }
269
            }
270
271
            $fields->addFieldToTab(
272
                'Root.Main',
273
                DropdownField::create(
274
                    'ExtraClass',
275
                    _t(__CLASS__.'.EXTRACLASS_TITLE', 'Extra Styling/Layout'),
276
                    $cssList
277
                )->setDescription(_t(
278
                    __CLASS__.'.EXTRACLASS_SELECT',
279
                    'Select from the list of allowed styles'
280
                ))
281
            );
282
        } else {
283
            $fields->addFieldToTab(
284
                'Root.Main',
285
                TextField::create(
286
                    'ExtraClass',
287
                    _t(__CLASS__.'.EXTRACLASS_Title', 'Extra CSS classes')
288
                )->setDescription(_t(
289
                    __CLASS__.'.EXTRACLASS_MULTIPLE',
290
                    'Separate each CSS class with a single space'
291
                ))
292
            );
293
        }
294
295
        // Validation
296
        $validationFields = $this->getFieldValidationOptions();
297
        if ($validationFields && $validationFields->count()) {
298
            $fields->addFieldsToTab('Root.Validation', $validationFields);
299
            $fields->fieldByName('Root.Validation')->setTitle(_t(__CLASS__.'.VALIDATION', 'Validation'));
300
        }
301
302
        // Add display rule fields
303
        $displayFields = $this->getDisplayRuleFields();
304
        if ($displayFields && $displayFields->count()) {
305
            $fields->addFieldsToTab('Root.DisplayRules', $displayFields);
306
        }
307
308
        // Placeholder
309
        if ($this->config()->has_placeholder) {
310
            $fields->addFieldToTab(
311
                'Root.Main',
312
                TextField::create(
313
                    'Placeholder',
314
                    _t(__CLASS__.'.PLACEHOLDER', 'Placeholder')
315
                )
316
            );
317
        }
318
319
        $this->extend('updateCMSFields', $fields);
320
321
        return $fields;
322
    }
323
324
325
    public function requireDefaultRecords()
326
    {
327
        parent::requireDefaultRecords();
328
329
        // make sure to migrate the class across (prior to v5.x)
330
        DB::query("UPDATE EditableFormField SET ParentClass = 'Page' WHERE ParentClass IS NULL");
331
        DB::query("UPDATE EditableFormField_Live SET ParentClass = 'Page' WHERE ParentClass IS NULL");
332
        DB::query("UPDATE EditableFormField_Versions SET ParentClass = 'Page' WHERE ParentClass IS NULL");
333
    }
334
335
    /**
336
     * Return fields to display on the 'Display Rules' tab
337
     *
338
     * @return FieldList
339
     */
340
    protected function getDisplayRuleFields()
341
    {
342
        $allowedClasses = array_keys($this->getEditableFieldClasses(false));
343
        $editableColumns = new GridFieldEditableColumns();
344
        $editableColumns->setDisplayFields([
345
            '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

345
            '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...
346
                    return DropdownField::create($column, '', EditableFormField::get()->filter([
347
                            '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...
348
                            'ClassName' => $allowedClasses,
349
                        ])->exclude([
350
                            'ID' => $this->ID,
351
                        ])->map('ID', 'Title'));
352
            },
353
            '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

353
            '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...
354
                $options = Config::inst()->get(EditableCustomRule::class, 'condition_options');
355
356
                return DropdownField::create($column, '', $options);
357
            },
358
            '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

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

857
    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...
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

857
    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...
858
    {
859
        return sprintf("$(%s)", $this->getSelectorFieldOnly());
860
    }
861
862
    /**
863
     * @return string
864
     */
865
    public function getSelectorFieldOnly()
866
    {
867
        return "[name='{$this->Name}']";
868
    }
869
870
871
    /**
872
     * Get the list of classes that can be selected and used as data-values
873
     *
874
     * @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...
875
     * @return array
876
     */
877
    public function getEditableFieldClasses($includeLiterals = true)
878
    {
879
        $classes = ClassInfo::getValidSubClasses(EditableFormField::class);
880
881
        // Remove classes we don't want to display in the dropdown.
882
        $editableFieldClasses = [];
883
        foreach ($classes as $class) {
884
            // Skip abstract / hidden classes
885
            if (Config::inst()->get($class, 'abstract', Config::UNINHERITED)
886
                || Config::inst()->get($class, 'hidden')
887
            ) {
888
                continue;
889
            }
890
891
            if (!$includeLiterals && Config::inst()->get($class, 'literal')) {
892
                continue;
893
            }
894
895
            $singleton = singleton($class);
896
            if (!$singleton->canCreate()) {
897
                continue;
898
            }
899
900
            $editableFieldClasses[$class] = $singleton->i18n_singular_name();
901
        }
902
903
        asort($editableFieldClasses);
904
        return $editableFieldClasses;
905
    }
906
907
    /**
908
     * @return EditableFormField\Validator
909
     */
910
    public function getCMSValidator()
911
    {
912
        return EditableFormField\Validator::create()
913
            ->setRecord($this);
914
    }
915
916
    /**
917
     * Extracts info from DisplayRules into array so UserDefinedForm->buildWatchJS can run through it.
918
     * @return array|null
919
     */
920
    public function formatDisplayRules()
921
    {
922
        $holderSelector = $this->getSelectorOnly();
923
        $result = [
924
            'targetFieldID' => $holderSelector,
925
            'conjunction'   => $this->DisplayRulesConjunctionNice(),
926
            'selectors'     => [],
927
            'events'        => [],
928
            'operations'    => [],
929
            'initialState'  => $this->ShowOnLoadNice(),
930
            'view'          => [],
931
            'opposite'      => [],
932
        ];
933
934
        // Check for field dependencies / default
935
        /** @var EditableCustomRule $rule */
936
        foreach ($this->DisplayRules() as $rule) {
937
            // Get the field which is effected
938
            /** @var EditableFormField $formFieldWatch */
939
            $formFieldWatch = DataObject::get_by_id(EditableFormField::class, $rule->ConditionFieldID);
940
            // Skip deleted fields
941
            if (! $formFieldWatch) {
942
                continue;
943
            }
944
            $fieldToWatch = $formFieldWatch->getSelectorFieldOnly();
945
946
            $expression = $rule->buildExpression();
947
            if (!in_array($fieldToWatch, $result['selectors'])) {
948
                $result['selectors'][] = $fieldToWatch;
949
            }
950
            if (!in_array($expression['event'], $result['events'])) {
951
                $result['events'][] = $expression['event'];
952
            }
953
            $result['operations'][] = $expression['operation'];
954
955
            // View/Show should read
956
            $opposite = ($result['initialState'] === 'hide') ? 'show' : 'hide';
957
            $result['view'] = $rule->toggleDisplayText($result['initialState']);
958
            $result['opposite'] = $rule->toggleDisplayText($opposite);
959
        }
960
961
        return (count($result['selectors'])) ? $result : null;
962
    }
963
964
    /**
965
     * Replaces the set DisplayRulesConjunction with their JS logical operators
966
     * @return string
967
     */
968
    public function DisplayRulesConjunctionNice()
969
    {
970
        return (strtolower($this->DisplayRulesConjunction) === 'or') ? '||' : '&&';
971
    }
972
973
    /**
974
     * Replaces boolean ShowOnLoad with its JS string equivalent
975
     * @return string
976
     */
977
    public function ShowOnLoadNice()
978
    {
979
        return ($this->ShowOnLoad) ? 'show' : 'hide';
980
    }
981
982
    /**
983
     * Returns whether this is of type EditableCheckBoxField
984
     * @return bool
985
     */
986
    public function isCheckBoxField()
987
    {
988
        return false;
989
    }
990
991
    /**
992
     * Returns whether this is of type EditableRadioField
993
     * @return bool
994
     */
995
    public function isRadioField()
996
    {
997
        return false;
998
    }
999
1000
    /**
1001
     * Determined is this is of type EditableCheckboxGroupField
1002
     * @return bool
1003
     */
1004
    public function isCheckBoxGroupField()
1005
    {
1006
        return false;
1007
    }
1008
}
1009