Completed
Push — master ( 10f429...3b71b7 )
by Ingo
11:44
created

Form::getTemplates()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
341
				$this->addExtraClass($class);
342
			}
343
		}
344
	}
345
346
	/**
347
	 * Handle a form submission.  GET and POST requests behave identically.
348
	 * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
349
	 * and only triggers the requested form action/method
350
	 * if the form is valid.
351
	 *
352
	 * @param SS_HTTPRequest $request
353
	 * @throws SS_HTTPResponse_Exception
354
	 */
355
	public function httpSubmission($request) {
356
		// Strict method check
357
		if($this->strictFormMethodCheck) {
358
359
			// Throws an error if the method is bad...
360
			if($this->formMethod != $request->httpMethod()) {
361
				$response = Controller::curr()->getResponse();
362
				$response->addHeader('Allow', $this->formMethod);
363
				$this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
364
			}
365
366
			// ...and only uses the variables corresponding to that method type
367
			$vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
368
		} else {
369
			$vars = $request->requestVars();
370
		}
371
372
		// Populate the form
373
		$this->loadDataFrom($vars, true);
0 ignored issues
show
Bug introduced by
It seems like $vars defined by $request->requestVars() on line 369 can also be of type null; however, Form::loadDataFrom() does only seem to accept array|object<SilverStripe\ORM\DataObject>, 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...
374
375
		// Protection against CSRF attacks
376
		$token = $this->getSecurityToken();
377
		if( ! $token->checkRequest($request)) {
378
			$securityID = $token->getName();
379
			if (empty($vars[$securityID])) {
380
				$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
381
					"There seems to have been a technical problem. Please click the back button, ".
382
					"refresh your browser, and try again."
383
				));
384
			} else {
385
				// Clear invalid token on refresh
386
				$data = $this->getData();
387
				unset($data[$securityID]);
388
				Session::set("FormInfo.{$this->FormName()}.data", $data);
389
				Session::set("FormInfo.{$this->FormName()}.errors", array());
390
				$this->sessionMessage(
391
					_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
392
					"warning"
393
				);
394
				return $this->controller->redirectBack();
395
			}
396
		}
397
398
		// Determine the action button clicked
399
		$funcName = null;
400
		foreach($vars as $paramName => $paramVal) {
0 ignored issues
show
Bug introduced by
The expression $vars of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
401
			if(substr($paramName,0,7) == 'action_') {
402
				// Break off querystring arguments included in the action
403
				if(strpos($paramName,'?') !== false) {
404
					list($paramName, $paramVars) = explode('?', $paramName, 2);
405
					$newRequestParams = array();
406
					parse_str($paramVars, $newRequestParams);
407
					$vars = array_merge((array)$vars, (array)$newRequestParams);
408
				}
409
410
				// Cleanup action_, _x and _y from image fields
411
				$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
412
				break;
413
			}
414
		}
415
416
		// If the action wasn't set, choose the default on the form.
417
		if(!isset($funcName) && $defaultAction = $this->defaultAction()){
418
			$funcName = $defaultAction->actionName();
419
		}
420
421
		if(isset($funcName)) {
422
			Form::set_current_action($funcName);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
423
			$this->setButtonClicked($funcName);
424
		}
425
426
		// Permission checks (first on controller, then falling back to form)
427
		if(
428
			// Ensure that the action is actually a button or method on the form,
429
			// and not just a method on the controller.
430
			$this->controller->hasMethod($funcName)
431
			&& !$this->controller->checkAccessAction($funcName)
432
			// If a button exists, allow it on the controller
433
			// buttonClicked() validates that the action set above is valid
434
			&& !$this->buttonClicked()
435
		) {
436
			return $this->httpError(
437
				403,
438
				sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
439
			);
440
		} elseif(
441
			$this->hasMethod($funcName)
442
			&& !$this->checkAccessAction($funcName)
443
			// No checks for button existence or $allowed_actions is performed -
444
			// all form methods are callable (e.g. the legacy "callfieldmethod()")
445
		) {
446
			return $this->httpError(
447
				403,
448
				sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
449
			);
450
		}
451
		// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
452
		// explicitly in allowed_actions in order to run)
453
		// Uncomment the following for checking security against running actions on form fields
454
		/* 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...
455
			// Try to find a field that has the action, and allows it
456
			$fieldsHaveMethod = false;
457
			foreach ($this->Fields() as $field){
458
				if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
459
					$fieldsHaveMethod = true;
460
				}
461
			}
462
			if (!$fieldsHaveMethod) {
463
				return $this->httpError(
464
					403,
465
					sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
466
				);
467
			}
468
		}*/
469
470
		// Validate the form
471
		if(!$this->validate()) {
472
			return $this->getValidationErrorResponse();
473
		}
474
475
		// First, try a handler method on the controller (has been checked for allowed_actions above already)
476
		if($this->controller->hasMethod($funcName)) {
477
			return $this->controller->$funcName($vars, $this, $request);
478
		// Otherwise, try a handler method on the form object.
479
		} elseif($this->hasMethod($funcName)) {
480
			return $this->$funcName($vars, $this, $request);
481
		} elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
0 ignored issues
show
Bug introduced by
It seems like $this->Fields() can be null; however, checkFieldsForAction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
482
			return $field->$funcName($vars, $this, $request);
483
		}
484
485
		return $this->httpError(404);
486
	}
487
488
	/**
489
	 * @param string $action
490
	 * @return bool
491
	 */
492
	public function checkAccessAction($action) {
493
		if (parent::checkAccessAction($action)) {
494
			return true;
495
		}
496
497
		$actions = $this->getAllActions();
498
 		foreach ($actions as $formAction) {
499
			if ($formAction->actionName() === $action) {
500
				return true;
501
			}
502
		}
503
504
		// Always allow actions on fields
505
		$field = $this->checkFieldsForAction($this->Fields(), $action);
0 ignored issues
show
Bug introduced by
It seems like $this->Fields() can be null; however, checkFieldsForAction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
506
		if ($field && $field->checkAccessAction($action)) {
507
			return true;
508
		}
509
510
		return false;
511
	}
512
513
	/**
514
	 * @return callable
515
	 */
516
	public function getValidationResponseCallback() {
517
		return $this->validationResponseCallback;
518
	}
