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

Form::getHTMLID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Control\HTTPResponse_Exception;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\HTTPResponse;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Control\HTTP;
14
use SilverStripe\Control\RequestHandler;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\ORM\DataObjectInterface;
18
use SilverStripe\ORM\FieldType\DBHTMLText;
19
use SilverStripe\ORM\SS_List;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\ORM\ValidationResult;
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
    use FormMessage;
70
71
    /**
72
     * Form submission data is URL encoded
73
     */
74
    const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
75
76
    /**
77
     * Form submission data is multipart form
78
     */
79
    const ENC_TYPE_MULTIPART  = 'multipart/form-data';
80
81
    /**
82
     * Accessed by Form.ss; modified by {@link formHtmlContent()}.
83
     * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
84
     *
85
     * @var bool
86
     */
87
    public $IncludeFormTag = true;
88
89
    /**
90
     * @var FieldList
91
     */
92
    protected $fields;
93
94
    /**
95
     * @var FieldList
96
     */
97
    protected $actions;
98
99
    /**
100
     * @var Controller
101
     */
102
    protected $controller;
103
104
    /**
105
     * @var string
106
     */
107
    protected $name;
108
109
    /**
110
     * @var Validator
111
     */
112
    protected $validator;
113
114
    /**
115
     * @var callable {@see setValidationResponseCallback()}
116
     */
117
    protected $validationResponseCallback;
118
119
    /**
120
     * @var string
121
     */
122
    protected $formMethod = "POST";
123
124
    /**
125
     * @var boolean
126
     */
127
    protected $strictFormMethodCheck = false;
128
129
    /**
130
     * @var DataObject|null $record Populated by {@link loadDataFrom()}.
131
     */
132
    protected $record;
133
134
    /**
135
     * Keeps track of whether this form has a default action or not.
136
     * Set to false by $this->disableDefaultAction();
137
     *
138
     * @var boolean
139
     */
140
    protected $hasDefaultAction = true;
141
142
    /**
143
     * Target attribute of form-tag.
144
     * Useful to open a new window upon
145
     * form submission.
146
     *
147
     * @var string|null
148
     */
149
    protected $target;
150
151
    /**
152
     * Legend value, to be inserted into the
153
     * <legend> element before the <fieldset>
154
     * in Form.ss template.
155
     *
156
     * @var string|null
157
     */
158
    protected $legend;
159
160
    /**
161
     * The SS template to render this form HTML into.
162
     * Default is "Form", but this can be changed to
163
     * another template for customisation.
164
     *
165
     * @see Form->setTemplate()
166
     * @var string|null
167
     */
168
    protected $template;
169
170
    /**
171
     * @var callable|null
172
     */
173
    protected $buttonClickedFunc;
174
175
    /**
176
     * Should we redirect the user back down to the
177
     * the form on validation errors rather then just the page
178
     *
179
     * @var bool
180
     */
181
    protected $redirectToFormOnValidationError = false;
182
183
    /**
184
     * @var bool
185
     */
186
    protected $security = true;
187
188
    /**
189
     * @var SecurityToken|null
190
     */
191
    protected $securityToken = null;
192
193
    /**
194
     * @var array $extraClasses List of additional CSS classes for the form tag.
195
     */
196
    protected $extraClasses = array();
197
198
    /**
199
     * @config
200
     * @var array $default_classes The default classes to apply to the Form
201
     */
202
    private static $default_classes = array();
203
204
    /**
205
     * @var string|null
206
     */
207
    protected $encType;
208
209
    /**
210
     * @var array Any custom form attributes set through {@link setAttributes()}.
211
     * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
212
     */
213
    protected $attributes = array();
214
215
    /**
216
     * @var array
217
     */
218
    protected $validationExemptActions = array();
219
220
    private static $allowed_actions = array(
221
        'handleField',
222
        'httpSubmission',
223
        'forTemplate',
224
    );
225
226
    private static $casting = array(
227
        'AttributesHTML' => 'HTMLFragment',
228
        'FormAttributes' => 'HTMLFragment',
229
        'FormName' => 'Text',
230
        'Legend' => 'HTMLFragment',
231
    );
232
233
    /**
234
     * @var FormTemplateHelper
235
     */
236
    private $templateHelper = null;
237
238
    /**
239
     * @ignore
240
     */
241
    private $htmlID = null;
242
243
    /**
244
     * @ignore
245
     */
246
    private $formActionPath = false;
247
248
    /**
249
     * @var bool
250
     */
251
    protected $securityTokenAdded = false;
252
253
    /**
254
     * Create a new form, with the given fields an action buttons.
255
     *
256
     * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
257
     * @param string $name The method on the controller that will return this form object.
258
     * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
259
     * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
260
     *                           {@link FormAction} objects
261
     * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
262
     */
263
    public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null)
264
    {
265
        parent::__construct();
266
267
        $fields->setForm($this);
268
        $actions->setForm($this);
269
270
        $this->fields = $fields;
271
        $this->actions = $actions;
272
        $this->controller = $controller;
273
        $this->setName($name);
274
275
        if (!$this->controller) {
276
            user_error("$this->class form created without a controller", E_USER_ERROR);
277
        }
278
279
        // Form validation
280
        $this->validator = ($validator) ? $validator : new RequiredFields();
281
        $this->validator->setForm($this);
282
283
        // Form error controls
284
        $this->restoreFormState();
285
286
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
287
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
288
        if (method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod')
289
                && $controller->hasMethod('securityTokenEnabled'))) {
290
            $securityEnabled = $controller->securityTokenEnabled();
291
        } else {
292
            $securityEnabled = SecurityToken::is_enabled();
293
        }
294
295
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
296
297
        $this->setupDefaultClasses();
298
    }
299
300
    /**
301
     * @var array
302
     */
303
    private static $url_handlers = array(
304
        'field/$FieldName!' => 'handleField',
305
        'POST ' => 'httpSubmission',
306
        'GET ' => 'httpSubmission',
307
        'HEAD ' => 'httpSubmission',
308
    );
309
310
    /**
311
     * Load form state from session state
312
     * @return $this
313
     */
314
    public function restoreFormState()
315
    {
316
        // Restore messages
317
        $result = $this->getSessionValidationResult();
318
        if (isset($result)) {
319
            $this->loadMessagesFrom($result);
320
        }
321
322
        // load data in from previous submission upon error
323
        $data = $this->getSessionData();
324
        if (isset($data)) {
325
            $this->loadDataFrom($data);
326
        }
327
        return $this;
328
    }
329
330
    /**
331
     * Flush persistant form state details
332
     */
333
    public function clearFormState()
334
    {
335
        Session::clear("FormInfo.{$this->FormName()}.result");
336
        Session::clear("FormInfo.{$this->FormName()}.data");
337
    }
338
339
    /**
340
     * Return any form data stored in the session
341
     *
342
     * @return array
343
     */
344
    public function getSessionData()
345
    {
346
        return Session::get("FormInfo.{$this->FormName()}.data");
347
    }
