Completed
Push — master ( 1be2e7...d38097 )
by Sam
23s
created

Form::setValidationExemptActions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use InvalidArgumentException;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\HTTPResponse_Exception;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Control\Session;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Control\HTTP;
15
use SilverStripe\Control\RequestHandler;
16
use SilverStripe\Dev\Deprecation;
17
use SilverStripe\ORM\DataObject;
18
use SilverStripe\ORM\FieldType\DBField;
19
use SilverStripe\ORM\DataObjectInterface;
20
use SilverStripe\ORM\FieldType\DBHTMLText;
21
use SilverStripe\ORM\SS_List;
22
use SilverStripe\Security\SecurityToken;
23
use SilverStripe\Security\NullSecurityToken;
24
use SilverStripe\View\SSViewer;
25
26
/**
27
 * Base class for all forms.
28
 * The form class is an extensible base for all forms on a SilverStripe application.  It can be used
29
 * either by extending it, and creating processor methods on the subclass, or by creating instances
30
 * of form whose actions are handled by the parent controller.
31
 *
32
 * In either case, if you want to get a form to do anything, it must be inextricably tied to a
33
 * controller.  The constructor is passed a controller and a method on that controller.  This method
34
 * should return the form object, and it shouldn't require any arguments.  Parameters, if necessary,
35
 * can be passed using the URL or get variables.  These restrictions are in place so that we can
36
 * recreate the form object upon form submission, without the use of a session, which would be too
37
 * resource-intensive.
38
 *
39
 * You will need to create at least one method for processing the submission (through {@link FormAction}).
40
 * This method will be passed two parameters: the raw request data, and the form object.
41
 * Usually you want to save data into a {@link DataObject} by using {@link saveInto()}.
42
 * If you want to process the submitted data in any way, please use {@link getData()} rather than
43
 * the raw request data.
44
 *
45
 * <h2>Validation</h2>
46
 * Each form needs some form of {@link Validator} to trigger the {@link FormField->validate()} methods for each field.
47
 * You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads
48
 * depend on it.
49
 * The default validator is an instance of {@link RequiredFields}.
50
 * If you want to enforce serverside-validation to be ignored for a specific {@link FormField},
51
 * you need to subclass it.
52
 *
53
 * <h2>URL Handling</h2>
54
 * The form class extends {@link RequestHandler}, which means it can
55
 * be accessed directly through a URL. This can be handy for refreshing
56
 * a form by ajax, or even just displaying a single form field.
57
 * You can find out the base URL for your form by looking at the
58
 * <form action="..."> value. For example, the edit form in the CMS would be located at
59
 * "admin/EditForm". This URL will render the form without its surrounding
60
 * template when called through GET instead of POST.
61
 *
62
 * By appending to this URL, you can render individual form elements
63
 * through the {@link FormField->FieldHolder()} method.
64
 * For example, the "URLSegment" field in a standard CMS form would be
65
 * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
66
 */
67
class Form extends RequestHandler
68
{
69
70
    const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
71
    const ENC_TYPE_MULTIPART  = 'multipart/form-data';
72
73
    /**
74
     * Accessed by Form.ss; modified by {@link formHtmlContent()}.
75
     * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
76
     *
77
     * @var bool
78
     */
79
    public $IncludeFormTag = true;
80
81
    /**
82
     * @var FieldList
83
     */
84
    protected $fields;
85
86
    /**
87
     * @var FieldList
88
     */
89
    protected $actions;
90
91
    /**
92
     * @var Controller
93
     */
94
    protected $controller;
95
96
    /**
97
     * @var string
98
     */
99
    protected $name;
100
101
    /**
102
     * @var Validator
103
     */
104
    protected $validator;
105
106
    /**
107
     * @var callable {@see setValidationResponseCallback()}
108
     */
109
    protected $validationResponseCallback;
110
111
    /**
112
     * @var string
113
     */
114
    protected $formMethod = "POST";
115
116
    /**
117
     * @var boolean
118
     */
119
    protected $strictFormMethodCheck = false;
120
121
    /**
122
     * @var DataObject|null $record Populated by {@link loadDataFrom()}.
123
     */
124
    protected $record;
125
126
    /**
127
     * Keeps track of whether this form has a default action or not.
128
     * Set to false by $this->disableDefaultAction();
129
     *
130
     * @var boolean
131
     */
132
    protected $hasDefaultAction = true;
133
134
    /**
135
     * Target attribute of form-tag.
136
     * Useful to open a new window upon
137
     * form submission.
138
     *
139
     * @var string|null
140
     */
141
    protected $target;
142
143
    /**
144
     * Legend value, to be inserted into the
145
     * <legend> element before the <fieldset>
146
     * in Form.ss template.
147
     *
148
     * @var string|null
149
     */
150
    protected $legend;
151
152
    /**
153
     * The SS template to render this form HTML into.
154
     * Default is "Form", but this can be changed to
155
     * another template for customisation.
156
     *
157
     * @see Form->setTemplate()
158
     * @var string|null
159
     */
160
    protected $template;
161
162
    /**
163
     * @var callable|null
164
     */
165
    protected $buttonClickedFunc;
166
167
    /**
168
     * @var string|null
169
     */
170
    protected $message;
171
172
    /**
173
     * @var string|null
174
     */
175
    protected $messageType;
176
177
    /**
178
     * Should we redirect the user back down to the
179
     * the form on validation errors rather then just the page
180
     *
181
     * @var bool
182
     */
183
    protected $redirectToFormOnValidationError = false;
184
185
    /**
186
     * @var bool
187
     */
188
    protected $security = true;
189
190
    /**
191
     * @var SecurityToken|null
192
     */
193
    protected $securityToken = null;
194
195
    /**
196
     * @var array $extraClasses List of additional CSS classes for the form tag.
197
     */
198
    protected $extraClasses = array();
199
200
    /**
201
     * @config
202
     * @var array $default_classes The default classes to apply to the Form
203
     */
204
    private static $default_classes = array();
205
206
    /**
207
     * @var string|null
208
     */
209
    protected $encType;
210
211
    /**
212
     * @var array Any custom form attributes set through {@link setAttributes()}.
213
     * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
214
     */
215
    protected $attributes = array();
216
217
    /**
218
     * @var array
219
     */
220
    protected $validationExemptActions = array();
221
222
    private static $allowed_actions = array(
223
        'handleField',
224
        'httpSubmission',
225
        'forTemplate',
226
    );
227
228
    private static $casting = array(
229
        'AttributesHTML' => 'HTMLFragment',
230
        'FormAttributes' => 'HTMLFragment',
231
        'MessageType' => 'Text',
232
        'Message' => 'HTMLFragment',
233
        'FormName' => 'Text',
234
        'Legend' => 'HTMLFragment',
235
    );
236
237
    /**
238
     * @var FormTemplateHelper
239
     */
240
    private $templateHelper = null;
241
242
    /**
243
     * @ignore
244
     */
245
    private $htmlID = null;
246
247
    /**
248
     * @ignore
249
     */
250
    private $formActionPath = false;
251
252
    /**
253
     * @var bool
254
     */
255
    protected $securityTokenAdded = false;
256
257
    /**
258
     * Create a new form, with the given fields an action buttons.
259
     *
260
     * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
261
     * @param string $name The method on the controller that will return this form object.
262
     * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
263
     * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
264
     *                           {@link FormAction} objects
265
     * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
266
     */
267
    public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null)
