Completed
Push — master ( 5c98d3...0a7e4c )
by Loz
11:47
created

Form::checkAccessAction()   C

Complexity

Conditions 9
Paths 21

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

There are different options of fixing this problem.

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

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

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

Loading history...
341
				$this->addExtraClass($class);
342
			}
343
		}
344
	}
345
346
	/**
347
	 * Handle a form submission.  GET and POST requests behave identically.
348
	 * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
349
	 * and only triggers the requested form action/method
350
	 * if the form is valid.
351
	 *
352
	 * @param SS_HTTPRequest $request
353
	 * @throws SS_HTTPResponse_Exception
354
	 */
355
	public function httpSubmission($request) {
356
		// Strict method check
357
		if($this->strictFormMethodCheck) {
358
359
			// Throws an error if the method is bad...
360
			if($this->formMethod != $request->httpMethod()) {
361
				$response = Controller::curr()->getResponse();
362
				$response->addHeader('Allow', $this->formMethod);
363
				$this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
364
			}
365
366
			// ...and only uses the variables corresponding to that method type
367
			$vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
368
		} else {
369
			$vars = $request->requestVars();
370
		}
371
372
		// Populate the form
373
		$this->loadDataFrom($vars, true);
0 ignored issues
show
Bug introduced by
It seems like $vars defined by $request->requestVars() on line 369 can also be of type null; however, Form::loadDataFrom() does only seem to accept array|object<SilverStripe\ORM\DataObject>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
374
375
		// Protection against CSRF attacks
376
		$token = $this->getSecurityToken();
377
		if( ! $token->checkRequest($request)) {
378
			$securityID = $token->getName();
379
			if (empty($vars[$securityID])) {
380
				$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
381
					"There seems to have been a technical problem. Please click the back button, ".
382
					"refresh your browser, and try again."
383
				));
384
			} else {
385
				// Clear invalid token on refresh
386
				$data = $this->getData();
387
				unset($data[$securityID]);
388
				Session::set("FormInfo.{$this->FormName()}.data", $data);
389
				Session::set("FormInfo.{$this->FormName()}.errors", array());
390
				$this->sessionMessage(
391
					_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."),
392
					"warning"
393
				);
394
				return $this->controller->redirectBack();
395
			}
396
		}
397
398
		// Determine the action button clicked
399
		$funcName = null;
400
		foreach($vars as $paramName => $paramVal) {
0 ignored issues
show
Bug introduced by
The expression $vars of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
423
			$this->setButtonClicked($funcName);
424
		}
425
426
		// Permission checks (first on controller, then falling back to form)
427
		if(
428
			// Ensure that the action is actually a button or method on the form,
429
			// and not just a method on the controller.
430
			$this->controller->hasMethod($funcName)
431
			&& !$this->controller->checkAccessAction($funcName)
432
			// If a button exists, allow it on the controller
433
			// buttonClicked() validates that the action set above is valid
434
			&& !$this->buttonClicked()
435
		) {
436
			return $this->httpError(
437
				403,
438
				sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
439
			);
440
		} elseif(
441
			$this->hasMethod($funcName)
442
			&& !$this->checkAccessAction($funcName)
443
			// No checks for button existence or $allowed_actions is performed -
444
			// all form methods are callable (e.g. the legacy "callfieldmethod()")
445
		) {
446
			return $this->httpError(
447
				403,
448
				sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
449
			);
450
		}
451
		// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
452
		// explicitly in allowed_actions in order to run)
453
		// Uncomment the following for checking security against running actions on form fields
454
		/* else {
0 ignored issues
show
Unused Code Comprehensibility introduced by
51% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
455
			// Try to find a field that has the action, and allows it
456
			$fieldsHaveMethod = false;
457
			foreach ($this->Fields() as $field){
458
				if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
459
					$fieldsHaveMethod = true;
460
				}
461
			}
462
			if (!$fieldsHaveMethod) {
463
				return $this->httpError(
464
					403,
465
					sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
466
				);
467
			}
468
		}*/
469
470
		// Validate the form
471
		if(!$this->validate()) {
472
			return $this->getValidationErrorResponse();
473
		}
474
475
		// First, try a handler method on the controller (has been checked for allowed_actions above already)
476
		if($this->controller->hasMethod($funcName)) {
477
			return $this->controller->$funcName($vars, $this, $request);
478
		// Otherwise, try a handler method on the form object.
479
		} elseif($this->hasMethod($funcName)) {
480
			return $this->$funcName($vars, $this, $request);
481
		} elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
0 ignored issues
show
Bug introduced by
It seems like $this->Fields() can be null; however, checkFieldsForAction() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
482
			return $field->$funcName($vars, $this, $request);
483
		}
484
485
		return $this->httpError(404);
486
	}
487
488
	/**
489
	 * @param string $action
490
	 * @return bool
491
	 */
492
	public function checkAccessAction($action) {
493
		if (parent::checkAccessAction($action)) {
494
			return true;
495
		}
496
497
			// Always allow actions which map to buttons. See httpSubmission() for further access checks.
498
		$fields = $this->fields->dataFields() ?: array();
499
		$actions = $this->actions->dataFields() ?: array();
500
501
		$fieldsAndActions = array_merge($fields, $actions);
502
 		foreach ($fieldsAndActions as $fieldOrAction) {
503
			if ($fieldOrAction instanceof FormAction && $fieldOrAction->actionName() === $action) {
504
				return true;
505
			}
506
		}
507
508
		// Always allow actions on fields
509
		$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...
510
		if ($field && $field->checkAccessAction($action)) {
511
			return true;
512
		}
513
514
		return false;
515
	}
516
517
	/**
518
	 * @return callable
519
	 */
520
	public function getValidationResponseCallback() {
521
		return $this->validationResponseCallback;
522
	}
523
524
	/**
525
	 * Overrules validation error behaviour in {@link httpSubmission()}
526
	 * when validation has failed. Useful for optional handling of a certain accepted content type.
527
	 *
528
	 * The callback can opt out of handling specific responses by returning NULL,
529
	 * in which case the default form behaviour will kick in.
530
	 *
531
	 * @param $callback
532
	 * @return self
533
	 */