348
349
    /**
350
     * Store the given form data in the session
351
     *
352
     * @param array $data
353
     */
354
    public function setSessionData($data)
355
    {
356
        Session::set("FormInfo.{$this->FormName()}.data", $data);
357
    }
358
359
    /**
360
     * Return any ValidationResult instance stored for this object
361
     *
362
     * @return ValidationResult The ValidationResult object stored in the session
363
     */
364
    public function getSessionValidationResult()
365
    {
366
        $resultData = Session::get("FormInfo.{$this->FormName()}.result");
367
        if (isset($resultData)) {
368
            return unserialize($resultData);
369
        }
370
        return null;
371
    }
372
373
    /**
374
     * Sets the ValidationResult in the session to be used with the next view of this form.
375
     * @param ValidationResult $result The result to save
376
     * @param bool $combineWithExisting If true, then this will be added to the existing result.
377
     */
378
    public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
379
    {
380
        // Combine with existing result
381
        if ($combineWithExisting) {
382
            $existingResult = $this->getSessionValidationResult();
383
            if ($existingResult) {
384
                if ($result) {
385
                    $existingResult->combineAnd($result);
386
                } else {
387
                    $result = $existingResult;
388
                }
389
            }
390
        }
391
392
        // Serialise
393
        $resultData = $result ? serialize($result) : null;
394
        Session::set("FormInfo.{$this->FormName()}.result", $resultData);
395
    }
396
397
    public function clearMessage()
398
    {
399
        $this->setMessage(null);
400
        $this->clearFormState();
401
    }
402
403
    /**
404
     * Populate this form with messages from the given ValidationResult.
405
     * Note: This will not clear any pre-existing messages
406
     *
407
     * @param ValidationResult $result
408
     * @return $this
409
     */
410
    public function loadMessagesFrom($result)
411
    {
412
        // Set message on either a field or the parent form
413
        foreach ($result->getMessages() as $message) {
414
            $fieldName = $message['fieldName'];
415
            if ($fieldName) {
416
                $owner = $this->fields->dataFieldByName($fieldName) ?: $this;
417
            } else {
418
                $owner = $this;
419
            }
420
            $owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
421
        }
422
        return $this;
423
    }
424
425
    /**
426
     * Set message on a given field name. This message will not persist via redirect.
427
     *
428
     * @param string $fieldName
429
     * @param string $message
430
     * @param string $messageType
431
     * @param string $messageCast
432
     * @return $this
433
     */
434
    public function setFieldMessage(
435
        $fieldName,
436
        $message,
437
        $messageType = ValidationResult::TYPE_ERROR,
438
        $messageCast = ValidationResult::CAST_TEXT
439
    ) {
440
        $field = $this->fields->dataFieldByName($fieldName);
441
        if ($field) {
442
            $field->setMessage($message, $messageType, $messageCast);
443
        }
444
        return $this;
445
    }
446
447
    public function castingHelper($field)
448
    {
449
        // Override casting for field message
450
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
451
            return $helper;
452
        }
453
        return parent::castingHelper($field);
454
    }
455
456
    /**
457
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
458
     * after instantiation
459
     */
460
    protected function setupDefaultClasses()
461
    {
462
        $defaultClasses = self::config()->get('default_classes');
463
        if ($defaultClasses) {
464
            foreach ($defaultClasses as $class) {
465
                $this->addExtraClass($class);
466
            }
467
        }
468
    }
469
470
    /**
471
     * Handle a form submission.  GET and POST requests behave identically.
472
     * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
473
     * and only triggers the requested form action/method
474
     * if the form is valid.
475
     *
476
     * @param HTTPRequest $request
477
     * @return HTTPResponse
478
     * @throws HTTPResponse_Exception
479
     */
480
    public function httpSubmission($request)
481
    {
482
        // Strict method check
483
        if ($this->strictFormMethodCheck) {
484
            // Throws an error if the method is bad...
485
            if ($this->formMethod != $request->httpMethod()) {
486
                $response = Controller::curr()->getResponse();
487
                $response->addHeader('Allow', $this->formMethod);
488
                $this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
489
            }
490
491
            // ...and only uses the variables corresponding to that method type
492
            $vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
493
        } else {
494
            $vars = $request->requestVars();
495
        }
496
497
        // Ensure we only process saveable fields (non structural, readonly, or disabled)
498
        $allowedFields = array_keys($this->Fields()->saveableFields());
499
500
        // Populate the form
501
        $this->loadDataFrom($vars, true, $allowedFields);
502
503
        // Protection against CSRF attacks
504
        // @todo Move this to SecurityTokenField::validate()
505
        $token = $this->getSecurityToken();
506
        if (! $token->checkRequest($request)) {
507
            $securityID = $token->getName();
508
            if (empty($vars[$securityID])) {
509
                $this->httpError(400, _t(
510
                    "Form.CSRF_FAILED_MESSAGE",
511
                    "There seems to have been a technical problem. Please click the back button, ".
512
                    "refresh your browser, and try again."
513
                ));
514
            } else {
515
                // Clear invalid token on refresh
516
                $this->clearFormState();
517
                $data = $this->getData();
518
                unset($data[$securityID]);
519
                $this->setSessionData($data);
520
                $this->sessionError(_t(
521
                    "Form.CSRF_EXPIRED_MESSAGE",
522
                    "Your session has expired. Please re-submit the form."
523
                ));
524
525
                // Return the user
526
                return $this->controller->redirectBack();
527
            }
528
        }
529
530
        // Determine the action button clicked
531
        $funcName = null;
532
        foreach ($vars as $paramName => $paramVal) {
533
            if (substr($paramName, 0, 7) == 'action_') {
534
                // Break off querystring arguments included in the action
535
                if (strpos($paramName, '?') !== false) {
536
                    list($paramName, $paramVars) = explode('?', $paramName, 2);
537
                    $newRequestParams = array();
538
                    parse_str($paramVars, $newRequestParams);
539
                    $vars = array_merge((array)$vars, (array)$newRequestParams);
540
                }
541
542
                // Cleanup action_, _x and _y from image fields
543
                $funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
544
                break;
545
            }
546
        }
547
548
        // If the action wasn't set, choose the default on the form.
549
        if (!isset($funcName) && $defaultAction = $this->defaultAction()) {
550
            $funcName = $defaultAction->actionName();
551
        }
552
553
        if (isset($funcName)) {
554
            $this->setButtonClicked($funcName);
555
        }
556
557
        // Permission checks (first on controller, then falling back to form)
558
        if (// Ensure that the action is actually a button or method on the form,
559
            // and not just a method on the controller.
560
            $this->controller->hasMethod($funcName)
561
            && !$this->controller->checkAccessAction($funcName)
562
            // If a button exists, allow it on the controller
563
            // buttonClicked() validates that the action set above is valid
564
            && !$this->buttonClicked()
565
        ) {
566
            return $this->httpError(
567
                403,
568
                sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
569
            );
570
        } elseif ($this->hasMethod($funcName)
571
            && !$this->checkAccessAction($funcName)
572
            // No checks for button existence or $allowed_actions is performed -
573
            // all form methods are callable (e.g. the legacy "callfieldmethod()")
574
        ) {
575
            return $this->httpError(
576
                403,
577
                sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
578
            );
579
        }
580
581
        // Action handlers may throw ValidationExceptions.
582
        try {
583
            // Or we can use the Valiator attached to the form
584
            $result = $this->validationResult();
585
            if (!$result->isValid()) {
586
                return $this->getValidationErrorResponse($result);
587
            }
588
589
            // First, try a handler method on the controller (has been checked for allowed_actions above already)
590
            if ($this->controller->hasMethod($funcName)) {
591
                return $this->controller->$funcName($vars, $this, $request);
592
            }
593
594
            // Otherwise, try a handler method on the form object.
595
            if ($this->hasMethod($funcName)) {
596
                return $this->$funcName($vars, $this, $request);
597
            }
598
599
            // Check for inline actions
600
            if ($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
601
                return $field->$funcName($vars, $this, $request);
602
            }
603
        } catch (ValidationException $e) {
604
            // The ValdiationResult contains all the relevant metadata
605
            $result = $e->getResult();
606
            $this->loadMessagesFrom($result);
607
            return $this->getValidationErrorResponse($result);
608
        }
609
610
        return $this->httpError(404);
611
    }
