Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

Form::httpSubmission()   F

Complexity

Conditions 21
Paths 1125

Size

Total Lines 131
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 63
nc 1125
nop 1
dl 0
loc 131
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $vars defined by $request->requestVars() on line 355 can also be of type null; however, Form::loadDataFrom() does only seem to accept array|object<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...
360
361
		// Protection against CSRF attacks
362
		$token = $this->getSecurityToken();
363
		if( ! $token->checkRequest($request)) {
364
			$securityID = $token->getName();
365
			if (empty($vars[$securityID])) {
366
				$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
367
					"There seems to have been a technical problem. Please click the back button, ".
368
					"refresh your browser, and try again."
369
				));
370
			} else {
371
				// Clear invalid token on refresh
372
				$data = $this->getData();
373
				unset($data[$securityID]);
374
				Session::set("FormInfo.{$this->FormName()}.data", $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
375
				Session::set("FormInfo.{$this->FormName()}.errors", array());
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
376
				$this->sessionMessage(
377
					_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
378
					"warning"
379
				);
380
				return $this->controller->redirectBack();
381
			}
382
		}
383
384
		// Determine the action button clicked
385
		$funcName = null;
386
		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...
387
			if(substr($paramName,0,7) == 'action_') {
388
				// Break off querystring arguments included in the action
389
				if(strpos($paramName,'?') !== false) {
390
					list($paramName, $paramVars) = explode('?', $paramName, 2);
391
					$newRequestParams = array();
392
					parse_str($paramVars, $newRequestParams);
393
					$vars = array_merge((array)$vars, (array)$newRequestParams);
394
				}
395
396
				// Cleanup action_, _x and _y from image fields
397
				$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
398
				break;
399
			}
400
		}
401
402
		// If the action wasn't set, choose the default on the form.
403
		if(!isset($funcName) && $defaultAction = $this->defaultAction()){
404
			$funcName = $defaultAction->actionName();
405
		}
406
407
		if(isset($funcName)) {
408
			Form::set_current_action($funcName);
409
			$this->setButtonClicked($funcName);
410
		}
411
412
		// Permission checks (first on controller, then falling back to form)
413
		if(
414
			// Ensure that the action is actually a button or method on the form,
415
			// and not just a method on the controller.
416
			$this->controller->hasMethod($funcName)
417
			&& !$this->controller->checkAccessAction($funcName)
418
			// If a button exists, allow it on the controller
419
			&& !$this->actions->dataFieldByName('action_' . $funcName)
420
		) {
421
			return $this->httpError(
422
				403,
423
				sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
424
			);
425
		} elseif(
426
			$this->hasMethod($funcName)
427
			&& !$this->checkAccessAction($funcName)
428
			// No checks for button existence or $allowed_actions is performed -
429
			// all form methods are callable (e.g. the legacy "callfieldmethod()")
430
		) {
431
			return $this->httpError(
432
				403,
433
				sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
434
			);
435
		}
436
		// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
437
		// explicitly in allowed_actions in order to run)
438
		// Uncomment the following for checking security against running actions on form fields
439
		/* else {
440
			// Try to find a field that has the action, and allows it
441
			$fieldsHaveMethod = false;
442
			foreach ($this->Fields() as $field){
443
				if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
444
					$fieldsHaveMethod = true;
445
				}
446
			}
447
			if (!$fieldsHaveMethod) {
448
				return $this->httpError(
449
					403,
450
					sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
451
				);
452
			}
453
		}*/
454
455
		// Validate the form
456
		if(!$this->validate()) {
457
			return $this->getValidationErrorResponse();
458
		}
459
460
		// First, try a handler method on the controller (has been checked for allowed_actions above already)
461
		if($this->controller->hasMethod($funcName)) {
462
			return $this->controller->$funcName($vars, $this, $request);
463
		// Otherwise, try a handler method on the form object.
464
		} elseif($this->hasMethod($funcName)) {
465
			return $this->$funcName($vars, $this, $request);
466
		} 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...
467
			return $field->$funcName($vars, $this, $request);
468
		}
469
470
		return $this->httpError(404);
471
	}
472
473
	/**
474
	 * @param string $action
475
	 * @return bool
476
	 */
477
	public function checkAccessAction($action) {
478
		return (
479
			parent::checkAccessAction($action)
480
			// Always allow actions which map to buttons. See httpSubmission() for further access checks.
481
			|| $this->actions->dataFieldByName('action_' . $action)
482
			// Always allow actions on fields
483
			|| (
484
				$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...
485
				&& $field->checkAccessAction($action)
0 ignored issues
show
Bug introduced by
The variable $field seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
486
			)
487
		);
488
	}
489
490
	/**
491
	 * Returns the appropriate response up the controller chain
492
	 * if {@link validate()} fails (which is checked prior to executing any form actions).
493
	 * By default, returns different views for ajax/non-ajax request, and
494
	 * handles 'application/json' requests with a JSON object containing the error messages.
495
	 * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}.
496
	 *
497
	 * @return SS_HTTPResponse|string
498
	 */