534
	public function setValidationResponseCallback($callback) {
535
		$this->validationResponseCallback = $callback;
536
537
		return $this;
538
	}
539
540
	/**
541
	 * Returns the appropriate response up the controller chain
542
	 * if {@link validate()} fails (which is checked prior to executing any form actions).
543
	 * By default, returns different views for ajax/non-ajax request, and
544
	 * handles 'application/json' requests with a JSON object containing the error messages.
545
	 * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
546
	 * and can be overruled by setting {@link $validationResponseCallback}.
547
	 *
548
	 * @return SS_HTTPResponse|string
549
	 */
550
	protected function getValidationErrorResponse() {
551
		$callback = $this->getValidationResponseCallback();
552
		if($callback && $callbackResponse = $callback()) {
553
			return $callbackResponse;
554
		}
555
556
		$request = $this->getRequest();
557
		if($request->isAjax()) {
558
				// Special case for legacy Validator.js implementation
559
				// (assumes eval'ed javascript collected through FormResponse)
560
				$acceptType = $request->getHeader('Accept');
561
				if(strpos($acceptType, 'application/json') !== FALSE) {
562
					// Send validation errors back as JSON with a flag at the start
563
					$response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
564
					$response->addHeader('Content-Type', 'application/json');
565
				} else {
566
					$this->setupFormErrors();
567
					// Send the newly rendered form tag as HTML
568
					$response = new SS_HTTPResponse($this->forTemplate());
569
					$response->addHeader('Content-Type', 'text/html');
570
				}
571
572
				return $response;
573
			} else {
574
				if($this->getRedirectToFormOnValidationError()) {
575
					if($pageURL = $request->getHeader('Referer')) {
576
						if(Director::is_site_url($pageURL)) {
577
							// Remove existing pragmas
578
							$pageURL = preg_replace('/(#.*)/', '', $pageURL);
579
							$pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \Director::absoluteURL($pageURL, true) on line 579 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...
580
							return $this->controller->redirect($pageURL . '#' . $this->FormName());
581
						}
582
					}
583
				}
584
				return $this->controller->redirectBack();
585
			}
586
	}
587
588
	/**
589
	 * Fields can have action to, let's check if anyone of the responds to $funcname them
590
	 *
591
	 * @param SS_List|array $fields
592
	 * @param callable $funcName
593
	 * @return FormField
594
	 */
595
	protected function checkFieldsForAction($fields, $funcName) {
596
		foreach($fields as $field){
597
			if(method_exists($field, 'FieldList')) {
598
				if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
599
					return $field;
600
				}
601
			} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
602
				return $field;
603
			}
604
		}
605
	}
606
607
	/**
608
	 * Handle a field request.
609
	 * Uses {@link Form->dataFieldByName()} to find a matching field,
610
	 * and falls back to {@link FieldList->fieldByName()} to look
611
	 * for tabs instead. This means that if you have a tab and a
612
	 * formfield with the same name, this method gives priority
613
	 * to the formfield.
614
	 *
615
	 * @param SS_HTTPRequest $request
616
	 * @return FormField
617
	 */
618
	public function handleField($request) {
619
		$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
620
621
		if($field) {
622
			return $field;
623
		} else {
624
			// falling back to fieldByName, e.g. for getting tabs
625
			return $this->Fields()->fieldByName($request->param('FieldName'));
626
		}
627
	}
628
629
	/**
630
	 * Convert this form into a readonly form
631
	 */
632
	public function makeReadonly() {
633
		$this->transform(new ReadonlyTransformation());
634
	}
635
636
	/**
637
	 * Set whether the user should be redirected back down to the
638
	 * form on the page upon validation errors in the form or if
639
	 * they just need to redirect back to the page
640
	 *
641
	 * @param bool $bool Redirect to form on error?
642
	 * @return $this
643
	 */
644
	public function setRedirectToFormOnValidationError($bool) {
645
		$this->redirectToFormOnValidationError = $bool;
646
		return $this;
647
	}
648
649
	/**
650
	 * Get whether the user should be redirected back down to the
651
	 * form on the page upon validation errors
652
	 *
653
	 * @return bool
654
	 */
655
	public function getRedirectToFormOnValidationError() {
656
		return $this->redirectToFormOnValidationError;
657
	}
658
659
	/**
660
	 * Add a plain text error message to a field on this form.  It will be saved into the session
661
	 * and used the next time this form is displayed.
662
	 * @param string $fieldName
663
	 * @param string $message
664
	 * @param string $messageType
665
	 * @param bool $escapeHtml
666
	 */
667
	public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
668
		Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
669
			'fieldName' => $fieldName,
670
			'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
671
			'messageType' => $messageType,
672
		));
673
	}
674
675
	/**
676
	 * @param FormTransformation $trans
677
	 */
678
	public function transform(FormTransformation $trans) {
679
		$newFields = new FieldList();
680
		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...
681
			$newFields->push($field->transform($trans));
682
		}
683
		$this->fields = $newFields;
684
685
		$newActions = new FieldList();
686
		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...
687
			$newActions->push($action->transform($trans));
688
		}
689
		$this->actions = $newActions;
690
691
692
		// We have to remove validation, if the fields are not editable ;-)
693
		if($this->validator)
694
			$this->validator->removeValidation();
695
	}
696
697
	/**
698
	 * Get the {@link Validator} attached to this form.
699
	 * @return Validator
700
	 */
701
	public function getValidator() {
702
		return $this->validator;
703
	}
704
705
	/**
706
	 * Set the {@link Validator} on this form.
707
	 * @param Validator $validator
708
	 * @return $this
709
	 */
710
	public function setValidator(Validator $validator ) {
711
		if($validator) {
712
			$this->validator = $validator;
713
			$this->validator->setForm($this);
714
		}
715
		return $this;
716
	}
717
718
	/**
719
	 * Remove the {@link Validator} from this from.
720
	 */
721
	public function unsetValidator(){
722
		$this->validator = null;
723
		return $this;
724
	}