268
    {
269
        parent::__construct();
270
271
        $fields->setForm($this);
272
        $actions->setForm($this);
273
274
        $this->fields = $fields;
275
        $this->actions = $actions;
276
        $this->controller = $controller;
277
        $this->setName($name);
278
279
        if (!$this->controller) {
280
            user_error("$this->class form created without a controller", E_USER_ERROR);
281
        }
282
283
        // Form validation
284
        $this->validator = ($validator) ? $validator : new RequiredFields();
285
        $this->validator->setForm($this);
286
287
        // Form error controls
288
        $this->setupFormErrors();
289
290
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
291
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
292
        if (method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod')
293
                && $controller->hasMethod('securityTokenEnabled'))) {
294
            $securityEnabled = $controller->securityTokenEnabled();
295
        } else {
296
            $securityEnabled = SecurityToken::is_enabled();
297
        }
298
299
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
300
301
        $this->setupDefaultClasses();
302
    }
303
304
    /**
305
     * @var array
306
     */
307
    private static $url_handlers = array(
308
        'field/$FieldName!' => 'handleField',
309
        'POST ' => 'httpSubmission',
310
        'GET ' => 'httpSubmission',
311
        'HEAD ' => 'httpSubmission',
312
    );
313
314
    /**
315
     * Set up current form errors in session to
316
     * the current form if appropriate.
317
     *
318
     * @return $this
319
     */
320
    public function setupFormErrors()
321
    {
322
        $errorInfo = Session::get("FormInfo.{$this->FormName()}");
323
324
        if (isset($errorInfo['errors']) && is_array($errorInfo['errors'])) {
325
            foreach ($errorInfo['errors'] as $error) {
326
                $field = $this->fields->dataFieldByName($error['fieldName']);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $this->fields->dataField...me($error['fieldName']) (which targets SilverStripe\Forms\FieldList::dataFieldByName()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
327
328
                if (!$field) {
329
                    $errorInfo['message'] = $error['message'];
330
                    $errorInfo['type'] = $error['messageType'];
331
                } else {
332
                    $field->setError($error['message'], $error['messageType']);
333
                }
334
            }
335
336
            // load data in from previous submission upon error
337
            if (isset($errorInfo['data'])) {
338
                $this->loadDataFrom($errorInfo['data']);
339
            }
340
        }
341
342
        if (isset($errorInfo['message']) && isset($errorInfo['type'])) {
343
            $this->setMessage($errorInfo['message'], $errorInfo['type']);
344
        }
345
346
        return $this;
347
    }
348
349
    /**
350
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
351
     * after instantiation
352
     */
353
    protected function setupDefaultClasses()
354
    {
355
        $defaultClasses = self::config()->get('default_classes');
356
        if ($defaultClasses) {
357
            foreach ($defaultClasses as $class) {
358
                $this->addExtraClass($class);
359
            }
360
        }
361
    }
362
363
    /**
364
     * Handle a form submission.  GET and POST requests behave identically.
365
     * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
366
     * and only triggers the requested form action/method
367
     * if the form is valid.
368
     *
369
     * @param HTTPRequest $request
370
     * @throws HTTPResponse_Exception
371
     */
372
    public function httpSubmission($request)
373
    {
374
        // Strict method check
375
        if ($this->strictFormMethodCheck) {
376
            // Throws an error if the method is bad...
377
            if ($this->formMethod != $request->httpMethod()) {
378
                $response = Controller::curr()->getResponse();
379
                $response->addHeader('Allow', $this->formMethod);
380
                $this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
381
            }
382
383
            // ...and only uses the variables corresponding to that method type
384
            $vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
385
        } else {
386
            $vars = $request->requestVars();
387
        }
388
389
        // Populate the form
390
        $this->loadDataFrom($vars, true);
391
392
        // Protection against CSRF attacks
393
        $token = $this->getSecurityToken();
394
        if (! $token->checkRequest($request)) {
395
            $securityID = $token->getName();
396
            if (empty($vars[$securityID])) {
397
                $this->httpError(400, _t(
398
                    "Form.CSRF_FAILED_MESSAGE",
399
                    "There seems to have been a technical problem. Please click the back button, ".
400
                    "refresh your browser, and try again."
401
                ));
402
            } else {
403
                // Clear invalid token on refresh
404
                $data = $this->getData();
405
                unset($data[$securityID]);
406
                Session::set("FormInfo.{$this->FormName()}.data", $data);
407
                Session::set("FormInfo.{$this->FormName()}.errors", array());
408
                $this->sessionMessage(
409
                    _t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
410
                    "warning"
411
                );
412
                return $this->controller->redirectBack();
413
            }
414
        }
415
416
        // Determine the action button clicked
417
        $funcName = null;
418
        foreach ($vars as $paramName => $paramVal) {
419
            if (substr($paramName, 0, 7) == 'action_') {
420
                // Break off querystring arguments included in the action
421
                if (strpos($paramName, '?') !== false) {
422
                    list($paramName, $paramVars) = explode('?', $paramName, 2);
423
                    $newRequestParams = array();
424
                    parse_str($paramVars, $newRequestParams);
425
                    $vars = array_merge((array)$vars, (array)$newRequestParams);
426
                }
427
428
                // Cleanup action_, _x and _y from image fields
429
                $funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
430
                break;
431
            }
432
        }
433
434
        // If the action wasn't set, choose the default on the form.
435
        if (!isset($funcName) && $defaultAction = $this->defaultAction()) {
436
            $funcName = $defaultAction->actionName();
437
        }
438
439
        if (isset($funcName)) {
440
            $this->setButtonClicked($funcName);
441
        }
442
443
        // Permission checks (first on controller, then falling back to form)
444
        if (// Ensure that the action is actually a button or method on the form,
445
            // and not just a method on the controller.
446
            $this->controller->hasMethod($funcName)
447
            && !$this->controller->checkAccessAction($funcName)
448
            // If a button exists, allow it on the controller
449
            // buttonClicked() validates that the action set above is valid
450
            && !$this->buttonClicked()
451
        ) {
452
            return $this->httpError(
453
                403,
454
                sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
455
            );
456
        } elseif ($this->hasMethod($funcName)
457
            && !$this->checkAccessAction($funcName)
458
            // No checks for button existence or $allowed_actions is performed -
459
            // all form methods are callable (e.g. the legacy "callfieldmethod()")
460
        ) {
461
            return $this->httpError(
462
                403,
463
                sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
464
            );
465
        }
466
        // TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
467
        // explicitly in allowed_actions in order to run)
468
        // Uncomment the following for checking security against running actions on form fields
469
        /* else {
0 ignored issues
show
Unused Code Comprehensibility introduced by
51% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
470
			// Try to find a field that has the action, and allows it
471
			$fieldsHaveMethod = false;
472
			foreach ($this->Fields() as $field){
473
				if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
474
					$fieldsHaveMethod = true;
475
				}
476
			}
477
			if (!$fieldsHaveMethod) {
478
				return $this->httpError(
479
					403,
480
					sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
481
				);
482
			}
483
		}*/
484
485
        // Validate the form
486
        if (!$this->validate()) {
487
            return $this->getValidationErrorResponse();
488
        }
489
490
        // First, try a handler method on the controller (has been checked for allowed_actions above already)
491
        if ($this->controller->hasMethod($funcName)) {
492
            return $this->controller->$funcName($vars, $this, $request);
493
        // Otherwise, try a handler method on the form object.
494
        } elseif ($this->hasMethod($funcName)) {
495
            return $this->$funcName($vars, $this, $request);
496
        } elseif ($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
497
            return $field->$funcName($vars, $this, $request);
498
        }
499
500
        return $this->httpError(404);
501
    }
502
503
    /**
504
     * @param string $action
505
     * @return bool
506
     */
507
    public function checkAccessAction($action)
508
    {
509
        if (parent::checkAccessAction($action)) {
510
            return true;
511
        }
512
513
        $actions = $this->getAllActions();
514
        foreach ($actions as $formAction) {
515
            if ($formAction->actionName() === $action) {
516
                return true;
517
            }
518
        }
519
520
        // Always allow actions on fields
521
        $field = $this->checkFieldsForAction($this->Fields(), $action);
522
        if ($field && $field->checkAccessAction($action)) {
523
            return true;
524
        }
525
526
        return false;
527
    }
528
529
    /**
530
     * @return callable
531
     */
532
    public function getValidationResponseCallback()
533
    {
534
        return $this->validationResponseCallback;
535
    }
536
537
    /**
538
     * Overrules validation error behaviour in {@link httpSubmission()}
539
     * when validation has failed. Useful for optional handling of a certain accepted content type.
540
     *
541
     * The callback can opt out of handling specific responses by returning NULL,
542
     * in which case the default form behaviour will kick in.
543
     *
544
     * @param $callback
545
     * @return self
546
     */
547
    public function setValidationResponseCallback($callback)
548
    {
549
        $this->validationResponseCallback = $callback;
550
551
        return $this;
552
    }
553
554
    /**
555
     * Returns the appropriate response up the controller chain
556
     * if {@link validate()} fails (which is checked prior to executing any form actions).
557
     * By default, returns different views for ajax/non-ajax request, and
558
     * handles 'application/json' requests with a JSON object containing the error messages.
559
     * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
560
     * and can be overruled by setting {@link $validationResponseCallback}.
561
     *
562
     * @return HTTPResponse|string
563
     */
564
    protected function getValidationErrorResponse()
565
    {
566
        $callback = $this->getValidationResponseCallback();
567
        if ($callback && $callbackResponse = $callback()) {
568
            return $callbackResponse;
569
        }
570
571
        $request = $this->getRequest();
572
        if ($request->isAjax()) {
573
                // Special case for legacy Validator.js implementation
574
                // (assumes eval'ed javascript collected through FormResponse)
575
                $acceptType = $request->getHeader('Accept');
576
            if (strpos($acceptType, 'application/json') !== false) {
577
                // Send validation errors back as JSON with a flag at the start
578
                $response = new HTTPResponse(Convert::array2json($this->validator->getErrors()));
0 ignored issues
show
Bug introduced by
It seems like $this->validator->getErrors() targeting SilverStripe\Forms\Validator::getErrors() can also be of type null; however, SilverStripe\Core\Convert::array2json() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
579
                $response->addHeader('Content-Type', 'application/json');
580
            } else {
581
                $this->setupFormErrors();
582
                // Send the newly rendered form tag as HTML
583
                $response = new HTTPResponse($this->forTemplate());
584
                $response->addHeader('Content-Type', 'text/html');
585
            }
586
587
                return $response;
588
        } else {
589
            if ($this->getRedirectToFormOnValidationError()) {
590
                if ($pageURL = $request->getHeader('Referer')) {
591
                    if (Director::is_site_url($pageURL)) {
592
                        // Remove existing pragmas
593
                        $pageURL = preg_replace('/(#.*)/', '', $pageURL);
594
                        $pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \SilverStripe\Control\Di...luteURL($pageURL, true) on line 594 can also be of type array<integer,string>; however, SilverStripe\Control\Director::absoluteURL() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
595
                        return $this->controller->redirect($pageURL . '#' . $this->FormName());
596
                    }
597
                }
598
            }
599
            return $this->controller->redirectBack();
600
        }
601
    }
602
603
    /**
604
     * Fields can have action to, let's check if anyone of the responds to $funcname them
605
     *
606
     * @param SS_List|array $fields
607
     * @param callable $funcName
608
     * @return FormField
609
     */
610
    protected function checkFieldsForAction($fields, $funcName)
611
    {
612
        foreach ($fields as $field) {
613
            /** @skipUpgrade */
614
            if (method_exists($field, 'FieldList')) {
615
                if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
616
                    return $field;
617
                }
618
            } elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
619
                return $field;
620
            }
621
        }