612
613
    /**
614
     * @param string $action
615
     * @return bool
616
     */
617
    public function checkAccessAction($action)
618
    {
619
        if (parent::checkAccessAction($action)) {
620
            return true;
621
        }
622
623
        $actions = $this->getAllActions();
624
        foreach ($actions as $formAction) {
625
            if ($formAction->actionName() === $action) {
626
                return true;
627
            }
628
        }
629
630
            // Always allow actions on fields
631
        $field = $this->checkFieldsForAction($this->Fields(), $action);
632
        if ($field && $field->checkAccessAction($action)) {
633
            return true;
634
        }
635
636
        return false;
637
    }
638
639
    /**
640
     * @return callable
641
     */
642
    public function getValidationResponseCallback()
643
    {
644
        return $this->validationResponseCallback;
645
    }
646
647
    /**
648
     * Overrules validation error behaviour in {@link httpSubmission()}
649
     * when validation has failed. Useful for optional handling of a certain accepted content type.
650
     *
651
     * The callback can opt out of handling specific responses by returning NULL,
652
     * in which case the default form behaviour will kick in.
653
     *
654
     * @param $callback
655
     * @return self
656
     */
657
    public function setValidationResponseCallback($callback)
658
    {
659
        $this->validationResponseCallback = $callback;
660
661
        return $this;
662
    }
663
664
    /**
665
     * Returns the appropriate response up the controller chain
666
     * if {@link validate()} fails (which is checked prior to executing any form actions).
667
     * By default, returns different views for ajax/non-ajax request, and
668
     * handles 'application/json' requests with a JSON object containing the error messages.
669
     * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
670
     * and can be overruled by setting {@link $validationResponseCallback}.
671
     *
672
     * @param ValidationResult $result
673
     * @return HTTPResponse
674
     */
675
    protected function getValidationErrorResponse(ValidationResult $result)
676
    {
677
        // Check for custom handling mechanism
678
        $callback = $this->getValidationResponseCallback();
679
        if ($callback && $callbackResponse = call_user_func($callback, $result)) {
680
            return $callbackResponse;
681
        }
682
683
        // Check if handling via ajax
684
        if ($this->getRequest()->isAjax()) {
685
            return $this->getAjaxErrorResponse($result);
686
        }
687
688
        // Prior to redirection, persist this result in session to re-display on redirect
689
        $this->setSessionValidationResult($result);
690
        $this->setSessionData($this->getData());
691
692
        // Determine redirection method
693
        if ($this->getRedirectToFormOnValidationError() && ($pageURL = $this->getRedirectReferer())) {
694
            return $this->controller->redirect($pageURL . '#' . $this->FormName());
695
        }
696
        return $this->controller->redirectBack();
697
    }
698
699
    /**
700
     * Build HTTP error response for ajax requests
701
     *
702
     * @internal called from {@see Form::getValidationErrorResponse}
703
     * @param ValidationResult $result
704
     * @return HTTPResponse
705
     */
706
    protected function getAjaxErrorResponse(ValidationResult $result)
707
    {
708
        // Ajax form submissions accept json encoded errors by default
709
        $acceptType = $this->getRequest()->getHeader('Accept');
710
        if (strpos($acceptType, 'application/json') !== false) {
711
            // Send validation errors back as JSON with a flag at the start
712
            $response = new HTTPResponse(Convert::array2json($result->getMessages()));
713
            $response->addHeader('Content-Type', 'application/json');
714
            return $response;
715
        }
716
717
        // Send the newly rendered form tag as HTML
718
        $this->loadMessagesFrom($result);
719
        $response = new HTTPResponse($this->forTemplate());
720
        $response->addHeader('Content-Type', 'text/html');
721
        return $response;
722
    }
723
724
    /**
725
     * Get referrer to redirect back to and safely validates it
726
     *
727
     * @internal called from {@see Form::getValidationErrorResponse}
728
     * @return string|null
729
     */
730
    protected function getRedirectReferer()
731
    {
732
        $pageURL = $this->getRequest()->getHeader('Referer');
733
        if (!$pageURL) {
734
            return null;
735
        }
736
        if (!Director::is_site_url($pageURL)) {
737
            return null;
738
        }
739
740
        // Remove existing pragmas
741
        $pageURL = preg_replace('/(#.*)/', '', $pageURL);
742
        return Director::absoluteURL($pageURL);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by preg_replace('/(#.*)/', '', $pageURL) on line 741 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...
743
    }
744
745
    /**
746
     * Fields can have action to, let's check if anyone of the responds to $funcname them
747
     *
748
     * @param SS_List|array $fields
749
     * @param callable $funcName
750
     * @return FormField
751
     */
752
    protected function checkFieldsForAction($fields, $funcName)
753
    {
754
        foreach ($fields as $field) {
755
            /** @skipUpgrade */
756
            if (method_exists($field, 'FieldList')) {
757
                if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
758
                    return $field;
759
                }
760
            } elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
761
                return $field;
762
            }
763
        }
764
        return null;
765
    }
766
767
    /**
768
     * Handle a field request.
769
     * Uses {@link Form->dataFieldByName()} to find a matching field,
770
     * and falls back to {@link FieldList->fieldByName()} to look
771
     * for tabs instead. This means that if you have a tab and a
772
     * formfield with the same name, this method gives priority
773
     * to the formfield.
774
     *
775
     * @param HTTPRequest $request
776
     * @return FormField
777
     */
778
    public function handleField($request)
779
    {
780
        $field = $this->Fields()->dataFieldByName($request->param('FieldName'));
781
782
        if ($field) {
783
            return $field;
784
        } else {
785
            // falling back to fieldByName, e.g. for getting tabs
786
            return $this->Fields()->fieldByName($request->param('FieldName'));
787
        }
788
    }