725
726
	/**
727
	 * Set actions that are exempt from validation
728
	 *
729
	 * @param array
730
	 * @return $this
731
	 */
732
	public function setValidationExemptActions($actions) {
733
		$this->validationExemptActions = $actions;
734
		return $this;
735
	}
736
737
	/**
738
	 * Get a list of actions that are exempt from validation
739
	 *
740
	 * @return array
741
	 */
742
	public function getValidationExemptActions() {
743
		return $this->validationExemptActions;
744
	}
745
746
	/**
747
	 * Passed a FormAction, returns true if that action is exempt from Form validation
748
	 *
749
	 * @param FormAction $action
750
	 * @return bool
751
	 */
752
	public function actionIsValidationExempt($action) {
753
		if ($action->getValidationExempt()) {
754
			return true;
755
		}
756
		if (in_array($action->actionName(), $this->getValidationExemptActions())) {
757
			return true;
758
		}
759
		return false;
760
	}
761
762
	/**
763
	 * Convert this form to another format.
764
	 * @param FormTransformation $format
765
	 */
766
	public function transformTo(FormTransformation $format) {
767
		$newFields = new FieldList();
768
		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...
769
			$newFields->push($field->transformTo($format));
770
		}
771
		$this->fields = $newFields;
772
773
		// We have to remove validation, if the fields are not editable ;-)
774
		if($this->validator)
775
			$this->validator->removeValidation();
776
	}
777
778
779
	/**
780
	 * Generate extra special fields - namely the security token field (if required).
781
	 *
782
	 * @return FieldList
783
	 */
784
	public function getExtraFields() {
785
		$extraFields = new FieldList();
786
787
		$token = $this->getSecurityToken();
788
		if ($token) {
789
			$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...
790
			if($tokenField) $tokenField->setForm($this);
791
		}
792
		$this->securityTokenAdded = true;
793
794
		// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
795
		if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
796
			$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
797
			$methodField->setForm($this);
798
			$extraFields->push($methodField);
799
		}
800
801
		return $extraFields;
802
	}
803
804
	/**
805
	 * Return the form's fields - used by the templates
806
	 *
807
	 * @return FieldList The form fields
808
	 */
809
	public function Fields() {
810
		foreach($this->getExtraFields() as $field) {
811
			if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field);
812
		}
813
814
		return $this->fields;
815
	}
816
817
	/**
818
	 * Return all <input type="hidden"> fields
819
	 * in a form - including fields nested in {@link CompositeFields}.
820
	 * Useful when doing custom field layouts.
821
	 *
822
	 * @return FieldList
823
	 */
824
	public function HiddenFields() {
825
		return $this->Fields()->HiddenFields();
826
	}
827
828
	/**
829
	 * Return all fields except for the hidden fields.
830
	 * Useful when making your own simplified form layouts.
831
	 */
832
	public function VisibleFields() {
833
		return $this->Fields()->VisibleFields();
834
	}
835
836
	/**
837
	 * Setter for the form fields.
838
	 *
839
	 * @param FieldList $fields
840
	 * @return $this
841
	 */
842
	public function setFields($fields) {
843
		$this->fields = $fields;
844
		return $this;
845
	}
846
847
	/**
848
	 * Return the form's action buttons - used by the templates
849
	 *
850
	 * @return FieldList The action list
851
	 */
852
	public function Actions() {
853
		return $this->actions;
854
	}
855
856
	/**
857
	 * Setter for the form actions.
858
	 *
859
	 * @param FieldList $actions
860
	 * @return $this
861
	 */
862
	public function setActions($actions) {
863
		$this->actions = $actions;
864
		return $this;
865
	}
866
867
	/**
868
	 * Unset all form actions
869
	 */
870
	public function unsetAllActions(){
871
		$this->actions = new FieldList();
872
		return $this;
873
	}
874
875
	/**
876
	 * @param string $name
877
	 * @param string $value
878
	 * @return $this
879
	 */
880
	public function setAttribute($name, $value) {
881
		$this->attributes[$name] = $value;
882
		return $this;
883
	}
884
885
	/**
886
	 * @param string $name
887
	 * @return string
888
	 */
889
	public function getAttribute($name) {
890
		if(isset($this->attributes[$name])) return $this->attributes[$name];
891
	}
892
893
	/**
894
	 * @return array
895
	 */
896
	public function getAttributes() {
897
		$attrs = array(
898
			'id' => $this->FormName(),
899
			'action' => $this->FormAction(),
900
			'method' => $this->FormMethod(),
901
			'enctype' => $this->getEncType(),
902
			'target' => $this->target,
903
			'class' => $this->extraClass(),
904
		);
905
906
		if($this->validator && $this->validator->getErrors()) {
907
			if(!isset($attrs['class'])) $attrs['class'] = '';
908
			$attrs['class'] .= ' validationerror';
909
		}
910
911
		$attrs = array_merge($attrs, $this->attributes);
912
913
		return $attrs;
914
	}
915
916
	/**
917
	 * Return the attributes of the form tag - used by the templates.
918
	 *
919
	 * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
920
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
921
	 *
922
	 * @return string HTML attributes, ready for insertion into an HTML tag
923
	 */
924
	public function getAttributesHTML($attrs = null) {
925
		$exclude = (is_string($attrs)) ? func_get_args() : null;
926
927
		// Figure out if we can cache this form
928
		// - forms with validation shouldn't be cached, cos their error messages won't be shown
929
		// - forms with security tokens shouldn't be cached because security tokens expire
930
		$needsCacheDisabled = false;
931
		if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
932
		if ($this->FormMethod() != 'GET') $needsCacheDisabled = true;
933
		if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
934
			$needsCacheDisabled = true;
935
		}
936
937
		// If we need to disable cache, do it
938
		if ($needsCacheDisabled) HTTP::set_cache_age(0);
939
940
		$attrs = $this->getAttributes();
941
942
		// Remove empty
943
		$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...
944
945
		// Remove excluded
946
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
947
948
		// Prepare HTML-friendly 'method' attribute (lower-case)