499
	protected function getValidationErrorResponse() {
500
		$request = $this->getRequest();
501
		if($request->isAjax()) {
502
				// Special case for legacy Validator.js implementation
503
				// (assumes eval'ed javascript collected through FormResponse)
504
				$acceptType = $request->getHeader('Accept');
505
				if(strpos($acceptType, 'application/json') !== FALSE) {
506
					// Send validation errors back as JSON with a flag at the start
507
					$response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
508
					$response->addHeader('Content-Type', 'application/json');
509
				} else {
510
					$this->setupFormErrors();
511
					// Send the newly rendered form tag as HTML
512
					$response = new SS_HTTPResponse($this->forTemplate());
513
					$response->addHeader('Content-Type', 'text/html');
514
				}
515
516
				return $response;
517
			} else {
518
				if($this->getRedirectToFormOnValidationError()) {
519
					if($pageURL = $request->getHeader('Referer')) {
520
						if(Director::is_site_url($pageURL)) {
521
							// Remove existing pragmas
522
							$pageURL = preg_replace('/(#.*)/', '', $pageURL);
523
							$pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \Director::absoluteURL($pageURL, true) on line 523 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...
524
							return $this->controller->redirect($pageURL . '#' . $this->FormName());
525
						}
526
					}
527
				}
528
				return $this->controller->redirectBack();
529
			}
530
	}
531
532
	/**
533
	 * Fields can have action to, let's check if anyone of the responds to $funcname them
534
	 *
535
	 * @param SS_List|array $fields
536
	 * @param callable $funcName
537
	 * @return FormField
538
	 */
539
	protected function checkFieldsForAction($fields, $funcName) {
540
		foreach($fields as $field){
541
			if(method_exists($field, 'FieldList')) {
542
				if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
543
					return $field;
544
				}
545
			} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
546
				return $field;
547
			}
548
		}
549
	}
550
551
	/**
552
	 * Handle a field request.
553
	 * Uses {@link Form->dataFieldByName()} to find a matching field,
554
	 * and falls back to {@link FieldList->fieldByName()} to look
555
	 * for tabs instead. This means that if you have a tab and a
556
	 * formfield with the same name, this method gives priority
557
	 * to the formfield.
558
	 *
559
	 * @param SS_HTTPRequest $request
560
	 * @return FormField
561
	 */
562
	public function handleField($request) {
563
		$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
564
565
		if($field) {
566
			return $field;
567
		} else {
568
			// falling back to fieldByName, e.g. for getting tabs
569
			return $this->Fields()->fieldByName($request->param('FieldName'));
570
		}
571
	}
572
573
	/**
574
	 * Convert this form into a readonly form
575
	 */
576
	public function makeReadonly() {
577
		$this->transform(new ReadonlyTransformation());
578
	}
579
580
	/**
581
	 * Set whether the user should be redirected back down to the
582
	 * form on the page upon validation errors in the form or if
583
	 * they just need to redirect back to the page
584
	 *
585
	 * @param bool $bool Redirect to form on error?
586
	 * @return $this
587
	 */
588
	public function setRedirectToFormOnValidationError($bool) {
589
		$this->redirectToFormOnValidationError = $bool;
590
		return $this;
591
	}
592
593
	/**
594
	 * Get whether the user should be redirected back down to the
595
	 * form on the page upon validation errors
596
	 *
597
	 * @return bool
598
	 */
599
	public function getRedirectToFormOnValidationError() {
600
		return $this->redirectToFormOnValidationError;
601
	}
602
603
	/**
604
	 * Add a plain text error message to a field on this form.  It will be saved into the session
605
	 * and used the next time this form is displayed.
606
	 * @param string $fieldName
607
	 * @param string $message
608
	 * @param string $messageType
609
	 * @param bool $escapeHtml
610
	 */
611
	public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
612
		Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
613
			'fieldName' => $fieldName,
614
			'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
615
			'messageType' => $messageType,
616
		));
617
	}
618
619
	/**
620
	 * @param FormTransformation $trans
621
	 */
622
	public function transform(FormTransformation $trans) {
623
		$newFields = new FieldList();
624
		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...
625
			$newFields->push($field->transform($trans));
626
		}
627
		$this->fields = $newFields;
628
629
		$newActions = new FieldList();
630
		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...
631
			$newActions->push($action->transform($trans));
632
		}
633
		$this->actions = $newActions;
634
635
636
		// We have to remove validation, if the fields are not editable ;-)
637
		if($this->validator)
638
			$this->validator->removeValidation();
639
	}
640
641
	/**
642
	 * Get the {@link Validator} attached to this form.
643
	 * @return Validator
644
	 */
645
	public function getValidator() {
646
		return $this->validator;
647
	}
648
649
	/**
650
	 * Set the {@link Validator} on this form.
651
	 * @param Validator $validator
652
	 * @return $this
653
	 */
654
	public function setValidator(Validator $validator ) {
655
		if($validator) {
656
			$this->validator = $validator;
657
			$this->validator->setForm($this);
658
		}
659
		return $this;
660
	}
661
662
	/**
663
	 * Remove the {@link Validator} from this from.
664
	 */
665
	public function unsetValidator(){
666
		$this->validator = null;
667
		return $this;
668
	}
669
670
	/**
671
	 * Convert this form to another format.
672
	 * @param FormTransformation $format
673
	 */
674
	public function transformTo(FormTransformation $format) {
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->transformTo($format));
678
		}
679
		$this->fields = $newFields;
680
681
		// We have to remove validation, if the fields are not editable ;-)
682
		if($this->validator)
683
			$this->validator->removeValidation();
684
	}
685
686
687
	/**
688
	 * Generate extra special fields - namely the security token field (if required).
689
	 *
690
	 * @return FieldList
691
	 */
692
	public function getExtraFields() {
693
		$extraFields = new FieldList();
694
695
		$token = $this->getSecurityToken();
696
		if ($token) {
697
			$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...
698
			if($tokenField) $tokenField->setForm($this);
699
		}
700
		$this->securityTokenAdded = true;
701
702
		// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
703
		if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
704
			$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
705
			$methodField->setForm($this);
706
			$extraFields->push($methodField);
707
		}
708
709
		return $extraFields;
710
	}