519
520
	/**
521
	 * Overrules validation error behaviour in {@link httpSubmission()}
522
	 * when validation has failed. Useful for optional handling of a certain accepted content type.
523
	 *
524
	 * The callback can opt out of handling specific responses by returning NULL,
525
	 * in which case the default form behaviour will kick in.
526
	 *
527
	 * @param $callback
528
	 * @return self
529
	 */
530
	public function setValidationResponseCallback($callback) {
531
		$this->validationResponseCallback = $callback;
532
533
		return $this;
534
	}
535
536
	/**
537
	 * Returns the appropriate response up the controller chain
538
	 * if {@link validate()} fails (which is checked prior to executing any form actions).
539
	 * By default, returns different views for ajax/non-ajax request, and
540
	 * handles 'application/json' requests with a JSON object containing the error messages.
541
	 * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
542
	 * and can be overruled by setting {@link $validationResponseCallback}.
543
	 *
544
	 * @return SS_HTTPResponse|string
545
	 */
546
	protected function getValidationErrorResponse() {
547
		$callback = $this->getValidationResponseCallback();
548
		if($callback && $callbackResponse = $callback()) {
549
			return $callbackResponse;
550
		}
551
552
		$request = $this->getRequest();
553
		if($request->isAjax()) {
554
				// Special case for legacy Validator.js implementation
555
				// (assumes eval'ed javascript collected through FormResponse)
556
				$acceptType = $request->getHeader('Accept');
557
				if(strpos($acceptType, 'application/json') !== FALSE) {
558
					// Send validation errors back as JSON with a flag at the start
559
					$response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
560
					$response->addHeader('Content-Type', 'application/json');
561
				} else {
562
					$this->setupFormErrors();
563
					// Send the newly rendered form tag as HTML
564
					$response = new SS_HTTPResponse($this->forTemplate());
565
					$response->addHeader('Content-Type', 'text/html');
566
				}
567
568
				return $response;
569
			} else {
570
				if($this->getRedirectToFormOnValidationError()) {
571
					if($pageURL = $request->getHeader('Referer')) {
572
						if(Director::is_site_url($pageURL)) {
573
							// Remove existing pragmas
574
							$pageURL = preg_replace('/(#.*)/', '', $pageURL);
575
							$pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \Director::absoluteURL($pageURL, true) on line 575 can also be of type array<integer,string>; however, 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...
576
							return $this->controller->redirect($pageURL . '#' . $this->FormName());
577
						}
578
					}
579
				}
580
				return $this->controller->redirectBack();
581
			}
582
	}
583
584
	/**
585
	 * Fields can have action to, let's check if anyone of the responds to $funcname them
586
	 *
587
	 * @param SS_List|array $fields
588
	 * @param callable $funcName
589
	 * @return FormField
590
	 */
591
	protected function checkFieldsForAction($fields, $funcName) {
592
		foreach($fields as $field){
593
			if(method_exists($field, 'FieldList')) {
594
				if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
595
					return $field;
596
				}
597
			} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
598
				return $field;
599
			}
600
		}
601
	}
602
603
	/**
604
	 * Handle a field request.
605
	 * Uses {@link Form->dataFieldByName()} to find a matching field,
606
	 * and falls back to {@link FieldList->fieldByName()} to look
607
	 * for tabs instead. This means that if you have a tab and a
608
	 * formfield with the same name, this method gives priority
609
	 * to the formfield.
610
	 *
611
	 * @param SS_HTTPRequest $request
612
	 * @return FormField
613
	 */
614
	public function handleField($request) {
615
		$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
616
617
		if($field) {
618
			return $field;
619
		} else {
620
			// falling back to fieldByName, e.g. for getting tabs
621
			return $this->Fields()->fieldByName($request->param('FieldName'));
622
		}
623
	}
624
625
	/**
626
	 * Convert this form into a readonly form
627
	 */
628
	public function makeReadonly() {
629
		$this->transform(new ReadonlyTransformation());
630
	}
631
632
	/**
633
	 * Set whether the user should be redirected back down to the
634
	 * form on the page upon validation errors in the form or if
635
	 * they just need to redirect back to the page
636
	 *
637
	 * @param bool $bool Redirect to form on error?
638
	 * @return $this
639
	 */
640
	public function setRedirectToFormOnValidationError($bool) {
641
		$this->redirectToFormOnValidationError = $bool;
642
		return $this;
643
	}
644
645
	/**
646
	 * Get whether the user should be redirected back down to the
647
	 * form on the page upon validation errors
648
	 *
649
	 * @return bool
650
	 */
651
	public function getRedirectToFormOnValidationError() {
652
		return $this->redirectToFormOnValidationError;
653
	}
654
655
	/**
656
	 * Add a plain text error message to a field on this form.  It will be saved into the session
657
	 * and used the next time this form is displayed.
658
	 * @param string $fieldName
659
	 * @param string $message
660
	 * @param string $messageType
661
	 * @param bool $escapeHtml
662
	 */
663
	public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
664
		Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
665
			'fieldName' => $fieldName,
666
			'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
667
			'messageType' => $messageType,
668
		));
669
	}
670
671
	/**
672
	 * @param FormTransformation $trans
673
	 */
674
	public function transform(FormTransformation $trans) {
675
		$newFields = new FieldList();
676
		foreach($this->fields as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->fields of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
677
			$newFields->push($field->transform($trans));
678
		}
679
		$this->fields = $newFields;
680
681
		$newActions = new FieldList();
682
		foreach($this->actions as $action) {
0 ignored issues
show
Bug introduced by
The expression $this->actions of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
683
			$newActions->push($action->transform($trans));
684
		}
685
		$this->actions = $newActions;
686
687
688
		// We have to remove validation, if the fields are not editable ;-)
689
		if($this->validator)
690
			$this->validator->removeValidation();
691
	}
692
693
	/**
694
	 * Get the {@link Validator} attached to this form.
695
	 * @return Validator
696
	 */
697
	public function getValidator() {
698
		return $this->validator;
699
	}
700
701
	/**
702
	 * Set the {@link Validator} on this form.
703
	 * @param Validator $validator
704
	 * @return $this
705
	 */
706
	public function setValidator(Validator $validator ) {
707
		if($validator) {
708
			$this->validator = $validator;
709
			$this->validator->setForm($this);
710
		}
711
		return $this;
712
	}