949
		if (isset($attrs['method'])) {
950
			$attrs['method'] = strtolower($attrs['method']);
951
		}
952
953
		// Create markup
954
		$parts = array();
955
		foreach($attrs as $name => $value) {
956
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
957
		}
958
959
		return implode(' ', $parts);
960
	}
961
962
	public function FormAttributes() {
963
		return $this->getAttributesHTML();
964
	}
965
966
	/**
967
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
968
	 * another frame
969
	 *
970
	 * @param string|FormTemplateHelper
971
	 */
972
	public function setTemplateHelper($helper) {
973
		$this->templateHelper = $helper;
974
	}
975
976
	/**
977
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
978
	 * set, return the default helper.
979
	 *
980
	 * @return FormTemplateHelper
981
	 */
982
	public function getTemplateHelper() {
983
		if($this->templateHelper) {
984
			if(is_string($this->templateHelper)) {
985
				return Injector::inst()->get($this->templateHelper);
986
			}
987
988
			return $this->templateHelper;
989
		}
990
991
		return Injector::inst()->get('FormTemplateHelper');
992
	}
993
994
	/**
995
	 * Set the target of this form to any value - useful for opening the form
996
	 * contents in a new window or refreshing another frame.
997
	 *
998
	 * @param string $target The value of the target
999
	 * @return $this
1000
	 */
1001
	public function setTarget($target) {
1002
		$this->target = $target;
1003
1004
		return $this;
1005
	}
1006
1007
	/**
1008
	 * Set the legend value to be inserted into
1009
	 * the <legend> element in the Form.ss template.
1010
	 * @param string $legend
1011
	 * @return $this
1012
	 */
1013
	public function setLegend($legend) {
1014
		$this->legend = $legend;
1015
		return $this;
1016
	}
1017
1018
	/**
1019
	 * Set the SS template that this form should use
1020
	 * to render with. The default is "Form".
1021
	 *
1022
	 * @param string $template The name of the template (without the .ss extension)
1023
	 * @return $this
1024
	 */
1025
	public function setTemplate($template) {
1026
		$this->template = $template;
1027
		return $this;
1028
	}
1029
1030
	/**
1031
	 * Return the template to render this form with.
1032
	 * If the template isn't set, then default to the
1033
	 * form class name e.g "Form".
1034
	 *
1035
	 * @return string
1036
	 */
1037
	public function getTemplate() {
1038
		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...
1039
		else return $this->class;
1040
	}
1041
1042
	/**
1043
	 * Returns the encoding type for the form.
1044
	 *
1045
	 * By default this will be URL encoded, unless there is a file field present
1046
	 * in which case multipart is used. You can also set the enc type using
1047
	 * {@link setEncType}.
1048
	 */
1049
	public function getEncType() {
1050
		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...
1051
			return $this->encType;
1052
		}
1053
1054
		if ($fields = $this->fields->dataFields()) {
1055
			foreach ($fields as $field) {
1056
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
1057
			}
1058
		}
1059
1060
		return self::ENC_TYPE_URLENCODED;
1061
	}
1062
1063
	/**
1064
	 * Sets the form encoding type. The most common encoding types are defined
1065
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1066
	 *
1067
	 * @param string $encType
1068
	 * @return $this
1069
	 */
1070
	public function setEncType($encType) {
1071
		$this->encType = $encType;
1072
		return $this;
1073
	}
1074
1075
	/**
1076
	 * Returns the real HTTP method for the form:
1077
	 * GET, POST, PUT, DELETE or HEAD.
1078
	 * As most browsers only support GET and POST in
1079
	 * form submissions, all other HTTP methods are
1080
	 * added as a hidden field "_method" that
1081
	 * gets evaluated in {@link Director::direct()}.
1082
	 * See {@link FormMethod()} to get a HTTP method
1083
	 * for safe insertion into a <form> tag.
1084
	 *
1085
	 * @return string HTTP method
1086
	 */
1087
	public function FormHttpMethod() {
1088
		return $this->formMethod;
1089
	}
1090
1091
	/**
1092
	 * Returns the form method to be used in the <form> tag.
1093
	 * See {@link FormHttpMethod()} to get the "real" method.
1094
	 *
1095
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1096
	 */
1097
	public function FormMethod() {
1098
		if(in_array($this->formMethod,array('GET','POST'))) {
1099
			return $this->formMethod;
1100
		} else {
1101
			return 'POST';
1102
		}
1103
	}
1104
1105
	/**
1106
	 * Set the form method: GET, POST, PUT, DELETE.
1107
	 *
1108
	 * @param string $method
1109
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1110
	 * @return $this
1111
	 */
1112
	public function setFormMethod($method, $strict = null) {
1113
		$this->formMethod = strtoupper($method);
1114
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1115
		return $this;
1116
	}
1117
1118
	/**
1119
	 * If set to true, enforce the matching of the form method.
1120
	 *
1121
	 * This will mean two things:
1122
	 *  - GET vars will be ignored by a POST form, and vice versa
1123
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1124
	 *
1125
	 * If set to false (the default), then the form method is only used to construct the default
1126
	 * form.
1127
	 *
1128
	 * @param $bool boolean
1129
	 * @return $this
1130
	 */
1131
	public function setStrictFormMethodCheck($bool) {
1132
		$this->strictFormMethodCheck = (bool)$bool;
1133
		return $this;
1134
	}
1135
1136
	/**
1137
	 * @return boolean
1138
	 */
1139
	public function getStrictFormMethodCheck() {
1140
		return $this->strictFormMethodCheck;
1141
	}
1142
1143
	/**
1144
	 * Return the form's action attribute.
1145
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1146
	 *
1147
	 * @return string
1148
	 */
1149
	public function FormAction() {
1150
		if ($this->formActionPath) {
1151
			return $this->formActionPath;
1152
		} elseif($this->controller->hasMethod("FormObjectLink")) {
1153
			return $this->controller->FormObjectLink($this->name);
1154
		} else {
1155
			return Controller::join_links($this->controller->Link(), $this->name);
1156
		}
1157
	}