711
712
	/**
713
	 * Return the form's fields - used by the templates
714
	 *
715
	 * @return FieldList The form fields
716
	 */
717
	public function Fields() {
718
		foreach($this->getExtraFields() as $field) {
719
			if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field);
720
		}
721
722
		return $this->fields;
723
	}
724
725
	/**
726
	 * Return all <input type="hidden"> fields
727
	 * in a form - including fields nested in {@link CompositeFields}.
728
	 * Useful when doing custom field layouts.
729
	 *
730
	 * @return FieldList
731
	 */
732
	public function HiddenFields() {
733
		return $this->Fields()->HiddenFields();
734
	}
735
736
	/**
737
	 * Return all fields except for the hidden fields.
738
	 * Useful when making your own simplified form layouts.
739
	 */
740
	public function VisibleFields() {
741
		return $this->Fields()->VisibleFields();
742
	}
743
744
	/**
745
	 * Setter for the form fields.
746
	 *
747
	 * @param FieldList $fields
748
	 * @return $this
749
	 */
750
	public function setFields($fields) {
751
		$this->fields = $fields;
752
		return $this;
753
	}
754
755
	/**
756
	 * Return the form's action buttons - used by the templates
757
	 *
758
	 * @return FieldList The action list
759
	 */
760
	public function Actions() {
761
		return $this->actions;
762
	}
763
764
	/**
765
	 * Setter for the form actions.
766
	 *
767
	 * @param FieldList $actions
768
	 * @return $this
769
	 */
770
	public function setActions($actions) {
771
		$this->actions = $actions;
772
		return $this;
773
	}
774
775
	/**
776
	 * Unset all form actions
777
	 */
778
	public function unsetAllActions(){
779
		$this->actions = new FieldList();
780
		return $this;
781
	}
782
783
	/**
784
	 * @param string $name
785
	 * @param string $value
786
	 * @return $this
787
	 */
788
	public function setAttribute($name, $value) {
789
		$this->attributes[$name] = $value;
790
		return $this;
791
	}
792
793
	/**
794
	 * @return string $name
795
	 */
796
	public function getAttribute($name) {
797
		if(isset($this->attributes[$name])) return $this->attributes[$name];
798
	}
799
800
	/**
801
	 * @return array
802
	 */
803
	public function getAttributes() {
804
		$attrs = array(
805
			'id' => $this->FormName(),
806
			'action' => $this->FormAction(),
807
			'method' => $this->FormMethod(),
808
			'enctype' => $this->getEncType(),
809
			'target' => $this->target,
810
			'class' => $this->extraClass(),
811
		);
812
813
		if($this->validator && $this->validator->getErrors()) {
814
			if(!isset($attrs['class'])) $attrs['class'] = '';
815
			$attrs['class'] .= ' validationerror';
816
		}
817
818
		$attrs = array_merge($attrs, $this->attributes);
819
820
		return $attrs;
821
	}
822
823
	/**
824
	 * Return the attributes of the form tag - used by the templates.
825
	 *
826
	 * @param array Custom attributes to process. Falls back to {@link getAttributes()}.
827
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
828
	 *
829
	 * @return string HTML attributes, ready for insertion into an HTML tag
830
	 */
831
	public function getAttributesHTML($attrs = null) {
832
		$exclude = (is_string($attrs)) ? func_get_args() : null;
833
834
		// Figure out if we can cache this form
835
		// - forms with validation shouldn't be cached, cos their error messages won't be shown
836
		// - forms with security tokens shouldn't be cached because security tokens expire
837
		$needsCacheDisabled = false;
838
		if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
839
		if ($this->FormMethod() != 'GET') $needsCacheDisabled = true;
840
		if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
841
			$needsCacheDisabled = true;
842
		}
843
844
		// If we need to disable cache, do it
845
		if ($needsCacheDisabled) HTTP::set_cache_age(0);
846
847
		$attrs = $this->getAttributes();
848
849
		// Remove empty
850
		$attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);'));
851
852
		// Remove excluded
853
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
854
855
		// Prepare HTML-friendly 'method' attribute (lower-case)
856
		if (isset($attrs['method'])) {
857
			$attrs['method'] = strtolower($attrs['method']);
858
		}
859
860
		// Create markup
861
		$parts = array();
862 View Code Duplication
		foreach($attrs as $name => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
863
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
864
		}
865
866
		return implode(' ', $parts);
867
	}
868
869
	public function FormAttributes() {
870
		return $this->getAttributesHTML();
871
	}
872
873
	/**
874
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
875
	 * another frame
876
	 *
877
	 * @param string|FormTemplateHelper
878
	 */
879
	public function setTemplateHelper($helper) {
880
		$this->templateHelper = $helper;
881
	}
882
883
	/**
884
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
885
	 * set, return the default helper.
886
	 *
887
	 * @return FormTemplateHelper
888
	 */
889
	public function getTemplateHelper() {
890
		if($this->templateHelper) {
891
			if(is_string($this->templateHelper)) {
892
				return Injector::inst()->get($this->templateHelper);
893
			}
894
895
			return $this->templateHelper;
896
		}
897
898
		return Injector::inst()->get('FormTemplateHelper');
899
	}
900
901
	/**
902
	 * Set the target of this form to any value - useful for opening the form
903
	 * contents in a new window or refreshing another frame.
904
	 *
905
	 * @param target $target The value of the target
906
	 * @return $this
907
	 */
908
	public function setTarget($target) {
909
		$this->target = $target;
0 ignored issues
show
Documentation Bug introduced by
It seems like $target of type object<target> is incompatible with the declared type string|null of property $target.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
910
911
		return $this;
912
	}