789
790
    /**
791
     * Convert this form into a readonly form
792
     */
793
    public function makeReadonly()
794
    {
795
        $this->transform(new ReadonlyTransformation());
796
    }
797
798
    /**
799
     * Set whether the user should be redirected back down to the
800
     * form on the page upon validation errors in the form or if
801
     * they just need to redirect back to the page
802
     *
803
     * @param bool $bool Redirect to form on error?
804
     * @return $this
805
     */
806
    public function setRedirectToFormOnValidationError($bool)
807
    {
808
        $this->redirectToFormOnValidationError = $bool;
809
        return $this;
810
    }
811
812
    /**
813
     * Get whether the user should be redirected back down to the
814
     * form on the page upon validation errors
815
     *
816
     * @return bool
817
     */
818
    public function getRedirectToFormOnValidationError()
819
    {
820
        return $this->redirectToFormOnValidationError;
821
    }
822
823
    /**
824
     * @param FormTransformation $trans
825
     */
826
    public function transform(FormTransformation $trans)
827
    {
828
        $newFields = new FieldList();
829
        foreach ($this->fields as $field) {
830
            $newFields->push($field->transform($trans));
831
        }
832
        $this->fields = $newFields;
833
834
        $newActions = new FieldList();
835
        foreach ($this->actions as $action) {
836
            $newActions->push($action->transform($trans));
837
        }
838
        $this->actions = $newActions;
839
840
841
        // We have to remove validation, if the fields are not editable ;-)
842
        if ($this->validator) {
843
            $this->validator->removeValidation();
844
        }
845
    }
846
847
    /**
848
     * Get the {@link Validator} attached to this form.
849
     * @return Validator
850
     */
851
    public function getValidator()
852
    {
853
        return $this->validator;
854
    }
855
856
    /**
857
     * Set the {@link Validator} on this form.
858
     * @param Validator $validator
859
     * @return $this
860
     */
861
    public function setValidator(Validator $validator)
862
    {
863
        if ($validator) {
864
            $this->validator = $validator;
865
            $this->validator->setForm($this);
866
        }
867
        return $this;
868
    }
869
870
    /**
871
     * Remove the {@link Validator} from this from.
872
     */
873
    public function unsetValidator()
874
    {
875
        $this->validator = null;
876
        return $this;
877
    }
878
879
    /**
880
     * Set actions that are exempt from validation
881
     *
882
     * @param array
883
     * @return $this
884
     */
885
    public function setValidationExemptActions($actions)
886
    {
887
        $this->validationExemptActions = $actions;
888
        return $this;
889
    }
890
891
    /**
892
     * Get a list of actions that are exempt from validation
893
     *
894
     * @return array
895
     */
896
    public function getValidationExemptActions()
897
    {
898
        return $this->validationExemptActions;
899
    }
900
901
    /**
902
     * Passed a FormAction, returns true if that action is exempt from Form validation
903
     *
904
     * @param FormAction $action
905
     * @return bool
906
     */
907
    public function actionIsValidationExempt($action)
908
    {
909
        if ($action->getValidationExempt()) {
910
            return true;
911
        }
912
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
913
            return true;
914
        }
915
        return false;
916
    }
917
918
    /**
919
     * Generate extra special fields - namely the security token field (if required).
920
     *
921
     * @return FieldList
922
     */
923
    public function getExtraFields()
924
    {
925
        $extraFields = new FieldList();
926
927
        $token = $this->getSecurityToken();
928
        if ($token) {
929
            $tokenField = $token->updateFieldSet($this->fields);
930
            if ($tokenField) {
931
                $tokenField->setForm($this);
932
            }
933
        }
934
        $this->securityTokenAdded = true;
935
936
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
937
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
938
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
939
            $methodField->setForm($this);
940
            $extraFields->push($methodField);
941
        }
942
943
        return $extraFields;
944
    }
945
946
    /**
947
     * Return the form's fields - used by the templates
948
     *
949
     * @return FieldList The form fields
950
     */
951
    public function Fields()
952
    {
953
        foreach ($this->getExtraFields() as $field) {
954
            if (!$this->fields->fieldByName($field->getName())) {
955
                $this->fields->push($field);
956
            }
957
        }
958
959
        return $this->fields;
960
    }
961
962
    /**
963
     * Return all <input type="hidden"> fields
964
     * in a form - including fields nested in {@link CompositeFields}.
965
     * Useful when doing custom field layouts.
966
     *
967
     * @return FieldList
968
     */
969
    public function HiddenFields()
970
    {
971
        return $this->Fields()->HiddenFields();
972
    }
973
974
    /**
975
     * Return all fields except for the hidden fields.
976
     * Useful when making your own simplified form layouts.
977
     */
978
    public function VisibleFields()
979
    {
980
        return $this->Fields()->VisibleFields();
981
    }
982
983
    /**
984
     * Setter for the form fields.
985
     *
986
     * @param FieldList $fields
987
     * @return $this
988
     */
989
    public function setFields($fields)
990
    {
991
        $this->fields = $fields;
992
        return $this;
993
    }
994
995
    /**
996
     * Return the form's action buttons - used by the templates
997
     *
998
     * @return FieldList The action list
999
     */
1000
    public function Actions()
1001
    {
1002
        return $this->actions;
1003
    }
1004
1005
    /**
1006
     * Setter for the form actions.
1007
     *
1008
     * @param FieldList $actions
1009
     * @return $this
1010
     */
1011
    public function setActions($actions)
1012
    {
1013
        $this->actions = $actions;
1014
        return $this;
1015
    }
1016
1017
    /**
1018
     * Unset all form actions
1019
     */
1020
    public function unsetAllActions()
1021
    {
1022
        $this->actions = new FieldList();
1023
        return $this;
1024
    }
1025
1026
    /**
1027
     * @param string $name
1028
     * @param string $value
1029
     * @return $this
1030
     */
1031
    public function setAttribute($name, $value)
1032
    {
1033
        $this->attributes[$name] = $value;
1034
        return $this;
1035
    }
1036
1037
    /**
1038
     * @param string $name
1039
     * @return string
1040
     */
1041
    public function getAttribute($name)
1042
    {
1043
        if (isset($this->attributes[$name])) {
1044
            return $this->attributes[$name];
1045
        }
1046
        return null;
1047
    }
1048
1049
    /**
1050
     * @return array
1051
     */
1052
    public function getAttributes()
1053
    {
1054
        $attrs = array(
1055
            'id' => $this->FormName(),
1056
            'action' => $this->FormAction(),
1057
            'method' => $this->FormMethod(),
1058
            'enctype' => $this->getEncType(),
1059
            'target' => $this->target,
1060
            'class' => $this->extraClass(),
1061
        );
1062
1063
        if ($this->validator && $this->validator->getErrors()) {
1064
            if (!isset($attrs['class'])) {
1065
                $attrs['class'] = '';
1066
            }
1067
            $attrs['class'] .= ' validationerror';
1068
        }
1069
1070
        $attrs = array_merge($attrs, $this->attributes);
1071
1072
        return $attrs;
1073
    }