1158
1159
	/**
1160
	 * Set the form action attribute to a custom URL.
1161
	 *
1162
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1163
	 * recommended only for situations where you have two relatively distinct
1164
	 * parts of the system trying to communicate via a form post.
1165
	 *
1166
	 * @param string $path
1167
	 * @return $this
1168
	 */
1169
	public function setFormAction($path) {
1170
		$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...
1171
1172
		return $this;
1173
	}
1174
1175
	/**
1176
	 * Returns the name of the form.
1177
	 *
1178
	 * @return string
1179
	 */
1180
	public function FormName() {
1181
		return $this->getTemplateHelper()->generateFormID($this);
1182
	}
1183
1184
	/**
1185
	 * Set the HTML ID attribute of the form.
1186
	 *
1187
	 * @param string $id
1188
	 * @return $this
1189
	 */
1190
	public function setHTMLID($id) {
1191
		$this->htmlID = $id;
1192
1193
		return $this;
1194
	}
1195
1196
	/**
1197
	 * @return string
1198
	 */
1199
	public function getHTMLID() {
1200
		return $this->htmlID;
1201
	}
1202
1203
	/**
1204
	 * Returns this form's controller.
1205
	 *
1206
	 * @return Controller
1207
	 * @deprecated 4.0
1208
	 */
1209
	public function Controller() {
1210
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1211
1212
		return $this->getController();
1213
	}
1214
1215
	/**
1216
	 * Get the controller.
1217
	 *
1218
	 * @return Controller
1219
	 */
1220
	public function getController() {
1221
		return $this->controller;
1222
	}
1223
1224
	/**
1225
	 * Set the controller.
1226
	 *
1227
	 * @param Controller $controller
1228
	 * @return Form
1229
	 */
1230
	public function setController($controller) {
1231
		$this->controller = $controller;
1232
1233
		return $this;
1234
	}
1235
1236
	/**
1237
	 * Get the name of the form.
1238
	 *
1239
	 * @return string
1240
	 */
1241
	public function getName() {
1242
		return $this->name;
1243
	}
1244
1245
	/**
1246
	 * Set the name of the form.
1247
	 *
1248
	 * @param string $name
1249
	 * @return Form
1250
	 */
1251
	public function setName($name) {
1252
		$this->name = $name;
1253
1254
		return $this;
1255
	}
1256
1257
	/**
1258
	 * Returns an object where there is a method with the same name as each data
1259
	 * field on the form.
1260
	 *
1261
	 * That method will return the field itself.
1262
	 *
1263
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1264
	 */
1265
	public function FieldMap() {
1266
		return new Form_FieldMap($this);
1267
	}
1268
1269
	/**
1270
	 * The next functions store and modify the forms
1271
	 * message attributes. messages are stored in session under
1272
	 * $_SESSION[formname][message];
1273
	 *
1274
	 * @return string
1275
	 */
1276
	public function Message() {
1277
		$this->getMessageFromSession();
1278
1279
		return $this->message;
1280
	}
1281
1282
	/**
1283
	 * @return string
1284
	 */
1285
	public function MessageType() {
1286
		$this->getMessageFromSession();
1287
1288
		return $this->messageType;
1289
	}
1290
1291
	/**
1292
	 * @return string
1293
	 */
1294
	protected function getMessageFromSession() {
1295
		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...
1296
			return $this->message;
1297
		} else {
1298
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1299
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1300
1301
			return $this->message;
1302
		}
1303
	}
1304
1305
	/**
1306
	 * Set a status message for the form.
1307
	 *
1308
	 * @param string $message the text of the message
1309
	 * @param string $type Should be set to good, bad, or warning.
1310
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1311
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1312
	 *                            user supplied data in the message.
1313
	 * @return $this
1314
	 */
1315
	public function setMessage($message, $type, $escapeHtml = true) {
1316
		$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...
1317
		$this->messageType = $type;
1318
		return $this;
1319
	}
1320
1321
	/**
1322
	 * Set a message to the session, for display next time this form is shown.
1323
	 *
1324
	 * @param string $message the text of the message
1325
	 * @param string $type Should be set to good, bad, or warning.
1326
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1327
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1328
	 *                            user supplied data in the message.
1329
	 */
1330
	public function sessionMessage($message, $type, $escapeHtml = true) {
1331
		Session::set(
1332
			"FormInfo.{$this->FormName()}.formError.message",
1333
			$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...
1334
		);
1335
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1336
	}
1337
1338
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1339
		Session::set(
1340
			"FormInfo.{$formName}.formError.message",
1341
			$escapeHtml ? Convert::raw2xml($message) : $message
1342
		);
1343
		Session::set("FormInfo.{$formName}.formError.type", $type);
1344
	}
1345
1346
	public function clearMessage() {
1347
		$this->message  = null;
1348
		Session::clear("FormInfo.{$this->FormName()}.errors");
1349
		Session::clear("FormInfo.{$this->FormName()}.formError");
1350
		Session::clear("FormInfo.{$this->FormName()}.data");
1351
	}
1352
1353
	public function resetValidation() {
1354
		Session::clear("FormInfo.{$this->FormName()}.errors");
1355
		Session::clear("FormInfo.{$this->FormName()}.data");
1356
	}
1357
1358
	/**
1359
	 * Returns the DataObject that has given this form its data
1360
	 * through {@link loadDataFrom()}.
1361
	 *
1362
	 * @return DataObject
1363
	 */
1364
	public function getRecord() {
1365
		return $this->record;
1366
	}
1367
1368
	/**
1369
	 * Get the legend value to be inserted into the
1370
	 * <legend> element in Form.ss
1371
	 *
1372
	 * @return string
1373
	 */
1374
	public function getLegend() {
1375
		return $this->legend;
1376
	}
1377
1378
	/**
1379
	 * Processing that occurs before a form is executed.
1380
	 *
1381
	 * This includes form validation, if it fails, we redirect back
1382
	 * to the form with appropriate error messages.
1383
	 * Always return true if the current form action is exempt from validation
1384
	 *
1385
	 * Triggered through {@link httpSubmission()}.
1386
	 *
1387
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1388
	 * if it fails the form data will never reach this method.
1389
	 *
1390
	 * @return boolean
1391
	 */
