Completed
Push — master ( 01ba7c...6d88ca )
by Damian
21:36 queued 11:17
created

Form::getValidationErrorResponse()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 37
rs 5.3846
cc 8
eloc 23
nc 7
nop 0
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 callable {@see setValidationResponseCallback()}
84
	 */
85
	protected $validationResponseCallback;
86
87
	/**
88
	 * @var string
89
	 */
90
	protected $formMethod = "POST";
91
92
	/**
93
	 * @var boolean
94
	 */
95
	protected $strictFormMethodCheck = false;
96
97
	/**
98
	 * @var string|null
99
	 */
100
	protected static $current_action;
101
102
	/**
103
	 * @var DataObject|null $record Populated by {@link loadDataFrom()}.
104
	 */
105
	protected $record;
106
107
	/**
108
	 * Keeps track of whether this form has a default action or not.
109
	 * Set to false by $this->disableDefaultAction();
110
	 *
111
	 * @var boolean
112
	 */
113
	protected $hasDefaultAction = true;
114
115
	/**
116
	 * Target attribute of form-tag.
117
	 * Useful to open a new window upon
118
	 * form submission.
119
	 *
120
	 * @var string|null
121
	 */
122
	protected $target;
123
124
	/**
125
	 * Legend value, to be inserted into the
126
	 * <legend> element before the <fieldset>
127
	 * in Form.ss template.
128
	 *
129
	 * @var string|null
130
	 */
131
	protected $legend;
132
133
	/**
134
	 * The SS template to render this form HTML into.
135
	 * Default is "Form", but this can be changed to
136
	 * another template for customisation.
137
	 *
138
	 * @see Form->setTemplate()
139
	 * @var string|null
140
	 */
141
	protected $template;
142
143
	/**
144
	 * @var callable|null
145
	 */
146
	protected $buttonClickedFunc;
147
148
	/**
149
	 * @var string|null
150
	 */
151
	protected $message;
152
153
	/**
154
	 * @var string|null
155
	 */
156
	protected $messageType;
157
158
	/**
159
	 * Should we redirect the user back down to the
160
	 * the form on validation errors rather then just the page
161
	 *
162
	 * @var bool
163
	 */
164
	protected $redirectToFormOnValidationError = false;
165
166
	/**
167
	 * @var bool
168
	 */
169
	protected $security = true;
170
171
	/**
172
	 * @var SecurityToken|null
173
	 */
174
	protected $securityToken = null;
175
176
	/**
177
	 * @var array $extraClasses List of additional CSS classes for the form tag.
178
	 */
179
	protected $extraClasses = array();
180
181
	/**
182
	 * @config
183
	 * @var array $default_classes The default classes to apply to the Form
184
	 */
185
	private static $default_classes = array();
186
187
	/**
188
	 * @var string|null
189
	 */
190
	protected $encType;
191
192
	/**
193
	 * @var array Any custom form attributes set through {@link setAttributes()}.
194
	 * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
195
	 */
196
	protected $attributes = array();
197
198
	/**
199
	 * @var array
200
	 */
201
	protected $validationExemptActions = array();
202
203
	private static $allowed_actions = array(
204
		'handleField',
205
		'httpSubmission',
206
		'forTemplate',
207
	);
208
209
	/**
210
	 * @var FormTemplateHelper
211
	 */
212
	private $templateHelper = null;
213
214
	/**
215
	 * @ignore
216
	 */
217
	private $htmlID = null;
218
219
	/**
220
	 * @ignore
221
	 */
222
	private $formActionPath = false;
223
224
	/**
225
	 * @var bool
226
	 */
227
	protected $securityTokenAdded = false;
228
229
	/**
230
	 * Create a new form, with the given fields an action buttons.
231
	 *
232
	 * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
233
	 * @param string $name The method on the controller that will return this form object.
234
	 * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
235
	 * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
236
	 *                           {@link FormAction} objects
237
	 * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
238
	 */