1074
1075
    /**
1076
     * Return the attributes of the form tag - used by the templates.
1077
     *
1078
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
1079
     * If at least one argument is passed as a string, all arguments act as excludes by name.
1080
     *
1081
     * @return string HTML attributes, ready for insertion into an HTML tag
1082
     */
1083
    public function getAttributesHTML($attrs = null)
1084
    {
1085
        $exclude = (is_string($attrs)) ? func_get_args() : null;
1086
1087
        // Figure out if we can cache this form
1088
        // - forms with validation shouldn't be cached, cos their error messages won't be shown
1089
        // - forms with security tokens shouldn't be cached because security tokens expire
1090
        $needsCacheDisabled = false;
1091
        if ($this->getSecurityToken()->isEnabled()) {
1092
            $needsCacheDisabled = true;
1093
        }
1094
        if ($this->FormMethod() != 'GET') {
1095
            $needsCacheDisabled = true;
1096
        }
1097
        if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
1098
            $needsCacheDisabled = true;
1099
        }
1100
1101
        // If we need to disable cache, do it
1102
        if ($needsCacheDisabled) {
1103
            HTTP::set_cache_age(0);
1104
        }
1105
1106
        $attrs = $this->getAttributes();
1107
1108
        // Remove empty
1109
        $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...
1110
1111
        // Remove excluded
1112
        if ($exclude) {
1113
            $attrs = array_diff_key($attrs, array_flip($exclude));
1114
        }
1115
1116
        // Prepare HTML-friendly 'method' attribute (lower-case)
1117
        if (isset($attrs['method'])) {
1118
            $attrs['method'] = strtolower($attrs['method']);
1119
        }
1120
1121
        // Create markup
1122
        $parts = array();
1123
        foreach ($attrs as $name => $value) {
1124
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
1125
        }
1126
1127
        return implode(' ', $parts);
1128
    }
1129
1130
    public function FormAttributes()
1131
    {
1132
        return $this->getAttributesHTML();
1133
    }
1134
1135
    /**
1136
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
1137
     * another frame
1138
    *
1139
     * @param string|FormTemplateHelper
1140
    */
1141
    public function setTemplateHelper($helper)
1142
    {
1143
        $this->templateHelper = $helper;
1144
    }
1145
1146
    /**
1147
     * Return a {@link FormTemplateHelper} for this form. If one has not been
1148
     * set, return the default helper.
1149
     *
1150
     * @return FormTemplateHelper
1151
     */
1152
    public function getTemplateHelper()
1153
    {
1154
        if ($this->templateHelper) {
1155
            if (is_string($this->templateHelper)) {
1156
                return Injector::inst()->get($this->templateHelper);
1157
            }
1158
1159
            return $this->templateHelper;
1160
        }
1161
1162
        return FormTemplateHelper::singleton();
1163
    }
1164
1165
    /**
1166
     * Set the target of this form to any value - useful for opening the form
1167
     * contents in a new window or refreshing another frame.
1168
     *
1169
     * @param string $target The value of the target
1170
     * @return $this
1171
     */
1172
    public function setTarget($target)
1173
    {
1174
        $this->target = $target;
1175
1176
        return $this;
1177
    }
1178
1179
    /**
1180
     * Set the legend value to be inserted into
1181
     * the <legend> element in the Form.ss template.
1182
     * @param string $legend
1183
     * @return $this
1184
     */
1185
    public function setLegend($legend)
1186
    {
1187
        $this->legend = $legend;
1188
        return $this;
1189
    }
1190
1191
    /**
1192
     * Set the SS template that this form should use
1193
     * to render with. The default is "Form".
1194
     *
1195
     * @param string $template The name of the template (without the .ss extension)
1196
     * @return $this
1197
     */
1198
    public function setTemplate($template)
1199
    {
1200
        $this->template = $template;
1201
        return $this;
1202
    }
1203
1204
    /**
1205
     * Return the template to render this form with.
1206
     *
1207
     * @return string
1208
     */
1209
    public function getTemplate()
1210
    {
1211
        return $this->template;
1212
    }
1213
1214
    /**
1215
     * Returs the ordered list of preferred templates for rendering this form
1216
     * If the template isn't set, then default to the
1217
     * form class name e.g "Form".
1218
     *
1219
     * @return array
1220
     */
1221
    public function getTemplates()
1222
    {
1223
        $templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
1224
        // Prefer any custom template
1225
        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...
1226
            array_unshift($templates, $this->getTemplate());
1227
        }
1228
        return $templates;
1229
    }
1230
1231
    /**
1232
     * Returns the encoding type for the form.
1233
     *
1234
     * By default this will be URL encoded, unless there is a file field present
1235
     * in which case multipart is used. You can also set the enc type using
1236
     * {@link setEncType}.
1237
     */
1238
    public function getEncType()
1239
    {
1240
        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...
1241
            return $this->encType;
1242
        }
1243
1244
        if ($fields = $this->fields->dataFields()) {
1245
            foreach ($fields as $field) {
1246
                if ($field instanceof FileField) {
1247
                    return self::ENC_TYPE_MULTIPART;
1248
                }
1249
            }
1250
        }
1251
1252
        return self::ENC_TYPE_URLENCODED;
1253
    }
1254
1255
    /**
1256
     * Sets the form encoding type. The most common encoding types are defined
1257
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1258
     *
1259
     * @param string $encType
1260
     * @return $this
1261
     */
1262
    public function setEncType($encType)
1263
    {
1264
        $this->encType = $encType;
1265
        return $this;
1266
    }
1267
1268
    /**
1269
     * Returns the real HTTP method for the form:
1270
     * GET, POST, PUT, DELETE or HEAD.
1271
     * As most browsers only support GET and POST in
1272
     * form submissions, all other HTTP methods are
1273
     * added as a hidden field "_method" that
1274
     * gets evaluated in {@link Director::direct()}.
1275
     * See {@link FormMethod()} to get a HTTP method
1276
     * for safe insertion into a <form> tag.
1277
     *
1278
     * @return string HTTP method
1279
     */
1280
    public function FormHttpMethod()
1281
    {
1282
        return $this->formMethod;
1283
    }
1284
1285
    /**
1286
     * Returns the form method to be used in the <form> tag.
1287
     * See {@link FormHttpMethod()} to get the "real" method.
1288
     *
1289
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1290
     */
1291
    public function FormMethod()
1292
    {
1293
        if (in_array($this->formMethod, array('GET','POST'))) {
1294
            return $this->formMethod;
1295
        } else {
1296
            return 'POST';
1297
        }
1298
    }
1299
1300
    /**
1301
     * Set the form method: GET, POST, PUT, DELETE.
1302
     *
1303
     * @param string $method
1304
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1305
     * @return $this
1306
     */
1307
    public function setFormMethod($method, $strict = null)
1308
    {
1309
        $this->formMethod = strtoupper($method);
1310
        if ($strict !== null) {
1311
            $this->setStrictFormMethodCheck($strict);
1312
        }
1313
        return $this;
1314
    }