1392
	public function validate(){
1393
		$action = $this->buttonClicked();
1394
		if($action && $this->actionIsValidationExempt($action)) {
1395
			return true;
1396
		}
1397
1398
		if($this->validator){
1399
			$errors = $this->validator->validate();
1400
1401
			if($errors){
1402
				// Load errors into session and post back
1403
				$data = $this->getData();
1404
1405
				// Encode validation messages as XML before saving into session state
1406
				// 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...
1407
				$errors = array_map(function($error) {
1408
					// Encode message as XML by default
1409
					if($error['message'] instanceof DBField) {
1410
						$error['message'] = $error['message']->forTemplate();;
1411
					} else {
1412
						$error['message'] = Convert::raw2xml($error['message']);
1413
					}
1414
					return $error;
1415
				}, $errors);
1416
1417
				Session::set("FormInfo.{$this->FormName()}.errors", $errors);
1418
				Session::set("FormInfo.{$this->FormName()}.data", $data);
1419
1420
				return false;
1421
			}
1422
		}
1423
1424
		return true;
1425
	}
1426
1427
	const MERGE_DEFAULT = 0;
1428
	const MERGE_CLEAR_MISSING = 1;
1429
	const MERGE_IGNORE_FALSEISH = 2;
1430
1431
	/**
1432
	 * Load data from the given DataObject or array.
1433
	 *
1434
	 * It will call $object->MyField to get the value of MyField.
1435
	 * If you passed an array, it will call $object[MyField].
1436
	 * Doesn't save into dataless FormFields ({@link DatalessField}),
1437
	 * as determined by {@link FieldList->dataFields()}.
1438
	 *
1439
	 * By default, if a field isn't set (as determined by isset()),
1440
	 * its value will not be saved to the field, retaining
1441
	 * potential existing values.
1442
	 *
1443
	 * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1444
	 * Escaping happens automatically on saving the data through {@link saveInto()}.
1445
	 *
1446
	 * Escaping happens automatically on saving the data through
1447
	 * {@link saveInto()}.
1448
	 *
1449
	 * @uses FieldList->dataFields()
1450
	 * @uses FormField->setValue()
1451
	 *
1452
	 * @param array|DataObject $data
1453
	 * @param int $mergeStrategy
1454
	 *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1455
	 *  what that property/key's value is.
1456
	 *
1457
	 *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1458
	 *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1459
	 *  "left alone", meaning they retain any previous value.
1460
	 *
1461
	 *  You can pass a bitmask here to change this behaviour.
1462
	 *
1463
	 *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1464
	 *  {@link $data} are cleared.
1465
	 *
1466
	 *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1467
	 *  a field's value.
1468
	 *
1469
	 *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1470
	 *  CLEAR_MISSING
1471
	 *
1472
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1473
	 * form that has some fields that save to one object, and some that save to another.
1474
	 * @return Form
1475
	 */
1476
	public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) {
1477
		if(!is_object($data) && !is_array($data)) {
1478
			user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1479
			return $this;
1480
		}
1481
1482
		// Handle the backwards compatible case of passing "true" as the second argument
1483
		if ($mergeStrategy === true) {
1484
			$mergeStrategy = self::MERGE_CLEAR_MISSING;
1485
		}
1486
		else if ($mergeStrategy === false) {
1487
			$mergeStrategy = 0;
1488
		}
1489
1490
		// if an object is passed, save it for historical reference through {@link getRecord()}
1491
		if(is_object($data)) $this->record = $data;
1492
1493
		// dont include fields without data
1494
		$dataFields = $this->Fields()->dataFields();
1495
		if($dataFields) foreach($dataFields as $field) {
1496
			$name = $field->getName();
1497
1498
			// Skip fields that have been excluded
1499
			if($fieldList && !in_array($name, $fieldList)) continue;
1500
1501
			// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1502
			if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
1503
1504
			// Does this property exist on $data?
1505
			$exists = false;
1506
			// The value from $data for this field
1507
			$val = null;
1508
1509
			if(is_object($data)) {
1510
				$exists = (
1511
					isset($data->$name) ||
1512
					$data->hasMethod($name) ||
1513
					($data->hasMethod('hasField') && $data->hasField($name))
1514
				);
1515
1516
				if ($exists) {
1517
					$val = $data->__get($name);
1518
				}
1519
			}
1520
			else if(is_array($data)){
1521
				if(array_key_exists($name, $data)) {
1522
					$exists = true;
1523
					$val = $data[$name];
1524
				}
1525
				// If field is in array-notation we need to access nested data
1526
				else if(strpos($name,'[')) {
1527
					// First encode data using PHP's method of converting nested arrays to form data
1528
					$flatData = urldecode(http_build_query($data));
1529
					// Then pull the value out from that flattened string
1530
					preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches);
1531
1532
					if (isset($matches[1])) {
1533
						$exists = true;
1534
						$val = $matches[1];
1535
					}
1536
				}
1537
			}
1538
1539
			// save to the field if either a value is given, or loading of blank/undefined values is forced
1540
			if($exists){
1541
				if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
1542
					// pass original data as well so composite fields can act on the additional information
1543
					$field->setValue($val, $data);
1544
				}
1545
			}
1546
			else if(($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING){
1547
				$field->setValue($val, $data);
1548
			}
1549
		}
1550
1551
		return $this;
1552
	}
1553
1554
	/**
1555
	 * Save the contents of this form into the given data object.
1556
	 * It will make use of setCastedField() to do this.
1557
	 *
1558
	 * @param DataObjectInterface $dataObject The object to save data into
1559
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1560
	 * form that has some fields that save to one object, and some that save to another.
1561
	 */
1562
	public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
1563
		$dataFields = $this->fields->saveableFields();
1564
		$lastField = null;
