Completed
Push — 3 ( bc9e38...39c73e )
by Robbie
05:15 queued 10s
created

Form   F

Complexity

Total Complexity 248

Size/Duplication

Total Lines 1850
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 23

Importance

Changes 0
Metric Value
dl 0
loc 1850
rs 0.8
c 0
b 0
f 0
wmc 248
lcom 1
cbo 23

90 Methods

Rating   Name   Duplication   Size   Complexity  
B setupFormErrors() 0 25 8
A setupDefaultClasses() 0 8 3
F httpSubmission() 0 135 21
B checkAccessAction() 0 20 6
B getValidationErrorResponse() 0 32 6
A checkFieldsForAction() 0 11 6
A handleField() 0 10 2
A makeReadonly() 0 3 1
A setRedirectToFormOnValidationError() 0 4 1
A getRedirectToFormOnValidationError() 0 3 1
A addErrorMessage() 0 7 2
A transform() 0 18 4
A getValidator() 0 3 1
A setValidator() 0 7 2
A unsetValidator() 0 4 1
A transformTo() 0 11 3
A getExtraFields() 0 19 4
A Fields() 0 7 3
A HiddenFields() 0 3 1
A VisibleFields() 0 3 1
A setFields() 0 4 1
A Actions() 0 3 1
A setActions() 0 4 1
A unsetAllActions() 0 4 1
A setAttribute() 0 4 1
A getAttribute() 0 3 2
A getAttributes() 0 19 4
F getAttributesHTML() 0 41 12
A FormAttributes() 0 3 1
A setTemplateHelper() 0 3 1
A getTemplateHelper() 0 11 3
A setTarget() 0 5 1
A setLegend() 0 4 1
A setTemplate() 0 4 1
A getTemplate() 0 4 2
A getEncType() 0 13 5
A setEncType() 0 4 1
A FormHttpMethod() 0 3 1
A FormMethod() 0 7 2
A setFormMethod() 0 5 2
A setStrictFormMethodCheck() 0 4 1
A getStrictFormMethodCheck() 0 3 1
A FormAction() 0 16 3
A setFormAction() 0 5 1
A FormName() 0 3 1
A setHTMLID() 0 5 1
A getHTMLID() 0 3 1
A Controller() 0 5 1
A getController() 0 3 1
A setController() 0 5 1
A getName() 0 3 1
A setName() 0 5 1
A FieldMap() 0 3 1
A Message() 0 5 1
A MessageType() 0 5 1
A getMessageFromSession() 0 10 3
A setMessage() 0 5 2
A sessionMessage() 0 7 2
A messageForForm() 0 7 2
A clearMessage() 0 6 1
A resetValidation() 0 4 1
A getRecord() 0 3 1
A getLegend() 0 3 1
A validate() 0 29 4
D loadDataFrom() 0 94 29
B saveInto() 0 20 10
A getData() 0 14 4
B callfieldmethod() 0 22 6
A forTemplate() 0 11 1
A forAjaxTemplate() 0 13 1
A formHtmlContent() 0 13 1
A renderWithoutActionButton() 0 11 2
A setButtonClicked() 0 5 1
A buttonClicked() 0 10 3
A getAllActions() 0 11 3
A defaultAction() 0 5 3
A disableDefaultAction() 0 5 1
A disableSecurityToken() 0 5 1
A enableSecurityToken() 0 5 1
A getSecurityToken() 0 3 1
A single_field_required() 0 5 2
A current_action() 0 3 1
A set_current_action() 0 3 1
A extraClass() 0 3 1
A addExtraClass() 0 9 2
A removeExtraClass() 0 9 2
A debug() 0 12 3
A testSubmission() 0 5 1
A testAjaxSubmission() 0 4 1
B __construct() 0 44 11

How to fix   Complexity   

Complex Class

Complex classes like Form often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Form, and based on these observations, apply Extract Interface, too.

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(
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(
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) {
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
		// Ensure we only process saveable fields (non structural, readonly, or disabled)
359
		$allowedFields = array_keys($this->Fields()->saveableFields());
360
361
		// Populate the form
362
		$this->loadDataFrom($vars, true, $allowedFields);
0 ignored issues
show
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...
363
364
		// Protection against CSRF attacks
365
		$token = $this->getSecurityToken();
366
		if( ! $token->checkRequest($request)) {
367
			$securityID = $token->getName();
368
			if (empty($vars[$securityID])) {
369
				$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
370
					"There seems to have been a technical problem. Please click the back button, ".
371
					"refresh your browser, and try again."
372
				));
373
			} else {
374
				// Clear invalid token on refresh
375
				$data = $this->getData();
376
				unset($data[$securityID]);
377
				Session::set("FormInfo.{$this->FormName()}.data", $data);
378
				Session::set("FormInfo.{$this->FormName()}.errors", array());
379
				$this->sessionMessage(
380
					_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
381
					"warning"
382
				);
383
				return $this->controller->redirectBack();
384
			}
385
		}
386
387
		// Determine the action button clicked
388
		$funcName = null;
389
		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...
390
			if(substr($paramName,0,7) == 'action_') {
391
				// Break off querystring arguments included in the action
392
				if(strpos($paramName,'?') !== false) {
393
					list($paramName, $paramVars) = explode('?', $paramName, 2);
394
					$newRequestParams = array();
395
					parse_str($paramVars, $newRequestParams);
396
					$vars = array_merge((array)$vars, (array)$newRequestParams);
397
				}
398
399
				// Cleanup action_, _x and _y from image fields
400
				$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
401
				break;
402
			}
403
		}
404
405
		// If the action wasn't set, choose the default on the form.
406
		if(!isset($funcName) && $defaultAction = $this->defaultAction()){
407
			$funcName = $defaultAction->actionName();
408
		}
409
410
		if(isset($funcName)) {
411
			Form::set_current_action($funcName);
412
			$this->setButtonClicked($funcName);
413
		}
414
415
		// Permission checks (first on controller, then falling back to form)