622
        return null;
623
    }
624
625
    /**
626
     * Handle a field request.
627
     * Uses {@link Form->dataFieldByName()} to find a matching field,
628
     * and falls back to {@link FieldList->fieldByName()} to look
629
     * for tabs instead. This means that if you have a tab and a
630
     * formfield with the same name, this method gives priority
631
     * to the formfield.
632
     *
633
     * @param HTTPRequest $request
634
     * @return FormField
635
     */
636
    public function handleField($request)
637
    {
638
        $field = $this->Fields()->dataFieldByName($request->param('FieldName'));
639
640
        if ($field) {
641
            return $field;
642
        } else {
643
            // falling back to fieldByName, e.g. for getting tabs
644
            return $this->Fields()->fieldByName($request->param('FieldName'));
645
        }
646
    }
647
648
    /**
649
     * Convert this form into a readonly form
650
     */
651
    public function makeReadonly()
652
    {
653
        $this->transform(new ReadonlyTransformation());
654
    }
655
656
    /**
657
     * Set whether the user should be redirected back down to the
658
     * form on the page upon validation errors in the form or if
659
     * they just need to redirect back to the page
660
     *
661
     * @param bool $bool Redirect to form on error?
662
     * @return $this
663
     */
664
    public function setRedirectToFormOnValidationError($bool)
665
    {
666
        $this->redirectToFormOnValidationError = $bool;
667
        return $this;
668
    }
669
670
    /**
671
     * Get whether the user should be redirected back down to the
672
     * form on the page upon validation errors
673
     *
674
     * @return bool
675
     */
676
    public function getRedirectToFormOnValidationError()
677
    {
678
        return $this->redirectToFormOnValidationError;
679
    }
680
681
    /**
682
     * Add a plain text error message to a field on this form.  It will be saved into the session
683
     * and used the next time this form is displayed.
684
     * @param string $fieldName
685
     * @param string $message
686
     * @param string $messageType
687
     * @param bool $escapeHtml
688
     */
689
    public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true)
690
    {
691
        Session::add_to_array("FormInfo.{$this->FormName()}.errors", array(
692
            'fieldName' => $fieldName,
693
            'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
694
            'messageType' => $messageType,
695
        ));
696
    }
697
698
    /**
699
     * @param FormTransformation $trans
700
     */
701
    public function transform(FormTransformation $trans)
702
    {
703
        $newFields = new FieldList();
704
        foreach ($this->fields as $field) {
705
            $newFields->push($field->transform($trans));
706
        }
707
        $this->fields = $newFields;
708
709
        $newActions = new FieldList();
710
        foreach ($this->actions as $action) {
711
            $newActions->push($action->transform($trans));
712
        }
713
        $this->actions = $newActions;
714
715
716
        // We have to remove validation, if the fields are not editable ;-)
717
        if ($this->validator) {
718
            $this->validator->removeValidation();
719
        }
720
    }