1565
		if($dataFields) foreach($dataFields as $field) {
1566
			// Skip fields that have been excluded
1567
			if($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) continue;
1568
1569
1570
			$saveMethod = "save{$field->getName()}";
1571
1572
			if($field->getName() == "ClassName"){
1573
				$lastField = $field;
1574
			}else if( $dataObject->hasMethod( $saveMethod ) ){
1575
				$dataObject->$saveMethod( $field->dataValue());
1576
			} else if($field->getName() != "ID"){
1577
				$field->saveInto($dataObject);
1578
			}
1579
		}
1580
		if($lastField) $lastField->saveInto($dataObject);
1581
	}
1582
1583
	/**
1584
	 * Get the submitted data from this form through
1585
	 * {@link FieldList->dataFields()}, which filters out
1586
	 * any form-specific data like form-actions.
1587
	 * Calls {@link FormField->dataValue()} on each field,
1588
	 * which returns a value suitable for insertion into a DataObject
1589
	 * property.
1590
	 *
1591
	 * @return array
1592
	 */
1593
	public function getData() {
1594
		$dataFields = $this->fields->dataFields();
1595
		$data = array();
1596
1597
		if($dataFields){
1598
			foreach($dataFields as $field) {
1599
				if($field->getName()) {
1600
					$data[$field->getName()] = $field->dataValue();
1601
				}
1602
			}
1603
		}
1604
1605
		return $data;
1606
	}
1607
1608
	/**
1609
	 * Call the given method on the given field.
1610
	 *
1611
	 * @param array $data
1612
	 * @return mixed
1613
	 */
1614
	public function callfieldmethod($data) {
1615
		$fieldName = $data['fieldName'];
1616
		$methodName = $data['methodName'];
1617
		$fields = $this->fields->dataFields();
1618
1619
		// special treatment needed for TableField-class and TreeDropdownField
1620
		if(strpos($fieldName, '[')) {
1621
			preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
1622
			preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
1623
			$tableFieldName = $fieldNameMatches[1][0];
1624
			$subFieldName = $subFieldMatches[1][1];
1625
		}
1626
1627
		if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
1628
			$field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
1629
			return $field->$methodName();
1630
		} else if(isset($fields[$fieldName])) {
1631
			return $fields[$fieldName]->$methodName();
1632
		} else {
1633
			user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
1634
		}
1635
	}
1636
1637
	/**
1638
	 * Return a rendered version of this form.
1639
	 *
1640
	 * This is returned when you access a form as $FormObject rather
1641
	 * than <% with FormObject %>
1642
	 *
1643
	 * @return DBHTMLText
1644
	 */
1645
	public function forTemplate() {
1646
		$return = $this->renderWith(array_merge(
1647
			(array)$this->getTemplate(),
1648
			array('Includes/Form')
1649
		));
1650
1651
		// Now that we're rendered, clear message
1652
		$this->clearMessage();
1653
1654
		return $return;
1655
	}
1656
1657
	/**
1658
	 * Return a rendered version of this form, suitable for ajax post-back.
1659
	 *
1660
	 * It triggers slightly different behaviour, such as disabling the rewriting
1661
	 * of # links.
1662
	 *
1663
	 * @return DBHTMLText
1664
	 */
1665
	public function forAjaxTemplate() {
1666
		$view = new SSViewer(array(
1667
			$this->getTemplate(),
1668
			'Form'
1669
		));
1670
1671
		$return = $view->dontRewriteHashlinks()->process($this);
1672
1673
		// Now that we're rendered, clear message
1674
		$this->clearMessage();
1675
1676
		return $return;
1677
	}
1678
1679
	/**
1680
	 * Returns an HTML rendition of this form, without the <form> tag itself.
1681
	 *
1682
	 * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1683
	 * and _form_enctype.  These are the attributes of the form.  These fields
1684
	 * can be used to send the form to Ajax.
1685
	 *
1686
	 * @return DBHTMLText
1687
	 */
1688
	public function formHtmlContent() {
1689
		$this->IncludeFormTag = false;
1690
		$content = $this->forTemplate();
1691
		$this->IncludeFormTag = true;
1692
1693
		$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1694
			. " value=\"" . $this->FormAction() . "\" />\n";
1695
		$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1696
		$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1697
		$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1698
1699
		return $content;
1700
	}
1701
1702
	/**
1703
	 * Render this form using the given template, and return the result as a string
1704
	 * You can pass either an SSViewer or a template name
1705
	 * @param string|array $template
1706
	 * @return DBHTMLText
1707
	 */
1708
	public function renderWithoutActionButton($template) {
1709
		$custom = $this->customise(array(
1710
			"Actions" => "",
1711
		));
1712
1713
		if(is_string($template)) {
1714
			$template = new SSViewer($template);
1715
		}
1716
1717
		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...
1718
	}
1719
1720
1721
	/**
1722
	 * Sets the button that was clicked.  This should only be called by the Controller.
1723
	 *
1724
	 * @param callable $funcName The name of the action method that will be called.
1725
	 * @return $this
1726
	 */
1727
	public function setButtonClicked($funcName) {
1728
		$this->buttonClickedFunc = $funcName;
1729
1730
		return $this;
1731
	}
1732
1733
	/**
1734
	 * @return FormAction
1735
	 */
1736
	public function buttonClicked() {
1737
		$fields = $this->fields->dataFields() ?: array();
1738
		$actions = $this->actions->dataFields() ?: array();
1739
1740
 		if(!$actions && !$fields) {
1741
			return null;
1742
		}
1743
1744
		$fieldsAndActions = array_merge($fields, $actions);
1745
 		foreach ($fieldsAndActions as $fieldOrAction) {
1746
			if ($fieldOrAction instanceof FormAction && $this->buttonClickedFunc === $fieldOrAction->actionName()) {
1747
				return $fieldOrAction;
1748
			}
1749
		}
1750
1751
		return null;
1752
	}
1753
1754
	/**
1755
	 * Return the default button that should be clicked when another one isn't
1756
	 * available.
1757
	 *
1758
	 * @return FormAction
1759
	 */