416
		if(
417
			// Ensure that the action is actually a button or method on the form,
418
			// and not just a method on the controller.
419
			$this->controller->hasMethod($funcName)
420
			&& !$this->controller->checkAccessAction($funcName)
421
			// If a button exists, allow it on the controller
422
			// buttonClicked() validates that the action set above is valid
423
			&& !$this->buttonClicked()
424
		) {
425
			return $this->httpError(
426
				403,
427
				sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
428
			);
429
		} elseif(
430
			$this->hasMethod($funcName)
431
			&& !$this->checkAccessAction($funcName)
432
			// No checks for button existence or $allowed_actions is performed -
433
			// all form methods are callable (e.g. the legacy "callfieldmethod()")
434
		) {
435
			return $this->httpError(
436
				403,
437
				sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
438
			);
439
		}
440
		// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
441
		// explicitly in allowed_actions in order to run)
442
		// Uncomment the following for checking security against running actions on form fields
443
		/* else {
444
			// Try to find a field that has the action, and allows it
445
			$fieldsHaveMethod = false;
446
			foreach ($this->Fields() as $field){
447
				if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
448
					$fieldsHaveMethod = true;
449
				}
450
			}
451
			if (!$fieldsHaveMethod) {
452
				return $this->httpError(
453
					403,
454
					sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
455
				);
456
			}
457
		}*/
458
459
		// Validate the form
460
		if(!$this->validate()) {
461
			return $this->getValidationErrorResponse();
462
		}
463
464
		// First, try a handler method on the controller (has been checked for allowed_actions above already)
465
		if($this->controller->hasMethod($funcName)) {
466
			return $this->controller->$funcName($vars, $this, $request);
467
		// Otherwise, try a handler method on the form object.
468
		} elseif($this->hasMethod($funcName)) {
469
			return $this->$funcName($vars, $this, $request);
470
		} 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...
471
			return $field->$funcName($vars, $this, $request);
472
		}
473
474
		return $this->httpError(404);
475
	}
476
477
	/**
478
	 * @param string $action
479
	 * @return bool
480
	 */
481
	public function checkAccessAction($action) {
482
		if (parent::checkAccessAction($action)) {
483
			return true;
484
		}
485
486
		$actions = $this->getAllActions();
487
 		foreach ($actions as $formAction) {
488
			if ($formAction->actionName() === $action) {
489
				return true;
490
			}
491
		}
492
493
		// Always allow actions on fields
494
		$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...
495
		if ($field && $field->checkAccessAction($action)) {
496
			return true;
497
		}
498
499
		return false;
500
	}
501
502
	/**
503
	 * Returns the appropriate response up the controller chain
504
	 * if {@link validate()} fails (which is checked prior to executing any form actions).
505
	 * By default, returns different views for ajax/non-ajax request, and
506
	 * handles 'application/json' requests with a JSON object containing the error messages.
507
	 * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}.
508
	 *
509
	 * @return SS_HTTPResponse|string
510
	 */
511
	protected function getValidationErrorResponse() {
512
		$request = $this->getRequest();
513
		if($request->isAjax()) {
514
				// Special case for legacy Validator.js implementation
515
				// (assumes eval'ed javascript collected through FormResponse)
516
				$acceptType = $request->getHeader('Accept');
517
				if(strpos($acceptType, 'application/json') !== FALSE) {
518
					// Send validation errors back as JSON with a flag at the start
519
					$response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
520
					$response->addHeader('Content-Type', 'application/json');
521
				} else {
522
					$this->setupFormErrors();
523
					// Send the newly rendered form tag as HTML
524
					$response = new SS_HTTPResponse($this->forTemplate());
525
					$response->addHeader('Content-Type', 'text/html');
526
				}
527
528
				return $response;
529
			} else {
530
				if($this->getRedirectToFormOnValidationError()) {
531
					if($pageURL = $request->getHeader('Referer')) {
532
						if(Director::is_site_url($pageURL)) {
533
							// Remove existing pragmas
534
							$pageURL = preg_replace('/(#.*)/', '', $pageURL);
535
							$pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \Director::absoluteURL($pageURL, true) on line 535 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...
536
							return $this->controller->redirect($pageURL . '#' . $this->FormName());
537
						}
538
					}
539
				}
540
				return $this->controller->redirectBack();
541
			}
542
	}
543
544
	/**
545
	 * Fields can have action to, let's check if anyone of the responds to $funcname them
546
	 *
547
	 * @param SS_List|array $fields
548
	 * @param callable $funcName
549
	 * @return FormField
550
	 */
551
	protected function checkFieldsForAction($fields, $funcName) {
552
		foreach($fields as $field){
553
			if(method_exists($field, 'FieldList')) {
554
				if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
555
					return $field;
556
				}
557
			} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
558
				return $field;
559
			}
560
		}
561
	}
562
563
	/**
564
	 * Handle a field request.
565
	 * Uses {@link Form->dataFieldByName()} to find a matching field,
566
	 * and falls back to {@link FieldList->fieldByName()} to look
567
	 * for tabs instead. This means that if you have a tab and a
568
	 * formfield with the same name, this method gives priority
569
	 * to the formfield.
570
	 *
571
	 * @param SS_HTTPRequest $request
572
	 * @return FormField
573
	 */
574
	public function handleField($request) {
575
		$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
576
577
		if($field) {
578
			return $field;
579
		} else {
580
			// falling back to fieldByName, e.g. for getting tabs
581
			return $this->Fields()->fieldByName($request->param('FieldName'));
582
		}
583
	}
584
585
	/**
586
	 * Convert this form into a readonly form
587
	 */
588
	public function makeReadonly() {
589
		$this->transform(new ReadonlyTransformation());
590
	}
591
592
	/**
593
	 * Set whether the user should be redirected back down to the
594
	 * form on the page upon validation errors in the form or if
595
	 * they just need to redirect back to the page
596
	 *
597
	 * @param bool $bool Redirect to form on error?
598
	 * @return $this
599
	 */
600
	public function setRedirectToFormOnValidationError($bool) {
601
		$this->redirectToFormOnValidationError = $bool;
602
		return $this;
603
	}
604
605
	/**
606
	 * Get whether the user should be redirected back down to the
607
	 * form on the page upon validation errors
608
	 *
609
	 * @return bool
610
	 */
611
	public function getRedirectToFormOnValidationError() {
612
		return $this->redirectToFormOnValidationError;
613
	}
614
615
	/**
616
	 * Add a plain text error message to a field on this form.  It will be saved into the session
617
	 * and used the next time this form is displayed.
618
	 * @param string $fieldName
619
	 * @param string $message
620
	 * @param string $messageType
621
	 * @param bool $escapeHtml
622
	 */