239
	public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null) {
240
		parent::__construct();
241
242
		$fields->setForm($this);
243
		$actions->setForm($this);
244
245
		$this->fields = $fields;
246
		$this->actions = $actions;
247
		$this->controller = $controller;
248
		$this->name = $name;
249
250
		if(!$this->controller) user_error("$this->class form created without a controller", E_USER_ERROR);
251
252
		// Form validation
253
		$this->validator = ($validator) ? $validator : new RequiredFields();
254
		$this->validator->setForm($this);
255
256
		// Form error controls
257
		$this->setupFormErrors();
258
259
		// Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
260
		// method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
261
		if(method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod')
262
				&& $controller->hasMethod('securityTokenEnabled'))) {
263
264
			$securityEnabled = $controller->securityTokenEnabled();
265
		} else {
266
			$securityEnabled = SecurityToken::is_enabled();
267
		}
268
269
		$this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
270
271
		$this->setupDefaultClasses();
272
	}
273
274
	/**
275
	 * @var array
276
	 */
277
	private static $url_handlers = array(
278
		'field/$FieldName!' => 'handleField',
279
		'POST ' => 'httpSubmission',
280
		'GET ' => 'httpSubmission',
281
		'HEAD ' => 'httpSubmission',
282
	);
283
284
	/**
285
	 * Set up current form errors in session to
286
	 * the current form if appropriate.
287
	 *
288
	 * @return $this
289
	 */
290
	public function setupFormErrors() {
291
		$errorInfo = Session::get("FormInfo.{$this->FormName()}");
292
293
		if(isset($errorInfo['errors']) && is_array($errorInfo['errors'])) {
294
			foreach($errorInfo['errors'] as $error) {
295
				$field = $this->fields->dataFieldByName($error['fieldName']);
296
297
				if(!$field) {
298
					$errorInfo['message'] = $error['message'];
299
					$errorInfo['type'] = $error['messageType'];
300
				} else {
301
					$field->setError($error['message'], $error['messageType']);
302
				}
303
			}
304
305
			// load data in from previous submission upon error
306
			if(isset($errorInfo['data'])) $this->loadDataFrom($errorInfo['data']);
307
		}
308
309
		if(isset($errorInfo['message']) && isset($errorInfo['type'])) {
310
			$this->setMessage($errorInfo['message'], $errorInfo['type']);
311
		}
312
313
		return $this;
314
	}
315
316
	/**
317
	 * set up the default classes for the form. This is done on construct so that the default classes can be removed
318
	 * after instantiation
319
	 */
320
	protected function setupDefaultClasses() {
321
		$defaultClasses = self::config()->get('default_classes');
322
		if ($defaultClasses) {
323
			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...
324
				$this->addExtraClass($class);
325
			}
326
		}
327
	}
328
329
	/**
330
	 * Handle a form submission.  GET and POST requests behave identically.
331
	 * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
332
	 * and only triggers the requested form action/method
333
	 * if the form is valid.
334
	 *
335
	 * @param SS_HTTPRequest $request
336
	 * @throws SS_HTTPResponse_Exception
337
	 */
338
	public function httpSubmission($request) {
339
		// Strict method check
340
		if($this->strictFormMethodCheck) {
341
342
			// Throws an error if the method is bad...
343
			if($this->formMethod != $request->httpMethod()) {
344
				$response = Controller::curr()->getResponse();
345
				$response->addHeader('Allow', $this->formMethod);
346
				$this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
347
			}
348
349
			// ...and only uses the variables corresponding to that method type
350
			$vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
351
		} else {
352
			$vars = $request->requestVars();
353
		}
354
355
		// Populate the form
356
		$this->loadDataFrom($vars, true);
0 ignored issues
show
Bug introduced by
It seems like $vars defined by $request->requestVars() on line 352 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...
357
358
		// Protection against CSRF attacks
359
		$token = $this->getSecurityToken();
360
		if( ! $token->checkRequest($request)) {
361
			$securityID = $token->getName();
362
			if (empty($vars[$securityID])) {
363
				$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
364
					"There seems to have been a technical problem. Please click the back button, ".
365
					"refresh your browser, and try again."
366
				));
367
			} else {
368
				// Clear invalid token on refresh
369
				$data = $this->getData();
370
				unset($data[$securityID]);
371
				Session::set("FormInfo.{$this->FormName()}.data", $data);
372
				Session::set("FormInfo.{$this->FormName()}.errors", array());
373
				$this->sessionMessage(
374
					_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
375
					"warning"
376
				);
377
				return $this->controller->redirectBack();
378
			}