1760
	public function defaultAction() {
1761
		if($this->hasDefaultAction && $this->actions) {
1762
			return $this->actions->First();
1763
		}
1764
	}
1765
1766
	/**
1767
	 * Disable the default button.
1768
	 *
1769
	 * Ordinarily, when a form is processed and no action_XXX button is
1770
	 * available, then the first button in the actions list will be pressed.
1771
	 * However, if this is "delete", for example, this isn't such a good idea.
1772
	 *
1773
	 * @return Form
1774
	 */
1775
	public function disableDefaultAction() {
1776
		$this->hasDefaultAction = false;
1777
1778
		return $this;
1779
	}
1780
1781
	/**
1782
	 * Disable the requirement of a security token on this form instance. This
1783
	 * security protects against CSRF attacks, but you should disable this if
1784
	 * you don't want to tie a form to a session - eg a search form.
1785
	 *
1786
	 * Check for token state with {@link getSecurityToken()} and
1787
	 * {@link SecurityToken->isEnabled()}.
1788
	 *
1789
	 * @return Form
1790
	 */
1791
	public function disableSecurityToken() {
1792
		$this->securityToken = new NullSecurityToken();
1793
1794
		return $this;
1795
	}
1796
1797
	/**
1798
	 * Enable {@link SecurityToken} protection for this form instance.
1799
	 *
1800
	 * Check for token state with {@link getSecurityToken()} and
1801
	 * {@link SecurityToken->isEnabled()}.
1802
	 *
1803
	 * @return Form
1804
	 */
1805
	public function enableSecurityToken() {
1806
		$this->securityToken = new SecurityToken();
1807
1808
		return $this;
1809
	}
1810
1811
	/**
1812
	 * Returns the security token for this form (if any exists).
1813
	 *
1814
	 * Doesn't check for {@link securityTokenEnabled()}.
1815
	 *
1816
	 * Use {@link SecurityToken::inst()} to get a global token.
1817
	 *
1818
	 * @return SecurityToken|null
1819
	 */
1820
	public function getSecurityToken() {
1821
		return $this->securityToken;
1822
	}
1823
1824
	/**
1825
	 * Returns the name of a field, if that's the only field that the current
1826
	 * controller is interested in.
1827
	 *
1828
	 * It checks for a call to the callfieldmethod action.
1829
	 *
1830
	 * @return string
1831
	 */
1832
	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...
1833
		if(self::current_action() == 'callfieldmethod') {
1834
			return $_REQUEST['fieldName'];
1835
	}
1836
	}
1837
1838
	/**
1839
	 * Return the current form action being called, if available.
1840
	 *
1841
	 * @return string
1842
	 */
1843
	public static function current_action() {
1844
		return self::$current_action;
1845
	}
1846
1847
	/**
1848
	 * Set the current form action. Should only be called by {@link Controller}.
1849
	 *
1850
	 * @param string $action
1851
	 */
1852
	public static function set_current_action($action) {
1853
		self::$current_action = $action;
1854
	}
1855
1856
	/**
1857
	 * Compiles all CSS-classes.
1858
	 *
1859
	 * @return string
1860
	 */
1861
	public function extraClass() {
1862
		return implode(array_unique($this->extraClasses), ' ');
1863
	}
1864
1865
	/**
1866
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1867
	 * be added by delimiting a string with spaces.
1868
	 *
1869
	 * @param string $class A string containing a classname or several class
1870
	 *                names delimited by a single space.
1871
	 * @return $this
1872
	 */
1873
	public function addExtraClass($class) {
1874
		//split at white space
1875
		$classes = preg_split('/\s+/', $class);
1876
		foreach($classes as $class) {
1877
			//add classes one by one
1878
			$this->extraClasses[$class] = $class;
1879
		}
1880
		return $this;
1881
	}
1882
1883
	/**
1884
	 * Remove a CSS-class from the form-container. Multiple class names can
1885
	 * be passed through as a space delimited string
1886
	 *
1887
	 * @param string $class
1888
	 * @return $this
1889
	 */
1890
	public function removeExtraClass($class) {
1891
		//split at white space
1892
		$classes = preg_split('/\s+/', $class);
1893
		foreach ($classes as $class) {
1894
			//unset one by one
1895
			unset($this->extraClasses[$class]);
1896
		}
1897
		return $this;
1898
	}
1899
1900
	public function debug() {
1901
		$result = "<h3>$this->class</h3><ul>";
1902
		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...
1903
			$result .= "<li>$field" . $field->debug() . "</li>";
1904
		}
1905
		$result .= "</ul>";
1906
1907
		if( $this->validator )
1908
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1909
1910
		return $result;
1911
	}
1912
1913
1914
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1915
	// TESTING HELPERS
1916
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1917
1918
	/**
1919
	 * Test a submission of this form.
1920
	 * @param string $action
1921
	 * @param array $data
1922
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1923
	 * your unit test.
1924
	 * @throws SS_HTTPResponse_Exception
1925
	 */
1926
	public function testSubmission($action, $data) {
1927
		$data['action_' . $action] = true;
1928
1929
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1930
	}
1931
1932
	/**
1933
	 * Test an ajax submission of this form.
1934
	 *
1935
	 * @param string $action
1936
	 * @param array $data
1937
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1938
	 * your unit test.
1939
	 */
1940
	public function testAjaxSubmission($action, $data) {
1941
		$data['ajax'] = 1;
1942
		return $this->testSubmission($action, $data);
1943
	}
1944
}
1945
1946
/**
1947
 * @package forms
1948
 * @subpackage core
1949
 */
1950
class Form_FieldMap extends ViewableData {
1951
1952
	protected $form;
1953
1954
	public function __construct($form) {
1955
		$this->form = $form;
1956
		parent::__construct();
1957
	}
1958
1959
	/**
1960
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1961
	 * @param string $method
1962
	 * @return bool
1963
	 */
1964
	public function hasMethod($method) {
1965
		return true;
1966
	}
1967
1968
	public function __call($method, $args = null) {
1969
		return $this->form->Fields()->fieldByName($method);
1970
	}
1971
}
1972