713
714
	/**
715
	 * Remove the {@link Validator} from this from.
716
	 */
717
	public function unsetValidator(){
718
		$this->validator = null;
719
		return $this;
720
	}
721
722
	/**
723
	 * Set actions that are exempt from validation
724
	 *
725
	 * @param array
726
	 * @return $this
727
	 */
728
	public function setValidationExemptActions($actions) {
729
		$this->validationExemptActions = $actions;
730
		return $this;
731
	}
732
733
	/**
734
	 * Get a list of actions that are exempt from validation
735
	 *
736
	 * @return array
737
	 */
738
	public function getValidationExemptActions() {
739
		return $this->validationExemptActions;
740
	}
741
742
	/**
743
	 * Passed a FormAction, returns true if that action is exempt from Form validation
744
	 *
745
	 * @param FormAction $action
746
	 * @return bool
747
	 */
748
	public function actionIsValidationExempt($action) {
749
		if ($action->getValidationExempt()) {
750
			return true;
751
		}
752
		if (in_array($action->actionName(), $this->getValidationExemptActions())) {
753
			return true;
754
		}
755
		return false;
756
	}
757
758
	/**
759
	 * Convert this form to another format.
760
	 * @param FormTransformation $format
761
	 */
762
	public function transformTo(FormTransformation $format) {
763
		$newFields = new FieldList();
764
		foreach($this->fields as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->fields of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
765
			$newFields->push($field->transformTo($format));
766
		}
767
		$this->fields = $newFields;
768
769
		// We have to remove validation, if the fields are not editable ;-)
770
		if($this->validator)
771
			$this->validator->removeValidation();
772
	}
773
774
775
	/**
776
	 * Generate extra special fields - namely the security token field (if required).
777
	 *
778
	 * @return FieldList
779
	 */
780
	public function getExtraFields() {
781
		$extraFields = new FieldList();
782
783
		$token = $this->getSecurityToken();
784
		if ($token) {
785
			$tokenField = $token->updateFieldSet($this->fields);
0 ignored issues
show
Bug introduced by
It seems like $this->fields can be null; however, updateFieldSet() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
786
			if($tokenField) $tokenField->setForm($this);
787
		}
788
		$this->securityTokenAdded = true;
789
790
		// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
791
		if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
792
			$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
793
			$methodField->setForm($this);
794
			$extraFields->push($methodField);
795
		}
796
797
		return $extraFields;
798
	}
799
800
	/**
801
	 * Return the form's fields - used by the templates
802
	 *
803
	 * @return FieldList The form fields
804
	 */
805
	public function Fields() {
806
		foreach($this->getExtraFields() as $field) {
807
			if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field);
808
		}
809
810
		return $this->fields;
811
	}
812
813
	/**
814
	 * Return all <input type="hidden"> fields
815
	 * in a form - including fields nested in {@link CompositeFields}.
816
	 * Useful when doing custom field layouts.
817
	 *
818
	 * @return FieldList
819
	 */
820
	public function HiddenFields() {
821
		return $this->Fields()->HiddenFields();
822
	}
823
824
	/**
825
	 * Return all fields except for the hidden fields.
826
	 * Useful when making your own simplified form layouts.
827
	 */
828
	public function VisibleFields() {
829
		return $this->Fields()->VisibleFields();
830
	}
831
832
	/**
833
	 * Setter for the form fields.
834
	 *
835
	 * @param FieldList $fields
836
	 * @return $this
837
	 */
838
	public function setFields($fields) {
839
		$this->fields = $fields;
840
		return $this;
841
	}
842
843
	/**
844
	 * Return the form's action buttons - used by the templates
845
	 *
846
	 * @return FieldList The action list
847
	 */
848
	public function Actions() {
849
		return $this->actions;
850
	}
851
852
	/**
853
	 * Setter for the form actions.
854
	 *
855
	 * @param FieldList $actions
856
	 * @return $this
857
	 */
858
	public function setActions($actions) {
859
		$this->actions = $actions;
860
		return $this;
861
	}
862
863
	/**
864
	 * Unset all form actions
865
	 */
866
	public function unsetAllActions(){
867
		$this->actions = new FieldList();
868
		return $this;
869
	}
870
871
	/**
872
	 * @param string $name
873
	 * @param string $value
874
	 * @return $this
875
	 */
876
	public function setAttribute($name, $value) {
877
		$this->attributes[$name] = $value;
878
		return $this;
879
	}
880
881
	/**
882
	 * @param string $name
883
	 * @return string
884
	 */
885
	public function getAttribute($name) {
886
		if(isset($this->attributes[$name])) return $this->attributes[$name];
887
	}
888
889
	/**
890
	 * @return array
891
	 */
892
	public function getAttributes() {
893
		$attrs = array(
894
			'id' => $this->FormName(),
895
			'action' => $this->FormAction(),
896
			'method' => $this->FormMethod(),
897
			'enctype' => $this->getEncType(),
898
			'target' => $this->target,
899
			'class' => $this->extraClass(),
900
		);
901
902
		if($this->validator && $this->validator->getErrors()) {
903
			if(!isset($attrs['class'])) $attrs['class'] = '';
904
			$attrs['class'] .= ' validationerror';
905
		}
906
907
		$attrs = array_merge($attrs, $this->attributes);
908
909
		return $attrs;
910
	}
911
912
	/**
913
	 * Return the attributes of the form tag - used by the templates.
914
	 *
915
	 * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
916
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
917
	 *
918
	 * @return string HTML attributes, ready for insertion into an HTML tag
919
	 */
920
	public function getAttributesHTML($attrs = null) {
921
		$exclude = (is_string($attrs)) ? func_get_args() : null;
922
923
		// Figure out if we can cache this form
924
		// - forms with validation shouldn't be cached, cos their error messages won't be shown
925
		// - forms with security tokens shouldn't be cached because security tokens expire
926
		$needsCacheDisabled = false;
927
		if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
928
		if ($this->FormMethod() != 'GET') $needsCacheDisabled = true;
929
		if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
930
			$needsCacheDisabled = true;
931
		}
932
933
		// If we need to disable cache, do it
934
		if ($needsCacheDisabled) HTTP::set_cache_age(0);