379
		}
380
381
		// Determine the action button clicked
382
		$funcName = null;
383
		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...
384
			if(substr($paramName,0,7) == 'action_') {
385
				// Break off querystring arguments included in the action
386
				if(strpos($paramName,'?') !== false) {
387
					list($paramName, $paramVars) = explode('?', $paramName, 2);
388
					$newRequestParams = array();
389
					parse_str($paramVars, $newRequestParams);
390
					$vars = array_merge((array)$vars, (array)$newRequestParams);
391
				}
392
393
				// Cleanup action_, _x and _y from image fields
394
				$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
395
				break;
396
			}
397
		}
398
399
		// If the action wasn't set, choose the default on the form.
400
		if(!isset($funcName) && $defaultAction = $this->defaultAction()){
401
			$funcName = $defaultAction->actionName();
402
		}
403
404
		if(isset($funcName)) {
405
			Form::set_current_action($funcName);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
406
			$this->setButtonClicked($funcName);
407
		}
408
409
		// Permission checks (first on controller, then falling back to form)
410
		if(
411
			// Ensure that the action is actually a button or method on the form,
412
			// and not just a method on the controller.
413
			$this->controller->hasMethod($funcName)
414
			&& !$this->controller->checkAccessAction($funcName)
415
			// If a button exists, allow it on the controller
416
			&& !$this->actions->dataFieldByName('action_' . $funcName)
417
		) {
418
			return $this->httpError(
419
				403,
420
				sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
421
			);
422
		} elseif(
423
			$this->hasMethod($funcName)
424
			&& !$this->checkAccessAction($funcName)
425
			// No checks for button existence or $allowed_actions is performed -
426
			// all form methods are callable (e.g. the legacy "callfieldmethod()")
427
		) {
428
			return $this->httpError(
429
				403,
430
				sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
431
			);
432
		}
433
		// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
434
		// explicitly in allowed_actions in order to run)
435
		// Uncomment the following for checking security against running actions on form fields
436
		/* else {
0 ignored issues
show
Unused Code Comprehensibility introduced by
51% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
896
897
		// Remove excluded
898
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
899
900
		// Prepare HTML-friendly 'method' attribute (lower-case)
901
		if (isset($attrs['method'])) {
902
			$attrs['method'] = strtolower($attrs['method']);
903
		}
904
905
		// Create markup
906
		$parts = array();
907
		foreach($attrs as $name => $value) {
908
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
909
		}
910
911
		return implode(' ', $parts);
912
	}
913
914
	public function FormAttributes() {
915
		return $this->getAttributesHTML();
916
	}
917
918
	/**
919
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
920
	 * another frame
921
	 *
922
	 * @param string|FormTemplateHelper
923
	 */
924
	public function setTemplateHelper($helper) {
925
		$this->templateHelper = $helper;
926
	}
927
928
	/**
929
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
930
	 * set, return the default helper.
931
	 *
932
	 * @return FormTemplateHelper
933
	 */
934
	public function getTemplateHelper() {
935
		if($this->templateHelper) {
936
			if(is_string($this->templateHelper)) {
937
				return Injector::inst()->get($this->templateHelper);
938
			}
939
940
			return $this->templateHelper;
941
		}
942
943
		return Injector::inst()->get('FormTemplateHelper');
944
	}