913
914
	/**
915
	 * Set the legend value to be inserted into
916
	 * the <legend> element in the Form.ss template.
917
	 * @param string $legend
918
	 * @return $this
919
	 */
920
	public function setLegend($legend) {
921
		$this->legend = $legend;
922
		return $this;
923
	}
924
925
	/**
926
	 * Set the SS template that this form should use
927
	 * to render with. The default is "Form".
928
	 *
929
	 * @param string $template The name of the template (without the .ss extension)
930
	 * @return $this
931
	 */
932
	public function setTemplate($template) {
933
		$this->template = $template;
934
		return $this;
935
	}
936
937
	/**
938
	 * Return the template to render this form with.
939
	 * If the template isn't set, then default to the
940
	 * form class name e.g "Form".
941
	 *
942
	 * @return string
943
	 */
944
	public function getTemplate() {
945
		if($this->template) return $this->template;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->template 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...
946
		else return $this->class;
947
	}
948
949
	/**
950
	 * Returns the encoding type for the form.
951
	 *
952
	 * By default this will be URL encoded, unless there is a file field present
953
	 * in which case multipart is used. You can also set the enc type using
954
	 * {@link setEncType}.
955
	 */
956
	public function getEncType() {
957
		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...
958
			return $this->encType;
959
		}
960
961
		if ($fields = $this->fields->dataFields()) {
962
			foreach ($fields as $field) {
963
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
964
			}
965
		}
966
967
		return self::ENC_TYPE_URLENCODED;
968
	}
969
970
	/**
971
	 * Sets the form encoding type. The most common encoding types are defined
972
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
973
	 *
974
	 * @param string $encType
975
	 * @return $this
976
	 */
977
	public function setEncType($encType) {
978
		$this->encType = $encType;
979
		return $this;
980
	}
981
982
	/**
983
	 * Returns the real HTTP method for the form:
984
	 * GET, POST, PUT, DELETE or HEAD.
985
	 * As most browsers only support GET and POST in
986
	 * form submissions, all other HTTP methods are
987
	 * added as a hidden field "_method" that
988
	 * gets evaluated in {@link Director::direct()}.
989
	 * See {@link FormMethod()} to get a HTTP method
990
	 * for safe insertion into a <form> tag.
991
	 *
992
	 * @return string HTTP method
993
	 */
994
	public function FormHttpMethod() {
995
		return $this->formMethod;
996
	}
997
998
	/**
999
	 * Returns the form method to be used in the <form> tag.
1000
	 * See {@link FormHttpMethod()} to get the "real" method.
1001
	 *
1002
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1003
	 */
1004
	public function FormMethod() {
1005
		if(in_array($this->formMethod,array('GET','POST'))) {
1006
			return $this->formMethod;
1007
		} else {
1008
			return 'POST';
1009
		}
1010
	}
1011
1012
	/**
1013
	 * Set the form method: GET, POST, PUT, DELETE.
1014
	 *
1015
	 * @param string $method
1016
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1017
	 * @return $this
1018
	 */
1019
	public function setFormMethod($method, $strict = null) {
1020
		$this->formMethod = strtoupper($method);
1021
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1022
		return $this;
1023
	}
1024
1025
	/**
1026
	 * If set to true, enforce the matching of the form method.
1027
	 *
1028
	 * This will mean two things:
1029
	 *  - GET vars will be ignored by a POST form, and vice versa
1030
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1031
	 *
1032
	 * If set to false (the default), then the form method is only used to construct the default
1033
	 * form.
1034
	 *
1035
	 * @param $bool boolean
1036
	 * @return $this
1037
	 */
1038
	public function setStrictFormMethodCheck($bool) {
1039
		$this->strictFormMethodCheck = (bool)$bool;
1040
		return $this;
1041
	}
1042
1043
	/**
1044
	 * @return boolean
1045
	 */
1046
	public function getStrictFormMethodCheck() {
1047
		return $this->strictFormMethodCheck;
1048
	}
1049
1050
	/**
1051
	 * Return the form's action attribute.
1052
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1053
	 *
1054
	 * @return string
1055
	 */
1056
	public function FormAction() {
1057
		if ($this->formActionPath) {
1058
			return $this->formActionPath;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->formActionPath; (boolean) is incompatible with the return type documented by Form::FormAction of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1059
		} elseif($this->controller->hasMethod("FormObjectLink")) {
1060
			return $this->controller->FormObjectLink($this->name);
1061
		} else {
1062
			return Controller::join_links($this->controller->Link(), $this->name);
1063
		}
1064
	}
1065
1066
	/**
1067
	 * Set the form action attribute to a custom URL.
1068
	 *
1069
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1070
	 * recommended only for situations where you have two relatively distinct
1071
	 * parts of the system trying to communicate via a form post.
1072
	 *
1073
	 * @param string $path
1074
	 * @return $this
1075
	 */
1076
	public function setFormAction($path) {
1077
		$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...
1078
1079
		return $this;
1080
	}
1081
1082
	/**
1083
	 * Returns the name of the form.
1084
	 *
1085
	 * @return string
1086
	 */
1087
	public function FormName() {
1088
		return $this->getTemplateHelper()->generateFormID($this);
1089
	}
1090
1091
	/**
1092
	 * Set the HTML ID attribute of the form.
1093
	 *
1094
	 * @param string $id
1095
	 * @return $this
1096
	 */
1097
	public function setHTMLID($id) {
1098
		$this->htmlID = $id;
1099
1100
		return $this;
1101
	}
1102
1103
	/**
1104
	 * @return string
1105
	 */
1106
	public function getHTMLID() {
1107
		return $this->htmlID;
1108
	}