935
936
		$attrs = $this->getAttributes();
937
938
		// Remove empty
939
		$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...
940
941
		// Remove excluded
942
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
943
944
		// Prepare HTML-friendly 'method' attribute (lower-case)
945
		if (isset($attrs['method'])) {
946
			$attrs['method'] = strtolower($attrs['method']);
947
		}
948
949
		// Create markup
950
		$parts = array();
951
		foreach($attrs as $name => $value) {
952
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
953
		}
954
955
		return implode(' ', $parts);
956
	}
957
958
	public function FormAttributes() {
959
		return $this->getAttributesHTML();
960
	}
961
962
	/**
963
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
964
	 * another frame
965
	 *
966
	 * @param string|FormTemplateHelper
967
	 */
968
	public function setTemplateHelper($helper) {
969
		$this->templateHelper = $helper;
970
	}
971
972
	/**
973
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
974
	 * set, return the default helper.
975
	 *
976
	 * @return FormTemplateHelper
977
	 */
978
	public function getTemplateHelper() {
979
		if($this->templateHelper) {
980
			if(is_string($this->templateHelper)) {
981
				return Injector::inst()->get($this->templateHelper);
982
			}
983
984
			return $this->templateHelper;
985
		}
986
987
		return Injector::inst()->get('FormTemplateHelper');
988
	}
989
990
	/**
991
	 * Set the target of this form to any value - useful for opening the form
992
	 * contents in a new window or refreshing another frame.
993
	 *
994
	 * @param string $target The value of the target
995
	 * @return $this
996
	 */
997
	public function setTarget($target) {
998
		$this->target = $target;
999
1000
		return $this;
1001
	}
1002
1003
	/**
1004
	 * Set the legend value to be inserted into
1005
	 * the <legend> element in the Form.ss template.
1006
	 * @param string $legend
1007
	 * @return $this
1008
	 */
1009
	public function setLegend($legend) {
1010
		$this->legend = $legend;
1011
		return $this;
1012
	}
1013
1014
	/**
1015
	 * Set the SS template that this form should use
1016
	 * to render with. The default is "Form".
1017
	 *
1018
	 * @param string $template The name of the template (without the .ss extension)
1019
	 * @return $this
1020
	 */
1021
	public function setTemplate($template) {
1022
		$this->template = $template;
1023
		return $this;
1024
	}
1025
1026
	/**
1027
	 * Return the template to render this form with.
1028
	 *
1029
	 * @return string
1030
	 */
1031
	public function getTemplate() {
1032
		return $this->template;
1033
	}
1034
1035
	/**
1036
	 * Returs the ordered list of preferred templates for rendering this form
1037
	 * If the template isn't set, then default to the
1038
	 * form class name e.g "Form".
1039
	 *
1040
	 * @return array
1041
	 */
1042
	public function getTemplates() {
1043
		$templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
1044
		// Prefer any custom template
1045
		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...
1046
			array_unshift($templates, $this->getTemplate());
1047
		}
1048
		return $templates;
1049
	}
1050
1051
	/**
1052
	 * Returns the encoding type for the form.
1053
	 *
1054
	 * By default this will be URL encoded, unless there is a file field present
1055
	 * in which case multipart is used. You can also set the enc type using
1056
	 * {@link setEncType}.
1057
	 */
1058
	public function getEncType() {
1059
		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...
1060
			return $this->encType;
1061
		}
1062
1063
		if ($fields = $this->fields->dataFields()) {
1064
			foreach ($fields as $field) {
1065
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
1066
			}
1067
		}
1068
1069
		return self::ENC_TYPE_URLENCODED;
1070
	}
1071
1072
	/**
1073
	 * Sets the form encoding type. The most common encoding types are defined
1074
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1075
	 *
1076
	 * @param string $encType
1077
	 * @return $this
1078
	 */
1079
	public function setEncType($encType) {
1080
		$this->encType = $encType;
1081
		return $this;
1082
	}
1083
1084
	/**
1085
	 * Returns the real HTTP method for the form:
1086
	 * GET, POST, PUT, DELETE or HEAD.
1087
	 * As most browsers only support GET and POST in
1088
	 * form submissions, all other HTTP methods are
1089
	 * added as a hidden field "_method" that
1090
	 * gets evaluated in {@link Director::direct()}.
1091
	 * See {@link FormMethod()} to get a HTTP method
1092
	 * for safe insertion into a <form> tag.
1093
	 *
1094
	 * @return string HTTP method
1095
	 */
1096
	public function FormHttpMethod() {
1097
		return $this->formMethod;
1098
	}
1099
1100
	/**
1101
	 * Returns the form method to be used in the <form> tag.
1102
	 * See {@link FormHttpMethod()} to get the "real" method.
1103
	 *
1104
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1105
	 */
1106
	public function FormMethod() {
1107
		if(in_array($this->formMethod,array('GET','POST'))) {
1108
			return $this->formMethod;
1109
		} else {
1110
			return 'POST';
1111
		}
1112
	}
1113
1114
	/**
1115
	 * Set the form method: GET, POST, PUT, DELETE.
1116
	 *
1117
	 * @param string $method
1118
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1119
	 * @return $this
1120
	 */
1121
	public function setFormMethod($method, $strict = null) {
1122
		$this->formMethod = strtoupper($method);
1123
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1124
		return $this;
1125
	}
1126
1127
	/**
1128
	 * If set to true, enforce the matching of the form method.
1129
	 *
1130
	 * This will mean two things:
1131
	 *  - GET vars will be ignored by a POST form, and vice versa
1132
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1133
	 *
1134
	 * If set to false (the default), then the form method is only used to construct the default
1135
	 * form.
1136
	 *
1137
	 * @param $bool boolean
1138
	 * @return $this
1139
	 */
1140
	public function setStrictFormMethodCheck($bool) {
1141
		$this->strictFormMethodCheck = (bool)$bool;
1142
		return $this;
1143
	}
1144
1145
	/**
1146
	 * @return boolean
1147
	 */
1148
	public function getStrictFormMethodCheck() {
1149
		return $this->strictFormMethodCheck;
1150
	}
1151
1152
	/**
1153
	 * Return the form's action attribute.
1154
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1155
	 *
1156
	 * @return string
1157
	 */