945
946
	/**
947
	 * Set the target of this form to any value - useful for opening the form
948
	 * contents in a new window or refreshing another frame.
949
	 *
950
	 * @param target $target The value of the target
951
	 * @return $this
952
	 */
953
	public function setTarget($target) {
954
		$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...
955
956
		return $this;
957
	}
958
959
	/**
960
	 * Set the legend value to be inserted into
961
	 * the <legend> element in the Form.ss template.
962
	 * @param string $legend
963
	 * @return $this
964
	 */
965
	public function setLegend($legend) {
966
		$this->legend = $legend;
967
		return $this;
968
	}
969
970
	/**
971
	 * Set the SS template that this form should use
972
	 * to render with. The default is "Form".
973
	 *
974
	 * @param string $template The name of the template (without the .ss extension)
975
	 * @return $this
976
	 */
977
	public function setTemplate($template) {
978
		$this->template = $template;
979
		return $this;
980
	}
981
982
	/**
983
	 * Return the template to render this form with.
984
	 * If the template isn't set, then default to the
985
	 * form class name e.g "Form".
986
	 *
987
	 * @return string
988
	 */
989
	public function getTemplate() {
990
		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...
991
		else return $this->class;
992
	}
993
994
	/**
995
	 * Returns the encoding type for the form.
996
	 *
997
	 * By default this will be URL encoded, unless there is a file field present
998
	 * in which case multipart is used. You can also set the enc type using
999
	 * {@link setEncType}.
1000
	 */
1001
	public function getEncType() {
1002
		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...
1003
			return $this->encType;
1004
		}
1005
1006
		if ($fields = $this->fields->dataFields()) {
1007
			foreach ($fields as $field) {
1008
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
1009
			}
1010
		}
1011
1012
		return self::ENC_TYPE_URLENCODED;
1013
	}
1014
1015
	/**
1016
	 * Sets the form encoding type. The most common encoding types are defined
1017
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1018
	 *
1019
	 * @param string $encType
1020
	 * @return $this
1021
	 */
1022
	public function setEncType($encType) {
1023
		$this->encType = $encType;
1024
		return $this;
1025
	}
1026
1027
	/**
1028
	 * Returns the real HTTP method for the form:
1029
	 * GET, POST, PUT, DELETE or HEAD.
1030
	 * As most browsers only support GET and POST in
1031
	 * form submissions, all other HTTP methods are
1032
	 * added as a hidden field "_method" that
1033
	 * gets evaluated in {@link Director::direct()}.
1034
	 * See {@link FormMethod()} to get a HTTP method
1035
	 * for safe insertion into a <form> tag.
1036
	 *
1037
	 * @return string HTTP method
1038
	 */
1039
	public function FormHttpMethod() {
1040
		return $this->formMethod;
1041
	}
1042
1043
	/**
1044
	 * Returns the form method to be used in the <form> tag.
1045
	 * See {@link FormHttpMethod()} to get the "real" method.
1046
	 *
1047
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1048
	 */
1049
	public function FormMethod() {
1050
		if(in_array($this->formMethod,array('GET','POST'))) {
1051
			return $this->formMethod;
1052
		} else {
1053
			return 'POST';
1054
		}
1055
	}
1056
1057
	/**
1058
	 * Set the form method: GET, POST, PUT, DELETE.
1059
	 *
1060
	 * @param string $method
1061
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1062
	 * @return $this
1063
	 */
1064
	public function setFormMethod($method, $strict = null) {
1065
		$this->formMethod = strtoupper($method);
1066
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1067
		return $this;
1068
	}