623
	public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
624
		Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
625
			'fieldName' => $fieldName,
626
			'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
627
			'messageType' => $messageType,
628
		));
629
	}
630
631
	/**
632
	 * @param FormTransformation $trans
633
	 */
634
	public function transform(FormTransformation $trans) {
635
		$newFields = new FieldList();
636
		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...
637
			$newFields->push($field->transform($trans));
638
		}
639
		$this->fields = $newFields;
640
641
		$newActions = new FieldList();
642
		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...
643
			$newActions->push($action->transform($trans));
644
		}
645
		$this->actions = $newActions;
646
647
648
		// We have to remove validation, if the fields are not editable ;-)
649
		if($this->validator)
650
			$this->validator->removeValidation();
651
	}
652
653
	/**
654
	 * Get the {@link Validator} attached to this form.
655
	 * @return Validator
656
	 */
657
	public function getValidator() {
658
		return $this->validator;
659
	}
660
661
	/**
662
	 * Set the {@link Validator} on this form.
663
	 * @param Validator $validator
664
	 * @return $this
665
	 */
666
	public function setValidator(Validator $validator ) {
667
		if($validator) {
668
			$this->validator = $validator;
669
			$this->validator->setForm($this);
670
		}
671
		return $this;
672
	}
673
674
	/**
675
	 * Remove the {@link Validator} from this from.
676
	 */
677
	public function unsetValidator(){
678
		$this->validator = null;
679
		return $this;
680
	}
681
682
	/**
683
	 * Convert this form to another format.
684
	 * @param FormTransformation $format
685
	 */
686
	public function transformTo(FormTransformation $format) {
687
		$newFields = new FieldList();
688
		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...
689
			$newFields->push($field->transformTo($format));
690
		}
691
		$this->fields = $newFields;
692
693
		// We have to remove validation, if the fields are not editable ;-)
694
		if($this->validator)
695
			$this->validator->removeValidation();
696
	}
697
698
699
	/**
700
	 * Generate extra special fields - namely the security token field (if required).
701
	 *
702
	 * @return FieldList
703
	 */
704
	public function getExtraFields() {
705
		$extraFields = new FieldList();
706
707
		$token = $this->getSecurityToken();
708
		if ($token) {
709
			$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...
710
			if($tokenField) $tokenField->setForm($this);
711
		}
712
		$this->securityTokenAdded = true;
713
714
		// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
715
		if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
716
			$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
717
			$methodField->setForm($this);
718
			$extraFields->push($methodField);
719
		}
720
721
		return $extraFields;
722
	}
723
724
	/**
725
	 * Return the form's fields - used by the templates
726
	 *
727
	 * @return FieldList The form fields
728
	 */
729
	public function Fields() {
730
		foreach($this->getExtraFields() as $field) {
731
			if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field);
732
		}
733
734
		return $this->fields;
735
	}
736
737
	/**
738
	 * Return all <input type="hidden"> fields
739
	 * in a form - including fields nested in {@link CompositeFields}.
740
	 * Useful when doing custom field layouts.
741
	 *
742
	 * @return FieldList
743
	 */
744
	public function HiddenFields() {
745
		return $this->Fields()->HiddenFields();
746
	}
747
748
	/**
749
	 * Return all fields except for the hidden fields.
750
	 * Useful when making your own simplified form layouts.
751
	 */
752
	public function VisibleFields() {
753
		return $this->Fields()->VisibleFields();
754
	}
755
756
	/**
757
	 * Setter for the form fields.
758
	 *
759
	 * @param FieldList $fields
760
	 * @return $this
761
	 */
762
	public function setFields($fields) {
763
		$this->fields = $fields;
764
		return $this;
765
	}
766
767
	/**
768
	 * Return the form's action buttons - used by the templates
769
	 *
770
	 * @return FieldList The action list
771
	 */
772
	public function Actions() {
773
		return $this->actions;
774
	}
775
776
	/**
777
	 * Setter for the form actions.
778
	 *
779
	 * @param FieldList $actions
780
	 * @return $this
781
	 */
782
	public function setActions($actions) {
783
		$this->actions = $actions;
784
		return $this;
785
	}
786
787
	/**
788
	 * Unset all form actions
789
	 */
790
	public function unsetAllActions(){
791
		$this->actions = new FieldList();
792
		return $this;
793
	}
794
795
	/**
796
	 * @param string $name
797
	 * @param string $value
798
	 * @return $this
799
	 */
800
	public function setAttribute($name, $value) {
801
		$this->attributes[$name] = $value;
802
		return $this;
803
	}
804
805
	/**
806
	 * @return string $name
807
	 */
808
	public function getAttribute($name) {
809
		if(isset($this->attributes[$name])) return $this->attributes[$name];
810
	}
811
812
	/**
813
	 * @return array
814
	 */
815
	public function getAttributes() {
816
		$attrs = array(
817
			'id' => $this->FormName(),
818
			'action' => $this->FormAction(),
819
			'method' => $this->FormMethod(),
820
			'enctype' => $this->getEncType(),
821
			'target' => $this->target,
822
			'class' => $this->extraClass(),
823
		);
824
825
		if($this->validator && $this->validator->getErrors()) {
826
			if(!isset($attrs['class'])) $attrs['class'] = '';
827
			$attrs['class'] .= ' validationerror';
828
		}
829
830
		$attrs = array_merge($attrs, $this->attributes);
831
832
		return $attrs;
833
	}
834
835
	/**
836
	 * Return the attributes of the form tag - used by the templates.
837
	 *
838
	 * @param array Custom attributes to process. Falls back to {@link getAttributes()}.
839
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
840
	 *
841
	 * @return string HTML attributes, ready for insertion into an HTML tag
842
	 */
843
	public function getAttributesHTML($attrs = null) {
844
		$exclude = (is_string($attrs)) ? func_get_args() : null;
845
846
		// Figure out if we can cache this form
847
		// - forms with validation shouldn't be cached, cos their error messages won't be shown
848
		// - forms with security tokens shouldn't be cached because security tokens expire
849
		$needsCacheDisabled = false;
850
		if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
851
		if ($this->FormMethod() != 'GET') $needsCacheDisabled = true;
852
		if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
853
			$needsCacheDisabled = true;
854
		}