1158
	public function FormAction() {
1159
		if ($this->formActionPath) {
1160
			return $this->formActionPath;
1161
		} elseif($this->controller->hasMethod("FormObjectLink")) {
1162
			return $this->controller->FormObjectLink($this->name);
1163
		} else {
1164
			return Controller::join_links($this->controller->Link(), $this->name);
1165
		}
1166
	}
1167
1168
	/**
1169
	 * Set the form action attribute to a custom URL.
1170
	 *
1171
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1172
	 * recommended only for situations where you have two relatively distinct
1173
	 * parts of the system trying to communicate via a form post.
1174
	 *
1175
	 * @param string $path
1176
	 * @return $this
1177
	 */
1178
	public function setFormAction($path) {
1179
		$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...
1180
1181
		return $this;
1182
	}
1183
1184
	/**
1185
	 * Returns the name of the form.
1186
	 *
1187
	 * @return string
1188
	 */
1189
	public function FormName() {
1190
		return $this->getTemplateHelper()->generateFormID($this);
1191
	}
1192
1193
	/**
1194
	 * Set the HTML ID attribute of the form.
1195
	 *
1196
	 * @param string $id
1197
	 * @return $this
1198
	 */
1199
	public function setHTMLID($id) {
1200
		$this->htmlID = $id;
1201
1202
		return $this;
1203
	}
1204
1205
	/**
1206
	 * @return string
1207
	 */
1208
	public function getHTMLID() {
1209
		return $this->htmlID;
1210
	}
1211
1212
	/**
1213
	 * Returns this form's controller.
1214
	 *
1215
	 * @return Controller
1216
	 * @deprecated 4.0
1217
	 */
1218
	public function Controller() {
1219
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1220
1221
		return $this->getController();
1222
	}
1223
1224
	/**
1225
	 * Get the controller.
1226
	 *
1227
	 * @return Controller
1228
	 */
1229
	public function getController() {
1230
		return $this->controller;
1231
	}
1232
1233
	/**
1234
	 * Set the controller.
1235
	 *
1236
	 * @param Controller $controller
1237
	 * @return Form
1238
	 */
1239
	public function setController($controller) {
1240
		$this->controller = $controller;
1241
1242
		return $this;
1243
	}
1244
1245
	/**
1246
	 * Get the name of the form.
1247
	 *
1248
	 * @return string
1249
	 */
1250
	public function getName() {
1251
		return $this->name;
1252
	}
1253
1254
	/**
1255
	 * Set the name of the form.
1256
	 *
1257
	 * @param string $name
1258
	 * @return Form
1259
	 */
1260
	public function setName($name) {
1261
		$this->name = $name;
1262
1263
		return $this;
1264
	}
1265
1266
	/**
1267
	 * Returns an object where there is a method with the same name as each data
1268
	 * field on the form.
1269
	 *
1270
	 * That method will return the field itself.
1271
	 *
1272
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1273
	 */
1274
	public function FieldMap() {
1275
		return new Form_FieldMap($this);
1276
	}
1277
1278
	/**
1279
	 * The next functions store and modify the forms
1280
	 * message attributes. messages are stored in session under
1281
	 * $_SESSION[formname][message];
1282
	 *
1283
	 * @return string
1284
	 */
1285
	public function Message() {
1286
		$this->getMessageFromSession();
1287
1288
		return $this->message;
1289
	}
1290
1291
	/**
1292
	 * @return string
1293
	 */
1294
	public function MessageType() {
1295
		$this->getMessageFromSession();
1296
1297
		return $this->messageType;
1298
	}
1299
1300
	/**
1301
	 * @return string
1302
	 */
1303
	protected function getMessageFromSession() {
1304
		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...
1305
			return $this->message;
1306
		} else {
1307
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1308
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1309
1310
			return $this->message;
1311
		}
1312
	}
1313
1314
	/**
1315
	 * Set a status message for the form.
1316
	 *
1317
	 * @param string $message the text of the message
1318
	 * @param string $type Should be set to good, bad, or warning.
1319
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1320
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1321
	 *                            user supplied data in the message.
1322
	 * @return $this
1323
	 */
1324
	public function setMessage($message, $type, $escapeHtml = true) {
1325
		$this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message;
0 ignored issues
show
Documentation Bug introduced by
It seems like $escapeHtml ? \Convert::...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...
1326
		$this->messageType = $type;
1327
		return $this;
1328
	}
1329
1330
	/**
1331
	 * Set a message to the session, for display next time this form is shown.
1332
	 *
1333
	 * @param string $message the text of the message
1334
	 * @param string $type Should be set to good, bad, or warning.
1335
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1336
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1337
	 *                            user supplied data in the message.
1338
	 */
1339
	public function sessionMessage($message, $type, $escapeHtml = true) {
1340
		Session::set(
1341
			"FormInfo.{$this->FormName()}.formError.message",
1342
			$escapeHtml ? Convert::raw2xml($message) : $message
0 ignored issues
show
Bug introduced by
It seems like $escapeHtml ? \Convert::...ml($message) : $message can also be of type array; however, 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...
1343
		);
1344
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1345
	}
1346
1347
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1348
		Session::set(
1349
			"FormInfo.{$formName}.formError.message",
1350
			$escapeHtml ? Convert::raw2xml($message) : $message
1351
		);
1352
		Session::set("FormInfo.{$formName}.formError.type", $type);
1353
	}
1354
1355
	public function clearMessage() {
1356
		$this->message  = null;
1357
		Session::clear("FormInfo.{$this->FormName()}.errors");
1358
		Session::clear("FormInfo.{$this->FormName()}.formError");
1359
		Session::clear("FormInfo.{$this->FormName()}.data");
1360
	}
1361
1362
	public function resetValidation() {
1363
		Session::clear("FormInfo.{$this->FormName()}.errors");
1364
		Session::clear("FormInfo.{$this->FormName()}.data");
1365
	}
1366
1367
	/**
1368
	 * Returns the DataObject that has given this form its data
1369
	 * through {@link loadDataFrom()}.
1370
	 *
1371
	 * @return DataObject
1372
	 */
1373
	public function getRecord() {
1374
		return $this->record;
1375
	}
1376
1377
	/**
1378
	 * Get the legend value to be inserted into the
1379
	 * <legend> element in Form.ss
1380
	 *
1381
	 * @return string
1382
	 */