1069
1070
	/**
1071
	 * If set to true, enforce the matching of the form method.
1072
	 *
1073
	 * This will mean two things:
1074
	 *  - GET vars will be ignored by a POST form, and vice versa
1075
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1076
	 *
1077
	 * If set to false (the default), then the form method is only used to construct the default
1078
	 * form.
1079
	 *
1080
	 * @param $bool boolean
1081
	 * @return $this
1082
	 */
1083
	public function setStrictFormMethodCheck($bool) {
1084
		$this->strictFormMethodCheck = (bool)$bool;
1085
		return $this;
1086
	}
1087
1088
	/**
1089
	 * @return boolean
1090
	 */
1091
	public function getStrictFormMethodCheck() {
1092
		return $this->strictFormMethodCheck;
1093
	}
1094
1095
	/**
1096
	 * Return the form's action attribute.
1097
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1098
	 *
1099
	 * @return string
1100
	 */
1101
	public function FormAction() {
1102
		if ($this->formActionPath) {
1103
			return $this->formActionPath;
1104
		} elseif($this->controller->hasMethod("FormObjectLink")) {
1105
			return $this->controller->FormObjectLink($this->name);
1106
		} else {
1107
			return Controller::join_links($this->controller->Link(), $this->name);
1108
		}
1109
	}
1110
1111
	/**
1112
	 * Set the form action attribute to a custom URL.
1113
	 *
1114
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1115
	 * recommended only for situations where you have two relatively distinct
1116
	 * parts of the system trying to communicate via a form post.
1117
	 *
1118
	 * @param string $path
1119
	 * @return $this
1120
	 */
1121
	public function setFormAction($path) {
1122
		$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...
1123
1124
		return $this;
1125
	}
1126
1127
	/**
1128
	 * Returns the name of the form.
1129
	 *
1130
	 * @return string
1131
	 */
1132
	public function FormName() {
1133
		return $this->getTemplateHelper()->generateFormID($this);
1134
	}
1135
1136
	/**
1137
	 * Set the HTML ID attribute of the form.
1138
	 *
1139
	 * @param string $id
1140
	 * @return $this
1141
	 */
1142
	public function setHTMLID($id) {
1143
		$this->htmlID = $id;
1144
1145
		return $this;
1146
	}
1147
1148
	/**
1149
	 * @return string
1150
	 */
1151
	public function getHTMLID() {
1152
		return $this->htmlID;
1153
	}
1154
1155
	/**
1156
	 * Returns this form's controller.
1157
	 *
1158
	 * @return Controller
1159
	 * @deprecated 4.0
1160
	 */
1161
	public function Controller() {
1162
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1163
1164
		return $this->getController();
1165
	}
1166
1167
	/**
1168
	 * Get the controller.
1169
	 *
1170
	 * @return Controller
1171
	 */
1172
	public function getController() {
1173
		return $this->controller;
1174
	}
1175
1176
	/**
1177
	 * Set the controller.
1178
	 *
1179
	 * @param Controller $controller
1180
	 * @return Form
1181
	 */
1182
	public function setController($controller) {
1183
		$this->controller = $controller;
1184
1185
		return $this;
1186
	}
1187
1188
	/**
1189
	 * Get the name of the form.
1190
	 *
1191
	 * @return string
1192
	 */
1193
	public function getName() {
1194
		return $this->name;
1195
	}
1196
1197
	/**
1198
	 * Set the name of the form.
1199
	 *
1200
	 * @param string $name
1201
	 * @return Form
1202
	 */
1203
	public function setName($name) {
1204
		$this->name = $name;
1205
1206
		return $this;
1207
	}
1208
1209
	/**
1210
	 * Returns an object where there is a method with the same name as each data
1211
	 * field on the form.
1212
	 *
1213
	 * That method will return the field itself.
1214
	 *
1215
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1216
	 */
1217
	public function FieldMap() {
1218
		return new Form_FieldMap($this);
1219
	}
1220
1221
	/**
1222
	 * The next functions store and modify the forms
1223
	 * message attributes. messages are stored in session under
1224
	 * $_SESSION[formname][message];
1225
	 *
1226
	 * @return string
1227
	 */