855
856
		// If we need to disable cache, do it
857
		if ($needsCacheDisabled) {
858
			HTTPCacheControl::singleton()->disableCache(true);
859
		}
860
861
		$attrs = $this->getAttributes();
862
863
		// Remove empty
864
		$attrs = array_filter((array)$attrs, function($v) {
865
		    return ($v || $v === 0);
866
        });
867
868
		// Remove excluded
869
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
870
871
		// Prepare HTML-friendly 'method' attribute (lower-case)
872
		if (isset($attrs['method'])) {
873
			$attrs['method'] = strtolower($attrs['method']);
874
		}
875
876
		// Create markup
877
		$parts = array();
878
		foreach($attrs as $name => $value) {
879
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
880
		}
881
882
		return implode(' ', $parts);
883
	}
884
885
	public function FormAttributes() {
886
		return $this->getAttributesHTML();
887
	}
888
889
	/**
890
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
891
	 * another frame
892
	 *
893
	 * @param string|FormTemplateHelper
894
	 */
895
	public function setTemplateHelper($helper) {
896
		$this->templateHelper = $helper;
897
	}
898
899
	/**
900
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
901
	 * set, return the default helper.
902
	 *
903
	 * @return FormTemplateHelper
904
	 */
905
	public function getTemplateHelper() {
906
		if($this->templateHelper) {
907
			if(is_string($this->templateHelper)) {
908
				return Injector::inst()->get($this->templateHelper);
909
			}
910
911
			return $this->templateHelper;
912
		}
913
914
		return Injector::inst()->get('FormTemplateHelper');
915
	}
916
917
	/**
918
	 * Set the target of this form to any value - useful for opening the form
919
	 * contents in a new window or refreshing another frame.
920
	 *
921
	 * @param target $target The value of the target
922
	 * @return $this
923
	 */
924
	public function setTarget($target) {
925
		$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...
926
927
		return $this;
928
	}
929
930
	/**
931
	 * Set the legend value to be inserted into
932
	 * the <legend> element in the Form.ss template.
933
	 * @param string $legend
934
	 * @return $this
935
	 */
936
	public function setLegend($legend) {
937
		$this->legend = $legend;
938
		return $this;
939
	}
940
941
	/**
942
	 * Set the SS template that this form should use
943
	 * to render with. The default is "Form".
944
	 *
945
	 * @param string $template The name of the template (without the .ss extension)
946
	 * @return $this
947
	 */
948
	public function setTemplate($template) {
949
		$this->template = $template;
950
		return $this;
951
	}
952
953
	/**
954
	 * Return the template to render this form with.
955
	 * If the template isn't set, then default to the
956
	 * form class name e.g "Form".
957
	 *
958
	 * @return string
959
	 */
960
	public function getTemplate() {
961
		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...
962
		else return $this->class;
963
	}
964
965
	/**
966
	 * Returns the encoding type for the form.
967
	 *
968
	 * By default this will be URL encoded, unless there is a file field present
969
	 * in which case multipart is used. You can also set the enc type using
970
	 * {@link setEncType}.
971
	 */
972
	public function getEncType() {
973
		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...
974
			return $this->encType;
975
		}
976
977
		if ($fields = $this->fields->dataFields()) {
978
			foreach ($fields as $field) {
979
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
980
			}
981
		}
982
983
		return self::ENC_TYPE_URLENCODED;
984
	}
985
986
	/**
987
	 * Sets the form encoding type. The most common encoding types are defined
988
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
989
	 *
990
	 * @param string $encType
991
	 * @return $this
992
	 */
993
	public function setEncType($encType) {
994
		$this->encType = $encType;
995
		return $this;
996
	}
997
998
	/**
999
	 * Returns the real HTTP method for the form:
1000
	 * GET, POST, PUT, DELETE or HEAD.
1001
	 * As most browsers only support GET and POST in
1002
	 * form submissions, all other HTTP methods are
1003
	 * added as a hidden field "_method" that
1004
	 * gets evaluated in {@link Director::direct()}.
1005
	 * See {@link FormMethod()} to get a HTTP method
1006
	 * for safe insertion into a <form> tag.
1007
	 *
1008
	 * @return string HTTP method
1009
	 */
1010
	public function FormHttpMethod() {
1011
		return $this->formMethod;
1012
	}
1013
1014
	/**
1015
	 * Returns the form method to be used in the <form> tag.
1016
	 * See {@link FormHttpMethod()} to get the "real" method.
1017
	 *
1018
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1019
	 */
1020
	public function FormMethod() {
1021
		if(in_array($this->formMethod,array('GET','POST'))) {
1022
			return $this->formMethod;
1023
		} else {
1024
			return 'POST';
1025
		}
1026
	}
1027
1028
	/**
1029
	 * Set the form method: GET, POST, PUT, DELETE.
1030
	 *
1031
	 * @param string $method
1032
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1033
	 * @return $this
1034
	 */
1035
	public function setFormMethod($method, $strict = null) {
1036
		$this->formMethod = strtoupper($method);
1037
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1038
		return $this;
1039
	}
1040
1041
	/**
1042
	 * If set to true, enforce the matching of the form method.
1043
	 *
1044
	 * This will mean two things:
1045
	 *  - GET vars will be ignored by a POST form, and vice versa
1046
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1047
	 *
1048
	 * If set to false (the default), then the form method is only used to construct the default
1049
	 * form.
1050
	 *
1051
	 * @param $bool boolean
1052
	 * @return $this
1053
	 */
1054
	public function setStrictFormMethodCheck($bool) {
1055
		$this->strictFormMethodCheck = (bool)$bool;
1056
		return $this;
1057
	}
1058
1059
	/**
1060
	 * @return boolean
1061
	 */
1062
	public function getStrictFormMethodCheck() {
1063
		return $this->strictFormMethodCheck;
1064
	}
1065
1066
	/**
1067
	 * Return the form's action attribute.
1068
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1069
	 *
1070
	 * @return string
1071
	 */