721
722
    /**
723
     * Get the {@link Validator} attached to this form.
724
     * @return Validator
725
     */
726
    public function getValidator()
727
    {
728
        return $this->validator;
729
    }
730
731
    /**
732
     * Set the {@link Validator} on this form.
733
     * @param Validator $validator
734
     * @return $this
735
     */
736
    public function setValidator(Validator $validator)
737
    {
738
        if ($validator) {
739
            $this->validator = $validator;
740
            $this->validator->setForm($this);
741
        }
742
        return $this;
743
    }
744
745
    /**
746
     * Remove the {@link Validator} from this from.
747
     */
748
    public function unsetValidator()
749
    {
750
        $this->validator = null;
751
        return $this;
752
    }
753
754
    /**
755
     * Set actions that are exempt from validation
756
     *
757
     * @param array
758
     * @return $this
759
     */
760
    public function setValidationExemptActions($actions)
761
    {
762
        $this->validationExemptActions = $actions;
763
        return $this;
764
    }
765
766
    /**
767
     * Get a list of actions that are exempt from validation
768
     *
769
     * @return array
770
     */
771
    public function getValidationExemptActions()
772
    {
773
        return $this->validationExemptActions;
774
    }
775
776
    /**
777
     * Passed a FormAction, returns true if that action is exempt from Form validation
778
     *
779
     * @param FormAction $action
780
     * @return bool
781
     */
782
    public function actionIsValidationExempt($action)
783
    {
784
        if ($action->getValidationExempt()) {
785
            return true;
786
        }
787
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
788
            return true;
789
        }
790
        return false;
791
    }
792
793
    /**
794
     * Generate extra special fields - namely the security token field (if required).
795
     *
796
     * @return FieldList
797
     */
798
    public function getExtraFields()
799
    {
800
        $extraFields = new FieldList();
801
802
        $token = $this->getSecurityToken();
803
        if ($token) {
804
            $tokenField = $token->updateFieldSet($this->fields);
805
            if ($tokenField) {
806
                $tokenField->setForm($this);
807
            }
808
        }
809
        $this->securityTokenAdded = true;
810
811
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
812
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
813
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
814
            $methodField->setForm($this);
815
            $extraFields->push($methodField);
816
        }
817
818
        return $extraFields;
819
    }
820
821
    /**
822
     * Return the form's fields - used by the templates
823
     *
824
     * @return FieldList The form fields
825
     */
826
    public function Fields()
827
    {
828
        foreach ($this->getExtraFields() as $field) {
829
            if (!$this->fields->fieldByName($field->getName())) {
830
                $this->fields->push($field);
831
            }
832
        }
833
834
        return $this->fields;
835
    }
836
837
    /**
838
     * Return all <input type="hidden"> fields
839
     * in a form - including fields nested in {@link CompositeFields}.
840
     * Useful when doing custom field layouts.
841
     *
842
     * @return FieldList
843
     */
844
    public function HiddenFields()
845
    {
846
        return $this->Fields()->HiddenFields();
847
    }
848
849
    /**
850
     * Return all fields except for the hidden fields.
851
     * Useful when making your own simplified form layouts.
852
     */
853
    public function VisibleFields()
854
    {
855
        return $this->Fields()->VisibleFields();
856
    }
857
858
    /**
859
     * Setter for the form fields.
860
     *
861
     * @param FieldList $fields
862
     * @return $this
863
     */
864
    public function setFields($fields)
865
    {
866
        $this->fields = $fields;
867
        return $this;
868
    }
869
870
    /**
871
     * Return the form's action buttons - used by the templates
872
     *
873
     * @return FieldList The action list
874
     */
875
    public function Actions()
876
    {
877
        return $this->actions;
878
    }
879
880
    /**
881
     * Setter for the form actions.
882
     *
883
     * @param FieldList $actions
884
     * @return $this
885
     */
886
    public function setActions($actions)
887
    {
888
        $this->actions = $actions;
889
        return $this;
890
    }
891
892
    /**
893
     * Unset all form actions
894
     */
895
    public function unsetAllActions()
896
    {
897
        $this->actions = new FieldList();
898
        return $this;
899
    }
900
901
    /**
902
     * @param string $name
903
     * @param string $value
904
     * @return $this
905
     */
906
    public function setAttribute($name, $value)
907
    {
908
        $this->attributes[$name] = $value;
909
        return $this;
910
    }
911
912
    /**
913
     * @param string $name
914
     * @return string
915
     */
916
    public function getAttribute($name)
917
    {
918
        if (isset($this->attributes[$name])) {
919
            return $this->attributes[$name];
920
        }
921
        return null;
922
    }
923
924
    /**
925
     * @return array
926
     */
927
    public function getAttributes()
928
    {
929
        $attrs = array(
930
            'id' => $this->FormName(),
931
            'action' => $this->FormAction(),
932
            'method' => $this->FormMethod(),
933
            'enctype' => $this->getEncType(),
934
            'target' => $this->target,
935
            'class' => $this->extraClass(),
936
        );
937
938
        if ($this->validator && $this->validator->getErrors()) {
939
            if (!isset($attrs['class'])) {
940
                $attrs['class'] = '';
941
            }
942
            $attrs['class'] .= ' validationerror';
943
        }
944
945
        $attrs = array_merge($attrs, $this->attributes);
946
947
        return $attrs;
948
    }
949
950
    /**
951
     * Return the attributes of the form tag - used by the templates.
952
     *
953
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
954
     * If at least one argument is passed as a string, all arguments act as excludes by name.
955
     *
956
     * @return string HTML attributes, ready for insertion into an HTML tag
957
     */
958
    public function getAttributesHTML($attrs = null)
959
    {
960
        $exclude = (is_string($attrs)) ? func_get_args() : null;
961
962
        // Figure out if we can cache this form
963
        // - forms with validation shouldn't be cached, cos their error messages won't be shown
964
        // - forms with security tokens shouldn't be cached because security tokens expire
965
        $needsCacheDisabled = false;
966
        if ($this->getSecurityToken()->isEnabled()) {
967
            $needsCacheDisabled = true;
968
        }
969
        if ($this->FormMethod() != 'GET') {
970
            $needsCacheDisabled = true;
971
        }
972
        if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
973
            $needsCacheDisabled = true;
974
        }
975
976
        // If we need to disable cache, do it
977
        if ($needsCacheDisabled) {
978
            HTTP::set_cache_age(0);
979
        }
980
981
        $attrs = $this->getAttributes();
982
983
        // Remove empty
984
        $attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);'));
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
985
986
        // Remove excluded
987
        if ($exclude) {
988
            $attrs = array_diff_key($attrs, array_flip($exclude));
989
        }
990
991
        // Prepare HTML-friendly 'method' attribute (lower-case)
992
        if (isset($attrs['method'])) {
993
            $attrs['method'] = strtolower($attrs['method']);
994
        }
995
996
        // Create markup
997
        $parts = array();
998
        foreach ($attrs as $name => $value) {
999
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
1000
        }
1001
1002
        return implode(' ', $parts);
1003
    }
1004
1005
    public function FormAttributes()
1006
    {
1007
        return $this->getAttributesHTML();
1008
    }
1009
1010
    /**
1011
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
1012
     * another frame
1013
     *
1014
     * @param string|FormTemplateHelper
1015
     */