1109
1110
	/**
1111
	 * Returns this form's controller.
1112
	 *
1113
	 * @return Controller
1114
	 * @deprecated 4.0
1115
	 */
1116
	public function Controller() {
1117
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1118
1119
		return $this->getController();
1120
	}
1121
1122
	/**
1123
	 * Get the controller.
1124
	 *
1125
	 * @return Controller
1126
	 */
1127
	public function getController() {
1128
		return $this->controller;
1129
	}
1130
1131
	/**
1132
	 * Set the controller.
1133
	 *
1134
	 * @param Controller $controller
1135
	 * @return Form
1136
	 */
1137
	public function setController($controller) {
1138
		$this->controller = $controller;
1139
1140
		return $this;
1141
	}
1142
1143
	/**
1144
	 * Get the name of the form.
1145
	 *
1146
	 * @return string
1147
	 */
1148
	public function getName() {
1149
		return $this->name;
1150
	}
1151
1152
	/**
1153
	 * Set the name of the form.
1154
	 *
1155
	 * @param string $name
1156
	 * @return Form
1157
	 */
1158
	public function setName($name) {
1159
		$this->name = $name;
1160
1161
		return $this;
1162
	}
1163
1164
	/**
1165
	 * Returns an object where there is a method with the same name as each data
1166
	 * field on the form.
1167
	 *
1168
	 * That method will return the field itself.
1169
	 *
1170
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1171
	 */
1172
	public function FieldMap() {
1173
		return new Form_FieldMap($this);
1174
	}
1175
1176
	/**
1177
	 * The next functions store and modify the forms
1178
	 * message attributes. messages are stored in session under
1179
	 * $_SESSION[formname][message];
1180
	 *
1181
	 * @return string
1182
	 */
1183
	public function Message() {
1184
		$this->getMessageFromSession();
1185
1186
		return $this->message;
1187
	}
1188
1189
	/**
1190
	 * @return string
1191
	 */
1192
	public function MessageType() {
1193
		$this->getMessageFromSession();
1194
1195
		return $this->messageType;
1196
	}
1197
1198
	/**
1199
	 * @return string
1200
	 */
1201
	protected function getMessageFromSession() {
1202
		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...
1203
			return $this->message;
1204
		} else {
1205
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1206
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1207
1208
			return $this->message;
1209
		}
1210
	}
1211
1212
	/**
1213
	 * Set a status message for the form.
1214
	 *
1215
	 * @param string $message the text of the message
1216
	 * @param string $type Should be set to good, bad, or warning.
1217
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1218
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1219
	 *                            user supplied data in the message.
1220
	 * @return $this
1221
	 */
1222
	public function setMessage($message, $type, $escapeHtml = true) {
1223
		$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...
1224
		$this->messageType = $type;
1225
		return $this;
1226
	}
1227
1228
	/**
1229
	 * Set a message to the session, for display next time this form is shown.
1230
	 *
1231
	 * @param string $message the text of the message
1232
	 * @param string $type Should be set to good, bad, or warning.
1233
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1234
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1235
	 *                            user supplied data in the message.
1236
	 */
1237
	public function sessionMessage($message, $type, $escapeHtml = true) {
1238
		Session::set(
1239
			"FormInfo.{$this->FormName()}.formError.message",
1240
			$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...
1241
		);
1242
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1243
	}
1244
1245
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1246
		Session::set(
1247
			"FormInfo.{$formName}.formError.message",
1248
			$escapeHtml ? Convert::raw2xml($message) : $message
1249
		);
1250
		Session::set("FormInfo.{$formName}.formError.type", $type);
1251
	}
1252
1253
	public function clearMessage() {
1254
		$this->message  = null;
1255
		Session::clear("FormInfo.{$this->FormName()}.errors");
1256
		Session::clear("FormInfo.{$this->FormName()}.formError");
1257
		Session::clear("FormInfo.{$this->FormName()}.data");
1258
	}
1259
1260
	public function resetValidation() {
1261
		Session::clear("FormInfo.{$this->FormName()}.errors");
1262
		Session::clear("FormInfo.{$this->FormName()}.data");
1263
	}
1264
1265
	/**
1266
	 * Returns the DataObject that has given this form its data
1267
	 * through {@link loadDataFrom()}.
1268
	 *
1269
	 * @return DataObject
1270
	 */
1271
	public function getRecord() {
1272
		return $this->record;
1273
	}
1274
1275
	/**
1276
	 * Get the legend value to be inserted into the
1277
	 * <legend> element in Form.ss
1278
	 *
1279
	 * @return string
1280
	 */
1281
	public function getLegend() {
1282
		return $this->legend;
1283
	}
1284
1285
	/**
1286
	 * Processing that occurs before a form is executed.
1287
	 *
1288
	 * This includes form validation, if it fails, we redirect back
1289
	 * to the form with appropriate error messages.
1290
	 *
1291
	 * Triggered through {@link httpSubmission()}.
1292
	 *
1293
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1294
	 * if it fails the form data will never reach this method.
1295
	 *
1296
	 * @return boolean
1297
	 */