1315
1316
    /**
1317
     * If set to true, enforce the matching of the form method.
1318
     *
1319
     * This will mean two things:
1320
     *  - GET vars will be ignored by a POST form, and vice versa
1321
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1322
     *
1323
     * If set to false (the default), then the form method is only used to construct the default
1324
     * form.
1325
     *
1326
     * @param $bool boolean
1327
     * @return $this
1328
     */
1329
    public function setStrictFormMethodCheck($bool)
1330
    {
1331
        $this->strictFormMethodCheck = (bool)$bool;
1332
        return $this;
1333
    }
1334
1335
    /**
1336
     * @return boolean
1337
     */
1338
    public function getStrictFormMethodCheck()
1339
    {
1340
        return $this->strictFormMethodCheck;
1341
    }
1342
1343
    /**
1344
     * Return the form's action attribute.
1345
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1346
     *
1347
     * @return string
1348
     */
1349
    public function FormAction()
1350
    {
1351
        if ($this->formActionPath) {
1352
            return $this->formActionPath;
1353
        } elseif ($this->controller->hasMethod("FormObjectLink")) {
1354
            return $this->controller->FormObjectLink($this->name);
1355
        } else {
1356
            return Controller::join_links($this->controller->Link(), $this->name);
1357
        }
1358
    }
1359
1360
    /**
1361
     * Set the form action attribute to a custom URL.
1362
     *
1363
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1364
     * recommended only for situations where you have two relatively distinct
1365
     * parts of the system trying to communicate via a form post.
1366
     *
1367
     * @param string $path
1368
     * @return $this
1369
     */
1370
    public function setFormAction($path)
1371
    {
1372
        $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...
1373
1374
        return $this;
1375
    }
1376
1377
    /**
1378
     * Returns the name of the form.
1379
     *
1380
     * @return string
1381
     */
1382
    public function FormName()
1383
    {
1384
        return $this->getTemplateHelper()->generateFormID($this);
1385
    }
1386
1387
    /**
1388
     * Set the HTML ID attribute of the form.
1389
     *
1390
     * @param string $id
1391
     * @return $this
1392
     */
1393
    public function setHTMLID($id)
1394
    {
1395
        $this->htmlID = $id;
1396
1397
        return $this;
1398
    }
1399
1400
    /**
1401
     * @return string
1402
     */
1403
    public function getHTMLID()
1404
    {
1405
        return $this->htmlID;
1406
    }
1407
1408
    /**
1409
     * Get the controller.
1410
     *
1411
     * @return Controller
1412
     */
1413
    public function getController()
1414
    {
1415
        return $this->controller;
1416
    }
1417
1418
    /**
1419
     * Set the controller.
1420
     *
1421
     * @param Controller $controller
1422
     * @return Form
1423
     */
1424
    public function setController($controller)
1425
    {
1426
        $this->controller = $controller;
1427
1428
        return $this;
1429
    }
1430
1431
    /**
1432
     * Get the name of the form.
1433
     *
1434
     * @return string
1435
     */
1436
    public function getName()
1437
    {
1438
        return $this->name;
1439
    }
1440
1441
    /**
1442
     * Set the name of the form.
1443
     *
1444
     * @param string $name
1445
     * @return Form
1446
     */
1447
    public function setName($name)
1448
    {
1449
        $this->name = $name;
1450
1451
        return $this;
1452
    }
1453
1454
    /**
1455
     * Returns an object where there is a method with the same name as each data
1456
     * field on the form.
1457
     *
1458
     * That method will return the field itself.
1459
     *
1460
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1461
     */
1462
    public function FieldMap()
1463
    {
1464
        return new Form_FieldMap($this);
1465
    }
1466
1467
    /**
1468
     * Set a message to the session, for display next time this form is shown.
1469
     *
1470
     * @param string $message the text of the message
1471
     * @param string $type Should be set to good, bad, or warning.
1472
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1473
     * Bool values will be treated as plain text flag.
1474
     */
1475
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1476
    {
1477
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1475 can also be of type boolean; however, SilverStripe\Forms\FormMessage::setMessage() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
1478
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1479
        $result->addMessage($message, $type, null, $cast);
1480
        $this->setSessionValidationResult($result);
1481
    }
1482
1483
    /**
1484
     * Set an error to the session, for display next time this form is shown.
1485
     *
1486
     * @param string $message the text of the message
1487
     * @param string $type Should be set to good, bad, or warning.
1488
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1489
     * Bool values will be treated as plain text flag.
1490
     */
1491
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1492
    {
1493
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1491 can also be of type boolean; however, SilverStripe\Forms\FormMessage::setMessage() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
1494
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1495
        $result->addError($message, $type, null, $cast);
1496
        $this->setSessionValidationResult($result);
1497
    }
1498
1499
    /**
1500
     * Returns the DataObject that has given this form its data
1501
     * through {@link loadDataFrom()}.
1502
     *
1503
     * @return DataObject
1504
     */
1505
    public function getRecord()
1506
    {
1507
        return $this->record;
1508
    }
1509
1510
    /**
1511
     * Get the legend value to be inserted into the
1512
     * <legend> element in Form.ss
1513
     *
1514
     * @return string
1515
     */
1516
    public function getLegend()
1517
    {
1518
        return $this->legend;
1519
    }
1520
1521
    /**
1522
     * Processing that occurs before a form is executed.
1523
     *
1524
     * This includes form validation, if it fails, we throw a ValidationException
1525
     *
1526
     * This includes form validation, if it fails, we redirect back
1527
     * to the form with appropriate error messages.
1528
     * Always return true if the current form action is exempt from validation
1529
     *
1530
     * Triggered through {@link httpSubmission()}.
1531
     *
1532
     *
1533
     * Note that CSRF protection takes place in {@link httpSubmission()},
1534
     * if it fails the form data will never reach this method.
1535
     *
1536
     * @return ValidationResult
1537
     */
1538
    public function validationResult()
1539
    {
1540
        // Opportunity to invalidate via validator
1541
        $action = $this->buttonClicked();
1542
        if ($action && $this->actionIsValidationExempt($action)) {
1543
            return ValidationResult::create();
1544
        }
1545
1546
        // Invoke validator
1547
        if ($this->validator) {
1548
            $result = $this->validator->validate();
1549
            $this->loadMessagesFrom($result);
1550
            return $result;
1551
        }
1552
1553
        // Successful result
1554
        return ValidationResult::create();
1555
    }
1556
1557
    const MERGE_DEFAULT = 0;
1558
    const MERGE_CLEAR_MISSING = 1;
1559
    const MERGE_IGNORE_FALSEISH = 2;