1016
    public function setTemplateHelper($helper)
1017
    {
1018
        $this->templateHelper = $helper;
1019
    }
1020
1021
    /**
1022
     * Return a {@link FormTemplateHelper} for this form. If one has not been
1023
     * set, return the default helper.
1024
     *
1025
     * @return FormTemplateHelper
1026
     */
1027
    public function getTemplateHelper()
1028
    {
1029
        if ($this->templateHelper) {
1030
            if (is_string($this->templateHelper)) {
1031
                return Injector::inst()->get($this->templateHelper);
1032
            }
1033
1034
            return $this->templateHelper;
1035
        }
1036
1037
        return FormTemplateHelper::singleton();
1038
    }
1039
1040
    /**
1041
     * Set the target of this form to any value - useful for opening the form
1042
     * contents in a new window or refreshing another frame.
1043
     *
1044
     * @param string $target The value of the target
1045
     * @return $this
1046
     */
1047
    public function setTarget($target)
1048
    {
1049
        $this->target = $target;
1050
1051
        return $this;
1052
    }
1053
1054
    /**
1055
     * Set the legend value to be inserted into
1056
     * the <legend> element in the Form.ss template.
1057
     * @param string $legend
1058
     * @return $this
1059
     */
1060
    public function setLegend($legend)
1061
    {
1062
        $this->legend = $legend;
1063
        return $this;
1064
    }
1065
1066
    /**
1067
     * Set the SS template that this form should use
1068
     * to render with. The default is "Form".
1069
     *
1070
     * @param string $template The name of the template (without the .ss extension)
1071
     * @return $this
1072
     */
1073
    public function setTemplate($template)
1074
    {
1075
        $this->template = $template;
1076
        return $this;
1077
    }
1078
1079
    /**
1080
     * Return the template to render this form with.
1081
     *
1082
     * @return string
1083
     */
1084
    public function getTemplate()
1085
    {
1086
        return $this->template;
1087
    }
1088
1089
    /**
1090
     * Returs the ordered list of preferred templates for rendering this form
1091
     * If the template isn't set, then default to the
1092
     * form class name e.g "Form".
1093
     *
1094
     * @return array
1095
     */
1096
    public function getTemplates()
1097
    {
1098
        $templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
1099
        // Prefer any custom template
1100
        if ($this->getTemplate()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getTemplate() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1101
            array_unshift($templates, $this->getTemplate());
1102
        }
1103
        return $templates;
1104
    }
1105
1106
    /**
1107
     * Returns the encoding type for the form.
1108
     *
1109
     * By default this will be URL encoded, unless there is a file field present
1110
     * in which case multipart is used. You can also set the enc type using
1111
     * {@link setEncType}.
1112
     */
1113
    public function getEncType()
1114
    {
1115
        if ($this->encType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->encType of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1116
            return $this->encType;
1117
        }
1118
1119
        if ($fields = $this->fields->dataFields()) {
1120
            foreach ($fields as $field) {
1121
                if ($field instanceof FileField) {
1122
                    return self::ENC_TYPE_MULTIPART;
1123
                }
1124
            }
1125
        }
1126
1127
        return self::ENC_TYPE_URLENCODED;
1128
    }
1129
1130
    /**
1131
     * Sets the form encoding type. The most common encoding types are defined
1132
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1133
     *
1134
     * @param string $encType
1135
     * @return $this
1136
     */
1137
    public function setEncType($encType)
1138
    {
1139
        $this->encType = $encType;
1140
        return $this;
1141
    }
1142
1143
    /**
1144
     * Returns the real HTTP method for the form:
1145
     * GET, POST, PUT, DELETE or HEAD.
1146
     * As most browsers only support GET and POST in
1147
     * form submissions, all other HTTP methods are
1148
     * added as a hidden field "_method" that
1149
     * gets evaluated in {@link Director::direct()}.
1150
     * See {@link FormMethod()} to get a HTTP method
1151
     * for safe insertion into a <form> tag.
1152
     *
1153
     * @return string HTTP method
1154
     */
1155
    public function FormHttpMethod()
1156
    {
1157
        return $this->formMethod;
1158
    }
1159
1160
    /**
1161
     * Returns the form method to be used in the <form> tag.
1162
     * See {@link FormHttpMethod()} to get the "real" method.
1163
     *
1164
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1165
     */
1166
    public function FormMethod()
1167
    {
1168
        if (in_array($this->formMethod, array('GET','POST'))) {
1169
            return $this->formMethod;
1170
        } else {
1171
            return 'POST';
1172
        }
1173
    }
1174
1175
    /**
1176
     * Set the form method: GET, POST, PUT, DELETE.
1177
     *
1178
     * @param string $method
1179
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1180
     * @return $this
1181
     */
1182
    public function setFormMethod($method, $strict = null)
1183
    {
1184
        $this->formMethod = strtoupper($method);
1185
        if ($strict !== null) {
1186
            $this->setStrictFormMethodCheck($strict);
1187
        }
1188
        return $this;
1189
    }
1190
1191
    /**
1192
     * If set to true, enforce the matching of the form method.
1193
     *
1194
     * This will mean two things:
1195
     *  - GET vars will be ignored by a POST form, and vice versa
1196
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1197
     *
1198
     * If set to false (the default), then the form method is only used to construct the default
1199
     * form.
1200
     *
1201
     * @param $bool boolean
1202
     * @return $this
1203
     */
1204
    public function setStrictFormMethodCheck($bool)
1205
    {
1206
        $this->strictFormMethodCheck = (bool)$bool;
1207
        return $this;
1208
    }
1209
1210
    /**
1211
     * @return boolean
1212
     */
1213
    public function getStrictFormMethodCheck()
1214
    {
1215
        return $this->strictFormMethodCheck;
1216
    }
1217
1218
    /**
1219
     * Return the form's action attribute.
1220
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1221
     *
1222
     * @return string
1223
     */
1224
    public function FormAction()
1225
    {
1226
        if ($this->formActionPath) {
1227
            return $this->formActionPath;
1228
        } elseif ($this->controller->hasMethod("FormObjectLink")) {
1229
            return $this->controller->FormObjectLink($this->name);
1230
        } else {
1231
            return Controller::join_links($this->controller->Link(), $this->name);
1232
        }
1233
    }
1234
1235
    /**
1236
     * Set the form action attribute to a custom URL.
1237
     *
1238
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1239
     * recommended only for situations where you have two relatively distinct
1240
     * parts of the system trying to communicate via a form post.
1241
     *
1242
     * @param string $path
1243
     * @return $this
1244
     */
1245
    public function setFormAction($path)
1246
    {
1247
        $this->formActionPath = $path;
0 ignored issues
show
Documentation Bug introduced by
The property $formActionPath was declared of type boolean, but $path is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1248
1249
        return $this;
1250
    }
1251
1252
    /**
1253
     * Returns the name of the form.
1254
     *
1255
     * @return string
1256
     */
1257
    public function FormName()
1258
    {
1259
        return $this->getTemplateHelper()->generateFormID($this);
1260
    }
1261
1262
    /**
1263
     * Set the HTML ID attribute of the form.
1264
     *
1265
     * @param string $id
1266
     * @return $this
1267
     */
1268
    public function setHTMLID($id)
1269
    {
1270
        $this->htmlID = $id;
1271
1272
        return $this;
1273
    }