1298
	public function validate(){
1299
		if($this->validator){
1300
			$errors = $this->validator->validate();
1301
1302
			if($errors){
1303
				// Load errors into session and post back
1304
				$data = $this->getData();
1305
1306
				// Encode validation messages as XML before saving into session state
1307
				// As per Form::addErrorMessage()
1308
				$errors = array_map(function($error) {
1309
					// Encode message as XML by default
1310
					if($error['message'] instanceof DBField) {
1311
						$error['message'] = $error['message']->forTemplate();;
1312
					} else {
1313
						$error['message'] = Convert::raw2xml($error['message']);
1314
					}
1315
					return $error;
1316
				}, $errors);
1317
1318
				Session::set("FormInfo.{$this->FormName()}.errors", $errors);
0 ignored issues
show
Documentation introduced by
$errors is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1319
				Session::set("FormInfo.{$this->FormName()}.data", $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1320
1321
				return false;
1322
			}
1323
		}
1324
1325
		return true;
1326
	}
1327
1328
	const MERGE_DEFAULT = 0;
1329
	const MERGE_CLEAR_MISSING = 1;
1330
	const MERGE_IGNORE_FALSEISH = 2;
1331
1332
	/**
1333
	 * Load data from the given DataObject or array.
1334
	 *
1335
	 * It will call $object->MyField to get the value of MyField.
1336
	 * If you passed an array, it will call $object[MyField].
1337
	 * Doesn't save into dataless FormFields ({@link DatalessField}),
1338
	 * as determined by {@link FieldList->dataFields()}.
1339
	 *
1340
	 * By default, if a field isn't set (as determined by isset()),
1341
	 * its value will not be saved to the field, retaining
1342
	 * potential existing values.
1343
	 *
1344
	 * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1345
	 * Escaping happens automatically on saving the data through {@link saveInto()}.
1346
	 *
1347
	 * Escaping happens automatically on saving the data through
1348
	 * {@link saveInto()}.
1349
	 *
1350
	 * @uses FieldList->dataFields()
1351
	 * @uses FormField->setValue()
1352
	 *
1353
	 * @param array|DataObject $data
1354
	 * @param int $mergeStrategy
1355
	 *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1356
	 *  what that property/key's value is.
1357
	 *
1358
	 *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1359
	 *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1360
	 *  "left alone", meaning they retain any previous value.
1361
	 *
1362
	 *  You can pass a bitmask here to change this behaviour.
1363
	 *
1364
	 *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1365
	 *  {@link $data} are cleared.
1366
	 *
1367
	 *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1368
	 *  a field's value.
1369
	 *
1370
	 *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1371
	 *  CLEAR_MISSING
1372
	 *
1373
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1374
	 * form that has some fields that save to one object, and some that save to another.
1375
	 * @return Form
1376
	 */
1377
	public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) {
1378
		if(!is_object($data) && !is_array($data)) {
1379
			user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1380
			return $this;
1381
		}
1382
1383
		// Handle the backwards compatible case of passing "true" as the second argument
1384
		if ($mergeStrategy === true) {
1385
			$mergeStrategy = self::MERGE_CLEAR_MISSING;
1386
		}
1387
		else if ($mergeStrategy === false) {
1388
			$mergeStrategy = 0;
1389
		}
1390
1391
		// if an object is passed, save it for historical reference through {@link getRecord()}
1392
		if(is_object($data)) $this->record = $data;
1393
1394
		// dont include fields without data
1395
		$dataFields = $this->Fields()->dataFields();
1396
		if($dataFields) foreach($dataFields as $field) {
1397
			$name = $field->getName();
1398
1399
			// Skip fields that have been excluded
1400
			if($fieldList && !in_array($name, $fieldList)) continue;
1401
1402
			// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1403
			if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
1404
1405
			// Does this property exist on $data?
1406
			$exists = false;
1407
			// The value from $data for this field
1408
			$val = null;
1409
1410
			if(is_object($data)) {
1411
				$exists = (
1412
					isset($data->$name) ||
1413
					$data->hasMethod($name) ||
1414
					($data->hasMethod('hasField') && $data->hasField($name))
1415
				);
1416
1417
				if ($exists) {
1418
					$val = $data->__get($name);
1419
				}
1420
			}
1421
			else if(is_array($data)){
1422
				if(array_key_exists($name, $data)) {
1423
					$exists = true;
1424
					$val = $data[$name];
1425
				}
1426
				// If field is in array-notation we need to access nested data
1427
				else if(strpos($name,'[')) {
1428
					// First encode data using PHP's method of converting nested arrays to form data
1429
					$flatData = urldecode(http_build_query($data));
1430
					// Then pull the value out from that flattened string
1431
					preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches);
1432
1433
					if (isset($matches[1])) {
1434
						$exists = true;
1435
						$val = $matches[1];
1436
					}
1437
				}
1438
			}
1439
1440
			// save to the field if either a value is given, or loading of blank/undefined values is forced
1441
			if($exists){
1442
				if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
1443
					// pass original data as well so composite fields can act on the additional information
1444
					$field->setValue($val, $data);
1445
				}
1446
			}
1447
			else if(($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING){
1448
				$field->setValue($val, $data);
1449
			}
1450
		}
1451
1452
		return $this;
1453
	}
1454
1455
	/**
1456
	 * Save the contents of this form into the given data object.
1457
	 * It will make use of setCastedField() to do this.
1458
	 *
1459
	 * @param DataObjectInterface $dataObject The object to save data into
1460
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1461
	 * form that has some fields that save to one object, and some that save to another.
1462
	 */
1463
	public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
1464
		$dataFields = $this->fields->saveableFields();
1465
		$lastField = null;
1466
		if($dataFields) foreach($dataFields as $field) {
1467
			// Skip fields that have been excluded
1468
			if($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) continue;
1469
1470
1471
			$saveMethod = "save{$field->getName()}";
1472
1473
			if($field->getName() == "ClassName"){
1474
				$lastField = $field;
1475
			}else if( $dataObject->hasMethod( $saveMethod ) ){
1476
				$dataObject->$saveMethod( $field->dataValue());
1477
			} else if($field->getName() != "ID"){
1478
				$field->saveInto($dataObject);
1479
			}
1480
		}