1072
	public function FormAction() {
1073
		if ($this->formActionPath) {
1074
			return $this->formActionPath;
1075
		}
1076
1077
		// Respect FormObjectLink() method
1078
		if($this->controller->hasMethod("FormObjectLink")) {
1079
			$link = $this->controller->FormObjectLink($this->getName());
1080
		} else {
1081
			$link = Controller::join_links($this->controller->Link(), $this->getName());
1082
		}
1083
1084
		// Join with action and decorate
1085
        $this->extend('updateLink', $link);
1086
        return $link;
1087
	}
1088
1089
	/**
1090
	 * Set the form action attribute to a custom URL.
1091
	 *
1092
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1093
	 * recommended only for situations where you have two relatively distinct
1094
	 * parts of the system trying to communicate via a form post.
1095
	 *
1096
	 * @param string $path
1097
	 * @return $this
1098
	 */
1099
	public function setFormAction($path) {
1100
		$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...
1101
1102
		return $this;
1103
	}
1104
1105
	/**
1106
	 * Returns the name of the form.
1107
	 *
1108
	 * @return string
1109
	 */
1110
	public function FormName() {
1111
		return $this->getTemplateHelper()->generateFormID($this);
1112
	}
1113
1114
	/**
1115
	 * Set the HTML ID attribute of the form.
1116
	 *
1117
	 * @param string $id
1118
	 * @return $this
1119
	 */
1120
	public function setHTMLID($id) {
1121
		$this->htmlID = $id;
1122
1123
		return $this;
1124
	}
1125
1126
	/**
1127
	 * @return string
1128
	 */
1129
	public function getHTMLID() {
1130
		return $this->htmlID;
1131
	}
1132
1133
	/**
1134
	 * Returns this form's controller.
1135
	 *
1136
	 * @return Controller
1137
	 * @deprecated 4.0
1138
	 */
1139
	public function Controller() {
1140
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1141
1142
		return $this->getController();
1143
	}
1144
1145
	/**
1146
	 * Get the controller.
1147
	 *
1148
	 * @return Controller
1149
	 */
1150
	public function getController() {
1151
		return $this->controller;
1152
	}
1153
1154
	/**
1155
	 * Set the controller.
1156
	 *
1157
	 * @param Controller $controller
1158
	 * @return Form
1159
	 */
1160
	public function setController($controller) {
1161
		$this->controller = $controller;
1162
1163
		return $this;
1164
	}
1165
1166
	/**
1167
	 * Get the name of the form.
1168
	 *
1169
	 * @return string
1170
	 */
1171
	public function getName() {
1172
		return $this->name;
1173
	}
1174
1175
	/**
1176
	 * Set the name of the form.
1177
	 *
1178
	 * @param string $name
1179
	 * @return Form
1180
	 */
1181
	public function setName($name) {
1182
		$this->name = $name;
1183
1184
		return $this;
1185
	}
1186
1187
	/**
1188
	 * Returns an object where there is a method with the same name as each data
1189
	 * field on the form.
1190
	 *
1191
	 * That method will return the field itself.
1192
	 *
1193
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1194
	 */
1195
	public function FieldMap() {
1196
		return new Form_FieldMap($this);
1197
	}
1198
1199
	/**
1200
	 * The next functions store and modify the forms
1201
	 * message attributes. messages are stored in session under
1202
	 * $_SESSION[formname][message];
1203
	 *
1204
	 * @return string
1205
	 */
1206
	public function Message() {
1207
		$this->getMessageFromSession();
1208
1209
		return $this->message;
1210
	}
1211
1212
	/**
1213
	 * @return string
1214
	 */
1215
	public function MessageType() {
1216
		$this->getMessageFromSession();
1217
1218
		return $this->messageType;
1219
	}
1220
1221
	/**
1222
	 * @return string
1223
	 */
1224
	protected function getMessageFromSession() {
1225
		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...
1226
			return $this->message;
1227
		} else {
1228
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1229
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1230
1231
			return $this->message;
1232
		}
1233
	}
1234
1235
	/**
1236
	 * Set a status message for the form.
1237
	 *
1238
	 * @param string $message the text of the message
1239
	 * @param string $type Should be set to good, bad, or warning.
1240
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1241
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1242
	 *                            user supplied data in the message.
1243
	 * @return $this
1244
	 */
1245
	public function setMessage($message, $type, $escapeHtml = true) {
1246
		$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...
1247
		$this->messageType = $type;
1248
		return $this;
1249
	}
1250
1251
	/**
1252
	 * Set a message to the session, for display next time this form is shown.
1253
	 *
1254
	 * @param string $message the text of the message
1255
	 * @param string $type Should be set to good, bad, or warning.
1256
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1257
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1258
	 *                            user supplied data in the message.
1259
	 */
1260
	public function sessionMessage($message, $type, $escapeHtml = true) {
1261
		Session::set(
1262
			"FormInfo.{$this->FormName()}.formError.message",
1263
			$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...
1264
		);
1265
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1266
	}
1267
1268
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1269
		Session::set(
1270
			"FormInfo.{$formName}.formError.message",
1271
			$escapeHtml ? Convert::raw2xml($message) : $message
1272
		);
1273
		Session::set("FormInfo.{$formName}.formError.type", $type);
1274
	}
1275
1276
	public function clearMessage() {
1277
		$this->message  = null;
1278
		Session::clear("FormInfo.{$this->FormName()}.errors");
1279
		Session::clear("FormInfo.{$this->FormName()}.formError");
1280
		Session::clear("FormInfo.{$this->FormName()}.data");
1281
	}
1282
1283
	public function resetValidation() {
1284
		Session::clear("FormInfo.{$this->FormName()}.errors");
1285
		Session::clear("FormInfo.{$this->FormName()}.data");
1286
	}
1287
1288
	/**
1289
	 * Returns the DataObject that has given this form its data
1290
	 * through {@link loadDataFrom()}.
1291
	 *
1292
	 * @return DataObject
1293
	 */
1294
	public function getRecord() {
1295
		return $this->record;
1296
	}
1297
1298
	/**
1299
	 * Get the legend value to be inserted into the
1300
	 * <legend> element in Form.ss
1301
	 *
1302
	 * @return string
1303
	 */
1304
	public function getLegend() {
1305
		return $this->legend;
1306
	}
1307
1308
	/**
1309
	 * Processing that occurs before a form is executed.
1310
	 *
1311
	 * This includes form validation, if it fails, we redirect back
1312
	 * to the form with appropriate error messages.
1313
	 *
1314
	 * Triggered through {@link httpSubmission()}.
1315
	 *
1316
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1317
	 * if it fails the form data will never reach this method.
1318
	 *
1319
	 * @return boolean
1320
	 */