1274
1275
    /**
1276
     * @return string
1277
     */
1278
    public function getHTMLID()
1279
    {
1280
        return $this->htmlID;
1281
    }
1282
1283
    /**
1284
     * Get the controller.
1285
     *
1286
     * @return Controller
1287
     */
1288
    public function getController()
1289
    {
1290
        return $this->controller;
1291
    }
1292
1293
    /**
1294
     * Set the controller.
1295
     *
1296
     * @param Controller $controller
1297
     * @return Form
1298
     */
1299
    public function setController($controller)
1300
    {
1301
        $this->controller = $controller;
1302
1303
        return $this;
1304
    }
1305
1306
    /**
1307
     * Get the name of the form.
1308
     *
1309
     * @return string
1310
     */
1311
    public function getName()
1312
    {
1313
        return $this->name;
1314
    }
1315
1316
    /**
1317
     * Set the name of the form.
1318
     *
1319
     * @param string $name
1320
     * @return Form
1321
     */
1322
    public function setName($name)
1323
    {
1324
        $this->name = $name;
1325
1326
        return $this;
1327
    }
1328
1329
    /**
1330
     * Returns an object where there is a method with the same name as each data
1331
     * field on the form.
1332
     *
1333
     * That method will return the field itself.
1334
     *
1335
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1336
     */
1337
    public function FieldMap()
1338
    {
1339
        return new Form_FieldMap($this);
1340
    }
1341
1342
    /**
1343
     * The next functions store and modify the forms
1344
     * message attributes. messages are stored in session under
1345
     * $_SESSION[formname][message];
1346
     *
1347
     * @return string
1348
     */
1349
    public function Message()
1350
    {
1351
        $this->getMessageFromSession();
1352
1353
        return $this->message;
1354
    }
1355
1356
    /**
1357
     * @return string
1358
     */
1359
    public function MessageType()
1360
    {
1361
        $this->getMessageFromSession();
1362
1363
        return $this->messageType;
1364
    }
1365
1366
    /**
1367
     * @return string
1368
     */
1369
    protected function getMessageFromSession()
1370
    {
1371
        if ($this->message || $this->messageType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->message of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->messageType of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1372
            return $this->message;
1373
        } else {
1374
            $this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1375
            $this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1376
1377
            return $this->message;
1378
        }
1379
    }
1380
1381
    /**
1382
     * Set a status message for the form.
1383
     *
1384
     * @param string $message the text of the message
1385
     * @param string $type Should be set to good, bad, or warning.
1386
     * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1387
     *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1388
     *                            user supplied data in the message.
1389
     * @return $this
1390
     */
1391
    public function setMessage($message, $type, $escapeHtml = true)
1392
    {
1393
        $this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message;
0 ignored issues
show
Documentation Bug introduced by
It seems like $escapeHtml ? \SilverStr...ml($message) : $message can also be of type array. However, the property $message is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1394
        $this->messageType = $type;
1395
        return $this;
1396
    }
1397
1398
    /**
1399
     * Set a message to the session, for display next time this form is shown.
1400
     *
1401
     * @param string $message the text of the message
1402
     * @param string $type Should be set to good, bad, or warning.
1403
     * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1404
     *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1405
     *                            user supplied data in the message.
1406
     */
1407
    public function sessionMessage($message, $type, $escapeHtml = true)
1408
    {
1409
        Session::set(
1410
            "FormInfo.{$this->FormName()}.formError.message",
1411
            $escapeHtml ? Convert::raw2xml($message) : $message
0 ignored issues
show
Bug introduced by
It seems like $escapeHtml ? \SilverStr...ml($message) : $message can also be of type array; however, SilverStripe\Control\Session::set() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1412
        );
1413
        Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1414
    }
1415
1416
    public static function messageForForm($formName, $message, $type, $escapeHtml = true)
1417
    {
1418
        Session::set(
1419
            "FormInfo.{$formName}.formError.message",
1420
            $escapeHtml ? Convert::raw2xml($message) : $message
1421
        );
1422
        Session::set("FormInfo.{$formName}.formError.type", $type);
1423
    }
1424
1425
    public function clearMessage()
1426
    {
1427
        $this->message  = null;
1428
        Session::clear("FormInfo.{$this->FormName()}.errors");
1429
        Session::clear("FormInfo.{$this->FormName()}.formError");
1430
        Session::clear("FormInfo.{$this->FormName()}.data");
1431
    }
1432
1433
    public function resetValidation()
1434
    {
1435
        Session::clear("FormInfo.{$this->FormName()}.errors");
1436
        Session::clear("FormInfo.{$this->FormName()}.data");
1437
    }
1438
1439
    /**
1440
     * Returns the DataObject that has given this form its data
1441
     * through {@link loadDataFrom()}.
1442
     *
1443
     * @return DataObject
1444
     */
1445
    public function getRecord()
1446
    {
1447
        return $this->record;
1448
    }
1449
1450
    /**
1451
     * Get the legend value to be inserted into the
1452
     * <legend> element in Form.ss
1453
     *
1454
     * @return string
1455
     */
1456
    public function getLegend()
1457
    {
1458
        return $this->legend;
1459
    }
1460
1461
    /**
1462
     * Processing that occurs before a form is executed.
1463
     *
1464
     * This includes form validation, if it fails, we redirect back
1465
     * to the form with appropriate error messages.
1466
     * Always return true if the current form action is exempt from validation
1467
     *
1468
     * Triggered through {@link httpSubmission()}.
1469
     *
1470
     * Note that CSRF protection takes place in {@link httpSubmission()},
1471
     * if it fails the form data will never reach this method.
1472
     *
1473
     * @return boolean
1474
     */
1475
    public function validate()
1476
    {
1477
        $action = $this->buttonClicked();
1478
        if ($action && $this->actionIsValidationExempt($action)) {
1479
            return true;
1480
        }
1481
1482
        if ($this->validator) {
1483
            $errors = $this->validator->validate();
1484
1485
            if ($errors) {
1486
                // Load errors into session and post back
1487
                $data = $this->getData();
1488
1489
                // Encode validation messages as XML before saving into session state
1490
                // As per Form::addErrorMessage()
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1491
                $errors = array_map(function ($error) {
1492
                    // Encode message as XML by default
1493
                    if ($error['message'] instanceof DBField) {
1494
                        $error['message'] = $error['message']->forTemplate();
1495
                        ;
1496
                    } else {
1497
                        $error['message'] = Convert::raw2xml($error['message']);
1498
                    }
1499
                    return $error;
1500
                }, $errors);
1501
1502
                Session::set("FormInfo.{$this->FormName()}.errors", $errors);
1503
                Session::set("FormInfo.{$this->FormName()}.data", $data);
1504
1505
                return false;
1506
            }
1507
        }
1508
1509
        return true;
1510
    }
1511
1512
    const MERGE_DEFAULT = 0;
1513
    const MERGE_CLEAR_MISSING = 1;
1514
    const MERGE_IGNORE_FALSEISH = 2;