1383
	public function getLegend() {
1384
		return $this->legend;
1385
	}
1386
1387
	/**
1388
	 * Processing that occurs before a form is executed.
1389
	 *
1390
	 * This includes form validation, if it fails, we redirect back
1391
	 * to the form with appropriate error messages.
1392
	 * Always return true if the current form action is exempt from validation
1393
	 *
1394
	 * Triggered through {@link httpSubmission()}.
1395
	 *
1396
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1397
	 * if it fails the form data will never reach this method.
1398
	 *
1399
	 * @return boolean
1400
	 */
1401
	public function validate(){
1402
		$action = $this->buttonClicked();
1403
		if($action && $this->actionIsValidationExempt($action)) {
1404
			return true;
1405
		}
1406
1407
		if($this->validator){
1408
			$errors = $this->validator->validate();
1409
1410
			if($errors){
1411
				// Load errors into session and post back
1412
				$data = $this->getData();
1413
1414
				// Encode validation messages as XML before saving into session state
1415
				// 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...
1416
				$errors = array_map(function($error) {
1417
					// Encode message as XML by default
1418
					if($error['message'] instanceof DBField) {
1419
						$error['message'] = $error['message']->forTemplate();;
1420
					} else {
1421
						$error['message'] = Convert::raw2xml($error['message']);
1422
					}
1423
					return $error;
1424
				}, $errors);
1425
1426
				Session::set("FormInfo.{$this->FormName()}.errors", $errors);
1427
				Session::set("FormInfo.{$this->FormName()}.data", $data);
1428
1429
				return false;
1430
			}
1431
		}
1432
1433
		return true;
1434
	}
1435
1436
	const MERGE_DEFAULT = 0;
1437
	const MERGE_CLEAR_MISSING = 1;
1438
	const MERGE_IGNORE_FALSEISH = 2;
1439
1440
	/**
1441
	 * Load data from the given DataObject or array.
1442
	 *
1443
	 * It will call $object->MyField to get the value of MyField.
1444
	 * If you passed an array, it will call $object[MyField].
1445
	 * Doesn't save into dataless FormFields ({@link DatalessField}),
1446
	 * as determined by {@link FieldList->dataFields()}.
1447
	 *
1448
	 * By default, if a field isn't set (as determined by isset()),
1449
	 * its value will not be saved to the field, retaining
1450
	 * potential existing values.
1451
	 *
1452
	 * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1453
	 * Escaping happens automatically on saving the data through {@link saveInto()}.
1454
	 *
1455
	 * Escaping happens automatically on saving the data through
1456
	 * {@link saveInto()}.
1457
	 *
1458
	 * @uses FieldList->dataFields()
1459
	 * @uses FormField->setValue()
1460
	 *
1461
	 * @param array|DataObject $data
1462
	 * @param int $mergeStrategy
1463
	 *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1464
	 *  what that property/key's value is.
1465
	 *
1466
	 *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1467
	 *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1468
	 *  "left alone", meaning they retain any previous value.
1469
	 *
1470
	 *  You can pass a bitmask here to change this behaviour.
1471
	 *
1472
	 *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1473
	 *  {@link $data} are cleared.
1474
	 *
1475
	 *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1476
	 *  a field's value.
1477
	 *
1478
	 *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1479
	 *  CLEAR_MISSING
1480
	 *
1481
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1482
	 * form that has some fields that save to one object, and some that save to another.
1483
	 * @return Form
1484
	 */
1485
	public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) {
1486
		if(!is_object($data) && !is_array($data)) {
1487
			user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1488
			return $this;
1489
		}
1490
1491
		// Handle the backwards compatible case of passing "true" as the second argument
1492
		if ($mergeStrategy === true) {
1493
			$mergeStrategy = self::MERGE_CLEAR_MISSING;
1494
		}
1495
		else if ($mergeStrategy === false) {
1496
			$mergeStrategy = 0;
1497
		}
1498
1499
		// if an object is passed, save it for historical reference through {@link getRecord()}
1500
		if(is_object($data)) $this->record = $data;
1501
1502
		// dont include fields without data
1503
		$dataFields = $this->Fields()->dataFields();
1504
		if($dataFields) foreach($dataFields as $field) {
1505
			$name = $field->getName();
1506
1507
			// Skip fields that have been excluded
1508
			if($fieldList && !in_array($name, $fieldList)) continue;
1509
1510
			// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1511
			if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
1512
1513
			// Does this property exist on $data?
1514
			$exists = false;
1515
			// The value from $data for this field
1516
			$val = null;
1517
1518
			if(is_object($data)) {
1519
				$exists = (
1520
					isset($data->$name) ||
1521
					$data->hasMethod($name) ||
1522
					($data->hasMethod('hasField') && $data->hasField($name))
1523
				);
1524
1525
				if ($exists) {
1526
					$val = $data->__get($name);
1527
				}
1528
			}
1529
			else if(is_array($data)){
1530
				if(array_key_exists($name, $data)) {
1531
					$exists = true;
1532
					$val = $data[$name];
1533
				}
1534
				// If field is in array-notation we need to access nested data
1535
				else if(strpos($name,'[')) {
1536
					// First encode data using PHP's method of converting nested arrays to form data
1537
					$flatData = urldecode(http_build_query($data));
1538
					// Then pull the value out from that flattened string
1539
					preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches);
1540
1541
					if (isset($matches[1])) {
1542
						$exists = true;
1543
						$val = $matches[1];
1544
					}
1545
				}
1546
			}
1547
1548
			// save to the field if either a value is given, or loading of blank/undefined values is forced
1549
			if($exists){
1550
				if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
1551
					// pass original data as well so composite fields can act on the additional information
1552
					$field->setValue($val, $data);
1553
				}
1554
			}
1555
			else if(($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING){
1556
				$field->setValue($val, $data);
1557
			}
1558
		}
1559
1560
		return $this;
1561
	}
1562
1563
	/**
1564
	 * Save the contents of this form into the given data object.
1565
	 * It will make use of setCastedField() to do this.
1566
	 *
1567
	 * @param DataObjectInterface $dataObject The object to save data into
1568
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1569
	 * form that has some fields that save to one object, and some that save to another.
1570
	 */
1571
	public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