1560
1561
    /**
1562
     * Load data from the given DataObject or array.
1563
     *
1564
     * It will call $object->MyField to get the value of MyField.
1565
     * If you passed an array, it will call $object[MyField].
1566
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1567
     * as determined by {@link FieldList->dataFields()}.
1568
     *
1569
     * By default, if a field isn't set (as determined by isset()),
1570
     * its value will not be saved to the field, retaining
1571
     * potential existing values.
1572
     *
1573
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1574
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1575
     *
1576
     * Escaping happens automatically on saving the data through
1577
     * {@link saveInto()}.
1578
     *
1579
     * @uses FieldList->dataFields()
1580
     * @uses FormField->setValue()
1581
     *
1582
     * @param array|DataObject $data
1583
     * @param int $mergeStrategy
1584
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1585
     *  what that property/key's value is.
1586
     *
1587
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1588
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1589
     *  "left alone", meaning they retain any previous value.
1590
     *
1591
     *  You can pass a bitmask here to change this behaviour.
1592
     *
1593
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1594
     *  {@link $data} are cleared.
1595
     *
1596
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1597
     *  a field's value.
1598
     *
1599
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1600
     *  CLEAR_MISSING
1601
     *
1602
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1603
     * form that has some fields that save to one object, and some that save to another.
1604
     * @return $this
1605
     */
1606
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1607
    {
1608
        if (!is_object($data) && !is_array($data)) {
1609
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1610
            return $this;
1611
        }
1612
1613
        // Handle the backwards compatible case of passing "true" as the second argument
1614
        if ($mergeStrategy === true) {
1615
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1616
        } elseif ($mergeStrategy === false) {
1617
            $mergeStrategy = 0;
1618
        }
1619
1620
        // if an object is passed, save it for historical reference through {@link getRecord()}
1621
        if (is_object($data)) {
1622
            $this->record = $data;
1623
        }
1624
1625
        // dont include fields without data
1626
        $dataFields = $this->Fields()->dataFields();
1627
        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...
1628
            return $this;
1629
        }
1630
1631
        /** @var FormField $field */
1632
        foreach ($dataFields as $field) {
1633
            $name = $field->getName();
1634
1635
            // Skip fields that have been excluded
1636
            if ($fieldList && !in_array($name, $fieldList)) {
1637
                continue;
1638
            }
1639
1640
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1641
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1642
                continue;
1643
            }
1644
1645
            // Does this property exist on $data?
1646
            $exists = false;
1647
            // The value from $data for this field
1648
            $val = null;
1649
1650
            if (is_object($data)) {
1651
                $exists = (
1652
                isset($data->$name) ||
1653
                $data->hasMethod($name) ||
1654
                ($data->hasMethod('hasField') && $data->hasField($name))
1655
                    );
1656
1657
                if ($exists) {
1658
                    $val = $data->__get($name);
1659
                }
1660
            } elseif (is_array($data)) {
1661
                if (array_key_exists($name, $data)) {
1662
                    $exists = true;
1663
                    $val = $data[$name];
1664
                } // If field is in array-notation we need to access nested data
1665
                elseif (strpos($name, '[')) {
1666
                    // First encode data using PHP's method of converting nested arrays to form data
1667
                    $flatData = urldecode(http_build_query($data));
1668
                    // Then pull the value out from that flattened string
1669
                    preg_match('/' . addcslashes($name, '[]') . '=([^&]*)/', $flatData, $matches);
1670
1671
                    if (isset($matches[1])) {
1672
                        $exists = true;
1673
                        $val = $matches[1];
1674
                    }
1675
                }
1676
            }
1677
1678
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1679
            if ($exists) {
1680
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1681
                    // pass original data as well so composite fields can act on the additional information
1682
                    $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...
1683
                }
1684
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1685
                $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...
1686
            }
1687
        }
1688
        return $this;
1689
    }
1690
1691
    /**
1692
     * Save the contents of this form into the given data object.
1693
     * It will make use of setCastedField() to do this.
1694
     *
1695
     * @param DataObjectInterface $dataObject The object to save data into
1696
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1697
     * form that has some fields that save to one object, and some that save to another.
1698
     */
1699
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1700
    {
1701
        $dataFields = $this->fields->saveableFields();
1702
        $lastField = null;
1703
        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...
1704
            foreach ($dataFields as $field) {
1705
            // Skip fields that have been excluded
1706
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1707
                    continue;
1708
                }
1709
1710
                $saveMethod = "save{$field->getName()}";
1711
                if ($field->getName() == "ClassName") {
1712
                    $lastField = $field;
1713
                } elseif ($dataObject->hasMethod($saveMethod)) {
1714
                    $dataObject->$saveMethod($field->dataValue());
1715
                } elseif ($field->getName() !== "ID") {
1716
                    $field->saveInto($dataObject);
1717
                }
1718
            }
1719
        }
1720
        if ($lastField) {
1721
            $lastField->saveInto($dataObject);
1722
        }
1723
    }
1724
1725
    /**
1726
     * Get the submitted data from this form through
1727
     * {@link FieldList->dataFields()}, which filters out
1728
     * any form-specific data like form-actions.
1729
     * Calls {@link FormField->dataValue()} on each field,
1730
     * which returns a value suitable for insertion into a DataObject
1731
     * property.
1732
     *
1733
     * @return array
1734
     */
1735
    public function getData()
1736
    {
1737
        $dataFields = $this->fields->dataFields();
1738
        $data = array();
1739
1740
        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...
1741
            foreach ($dataFields as $field) {
1742
                if ($field->getName()) {
1743
                    $data[$field->getName()] = $field->dataValue();
1744
                }
1745
            }
1746
        }
1747
1748
        return $data;
1749
    }
1750
1751
    /**
1752
     * Return a rendered version of this form.
1753
     *
1754
     * This is returned when you access a form as $FormObject rather
1755
     * than <% with FormObject %>
1756
     *
1757
     * @return DBHTMLText
1758
     */
1759
    public function forTemplate()
1760
    {
1761
        $return = $this->renderWith($this->getTemplates());
1762
1763
        // Now that we're rendered, clear message
1764
        $this->clearMessage();
1765
1766
        return $return;
1767
    }
1768
1769
    /**
1770
     * Return a rendered version of this form, suitable for ajax post-back.
1771
     *
1772
     * It triggers slightly different behaviour, such as disabling the rewriting
1773
     * of # links.
1774
     *
1775
     * @return DBHTMLText
1776
     */
1777
    public function forAjaxTemplate()
1778
    {
1779
        $view = new SSViewer($this->getTemplates());
1780
1781
        $return = $view->dontRewriteHashlinks()->process($this);
1782
1783
        // Now that we're rendered, clear message
1784
        $this->clearMessage();
1785
1786
        return $return;
1787
    }
1788
1789
    /**
1790
     * Returns an HTML rendition of this form, without the <form> tag itself.
1791
     *
1792
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1793
     * and _form_enctype.  These are the attributes of the form.  These fields
1794
     * can be used to send the form to Ajax.
1795
     *
1796
     * @deprecated 5.0
1797
     * @return string
1798
     */
1799
    public function formHtmlContent()
1800
    {
1801
        Deprecation::notice('5.0');
1802
        $this->IncludeFormTag = false;
1803
        $content = $this->forTemplate();
1804
        $this->IncludeFormTag = true;
1805
1806
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1807
            . " value=\"" . $this->FormAction() . "\" />\n";
1808
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1809
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1810
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1811
1812
        return $content;
1813
    }