1515
1516
    /**
1517
     * Load data from the given DataObject or array.
1518
     *
1519
     * It will call $object->MyField to get the value of MyField.
1520
     * If you passed an array, it will call $object[MyField].
1521
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1522
     * as determined by {@link FieldList->dataFields()}.
1523
     *
1524
     * By default, if a field isn't set (as determined by isset()),
1525
     * its value will not be saved to the field, retaining
1526
     * potential existing values.
1527
     *
1528
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1529
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1530
     *
1531
     * Escaping happens automatically on saving the data through
1532
     * {@link saveInto()}.
1533
     *
1534
     * @uses FieldList->dataFields()
1535
     * @uses FormField->setValue()
1536
     *
1537
     * @param array|DataObject $data
1538
     * @param int $mergeStrategy
1539
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1540
     *  what that property/key's value is.
1541
     *
1542
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1543
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1544
     *  "left alone", meaning they retain any previous value.
1545
     *
1546
     *  You can pass a bitmask here to change this behaviour.
1547
     *
1548
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1549
     *  {@link $data} are cleared.
1550
     *
1551
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1552
     *  a field's value.
1553
     *
1554
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1555
     *  CLEAR_MISSING
1556
     *
1557
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1558
     * form that has some fields that save to one object, and some that save to another.
1559
     * @return Form
1560
     */
1561
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1562
    {
1563
        if (!is_object($data) && !is_array($data)) {
1564
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1565
            return $this;
1566
        }
1567
1568
        // Handle the backwards compatible case of passing "true" as the second argument
1569
        if ($mergeStrategy === true) {
1570
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1571
        } elseif ($mergeStrategy === false) {
1572
            $mergeStrategy = 0;
1573
        }
1574
1575
        // if an object is passed, save it for historical reference through {@link getRecord()}
1576
        if (is_object($data)) {
1577
            $this->record = $data;
1578
        }
1579
1580
        // dont include fields without data
1581
        $dataFields = $this->Fields()->dataFields();
1582
        if ($dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1583
            foreach ($dataFields as $field) {
1584
                $name = $field->getName();
1585
1586
                        // Skip fields that have been excluded
1587
                if ($fieldList && !in_array($name, $fieldList)) {
1588
                    continue;
1589
                }
1590
1591
                        // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1592
                if (is_array($data) && isset($data[$name . '_unchanged'])) {
1593
                    continue;
1594
                }
1595
1596
                        // Does this property exist on $data?
1597
                $exists = false;
1598
                        // The value from $data for this field
1599
                $val = null;
1600
1601
                if (is_object($data)) {
1602
                    $exists = (
1603
                    isset($data->$name) ||
1604
                    $data->hasMethod($name) ||
1605
                    ($data->hasMethod('hasField') && $data->hasField($name))
1606
                    );
1607
1608
                    if ($exists) {
1609
                        $val = $data->__get($name);
1610
                    }
1611
                } elseif (is_array($data)) {
1612
                    if (array_key_exists($name, $data)) {
1613
                        $exists = true;
1614
                        $val = $data[$name];
1615
                    } // If field is in array-notation we need to access nested data
1616
                    elseif (strpos($name, '[')) {
1617
                        // First encode data using PHP's method of converting nested arrays to form data
1618
                        $flatData = urldecode(http_build_query($data));
1619
                        // Then pull the value out from that flattened string
1620
                        preg_match('/' . addcslashes($name, '[]') . '=([^&]*)/', $flatData, $matches);
1621
1622
                        if (isset($matches[1])) {
1623
                            $exists = true;
1624
                            $val = $matches[1];
1625
                        }
1626
                    }
1627
                }
1628
1629
                        // save to the field if either a value is given, or loading of blank/undefined values is forced
1630
                if ($exists) {
1631
                    if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1632
                        // pass original data as well so composite fields can act on the additional information
1633
                        $field->setValue($val, $data);
0 ignored issues
show
Unused Code introduced by
The call to FormField::setValue() has too many arguments starting with $data.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

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

Loading history...
1634
                    }
1635
                } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1636
                    $field->setValue($val, $data);
0 ignored issues
show
Unused Code introduced by
The call to FormField::setValue() has too many arguments starting with $data.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

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

Loading history...
1637
                }
1638
            }
1639
        }
1640
1641
        return $this;
1642
    }
1643
1644
    /**
1645
     * Save the contents of this form into the given data object.
1646
     * It will make use of setCastedField() to do this.
1647
     *
1648
     * @param DataObjectInterface $dataObject The object to save data into
1649
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1650
     * form that has some fields that save to one object, and some that save to another.
1651
     */
1652
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1653
    {
1654
        $dataFields = $this->fields->saveableFields();
1655
        $lastField = null;
1656
        if ($dataFields) {
1657
            foreach ($dataFields as $field) {
1658
                        // Skip fields that have been excluded
1659
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1660
                    continue;
1661
                }
1662
1663
1664
                $saveMethod = "save{$field->getName()}";
1665
1666
                if ($field->getName() == "ClassName") {
1667
                    $lastField = $field;
1668
                } elseif ($dataObject->hasMethod($saveMethod)) {
1669
                    $dataObject->$saveMethod( $field->dataValue());
1670
                } elseif ($field->getName() != "ID") {
1671
                    $field->saveInto($dataObject);
1672
                }
1673
            }
1674
        }
1675
        if ($lastField) {
1676
            $lastField->saveInto($dataObject);
1677
        }
1678
    }
1679
1680
    /**
1681
     * Get the submitted data from this form through
1682
     * {@link FieldList->dataFields()}, which filters out
1683
     * any form-specific data like form-actions.
1684
     * Calls {@link FormField->dataValue()} on each field,
1685
     * which returns a value suitable for insertion into a DataObject
1686
     * property.
1687
     *
1688
     * @return array
1689
     */
1690
    public function getData()
1691
    {
1692
        $dataFields = $this->fields->dataFields();
1693
        $data = array();
1694
1695
        if ($dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1696
            foreach ($dataFields as $field) {
1697
                if ($field->getName()) {
1698
                    $data[$field->getName()] = $field->dataValue();
1699
                }
1700
            }
1701
        }
1702
1703
        return $data;
1704
    }
1705
1706
    /**
1707
     * Return a rendered version of this form.
1708
     *
1709
     * This is returned when you access a form as $FormObject rather
1710
     * than <% with FormObject %>
1711
     *
1712
     * @return DBHTMLText
1713
     */
1714
    public function forTemplate()
1715
    {
1716
        $return = $this->renderWith($this->getTemplates());
1717
1718
        // Now that we're rendered, clear message
1719
        $this->clearMessage();
1720
1721
        return $return;
1722
    }
1723
1724
    /**
1725
     * Return a rendered version of this form, suitable for ajax post-back.
1726
     *
1727
     * It triggers slightly different behaviour, such as disabling the rewriting
1728
     * of # links.
1729
     *
1730
     * @return DBHTMLText
1731
     */
1732
    public function forAjaxTemplate()
1733
    {
1734
        $view = new SSViewer($this->getTemplates());
1735
1736
        $return = $view->dontRewriteHashlinks()->process($this);
1737
1738
        // Now that we're rendered, clear message
1739
        $this->clearMessage();
1740
1741
        return $return;
1742
    }
1743
1744
    /**
1745
     * Returns an HTML rendition of this form, without the <form> tag itself.
1746
     *
1747
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1748
     * and _form_enctype.  These are the attributes of the form.  These fields
1749
     * can be used to send the form to Ajax.
1750
     *
1751
     * @deprecated 5.0
1752
     * @return string
1753
     */
1754
    public function formHtmlContent()