1481
		if($lastField) $lastField->saveInto($dataObject);
1482
	}
1483
1484
	/**
1485
	 * Get the submitted data from this form through
1486
	 * {@link FieldList->dataFields()}, which filters out
1487
	 * any form-specific data like form-actions.
1488
	 * Calls {@link FormField->dataValue()} on each field,
1489
	 * which returns a value suitable for insertion into a DataObject
1490
	 * property.
1491
	 *
1492
	 * @return array
1493
	 */
1494
	public function getData() {
1495
		$dataFields = $this->fields->dataFields();
1496
		$data = array();
1497
1498
		if($dataFields){
1499
			foreach($dataFields as $field) {
1500
				if($field->getName()) {
1501
					$data[$field->getName()] = $field->dataValue();
1502
				}
1503
			}
1504
		}
1505
1506
		return $data;
1507
	}
1508
1509
	/**
1510
	 * Call the given method on the given field.
1511
	 *
1512
	 * @param array $data
1513
	 * @return mixed
1514
	 */
1515
	public function callfieldmethod($data) {
1516
		$fieldName = $data['fieldName'];
1517
		$methodName = $data['methodName'];
1518
		$fields = $this->fields->dataFields();
1519
1520
		// special treatment needed for TableField-class and TreeDropdownField
1521
		if(strpos($fieldName, '[')) {
1522
			preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
1523
			preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
1524
			$tableFieldName = $fieldNameMatches[1][0];
1525
			$subFieldName = $subFieldMatches[1][1];
1526
		}
1527
1528
		if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
1529
			$field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
1530
			return $field->$methodName();
1531
		} else if(isset($fields[$fieldName])) {
1532
			return $fields[$fieldName]->$methodName();
1533
		} else {
1534
			user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
1535
		}
1536
	}
1537
1538
	/**
1539
	 * Return a rendered version of this form.
1540
	 *
1541
	 * This is returned when you access a form as $FormObject rather
1542
	 * than <% with FormObject %>
1543
	 *
1544
	 * @return HTML
1545
	 */
1546
	public function forTemplate() {
1547
		$return = $this->renderWith(array_merge(
1548
			(array)$this->getTemplate(),
1549
			array('Form')
1550
		));
1551
1552
		// Now that we're rendered, clear message
1553
		$this->clearMessage();
1554
1555
		return $return;
1556
	}
1557
1558
	/**
1559
	 * Return a rendered version of this form, suitable for ajax post-back.
1560
	 *
1561
	 * It triggers slightly different behaviour, such as disabling the rewriting
1562
	 * of # links.
1563
	 *
1564
	 * @return HTML
1565
	 */
1566
	public function forAjaxTemplate() {
1567
		$view = new SSViewer(array(
1568
			$this->getTemplate(),
1569
			'Form'
1570
		));
1571
1572
		$return = $view->dontRewriteHashlinks()->process($this);
1573
1574
		// Now that we're rendered, clear message
1575
		$this->clearMessage();
1576
1577
		return $return;
1578
	}
1579
1580
	/**
1581
	 * Returns an HTML rendition of this form, without the <form> tag itself.
1582
	 *
1583
	 * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1584
	 * and _form_enctype.  These are the attributes of the form.  These fields
1585
	 * can be used to send the form to Ajax.
1586
	 *
1587
	 * @return HTML
1588
	 */
1589
	public function formHtmlContent() {
1590
		$this->IncludeFormTag = false;
1591
		$content = $this->forTemplate();
1592
		$this->IncludeFormTag = true;
1593
1594
		$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1595
			. " value=\"" . $this->FormAction() . "\" />\n";
1596
		$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1597
		$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1598
		$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1599
1600
		return $content;
1601
	}
1602
1603
	/**
1604
	 * Render this form using the given template, and return the result as a string
1605
	 * You can pass either an SSViewer or a template name
1606
	 * @param string|array $template
1607
	 * @return HTMLText
1608
	 */
1609
	public function renderWithoutActionButton($template) {
1610
		$custom = $this->customise(array(
1611
			"Actions" => "",
1612
		));
1613
1614
		if(is_string($template)) {
1615
			$template = new SSViewer($template);
1616
		}
1617
1618
		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...
1619
	}
1620
1621
1622
	/**
1623
	 * Sets the button that was clicked.  This should only be called by the Controller.
1624
	 *
1625
	 * @param callable $funcName The name of the action method that will be called.
1626
	 * @return $this
1627
	 */
1628
	public function setButtonClicked($funcName) {
1629
		$this->buttonClickedFunc = $funcName;
1630
1631
		return $this;
1632
	}
1633
1634
	/**
1635
	 * @return FormAction
1636
	 */
1637
	public function buttonClicked() {
1638
		foreach($this->actions->dataFields() as $action) {
1639
			if($action->hasMethod('actionname') && $this->buttonClickedFunc == $action->actionName()) {
1640
				return $action;
1641
			}
1642
		}
1643
	}
1644
1645
	/**
1646
	 * Return the default button that should be clicked when another one isn't
1647
	 * available.
1648
	 *
1649
	 * @return FormAction
1650
	 */
1651
	public function defaultAction() {
1652
		if($this->hasDefaultAction && $this->actions) {
1653
			return $this->actions->First();
1654
	}
1655
	}
1656
1657
	/**
1658
	 * Disable the default button.
1659
	 *
1660
	 * Ordinarily, when a form is processed and no action_XXX button is
1661
	 * available, then the first button in the actions list will be pressed.
1662
	 * However, if this is "delete", for example, this isn't such a good idea.
1663
	 *
1664
	 * @return Form
1665
	 */