1572
		$dataFields = $this->fields->saveableFields();
1573
		$lastField = null;
1574
		if($dataFields) foreach($dataFields as $field) {
1575
			// Skip fields that have been excluded
1576
			if($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) continue;
1577
1578
1579
			$saveMethod = "save{$field->getName()}";
1580
1581
			if($field->getName() == "ClassName"){
1582
				$lastField = $field;
1583
			}else if( $dataObject->hasMethod( $saveMethod ) ){
1584
				$dataObject->$saveMethod( $field->dataValue());
1585
			} else if($field->getName() != "ID"){
1586
				$field->saveInto($dataObject);
1587
			}
1588
		}
1589
		if($lastField) $lastField->saveInto($dataObject);
1590
	}
1591
1592
	/**
1593
	 * Get the submitted data from this form through
1594
	 * {@link FieldList->dataFields()}, which filters out
1595
	 * any form-specific data like form-actions.
1596
	 * Calls {@link FormField->dataValue()} on each field,
1597
	 * which returns a value suitable for insertion into a DataObject
1598
	 * property.
1599
	 *
1600
	 * @return array
1601
	 */
1602
	public function getData() {
1603
		$dataFields = $this->fields->dataFields();
1604
		$data = array();
1605
1606
		if($dataFields){
1607
			foreach($dataFields as $field) {
1608
				if($field->getName()) {
1609
					$data[$field->getName()] = $field->dataValue();
1610
				}
1611
			}
1612
		}
1613
1614
		return $data;
1615
	}
1616
1617
	/**
1618
	 * Call the given method on the given field.
1619
	 *
1620
	 * @param array $data
1621
	 * @return mixed
1622
	 */
1623
	public function callfieldmethod($data) {
1624
		$fieldName = $data['fieldName'];
1625
		$methodName = $data['methodName'];
1626
		$fields = $this->fields->dataFields();
1627
1628
		// special treatment needed for TableField-class and TreeDropdownField
1629
		if(strpos($fieldName, '[')) {
1630
			preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
1631
			preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
1632
			$tableFieldName = $fieldNameMatches[1][0];
1633
			$subFieldName = $subFieldMatches[1][1];
1634
		}
1635
1636
		if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
1637
			$field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
1638
			return $field->$methodName();
1639
		} else if(isset($fields[$fieldName])) {
1640
			return $fields[$fieldName]->$methodName();
1641
		} else {
1642
			user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
1643
		}
1644
	}
1645
1646
	/**
1647
	 * Return a rendered version of this form.
1648
	 *
1649
	 * This is returned when you access a form as $FormObject rather
1650
	 * than <% with FormObject %>
1651
	 *
1652
	 * @return DBHTMLText
1653
	 */
1654
	public function forTemplate() {
1655
		$return = $this->renderWith($this->getTemplates());
1656
1657
		// Now that we're rendered, clear message
1658
		$this->clearMessage();
1659
1660
		return $return;
1661
	}
1662
1663
	/**
1664
	 * Return a rendered version of this form, suitable for ajax post-back.
1665
	 *
1666
	 * It triggers slightly different behaviour, such as disabling the rewriting
1667
	 * of # links.
1668
	 *
1669
	 * @return DBHTMLText
1670
	 */
1671
	public function forAjaxTemplate() {
1672
		$view = new SSViewer(array(
1673
			$this->getTemplate(),
1674
			'Form'
1675
		));
1676
1677
		$return = $view->dontRewriteHashlinks()->process($this);
1678
1679
		// Now that we're rendered, clear message
1680
		$this->clearMessage();
1681
1682
		return $return;
1683
	}
1684
1685
	/**
1686
	 * Returns an HTML rendition of this form, without the <form> tag itself.
1687
	 *
1688
	 * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1689
	 * and _form_enctype.  These are the attributes of the form.  These fields
1690
	 * can be used to send the form to Ajax.
1691
	 *
1692
	 * @return DBHTMLText
1693
	 */
1694
	public function formHtmlContent() {
1695
		$this->IncludeFormTag = false;
1696
		$content = $this->forTemplate();
1697
		$this->IncludeFormTag = true;
1698
1699
		$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1700
			. " value=\"" . $this->FormAction() . "\" />\n";
1701
		$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1702
		$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1703
		$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1704
1705
		return $content;
1706
	}
1707
1708
	/**
1709
	 * Render this form using the given template, and return the result as a string
1710
	 * You can pass either an SSViewer or a template name
1711
	 * @param string|array $template
1712
	 * @return DBHTMLText
1713
	 */
1714
	public function renderWithoutActionButton($template) {
1715
		$custom = $this->customise(array(
1716
			"Actions" => "",
1717
		));
1718
1719
		if(is_string($template)) {
1720
			$template = new SSViewer($template);
1721
		}
1722
1723
		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...
1724
	}
1725
1726
1727
	/**
1728
	 * Sets the button that was clicked.  This should only be called by the Controller.
1729
	 *
1730
	 * @param callable $funcName The name of the action method that will be called.
1731
	 * @return $this
1732
	 */
1733
	public function setButtonClicked($funcName) {
1734
		$this->buttonClickedFunc = $funcName;
1735
1736
		return $this;
1737
	}
1738
1739
	/**
1740
	 * @return FormAction
1741
	 */
1742
	public function buttonClicked() {
1743
		$actions = $this->getAllActions();
1744
 		foreach ($actions as $action) {
1745
			if ($this->buttonClickedFunc === $action->actionName()) {
1746
				return $action;
1747
			}
1748
		}
1749
1750
			return null;
1751
		}
1752
1753
	/**
1754
	 * Get a list of all actions, including those in the main "fields" FieldList
1755
	 *
1756
	 * @return array
1757
	 */
1758
	protected function getAllActions() {
1759
		$fields = $this->fields->dataFields() ?: array();
1760
		$actions = $this->actions->dataFields() ?: array();
1761
1762
		$fieldsAndActions = array_merge($fields, $actions);
1763
		$actions = array_filter($fieldsAndActions, function($fieldOrAction) {
1764
			return $fieldOrAction instanceof FormAction;
1765
		});
1766
1767
		return $actions;
1768
	}
1769
1770
	/**
1771
	 * Return the default button that should be clicked when another one isn't
1772
	 * available.
1773
	 *
1774
	 * @return FormAction
1775
	 */