1228
	public function Message() {
1229
		$this->getMessageFromSession();
1230
1231
		return $this->message;
1232
	}
1233
1234
	/**
1235
	 * @return string
1236
	 */
1237
	public function MessageType() {
1238
		$this->getMessageFromSession();
1239
1240
		return $this->messageType;
1241
	}
1242
1243
	/**
1244
	 * @return string
1245
	 */
1246
	protected function getMessageFromSession() {
1247
		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...
1248
			return $this->message;
1249
		} else {
1250
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1251
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1252
1253
			return $this->message;
1254
		}
1255
	}
1256
1257
	/**
1258
	 * Set a status message for the form.
1259
	 *
1260
	 * @param string $message the text of the message
1261
	 * @param string $type Should be set to good, bad, or warning.
1262
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1263
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1264
	 *                            user supplied data in the message.
1265
	 * @return $this
1266
	 */
1267
	public function setMessage($message, $type, $escapeHtml = true) {
1268
		$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...
1269
		$this->messageType = $type;
1270
		return $this;
1271
	}
1272
1273
	/**
1274
	 * Set a message to the session, for display next time this form is shown.
1275
	 *
1276
	 * @param string $message the text of the message
1277
	 * @param string $type Should be set to good, bad, or warning.
1278
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1279
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1280
	 *                            user supplied data in the message.
1281
	 */
1282
	public function sessionMessage($message, $type, $escapeHtml = true) {
1283
		Session::set(
1284
			"FormInfo.{$this->FormName()}.formError.message",
1285
			$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...
1286
		);
1287
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1288
	}
1289
1290
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1291
		Session::set(
1292
			"FormInfo.{$formName}.formError.message",
1293
			$escapeHtml ? Convert::raw2xml($message) : $message
1294
		);
1295
		Session::set("FormInfo.{$formName}.formError.type", $type);
1296
	}
1297
1298
	public function clearMessage() {
1299
		$this->message  = null;
1300
		Session::clear("FormInfo.{$this->FormName()}.errors");
1301
		Session::clear("FormInfo.{$this->FormName()}.formError");
1302
		Session::clear("FormInfo.{$this->FormName()}.data");
1303
	}
1304
1305
	public function resetValidation() {
1306
		Session::clear("FormInfo.{$this->FormName()}.errors");
1307
		Session::clear("FormInfo.{$this->FormName()}.data");
1308
	}
1309
1310
	/**
1311
	 * Returns the DataObject that has given this form its data
1312
	 * through {@link loadDataFrom()}.
1313
	 *
1314
	 * @return DataObject
1315
	 */
1316
	public function getRecord() {
1317
		return $this->record;
1318
	}
1319
1320
	/**
1321
	 * Get the legend value to be inserted into the
1322
	 * <legend> element in Form.ss
1323
	 *
1324
	 * @return string
1325
	 */
1326
	public function getLegend() {
1327
		return $this->legend;
1328
	}
1329
1330
	/**
1331
	 * Processing that occurs before a form is executed.
1332
	 *
1333
	 * This includes form validation, if it fails, we redirect back
1334
	 * to the form with appropriate error messages.
1335
	 * Always return true if the current form action is exempt from validation
1336
	 *
1337
	 * Triggered through {@link httpSubmission()}.
1338
	 *
1339
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1340
	 * if it fails the form data will never reach this method.
1341
	 *
1342
	 * @return boolean
1343
	 */