1321
	public function validate(){
1322
		if($this->validator){
1323
			$errors = $this->validator->validate();
1324
1325
			if($errors){
1326
				// Load errors into session and post back
1327
				$data = $this->getData();
1328
1329
				// Encode validation messages as XML before saving into session state
1330
				// As per Form::addErrorMessage()
1331
				$errors = array_map(function($error) {
1332
					// Encode message as XML by default
1333
					if($error['message'] instanceof DBField) {
1334
						$error['message'] = $error['message']->forTemplate();;
1335
					} else {
1336
						$error['message'] = Convert::raw2xml($error['message']);
1337
					}
1338
					return $error;
1339
				}, $errors);
1340
1341
				Session::set("FormInfo.{$this->FormName()}.errors", $errors);
1342
				Session::set("FormInfo.{$this->FormName()}.data", $data);
1343
1344
				return false;
1345
			}
1346
		}
1347
1348
		return true;
1349
	}
1350
1351
	const MERGE_DEFAULT = 0;
1352
	const MERGE_CLEAR_MISSING = 1;
1353
	const MERGE_IGNORE_FALSEISH = 2;
1354
1355
	/**
1356
	 * Load data from the given DataObject or array.
1357
	 *
1358
	 * It will call $object->MyField to get the value of MyField.
1359
	 * If you passed an array, it will call $object[MyField].
1360
	 * Doesn't save into dataless FormFields ({@link DatalessField}),
1361
	 * as determined by {@link FieldList->dataFields()}.
1362
	 *
1363
	 * By default, if a field isn't set (as determined by isset()),
1364
	 * its value will not be saved to the field, retaining
1365
	 * potential existing values.
1366
	 *
1367
	 * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1368
	 * Escaping happens automatically on saving the data through {@link saveInto()}.
1369
	 *
1370
	 * Escaping happens automatically on saving the data through
1371
	 * {@link saveInto()}.
1372
	 *
1373
	 * @uses FieldList->dataFields()
1374
	 * @uses FormField->setValue()
1375
	 *
1376
	 * @param array|DataObject $data
1377
	 * @param int $mergeStrategy
1378
	 *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1379
	 *  what that property/key's value is.
1380
	 *
1381
	 *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1382
	 *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1383
	 *  "left alone", meaning they retain any previous value.
1384
	 *
1385
	 *  You can pass a bitmask here to change this behaviour.
1386
	 *
1387
	 *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1388
	 *  {@link $data} are cleared.
1389
	 *
1390
	 *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1391
	 *  a field's value.
1392
	 *
1393
	 *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1394
	 *  CLEAR_MISSING
1395
	 *
1396
	 * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1397
	 * form that has some fields that save to one object, and some that save to another.
1398
	 * @return Form
1399
	 */
1400
	public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) {
1401
		if(!is_object($data) && !is_array($data)) {
1402
			user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1403
			return $this;
1404
		}
1405
1406
		// Handle the backwards compatible case of passing "true" as the second argument
1407
		if ($mergeStrategy === true) {
1408
			$mergeStrategy = self::MERGE_CLEAR_MISSING;
1409
		}
1410
		else if ($mergeStrategy === false) {
1411
			$mergeStrategy = 0;
1412
		}
1413
1414
		// if an object is passed, save it for historical reference through {@link getRecord()}
1415
		if(is_object($data)) $this->record = $data;
1416
1417
		// dont include fields without data
1418
		/** @var FormField[] $dataFields */
1419
		$dataFields = $this->Fields()->dataFields();
1420
		if($dataFields) foreach($dataFields as $field) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1421
			$name = $field->getName();
1422
1423
			// Skip fields that have been excluded
1424
			if($fieldList && !in_array($name, $fieldList)) continue;
1425
1426
			// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1427
			if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
1428
1429
			// Does this property exist on $data?
1430
			$exists = false;
1431
			// The value from $data for this field
1432
			$val = null;
1433
1434
			if(is_object($data)) {
1435
				$exists = (
1436
					isset($data->$name) ||
1437
					$data->hasMethod($name) ||
1438
					($data->hasMethod('hasField') && $data->hasField($name))
1439
				);
1440
1441
				if ($exists) {
1442
					$val = $data->__get($name);
1443
				}
1444
			}
1445
			else if(is_array($data)){
1446
				if(array_key_exists($name, $data)) {
1447
					$exists = true;
1448
					$val = $data[$name];
1449
				}
1450
				// If field is in array-notation we need to access nested data
1451
				else if(preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1452
					//discard first match which is just the whole string
1453
					array_shift($matches);
1454
1455
					$keys = array_pop($matches);
1456
					$name = array_shift($matches);
1457
					$name = array_shift($name);
1458
1459
					if (array_key_exists($name, $data)) {
1460
						$tmpData = &$data[$name];
1461
						// drill down into the data array looking for the corresponding value
1462
						foreach ($keys as $arrayKey) {
1463
							if ($arrayKey !== '') {
1464
								$tmpData = &$tmpData[$arrayKey];
1465
							} else {
1466
								//empty square brackets means new array
1467
								if (is_array($tmpData)) {
1468
									$tmpData = array_shift($tmpData);
1469
								}
1470
							}
1471
						}
1472
						if ($tmpData) {
1473
							$val = $tmpData;
1474
							$exists = true;
1475
						}
1476
					}
1477
				}
1478
			}
1479
1480
			// save to the field if either a value is given, or loading of blank/undefined values is forced
1481
			if($exists){
1482
				if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
1483
					// pass original data as well so composite fields can act on the additional information
1484
					$field->setValue($val, $data);
0 ignored issues
show
Unused Code introduced by
The call to FormField::setValue() has too many arguments starting with $data.

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

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

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

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

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

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

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

Loading history...
1489
			}
1490
		}
1491
1492
		return $this;
1493
	}
1494
1495
	/**
1496
	 * Save the contents of this form into the given data object.
1497
	 * It will make use of setCastedField() to do this.
1498
	 *
1499
	 * @param DataObjectInterface $dataObject The object to save data into
1500
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1501
	 * form that has some fields that save to one object, and some that save to another.
1502
	 */