1776
	public function defaultAction() {
1777
		if($this->hasDefaultAction && $this->actions) {
1778
			return $this->actions->First();
1779
		}
1780
	}
1781
1782
	/**
1783
	 * Disable the default button.
1784
	 *
1785
	 * Ordinarily, when a form is processed and no action_XXX button is
1786
	 * available, then the first button in the actions list will be pressed.
1787
	 * However, if this is "delete", for example, this isn't such a good idea.
1788
	 *
1789
	 * @return Form
1790
	 */
1791
	public function disableDefaultAction() {
1792
		$this->hasDefaultAction = false;
1793
1794
		return $this;
1795
	}
1796
1797
	/**
1798
	 * Disable the requirement of a security token on this form instance. This
1799
	 * security protects against CSRF attacks, but you should disable this if
1800
	 * you don't want to tie a form to a session - eg a search form.
1801
	 *
1802
	 * Check for token state with {@link getSecurityToken()} and
1803
	 * {@link SecurityToken->isEnabled()}.
1804
	 *
1805
	 * @return Form
1806
	 */
1807
	public function disableSecurityToken() {
1808
		$this->securityToken = new NullSecurityToken();
1809
1810
		return $this;
1811
	}
1812
1813
	/**
1814
	 * Enable {@link SecurityToken} protection for this form instance.
1815
	 *
1816
	 * Check for token state with {@link getSecurityToken()} and
1817
	 * {@link SecurityToken->isEnabled()}.
1818
	 *
1819
	 * @return Form
1820
	 */
1821
	public function enableSecurityToken() {
1822
		$this->securityToken = new SecurityToken();
1823
1824
		return $this;
1825
	}
1826
1827
	/**
1828
	 * Returns the security token for this form (if any exists).
1829
	 *
1830
	 * Doesn't check for {@link securityTokenEnabled()}.
1831
	 *
1832
	 * Use {@link SecurityToken::inst()} to get a global token.
1833
	 *
1834
	 * @return SecurityToken|null
1835
	 */
1836
	public function getSecurityToken() {
1837
		return $this->securityToken;
1838
	}
1839
1840
	/**
1841
	 * Returns the name of a field, if that's the only field that the current
1842
	 * controller is interested in.
1843
	 *
1844
	 * It checks for a call to the callfieldmethod action.
1845
	 *
1846
	 * @return string
1847
	 */
1848
	public static function single_field_required() {
0 ignored issues
show
Coding Style introduced by
single_field_required uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1849
		if(self::current_action() == 'callfieldmethod') {
1850
			return $_REQUEST['fieldName'];
1851
	}
1852
	}
1853
1854
	/**
1855
	 * Return the current form action being called, if available.
1856
	 *
1857
	 * @return string
1858
	 */
1859
	public static function current_action() {
1860
		return self::$current_action;
1861
	}
1862
1863
	/**
1864
	 * Set the current form action. Should only be called by {@link Controller}.
1865
	 *
1866
	 * @param string $action
1867
	 */
1868
	public static function set_current_action($action) {
1869
		self::$current_action = $action;
1870
	}
1871
1872
	/**
1873
	 * Compiles all CSS-classes.
1874
	 *
1875
	 * @return string
1876
	 */
1877
	public function extraClass() {
1878
		return implode(array_unique($this->extraClasses), ' ');
1879
	}
1880
1881
	/**
1882
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1883
	 * be added by delimiting a string with spaces.
1884
	 *
1885
	 * @param string $class A string containing a classname or several class
1886
	 *                names delimited by a single space.
1887
	 * @return $this
1888
	 */
1889
	public function addExtraClass($class) {
1890
		//split at white space
1891
		$classes = preg_split('/\s+/', $class);
1892
		foreach($classes as $class) {
1893
			//add classes one by one
1894
			$this->extraClasses[$class] = $class;
1895
		}
1896
		return $this;
1897
	}
1898
1899
	/**
1900
	 * Remove a CSS-class from the form-container. Multiple class names can
1901
	 * be passed through as a space delimited string
1902
	 *
1903
	 * @param string $class
1904
	 * @return $this
1905
	 */
1906
	public function removeExtraClass($class) {
1907
		//split at white space
1908
		$classes = preg_split('/\s+/', $class);
1909
		foreach ($classes as $class) {
1910
			//unset one by one
1911
			unset($this->extraClasses[$class]);
1912
		}
1913
		return $this;
1914
	}
1915
1916
	public function debug() {
1917
		$result = "<h3>$this->class</h3><ul>";
1918
		foreach($this->fields as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->fields of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1919
			$result .= "<li>$field" . $field->debug() . "</li>";
1920
		}
1921
		$result .= "</ul>";
1922
1923
		if( $this->validator )
1924
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1925
1926
		return $result;
1927
	}
1928
1929
1930
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1931
	// TESTING HELPERS
1932
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1933
1934
	/**
1935
	 * Test a submission of this form.
1936
	 * @param string $action
1937
	 * @param array $data
1938
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1939
	 * your unit test.
1940
	 * @throws SS_HTTPResponse_Exception
1941
	 */
1942
	public function testSubmission($action, $data) {
1943
		$data['action_' . $action] = true;
1944
1945
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1946
	}
1947
1948
	/**
1949
	 * Test an ajax submission of this form.
1950
	 *
1951
	 * @param string $action
1952
	 * @param array $data
1953
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1954
	 * your unit test.
1955
	 */
1956
	public function testAjaxSubmission($action, $data) {
1957
		$data['ajax'] = 1;
1958
		return $this->testSubmission($action, $data);
1959
	}
1960
}
1961
1962
/**
1963
 * @package forms
1964
 * @subpackage core
1965
 */
1966
class Form_FieldMap extends ViewableData {
1967
1968
	protected $form;
1969
1970
	public function __construct($form) {
1971
		$this->form = $form;
1972
		parent::__construct();
1973
	}
1974
1975
	/**
1976
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1977
	 * @param string $method
1978
	 * @return bool
1979
	 */
1980
	public function hasMethod($method) {
1981
		return true;
1982
	}
1983
1984
	public function __call($method, $args = null) {
1985
		return $this->form->Fields()->fieldByName($method);
1986
	}
1987
}
1988