1344
	public function validate(){
1345
		$buttonClicked = $this->buttonClicked();
1346
		if($buttonClicked && in_array($buttonClicked->actionName(), $this->getValidationExemptActions())) {
1347
			return true;
1348
		}
1349
1350
		if($this->validator){
1351
			$errors = $this->validator->validate();
1352
1353
			if($errors){
1354
				// Load errors into session and post back
1355
				$data = $this->getData();
1356
1357
				// Encode validation messages as XML before saving into session state
1358
				// As per Form::addErrorMessage()
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1778
		if(self::current_action() == 'callfieldmethod') {
1779
			return $_REQUEST['fieldName'];
1780
	}
1781
	}
1782
1783
	/**
1784
	 * Return the current form action being called, if available.
1785
	 *
1786
	 * @return string
1787
	 */
1788
	public static function current_action() {
1789
		return self::$current_action;
1790
	}
1791
1792
	/**
1793
	 * Set the current form action. Should only be called by {@link Controller}.
1794
	 *
1795
	 * @param string $action
1796
	 */
1797
	public static function set_current_action($action) {
1798
		self::$current_action = $action;
1799
	}
1800
1801
	/**
1802
	 * Compiles all CSS-classes.
1803
	 *
1804
	 * @return string
1805
	 */
1806
	public function extraClass() {
1807
		return implode(array_unique($this->extraClasses), ' ');
1808
	}
1809
1810
	/**
1811
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1812
	 * be added by delimiting a string with spaces.
1813
	 *
1814
	 * @param string $class A string containing a classname or several class
1815
	 *                names delimited by a single space.
1816
	 * @return $this
1817
	 */
1818
	public function addExtraClass($class) {
1819
		//split at white space
1820
		$classes = preg_split('/\s+/', $class);
1821
		foreach($classes as $class) {
1822
			//add classes one by one
1823
			$this->extraClasses[$class] = $class;
1824
		}
1825
		return $this;
1826
	}
1827
1828
	/**
1829
	 * Remove a CSS-class from the form-container. Multiple class names can
1830
	 * be passed through as a space delimited string
1831
	 *
1832
	 * @param string $class
1833
	 * @return $this
1834
	 */
1835
	public function removeExtraClass($class) {
1836
		//split at white space
1837
		$classes = preg_split('/\s+/', $class);
1838
		foreach ($classes as $class) {
1839
			//unset one by one
1840
			unset($this->extraClasses[$class]);
1841
		}
1842
		return $this;
1843
	}
1844
1845
	public function debug() {
1846
		$result = "<h3>$this->class</h3><ul>";
1847
		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...
1848
			$result .= "<li>$field" . $field->debug() . "</li>";
1849
		}
1850
		$result .= "</ul>";
1851
1852
		if( $this->validator )
1853
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1854
1855
		return $result;
1856
	}
1857
1858
1859
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1860
	// TESTING HELPERS
1861
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1862
1863
	/**
1864
	 * Test a submission of this form.
1865
	 * @param string $action
1866
	 * @param array $data
1867
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1868
	 * your unit test.
1869
	 * @throws SS_HTTPResponse_Exception
1870
	 */
1871
	public function testSubmission($action, $data) {
1872
		$data['action_' . $action] = true;
1873
1874
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1875
	}
1876
1877
	/**
1878
	 * Test an ajax submission of this form.
1879
	 *
1880
	 * @param string $action
1881
	 * @param array $data
1882
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1883
	 * your unit test.
1884
	 */
1885
	public function testAjaxSubmission($action, $data) {
1886
		$data['ajax'] = 1;
1887
		return $this->testSubmission($action, $data);
1888
	}
1889
}
1890
1891
/**
1892
 * @package forms
1893
 * @subpackage core
1894
 */
1895
class Form_FieldMap extends ViewableData {
1896
1897
	protected $form;
1898
1899
	public function __construct($form) {
1900
		$this->form = $form;
1901
		parent::__construct();
1902
	}
1903
1904
	/**
1905
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1906
	 * @param string $method
1907
	 * @return bool
1908
	 */
1909
	public function hasMethod($method) {
1910
		return true;
1911
	}
1912
1913
	public function __call($method, $args = null) {
1914
		return $this->form->Fields()->fieldByName($method);
1915
	}
1916
}
1917