1503
	public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
1504
		$dataFields = $this->fields->saveableFields();
1505
		$lastField = null;
1506
		if($dataFields) foreach($dataFields as $field) {
1507
			// Skip fields that have been excluded
1508
			if($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) continue;
1509
1510
1511
			$saveMethod = "save{$field->getName()}";
1512
1513
			if($field->getName() == "ClassName"){
1514
				$lastField = $field;
1515
			}else if( $dataObject->hasMethod( $saveMethod ) ){
1516
				$dataObject->$saveMethod( $field->dataValue());
1517
			} else if($field->getName() != "ID"){
1518
				$field->saveInto($dataObject);
1519
			}
1520
		}
1521
		if($lastField) $lastField->saveInto($dataObject);
1522
	}
1523
1524
	/**
1525
	 * Get the submitted data from this form through
1526
	 * {@link FieldList->dataFields()}, which filters out
1527
	 * any form-specific data like form-actions.
1528
	 * Calls {@link FormField->dataValue()} on each field,
1529
	 * which returns a value suitable for insertion into a DataObject
1530
	 * property.
1531
	 *
1532
	 * @return array
1533
	 */
1534
	public function getData() {
1535
		$dataFields = $this->fields->dataFields();
1536
		$data = array();
1537
1538
		if($dataFields){
1539
			foreach($dataFields as $field) {
1540
				if($field->getName()) {
1541
					$data[$field->getName()] = $field->dataValue();
1542
				}
1543
			}
1544
		}
1545
1546
		return $data;
1547
	}
1548
1549
	/**
1550
	 * Call the given method on the given field.
1551
	 *
1552
	 * @param array $data
1553
	 * @return mixed
1554
	 */
1555
	public function callfieldmethod($data) {
1556
		$fieldName = $data['fieldName'];
1557
		$methodName = $data['methodName'];
1558
		$fields = $this->fields->dataFields();
1559
1560
		// special treatment needed for TableField-class and TreeDropdownField
1561
		if(strpos($fieldName, '[')) {
1562
			preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
1563
			preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
1564
			$tableFieldName = $fieldNameMatches[1][0];
1565
			$subFieldName = $subFieldMatches[1][1];
1566
		}
1567
1568
		if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
1569
			$field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
1570
			return $field->$methodName();
1571
		} else if(isset($fields[$fieldName])) {
1572
			return $fields[$fieldName]->$methodName();
1573
		} else {
1574
			user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
1575
		}
1576
	}
1577
1578
	/**
1579
	 * Return a rendered version of this form.
1580
	 *
1581
	 * This is returned when you access a form as $FormObject rather
1582
	 * than <% with FormObject %>
1583
	 *
1584
	 * @return HTML
1585
	 */
1586
	public function forTemplate() {
1587
		$return = $this->renderWith(array_merge(
1588
			(array)$this->getTemplate(),
1589
			array('Form')
1590
		));
1591
1592
		// Now that we're rendered, clear message
1593
		$this->clearMessage();
1594
1595
		return $return;
1596
	}
1597
1598
	/**
1599
	 * Return a rendered version of this form, suitable for ajax post-back.
1600
	 *
1601
	 * It triggers slightly different behaviour, such as disabling the rewriting
1602
	 * of # links.
1603
	 *
1604
	 * @return HTML
1605
	 */
1606
	public function forAjaxTemplate() {
1607
		$view = new SSViewer(array(
1608
			$this->getTemplate(),
1609
			'Form'
1610
		));
1611
1612
		$return = $view->dontRewriteHashlinks()->process($this);
1613
1614
		// Now that we're rendered, clear message
1615
		$this->clearMessage();
1616
1617
		return $return;
1618
	}
1619
1620
	/**
1621
	 * Returns an HTML rendition of this form, without the <form> tag itself.
1622
	 *
1623
	 * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1624
	 * and _form_enctype.  These are the attributes of the form.  These fields
1625
	 * can be used to send the form to Ajax.
1626
	 *
1627
	 * @return HTML
1628
	 */
1629
	public function formHtmlContent() {
1630
		$this->IncludeFormTag = false;
1631
		$content = $this->forTemplate();
1632
		$this->IncludeFormTag = true;
1633
1634
		$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1635
			. " value=\"" . $this->FormAction() . "\" />\n";
1636
		$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1637
		$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1638
		$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1639
1640
		return $content;
1641
	}
1642
1643
	/**
1644
	 * Render this form using the given template, and return the result as a string
1645
	 * You can pass either an SSViewer or a template name
1646
	 * @param string|array $template
1647
	 * @return HTMLText
1648
	 */
1649
	public function renderWithoutActionButton($template) {
1650
		$custom = $this->customise(array(
1651
			"Actions" => "",
1652
		));
1653
1654
		if(is_string($template)) {
1655
			$template = new SSViewer($template);
1656
		}
1657
1658
		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...
1659
	}
1660
1661
1662
	/**
1663
	 * Sets the button that was clicked.  This should only be called by the Controller.
1664
	 *
1665
	 * @param callable $funcName The name of the action method that will be called.
1666
	 * @return $this
1667
	 */
1668
	public function setButtonClicked($funcName) {
1669
		$this->buttonClickedFunc = $funcName;
1670
1671
		return $this;
1672
	}
1673
1674
	/**
1675
	 * @return FormAction
1676
	 */
1677
	public function buttonClicked() {
1678
		$actions = $this->getAllActions();
1679
 		foreach ($actions as $action) {
1680
			if ($this->buttonClickedFunc === $action->actionName()) {
1681
				return $action;
1682
			}
1683
		}
1684
1685
		return null;
1686
	}
1687
1688
	/**
1689
	 * Get a list of all actions, including those in the main "fields" FieldList
1690
	 *
1691
	 * @return array
1692
	 */
1693
	protected function getAllActions() {
1694
		$fields = $this->fields->dataFields() ?: array();
1695
		$actions = $this->actions->dataFields() ?: array();
1696
1697
		$fieldsAndActions = array_merge($fields, $actions);
1698
		$actions = array_filter($fieldsAndActions, function($fieldOrAction) {
1699
			return $fieldOrAction instanceof FormAction;
1700
		});
1701
1702
		return $actions;
1703
	}