1814
1815
    /**
1816
     * Render this form using the given template, and return the result as a string
1817
     * You can pass either an SSViewer or a template name
1818
     * @param string|array $template
1819
     * @return DBHTMLText
1820
     */
1821
    public function renderWithoutActionButton($template)
1822
    {
1823
        $custom = $this->customise(array(
1824
            "Actions" => "",
1825
        ));
1826
1827
        if (is_string($template)) {
1828
            $template = new SSViewer($template);
1829
        }
1830
1831
        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...
1832
    }
1833
1834
1835
    /**
1836
     * Sets the button that was clicked.  This should only be called by the Controller.
1837
     *
1838
     * @param callable $funcName The name of the action method that will be called.
1839
     * @return $this
1840
     */
1841
    public function setButtonClicked($funcName)
1842
    {
1843
        $this->buttonClickedFunc = $funcName;
1844
1845
        return $this;
1846
    }
1847
1848
    /**
1849
     * @return FormAction
1850
     */
1851
    public function buttonClicked()
1852
    {
1853
        $actions = $this->getAllActions();
1854
        foreach ($actions as $action) {
1855
            if ($this->buttonClickedFunc === $action->actionName()) {
1856
                return $action;
1857
            }
1858
        }
1859
1860
        return null;
1861
    }
1862
1863
    /**
1864
     * Get a list of all actions, including those in the main "fields" FieldList
1865
     *
1866
     * @return array
1867
     */
1868
    protected function getAllActions()
1869
    {
1870
        $fields = $this->fields->dataFields() ?: array();
1871
        $actions = $this->actions->dataFields() ?: array();
1872
1873
        $fieldsAndActions = array_merge($fields, $actions);
1874
        $actions = array_filter($fieldsAndActions, function ($fieldOrAction) {
1875
            return $fieldOrAction instanceof FormAction;
1876
        });
1877
1878
        return $actions;
1879
    }
1880
1881
    /**
1882
     * Return the default button that should be clicked when another one isn't
1883
     * available.
1884
     *
1885
     * @return FormAction
1886
     */
1887
    public function defaultAction()
1888
    {
1889
        if ($this->hasDefaultAction && $this->actions) {
1890
            return $this->actions->first();
1891
        }
1892
        return null;
1893
    }
1894
1895
    /**
1896
     * Disable the default button.
1897
     *
1898
     * Ordinarily, when a form is processed and no action_XXX button is
1899
     * available, then the first button in the actions list will be pressed.
1900
     * However, if this is "delete", for example, this isn't such a good idea.
1901
     *
1902
     * @return Form
1903
     */
1904
    public function disableDefaultAction()
1905
    {
1906
        $this->hasDefaultAction = false;
1907
1908
        return $this;
1909
    }
1910
1911
    /**
1912
     * Disable the requirement of a security token on this form instance. This
1913
     * security protects against CSRF attacks, but you should disable this if
1914
     * you don't want to tie a form to a session - eg a search form.
1915
     *
1916
     * Check for token state with {@link getSecurityToken()} and
1917
     * {@link SecurityToken->isEnabled()}.
1918
     *
1919
     * @return Form
1920
     */
1921
    public function disableSecurityToken()
1922
    {
1923
        $this->securityToken = new NullSecurityToken();
1924
1925
        return $this;
1926
    }
1927
1928
    /**
1929
     * Enable {@link SecurityToken} protection for this form instance.
1930
     *
1931
     * Check for token state with {@link getSecurityToken()} and
1932
     * {@link SecurityToken->isEnabled()}.
1933
     *
1934
     * @return Form
1935
     */
1936
    public function enableSecurityToken()
1937
    {
1938
        $this->securityToken = new SecurityToken();
1939
1940
        return $this;
1941
    }
1942
1943
    /**
1944
     * Returns the security token for this form (if any exists).
1945
     *
1946
     * Doesn't check for {@link securityTokenEnabled()}.
1947
     *
1948
     * Use {@link SecurityToken::inst()} to get a global token.
1949
     *
1950
     * @return SecurityToken|null
1951
     */
1952
    public function getSecurityToken()
1953
    {
1954
        return $this->securityToken;
1955
    }
1956
1957
    /**
1958
     * Compiles all CSS-classes.
1959
     *
1960
     * @return string
1961
     */
1962
    public function extraClass()
1963
    {
1964
        return implode(array_unique($this->extraClasses), ' ');
1965
    }
1966
1967
    /**
1968
     * Add a CSS-class to the form-container. If needed, multiple classes can
1969
     * be added by delimiting a string with spaces.
1970
     *
1971
     * @param string $class A string containing a classname or several class
1972
     *              names delimited by a single space.
1973
     * @return $this
1974
     */
1975
    public function addExtraClass($class)
1976
    {
1977
        //split at white space
1978
        $classes = preg_split('/\s+/', $class);
1979
        foreach ($classes as $class) {
1980
            //add classes one by one
1981
            $this->extraClasses[$class] = $class;
1982
        }
1983
        return $this;
1984
    }
1985
1986
    /**
1987
     * Remove a CSS-class from the form-container. Multiple class names can
1988
     * be passed through as a space delimited string
1989
     *
1990
     * @param string $class
1991
     * @return $this
1992
     */
1993
    public function removeExtraClass($class)
1994
    {
1995
        //split at white space
1996
        $classes = preg_split('/\s+/', $class);
1997
        foreach ($classes as $class) {
1998
            //unset one by one
1999
            unset($this->extraClasses[$class]);
2000
        }
2001
        return $this;
2002
    }
2003
2004
    public function debug()
2005
    {
2006
        $result = "<h3>$this->class</h3><ul>";
2007
        foreach ($this->fields as $field) {
2008
            $result .= "<li>$field" . $field->debug() . "</li>";
2009
        }
2010
        $result .= "</ul>";
2011
2012
        if ($this->validator) {
2013
            /** @skipUpgrade */
2014
            $result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
2015
        }
2016
2017
        return $result;
2018
    }
2019
2020
2021
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022
    // TESTING HELPERS
2023
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2024
2025
    /**
2026
     * Test a submission of this form.
2027
     * @param string $action
2028
     * @param array $data
2029
     * @return HTTPResponse the response object that the handling controller produces.  You can interrogate this in
2030
     * your unit test.
2031
     * @throws HTTPResponse_Exception
2032
     */
2033
    public function testSubmission($action, $data)
2034
    {
2035
        $data['action_' . $action] = true;
2036
2037
        return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
2038
    }
2039
2040
    /**
2041
     * Test an ajax submission of this form.
2042
     *
2043
     * @param string $action
2044
     * @param array $data
2045
     * @return HTTPResponse the response object that the handling controller produces.  You can interrogate this in
2046
     * your unit test.
2047
     */
2048
    public function testAjaxSubmission($action, $data)
2049
    {
2050
        $data['ajax'] = 1;
2051
        return $this->testSubmission($action, $data);
2052
    }
2053
}
2054