1755
    {
1756
        Deprecation::notice('5.0');
1757
        $this->IncludeFormTag = false;
1758
        $content = $this->forTemplate();
1759
        $this->IncludeFormTag = true;
1760
1761
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1762
            . " value=\"" . $this->FormAction() . "\" />\n";
1763
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1764
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1765
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1766
1767
        return $content;
1768
    }
1769
1770
    /**
1771
     * Render this form using the given template, and return the result as a string
1772
     * You can pass either an SSViewer or a template name
1773
     * @param string|array $template
1774
     * @return DBHTMLText
1775
     */
1776
    public function renderWithoutActionButton($template)
1777
    {
1778
        $custom = $this->customise(array(
1779
            "Actions" => "",
1780
        ));
1781
1782
        if (is_string($template)) {
1783
            $template = new SSViewer($template);
1784
        }
1785
1786
        return $template->process($custom);
0 ignored issues
show
Bug introduced by
It seems like $template is not always an object, but can also be of type array. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1787
    }
1788
1789
1790
    /**
1791
     * Sets the button that was clicked.  This should only be called by the Controller.
1792
     *
1793
     * @param callable $funcName The name of the action method that will be called.
1794
     * @return $this
1795
     */
1796
    public function setButtonClicked($funcName)
1797
    {
1798
        $this->buttonClickedFunc = $funcName;
1799
1800
        return $this;
1801
    }
1802
1803
    /**
1804
     * @return FormAction
1805
     */
1806
    public function buttonClicked()
1807
    {
1808
        $actions = $this->getAllActions();
1809
        foreach ($actions as $action) {
1810
            if ($this->buttonClickedFunc === $action->actionName()) {
1811
                return $action;
1812
            }
1813
        }
1814
1815
        return null;
1816
    }
1817
1818
    /**
1819
     * Get a list of all actions, including those in the main "fields" FieldList
1820
     *
1821
     * @return array
1822
     */
1823
    protected function getAllActions()
1824
    {
1825
        $fields = $this->fields->dataFields() ?: array();
1826
        $actions = $this->actions->dataFields() ?: array();
1827
1828
        $fieldsAndActions = array_merge($fields, $actions);
1829
        $actions = array_filter($fieldsAndActions, function ($fieldOrAction) {
1830
            return $fieldOrAction instanceof FormAction;
1831
        });
1832
1833
        return $actions;
1834
    }
1835
1836
    /**
1837
     * Return the default button that should be clicked when another one isn't
1838
     * available.
1839
     *
1840
     * @return FormAction
1841
     */
1842
    public function defaultAction()
1843
    {
1844
        if ($this->hasDefaultAction && $this->actions) {
1845
            return $this->actions->first();
1846
        }
1847
        return null;
1848
    }
1849
1850
    /**
1851
     * Disable the default button.
1852
     *
1853
     * Ordinarily, when a form is processed and no action_XXX button is
1854
     * available, then the first button in the actions list will be pressed.
1855
     * However, if this is "delete", for example, this isn't such a good idea.
1856
     *
1857
     * @return Form
1858
     */
1859
    public function disableDefaultAction()
1860
    {
1861
        $this->hasDefaultAction = false;
1862
1863
        return $this;
1864
    }
1865
1866
    /**
1867
     * Disable the requirement of a security token on this form instance. This
1868
     * security protects against CSRF attacks, but you should disable this if
1869
     * you don't want to tie a form to a session - eg a search form.
1870
     *
1871
     * Check for token state with {@link getSecurityToken()} and
1872
     * {@link SecurityToken->isEnabled()}.
1873
     *
1874
     * @return Form
1875
     */
1876
    public function disableSecurityToken()
1877
    {
1878
        $this->securityToken = new NullSecurityToken();
1879
1880
        return $this;
1881
    }
1882
1883
    /**
1884
     * Enable {@link SecurityToken} protection for this form instance.
1885
     *
1886
     * Check for token state with {@link getSecurityToken()} and
1887
     * {@link SecurityToken->isEnabled()}.
1888
     *
1889
     * @return Form
1890
     */
1891
    public function enableSecurityToken()
1892
    {
1893
        $this->securityToken = new SecurityToken();
1894
1895
        return $this;
1896
    }
1897
1898
    /**
1899
     * Returns the security token for this form (if any exists).
1900
     *
1901
     * Doesn't check for {@link securityTokenEnabled()}.
1902
     *
1903
     * Use {@link SecurityToken::inst()} to get a global token.
1904
     *
1905
     * @return SecurityToken|null
1906
     */
1907
    public function getSecurityToken()
1908
    {
1909
        return $this->securityToken;
1910
    }
1911
1912
    /**
1913
     * Compiles all CSS-classes.
1914
     *
1915
     * @return string
1916
     */
1917
    public function extraClass()
1918
    {
1919
        return implode(array_unique($this->extraClasses), ' ');
1920
    }
1921
1922
    /**
1923
     * Add a CSS-class to the form-container. If needed, multiple classes can
1924
     * be added by delimiting a string with spaces.
1925
     *
1926
     * @param string $class A string containing a classname or several class
1927
     *                names delimited by a single space.
1928
     * @return $this
1929
     */
1930
    public function addExtraClass($class)
1931
    {
1932
        //split at white space
1933
        $classes = preg_split('/\s+/', $class);
1934
        foreach ($classes as $class) {
1935
            //add classes one by one
1936
            $this->extraClasses[$class] = $class;
1937
        }
1938
        return $this;
1939
    }
1940
1941
    /**
1942
     * Remove a CSS-class from the form-container. Multiple class names can
1943
     * be passed through as a space delimited string
1944
     *
1945
     * @param string $class
1946
     * @return $this
1947
     */
1948
    public function removeExtraClass($class)
1949
    {
1950
        //split at white space
1951
        $classes = preg_split('/\s+/', $class);
1952
        foreach ($classes as $class) {
1953
            //unset one by one
1954
            unset($this->extraClasses[$class]);
1955
        }
1956
        return $this;
1957
    }
1958
1959
    public function debug()
1960
    {
1961
        $result = "<h3>$this->class</h3><ul>";
1962
        foreach ($this->fields as $field) {
1963
            $result .= "<li>$field" . $field->debug() . "</li>";
1964
        }
1965
        $result .= "</ul>";
1966
1967
        if ($this->validator) {
1968
            /** @skipUpgrade */
1969
            $result .= '<h3>' . _t('Form.VALIDATOR', 'Validator') . '</h3>' . $this->validator->debug();
1970
        }
1971
1972
        return $result;
1973
    }
1974
1975
1976
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1977
    // TESTING HELPERS
1978
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1979
1980
    /**
1981
     * Test a submission of this form.
1982
     * @param string $action
1983
     * @param array $data
1984
     * @return HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1985
     * your unit test.
1986
     * @throws HTTPResponse_Exception
1987
     */
1988
    public function testSubmission($action, $data)
1989
    {
1990
        $data['action_' . $action] = true;
1991
1992
        return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1993
    }
1994
1995
    /**
1996
     * Test an ajax submission of this form.
1997
     *
1998
     * @param string $action
1999
     * @param array $data
2000
     * @return HTTPResponse the response object that the handling controller produces.  You can interrogate this in
2001
     * your unit test.
2002
     */
2003
    public function testAjaxSubmission($action, $data)
2004
    {
2005
        $data['ajax'] = 1;
2006
        return $this->testSubmission($action, $data);
2007
    }
2008
}
2009