1704
1705
	/**
1706
	 * Return the default button that should be clicked when another one isn't
1707
	 * available.
1708
	 *
1709
	 * @return FormAction
1710
	 */
1711
	public function defaultAction() {
1712
		if($this->hasDefaultAction && $this->actions) {
1713
			return $this->actions->First();
1714
		}
1715
	}
1716
1717
	/**
1718
	 * Disable the default button.
1719
	 *
1720
	 * Ordinarily, when a form is processed and no action_XXX button is
1721
	 * available, then the first button in the actions list will be pressed.
1722
	 * However, if this is "delete", for example, this isn't such a good idea.
1723
	 *
1724
	 * @return Form
1725
	 */
1726
	public function disableDefaultAction() {
1727
		$this->hasDefaultAction = false;
1728
1729
		return $this;
1730
	}
1731
1732
	/**
1733
	 * Disable the requirement of a security token on this form instance. This
1734
	 * security protects against CSRF attacks, but you should disable this if
1735
	 * you don't want to tie a form to a session - eg a search form.
1736
	 *
1737
	 * Check for token state with {@link getSecurityToken()} and
1738
	 * {@link SecurityToken->isEnabled()}.
1739
	 *
1740
	 * @return Form
1741
	 */
1742
	public function disableSecurityToken() {
1743
		$this->securityToken = new NullSecurityToken();
1744
1745
		return $this;
1746
	}
1747
1748
	/**
1749
	 * Enable {@link SecurityToken} protection for this form instance.
1750
	 *
1751
	 * Check for token state with {@link getSecurityToken()} and
1752
	 * {@link SecurityToken->isEnabled()}.
1753
	 *
1754
	 * @return Form
1755
	 */
1756
	public function enableSecurityToken() {
1757
		$this->securityToken = new SecurityToken();
1758
1759
		return $this;
1760
	}
1761
1762
	/**
1763
	 * Returns the security token for this form (if any exists).
1764
	 *
1765
	 * Doesn't check for {@link securityTokenEnabled()}.
1766
	 *
1767
	 * Use {@link SecurityToken::inst()} to get a global token.
1768
	 *
1769
	 * @return SecurityToken|null
1770
	 */
1771
	public function getSecurityToken() {
1772
		return $this->securityToken;
1773
	}
1774
1775
	/**
1776
	 * Returns the name of a field, if that's the only field that the current
1777
	 * controller is interested in.
1778
	 *
1779
	 * It checks for a call to the callfieldmethod action.
1780
	 *
1781
	 * @return string
1782
	 */
1783
	public static function single_field_required() {
1784
		if(self::current_action() == 'callfieldmethod') {
1785
			return $_REQUEST['fieldName'];
1786
	}
1787
	}
1788
1789
	/**
1790
	 * Return the current form action being called, if available.
1791
	 *
1792
	 * @return string
1793
	 */
1794
	public static function current_action() {
1795
		return self::$current_action;
1796
	}
1797
1798
	/**
1799
	 * Set the current form action. Should only be called by {@link Controller}.
1800
	 *
1801
	 * @param string $action
1802
	 */
1803
	public static function set_current_action($action) {
1804
		self::$current_action = $action;
1805
	}
1806
1807
	/**
1808
	 * Compiles all CSS-classes.
1809
	 *
1810
	 * @return string
1811
	 */
1812
	public function extraClass() {
1813
		return implode(' ', array_unique($this->extraClasses));
1814
	}
1815
1816
	/**
1817
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1818
	 * be added by delimiting a string with spaces.
1819
	 *
1820
	 * @param string $class A string containing a classname or several class
1821
	 *                names delimited by a single space.
1822
	 * @return $this
1823
	 */
1824
	public function addExtraClass($class) {
1825
		//split at white space
1826
		$classes = preg_split('/\s+/', $class);
1827
		foreach($classes as $class) {
1828
			//add classes one by one
1829
			$this->extraClasses[$class] = $class;
1830
		}
1831
		return $this;
1832
	}
1833
1834
	/**
1835
	 * Remove a CSS-class from the form-container. Multiple class names can
1836
	 * be passed through as a space delimited string
1837
	 *
1838
	 * @param string $class
1839
	 * @return $this
1840
	 */
1841
	public function removeExtraClass($class) {
1842
		//split at white space
1843
		$classes = preg_split('/\s+/', $class);
1844
		foreach ($classes as $class) {
1845
			//unset one by one
1846
			unset($this->extraClasses[$class]);
1847
		}
1848
		return $this;
1849
	}
1850
1851
	public function debug() {
1852
		$result = "<h3>$this->class</h3><ul>";
1853
		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...
1854
			$result .= "<li>$field" . $field->debug() . "</li>";
1855
		}
1856
		$result .= "</ul>";
1857
1858
		if( $this->validator )
1859
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1860
1861
		return $result;
1862
	}
1863
1864
1865
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1866
	// TESTING HELPERS
1867
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1868
1869
	/**
1870
	 * Test a submission of this form.
1871
	 * @param string $action
1872
	 * @param array $data
1873
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1874
	 * your unit test.
1875
	 * @throws SS_HTTPResponse_Exception
1876
	 */
1877
	public function testSubmission($action, $data) {
1878
		$data['action_' . $action] = true;
1879
1880
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1881
	}
1882
1883
	/**
1884
	 * Test an ajax submission of this form.
1885
	 *
1886
	 * @param string $action
1887
	 * @param array $data
1888
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1889
	 * your unit test.
1890
	 */
1891
	public function testAjaxSubmission($action, $data) {
1892
		$data['ajax'] = 1;
1893
		return $this->testSubmission($action, $data);
1894
	}
1895
}
1896
1897
/**
1898
 * @package forms
1899
 * @subpackage core
1900
 */
1901
class Form_FieldMap extends ViewableData {
1902
1903
	protected $form;
1904
1905
	public function __construct($form) {
1906
		$this->form = $form;
1907
		parent::__construct();
1908
	}
1909
1910
	/**
1911
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1912
	 * @param string $method
1913
	 * @return bool
1914
	 */
1915
	public function hasMethod($method) {
1916
		return true;
1917
	}
1918
1919
	public function __call($method, $args = null) {
1920
		return $this->form->Fields()->fieldByName($method);
1921
	}
1922
}
1923