1666
	public function disableDefaultAction() {
1667
		$this->hasDefaultAction = false;
1668
1669
		return $this;
1670
	}
1671
1672
	/**
1673
	 * Disable the requirement of a security token on this form instance. This
1674
	 * security protects against CSRF attacks, but you should disable this if
1675
	 * you don't want to tie a form to a session - eg a search form.
1676
	 *
1677
	 * Check for token state with {@link getSecurityToken()} and
1678
	 * {@link SecurityToken->isEnabled()}.
1679
	 *
1680
	 * @return Form
1681
	 */
1682
	public function disableSecurityToken() {
1683
		$this->securityToken = new NullSecurityToken();
1684
1685
		return $this;
1686
	}
1687
1688
	/**
1689
	 * Enable {@link SecurityToken} protection for this form instance.
1690
	 *
1691
	 * Check for token state with {@link getSecurityToken()} and
1692
	 * {@link SecurityToken->isEnabled()}.
1693
	 *
1694
	 * @return Form
1695
	 */
1696
	public function enableSecurityToken() {
1697
		$this->securityToken = new SecurityToken();
1698
1699
		return $this;
1700
	}
1701
1702
	/**
1703
	 * Returns the security token for this form (if any exists).
1704
	 *
1705
	 * Doesn't check for {@link securityTokenEnabled()}.
1706
	 *
1707
	 * Use {@link SecurityToken::inst()} to get a global token.
1708
	 *
1709
	 * @return SecurityToken|null
1710
	 */
1711
	public function getSecurityToken() {
1712
		return $this->securityToken;
1713
	}
1714
1715
	/**
1716
	 * Returns the name of a field, if that's the only field that the current
1717
	 * controller is interested in.
1718
	 *
1719
	 * It checks for a call to the callfieldmethod action.
1720
	 *
1721
	 * @return string
1722
	 */
1723
	public static function single_field_required() {
1724
		if(self::current_action() == 'callfieldmethod') {
1725
			return $_REQUEST['fieldName'];
1726
	}
1727
	}
1728
1729
	/**
1730
	 * Return the current form action being called, if available.
1731
	 *
1732
	 * @return string
1733
	 */
1734
	public static function current_action() {
1735
		return self::$current_action;
1736
	}
1737
1738
	/**
1739
	 * Set the current form action. Should only be called by {@link Controller}.
1740
	 *
1741
	 * @param string $action
1742
	 */
1743
	public static function set_current_action($action) {
1744
		self::$current_action = $action;
1745
	}
1746
1747
	/**
1748
	 * Compiles all CSS-classes.
1749
	 *
1750
	 * @return string
1751
	 */
1752
	public function extraClass() {
1753
		return implode(array_unique($this->extraClasses), ' ');
1754
	}
1755
1756
	/**
1757
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1758
	 * be added by delimiting a string with spaces.
1759
	 *
1760
	 * @param string $class A string containing a classname or several class
1761
	 *                names delimited by a single space.
1762
	 * @return $this
1763
	 */
1764 View Code Duplication
	public function addExtraClass($class) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1765
		//split at white space
1766
		$classes = preg_split('/\s+/', $class);
1767
		foreach($classes as $class) {
1768
			//add classes one by one
1769
			$this->extraClasses[$class] = $class;
1770
		}
1771
		return $this;
1772
	}
1773
1774
	/**
1775
	 * Remove a CSS-class from the form-container. Multiple class names can
1776
	 * be passed through as a space delimited string
1777
	 *
1778
	 * @param string $class
1779
	 * @return $this
1780
	 */
1781 View Code Duplication
	public function removeExtraClass($class) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1782
		//split at white space
1783
		$classes = preg_split('/\s+/', $class);
1784
		foreach ($classes as $class) {
1785
			//unset one by one
1786
			unset($this->extraClasses[$class]);
1787
		}
1788
		return $this;
1789
	}
1790
1791
	public function debug() {
1792
		$result = "<h3>$this->class</h3><ul>";
1793
		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...
1794
			$result .= "<li>$field" . $field->debug() . "</li>";
1795
		}
1796
		$result .= "</ul>";
1797
1798
		if( $this->validator )
1799
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1800
1801
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (string) is incompatible with the return type of the parent method ViewableData::Debug of type ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1802
	}
1803
1804
1805
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1806
	// TESTING HELPERS
1807
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1808
1809
	/**
1810
	 * Test a submission of this form.
1811
	 * @param string $action
1812
	 * @param array $data
1813
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1814
	 * your unit test.
1815
	 * @throws SS_HTTPResponse_Exception
1816
	 */
1817
	public function testSubmission($action, $data) {
1818
		$data['action_' . $action] = true;
1819
1820
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1821
	}
1822
1823
	/**
1824
	 * Test an ajax submission of this form.
1825
	 *
1826
	 * @param string $action
1827
	 * @param array $data
1828
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1829
	 * your unit test.
1830
	 */
1831
	public function testAjaxSubmission($action, $data) {
1832
		$data['ajax'] = 1;
1833
		return $this->testSubmission($action, $data);
1834
	}
1835
}
1836
1837
/**
1838
 * @package forms
1839
 * @subpackage core
1840
 */
1841
class Form_FieldMap extends ViewableData {
1842
1843
	protected $form;
1844
1845
	public function __construct($form) {
1846
		$this->form = $form;
1847
		parent::__construct();
1848
	}
1849
1850
	/**
1851
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1852
	 * @param string $method
1853
	 * @return bool
1854
	 */
1855
	public function hasMethod($method) {
1856
		return true;
1857
	}
1858
1859
	public function __call($method, $args = null) {
1860
		return $this->form->Fields()->fieldByName($method);
1861
	}
1862
}
1863