Completed
Push — master ( c7767b...f548dd )
by Daniel
11:31
created

Form::buttonClicked()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

There are different options of fixing this problem.

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

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

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

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

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
506
		if ($field && $field->checkAccessAction($action)) {
507
			return true;
508
		}
509
510
		return false;
511
	}
512
513
	/**
514
	 * @return callable
515
	 */
516
	public function getValidationResponseCallback() {
517
		return $this->validationResponseCallback;
518
	}
519
520
	/**
521
	 * Overrules validation error behaviour in {@link httpSubmission()}
522
	 * when validation has failed. Useful for optional handling of a certain accepted content type.
523
	 *
524
	 * The callback can opt out of handling specific responses by returning NULL,
525
	 * in which case the default form behaviour will kick in.
526
	 *
527
	 * @param $callback
528
	 * @return self
529
	 */
530
	public function setValidationResponseCallback($callback) {
531
		$this->validationResponseCallback = $callback;
532
533
		return $this;
534
	}
535
536
	/**
537
	 * Returns the appropriate response up the controller chain
538
	 * if {@link validate()} fails (which is checked prior to executing any form actions).
539
	 * By default, returns different views for ajax/non-ajax request, and
540
	 * handles 'application/json' requests with a JSON object containing the error messages.
541
	 * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
542
	 * and can be overruled by setting {@link $validationResponseCallback}.
543
	 *
544
	 * @return SS_HTTPResponse|string
545
	 */
546
	protected function getValidationErrorResponse() {
547
		$callback = $this->getValidationResponseCallback();
548
		if($callback && $callbackResponse = $callback()) {
549
			return $callbackResponse;
550
		}
551
552
		$request = $this->getRequest();
553
		if($request->isAjax()) {
554
				// Special case for legacy Validator.js implementation
555
				// (assumes eval'ed javascript collected through FormResponse)
556
				$acceptType = $request->getHeader('Accept');
557
				if(strpos($acceptType, 'application/json') !== FALSE) {
558
					// Send validation errors back as JSON with a flag at the start
559
					$response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
560
					$response->addHeader('Content-Type', 'application/json');
561
				} else {
562
					$this->setupFormErrors();
563
					// Send the newly rendered form tag as HTML
564
					$response = new SS_HTTPResponse($this->forTemplate());
565
					$response->addHeader('Content-Type', 'text/html');
566
				}
567
568
				return $response;
569
			} else {
570
				if($this->getRedirectToFormOnValidationError()) {
571
					if($pageURL = $request->getHeader('Referer')) {
572
						if(Director::is_site_url($pageURL)) {
573
							// Remove existing pragmas
574
							$pageURL = preg_replace('/(#.*)/', '', $pageURL);
575
							$pageURL = Director::absoluteURL($pageURL, true);
0 ignored issues
show
Bug introduced by
It seems like $pageURL defined by \Director::absoluteURL($pageURL, true) on line 575 can also be of type array<integer,string>; however, Director::absoluteURL() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
576
							return $this->controller->redirect($pageURL . '#' . $this->FormName());
577
						}
578
					}
579
				}
580
				return $this->controller->redirectBack();
581
			}
582
	}
583
584
	/**
585
	 * Fields can have action to, let's check if anyone of the responds to $funcname them
586
	 *
587
	 * @param SS_List|array $fields
588
	 * @param callable $funcName
589
	 * @return FormField
590
	 */
591
	protected function checkFieldsForAction($fields, $funcName) {
592
		foreach($fields as $field){
593
			if(method_exists($field, 'FieldList')) {
594
				if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
595
					return $field;
596
				}
597
			} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
598
				return $field;
599
			}
600
		}
601
	}
602
603
	/**
604
	 * Handle a field request.
605
	 * Uses {@link Form->dataFieldByName()} to find a matching field,
606
	 * and falls back to {@link FieldList->fieldByName()} to look
607
	 * for tabs instead. This means that if you have a tab and a
608
	 * formfield with the same name, this method gives priority
609
	 * to the formfield.
610
	 *
611
	 * @param SS_HTTPRequest $request
612
	 * @return FormField
613
	 */
614
	public function handleField($request) {
615
		$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
616
617
		if($field) {
618
			return $field;
619
		} else {
620
			// falling back to fieldByName, e.g. for getting tabs
621
			return $this->Fields()->fieldByName($request->param('FieldName'));
622
		}
623
	}
624
625
	/**
626
	 * Convert this form into a readonly form
627
	 */
628
	public function makeReadonly() {
629
		$this->transform(new ReadonlyTransformation());
630
	}
631
632
	/**
633
	 * Set whether the user should be redirected back down to the
634
	 * form on the page upon validation errors in the form or if
635
	 * they just need to redirect back to the page
636
	 *
637
	 * @param bool $bool Redirect to form on error?
638
	 * @return $this
639
	 */
640
	public function setRedirectToFormOnValidationError($bool) {
641
		$this->redirectToFormOnValidationError = $bool;
642
		return $this;
643
	}
644
645
	/**
646
	 * Get whether the user should be redirected back down to the
647
	 * form on the page upon validation errors
648
	 *
649
	 * @return bool
650
	 */
651
	public function getRedirectToFormOnValidationError() {
652
		return $this->redirectToFormOnValidationError;
653
	}
654
655
	/**
656
	 * Add a plain text error message to a field on this form.  It will be saved into the session
657
	 * and used the next time this form is displayed.
658
	 * @param string $fieldName
659
	 * @param string $message
660
	 * @param string $messageType
661
	 * @param bool $escapeHtml
662
	 */
663
	public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
664
		Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
665
			'fieldName' => $fieldName,
666
			'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
667
			'messageType' => $messageType,
668
		));
669
	}
670
671
	/**
672
	 * @param FormTransformation $trans
673
	 */
674
	public function transform(FormTransformation $trans) {
675
		$newFields = new FieldList();
676
		foreach($this->fields as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->fields of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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

There are different options of fixing this problem.

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

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

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

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

There are different options of fixing this problem.

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
786
			if($tokenField) $tokenField->setForm($this);
787
		}
788
		$this->securityTokenAdded = true;
789
790
		// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
791
		if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
792
			$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
793
			$methodField->setForm($this);
794
			$extraFields->push($methodField);
795
		}
796
797
		return $extraFields;
798
	}
799
800
	/**
801
	 * Return the form's fields - used by the templates
802
	 *
803
	 * @return FieldList The form fields
804
	 */
805
	public function Fields() {
806
		foreach($this->getExtraFields() as $field) {
807
			if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field);
808
		}
809
810
		return $this->fields;
811
	}
812
813
	/**
814
	 * Return all <input type="hidden"> fields
815
	 * in a form - including fields nested in {@link CompositeFields}.
816
	 * Useful when doing custom field layouts.
817
	 *
818
	 * @return FieldList
819
	 */
820
	public function HiddenFields() {
821
		return $this->Fields()->HiddenFields();
822
	}
823
824
	/**
825
	 * Return all fields except for the hidden fields.
826
	 * Useful when making your own simplified form layouts.
827
	 */
828
	public function VisibleFields() {
829
		return $this->Fields()->VisibleFields();
830
	}
831
832
	/**
833
	 * Setter for the form fields.
834
	 *
835
	 * @param FieldList $fields
836
	 * @return $this
837
	 */
838
	public function setFields($fields) {
839
		$this->fields = $fields;
840
		return $this;
841
	}
842
843
	/**
844
	 * Return the form's action buttons - used by the templates
845
	 *
846
	 * @return FieldList The action list
847
	 */
848
	public function Actions() {
849
		return $this->actions;
850
	}
851
852
	/**
853
	 * Setter for the form actions.
854
	 *
855
	 * @param FieldList $actions
856
	 * @return $this
857
	 */
858
	public function setActions($actions) {
859
		$this->actions = $actions;
860
		return $this;
861
	}
862
863
	/**
864
	 * Unset all form actions
865
	 */
866
	public function unsetAllActions(){
867
		$this->actions = new FieldList();
868
		return $this;
869
	}
870
871
	/**
872
	 * @param string $name
873
	 * @param string $value
874
	 * @return $this
875
	 */
876
	public function setAttribute($name, $value) {
877
		$this->attributes[$name] = $value;
878
		return $this;
879
	}
880
881
	/**
882
	 * @param string $name
883
	 * @return string
884
	 */
885
	public function getAttribute($name) {
886
		if(isset($this->attributes[$name])) return $this->attributes[$name];
887
	}
888
889
	/**
890
	 * @return array
891
	 */
892
	public function getAttributes() {
893
		$attrs = array(
894
			'id' => $this->FormName(),
895
			'action' => $this->FormAction(),
896
			'method' => $this->FormMethod(),
897
			'enctype' => $this->getEncType(),
898
			'target' => $this->target,
899
			'class' => $this->extraClass(),
900
		);
901
902
		if($this->validator && $this->validator->getErrors()) {
903
			if(!isset($attrs['class'])) $attrs['class'] = '';
904
			$attrs['class'] .= ' validationerror';
905
		}
906
907
		$attrs = array_merge($attrs, $this->attributes);
908
909
		return $attrs;
910
	}
911
912
	/**
913
	 * Return the attributes of the form tag - used by the templates.
914
	 *
915
	 * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
916
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
917
	 *
918
	 * @return string HTML attributes, ready for insertion into an HTML tag
919
	 */
920
	public function getAttributesHTML($attrs = null) {
921
		$exclude = (is_string($attrs)) ? func_get_args() : null;
922
923
		// Figure out if we can cache this form
924
		// - forms with validation shouldn't be cached, cos their error messages won't be shown
925
		// - forms with security tokens shouldn't be cached because security tokens expire
926
		$needsCacheDisabled = false;
927
		if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
928
		if ($this->FormMethod() != 'GET') $needsCacheDisabled = true;
929
		if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
930
			$needsCacheDisabled = true;
931
		}
932
933
		// If we need to disable cache, do it
934
		if ($needsCacheDisabled) HTTP::set_cache_age(0);
935
936
		$attrs = $this->getAttributes();
937
938
		// Remove empty
939
		$attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);'));
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

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

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

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
940
941
		// Remove excluded
942
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));
943
944
		// Prepare HTML-friendly 'method' attribute (lower-case)
945
		if (isset($attrs['method'])) {
946
			$attrs['method'] = strtolower($attrs['method']);
947
		}
948
949
		// Create markup
950
		$parts = array();
951
		foreach($attrs as $name => $value) {
952
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
953
		}
954
955
		return implode(' ', $parts);
956
	}
957
958
	public function FormAttributes() {
959
		return $this->getAttributesHTML();
960
	}
961
962
	/**
963
	 * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
964
	 * another frame
965
	 *
966
	 * @param string|FormTemplateHelper
967
	 */
968
	public function setTemplateHelper($helper) {
969
		$this->templateHelper = $helper;
970
	}
971
972
	/**
973
	 * Return a {@link FormTemplateHelper} for this form. If one has not been
974
	 * set, return the default helper.
975
	 *
976
	 * @return FormTemplateHelper
977
	 */
978
	public function getTemplateHelper() {
979
		if($this->templateHelper) {
980
			if(is_string($this->templateHelper)) {
981
				return Injector::inst()->get($this->templateHelper);
982
			}
983
984
			return $this->templateHelper;
985
		}
986
987
		return Injector::inst()->get('FormTemplateHelper');
988
	}
989
990
	/**
991
	 * Set the target of this form to any value - useful for opening the form
992
	 * contents in a new window or refreshing another frame.
993
	 *
994
	 * @param string $target The value of the target
995
	 * @return $this
996
	 */
997
	public function setTarget($target) {
998
		$this->target = $target;
999
1000
		return $this;
1001
	}
1002
1003
	/**
1004
	 * Set the legend value to be inserted into
1005
	 * the <legend> element in the Form.ss template.
1006
	 * @param string $legend
1007
	 * @return $this
1008
	 */
1009
	public function setLegend($legend) {
1010
		$this->legend = $legend;
1011
		return $this;
1012
	}
1013
1014
	/**
1015
	 * Set the SS template that this form should use
1016
	 * to render with. The default is "Form".
1017
	 *
1018
	 * @param string $template The name of the template (without the .ss extension)
1019
	 * @return $this
1020
	 */
1021
	public function setTemplate($template) {
1022
		$this->template = $template;
1023
		return $this;
1024
	}
1025
1026
	/**
1027
	 * Return the template to render this form with.
1028
	 * If the template isn't set, then default to the
1029
	 * form class name e.g "Form".
1030
	 *
1031
	 * @return string
1032
	 */
1033
	public function getTemplate() {
1034
		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...
1035
		else return $this->class;
1036
	}
1037
1038
	/**
1039
	 * Returns the encoding type for the form.
1040
	 *
1041
	 * By default this will be URL encoded, unless there is a file field present
1042
	 * in which case multipart is used. You can also set the enc type using
1043
	 * {@link setEncType}.
1044
	 */
1045
	public function getEncType() {
1046
		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...
1047
			return $this->encType;
1048
		}
1049
1050
		if ($fields = $this->fields->dataFields()) {
1051
			foreach ($fields as $field) {
1052
				if ($field instanceof FileField) return self::ENC_TYPE_MULTIPART;
1053
			}
1054
		}
1055
1056
		return self::ENC_TYPE_URLENCODED;
1057
	}
1058
1059
	/**
1060
	 * Sets the form encoding type. The most common encoding types are defined
1061
	 * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1062
	 *
1063
	 * @param string $encType
1064
	 * @return $this
1065
	 */
1066
	public function setEncType($encType) {
1067
		$this->encType = $encType;
1068
		return $this;
1069
	}
1070
1071
	/**
1072
	 * Returns the real HTTP method for the form:
1073
	 * GET, POST, PUT, DELETE or HEAD.
1074
	 * As most browsers only support GET and POST in
1075
	 * form submissions, all other HTTP methods are
1076
	 * added as a hidden field "_method" that
1077
	 * gets evaluated in {@link Director::direct()}.
1078
	 * See {@link FormMethod()} to get a HTTP method
1079
	 * for safe insertion into a <form> tag.
1080
	 *
1081
	 * @return string HTTP method
1082
	 */
1083
	public function FormHttpMethod() {
1084
		return $this->formMethod;
1085
	}
1086
1087
	/**
1088
	 * Returns the form method to be used in the <form> tag.
1089
	 * See {@link FormHttpMethod()} to get the "real" method.
1090
	 *
1091
	 * @return string Form HTTP method restricted to 'GET' or 'POST'
1092
	 */
1093
	public function FormMethod() {
1094
		if(in_array($this->formMethod,array('GET','POST'))) {
1095
			return $this->formMethod;
1096
		} else {
1097
			return 'POST';
1098
		}
1099
	}
1100
1101
	/**
1102
	 * Set the form method: GET, POST, PUT, DELETE.
1103
	 *
1104
	 * @param string $method
1105
	 * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1106
	 * @return $this
1107
	 */
1108
	public function setFormMethod($method, $strict = null) {
1109
		$this->formMethod = strtoupper($method);
1110
		if($strict !== null) $this->setStrictFormMethodCheck($strict);
1111
		return $this;
1112
	}
1113
1114
	/**
1115
	 * If set to true, enforce the matching of the form method.
1116
	 *
1117
	 * This will mean two things:
1118
	 *  - GET vars will be ignored by a POST form, and vice versa
1119
	 *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1120
	 *
1121
	 * If set to false (the default), then the form method is only used to construct the default
1122
	 * form.
1123
	 *
1124
	 * @param $bool boolean
1125
	 * @return $this
1126
	 */
1127
	public function setStrictFormMethodCheck($bool) {
1128
		$this->strictFormMethodCheck = (bool)$bool;
1129
		return $this;
1130
	}
1131
1132
	/**
1133
	 * @return boolean
1134
	 */
1135
	public function getStrictFormMethodCheck() {
1136
		return $this->strictFormMethodCheck;
1137
	}
1138
1139
	/**
1140
	 * Return the form's action attribute.
1141
	 * This is build by adding an executeForm get variable to the parent controller's Link() value
1142
	 *
1143
	 * @return string
1144
	 */
1145
	public function FormAction() {
1146
		if ($this->formActionPath) {
1147
			return $this->formActionPath;
1148
		} elseif($this->controller->hasMethod("FormObjectLink")) {
1149
			return $this->controller->FormObjectLink($this->name);
1150
		} else {
1151
			return Controller::join_links($this->controller->Link(), $this->name);
1152
		}
1153
	}
1154
1155
	/**
1156
	 * Set the form action attribute to a custom URL.
1157
	 *
1158
	 * Note: For "normal" forms, you shouldn't need to use this method.  It is
1159
	 * recommended only for situations where you have two relatively distinct
1160
	 * parts of the system trying to communicate via a form post.
1161
	 *
1162
	 * @param string $path
1163
	 * @return $this
1164
	 */
1165
	public function setFormAction($path) {
1166
		$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...
1167
1168
		return $this;
1169
	}
1170
1171
	/**
1172
	 * Returns the name of the form.
1173
	 *
1174
	 * @return string
1175
	 */
1176
	public function FormName() {
1177
		return $this->getTemplateHelper()->generateFormID($this);
1178
	}
1179
1180
	/**
1181
	 * Set the HTML ID attribute of the form.
1182
	 *
1183
	 * @param string $id
1184
	 * @return $this
1185
	 */
1186
	public function setHTMLID($id) {
1187
		$this->htmlID = $id;
1188
1189
		return $this;
1190
	}
1191
1192
	/**
1193
	 * @return string
1194
	 */
1195
	public function getHTMLID() {
1196
		return $this->htmlID;
1197
	}
1198
1199
	/**
1200
	 * Returns this form's controller.
1201
	 *
1202
	 * @return Controller
1203
	 * @deprecated 4.0
1204
	 */
1205
	public function Controller() {
1206
		Deprecation::notice('4.0', 'Use getController() rather than Controller() to access controller');
1207
1208
		return $this->getController();
1209
	}
1210
1211
	/**
1212
	 * Get the controller.
1213
	 *
1214
	 * @return Controller
1215
	 */
1216
	public function getController() {
1217
		return $this->controller;
1218
	}
1219
1220
	/**
1221
	 * Set the controller.
1222
	 *
1223
	 * @param Controller $controller
1224
	 * @return Form
1225
	 */
1226
	public function setController($controller) {
1227
		$this->controller = $controller;
1228
1229
		return $this;
1230
	}
1231
1232
	/**
1233
	 * Get the name of the form.
1234
	 *
1235
	 * @return string
1236
	 */
1237
	public function getName() {
1238
		return $this->name;
1239
	}
1240
1241
	/**
1242
	 * Set the name of the form.
1243
	 *
1244
	 * @param string $name
1245
	 * @return Form
1246
	 */
1247
	public function setName($name) {
1248
		$this->name = $name;
1249
1250
		return $this;
1251
	}
1252
1253
	/**
1254
	 * Returns an object where there is a method with the same name as each data
1255
	 * field on the form.
1256
	 *
1257
	 * That method will return the field itself.
1258
	 *
1259
	 * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1260
	 */
1261
	public function FieldMap() {
1262
		return new Form_FieldMap($this);
1263
	}
1264
1265
	/**
1266
	 * The next functions store and modify the forms
1267
	 * message attributes. messages are stored in session under
1268
	 * $_SESSION[formname][message];
1269
	 *
1270
	 * @return string
1271
	 */
1272
	public function Message() {
1273
		$this->getMessageFromSession();
1274
1275
		return $this->message;
1276
	}
1277
1278
	/**
1279
	 * @return string
1280
	 */
1281
	public function MessageType() {
1282
		$this->getMessageFromSession();
1283
1284
		return $this->messageType;
1285
	}
1286
1287
	/**
1288
	 * @return string
1289
	 */
1290
	protected function getMessageFromSession() {
1291
		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...
1292
			return $this->message;
1293
		} else {
1294
			$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
1295
			$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
1296
1297
			return $this->message;
1298
		}
1299
	}
1300
1301
	/**
1302
	 * Set a status message for the form.
1303
	 *
1304
	 * @param string $message the text of the message
1305
	 * @param string $type Should be set to good, bad, or warning.
1306
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1307
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1308
	 *                            user supplied data in the message.
1309
	 * @return $this
1310
	 */
1311
	public function setMessage($message, $type, $escapeHtml = true) {
1312
		$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...
1313
		$this->messageType = $type;
1314
		return $this;
1315
	}
1316
1317
	/**
1318
	 * Set a message to the session, for display next time this form is shown.
1319
	 *
1320
	 * @param string $message the text of the message
1321
	 * @param string $type Should be set to good, bad, or warning.
1322
	 * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
1323
	 *                            In that case, you might want to use {@link Convert::raw2xml()} to escape any
1324
	 *                            user supplied data in the message.
1325
	 */
1326
	public function sessionMessage($message, $type, $escapeHtml = true) {
1327
		Session::set(
1328
			"FormInfo.{$this->FormName()}.formError.message",
1329
			$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...
1330
		);
1331
		Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
1332
	}
1333
1334
	public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
1335
		Session::set(
1336
			"FormInfo.{$formName}.formError.message",
1337
			$escapeHtml ? Convert::raw2xml($message) : $message
1338
		);
1339
		Session::set("FormInfo.{$formName}.formError.type", $type);
1340
	}
1341
1342
	public function clearMessage() {
1343
		$this->message  = null;
1344
		Session::clear("FormInfo.{$this->FormName()}.errors");
1345
		Session::clear("FormInfo.{$this->FormName()}.formError");
1346
		Session::clear("FormInfo.{$this->FormName()}.data");
1347
	}
1348
1349
	public function resetValidation() {
1350
		Session::clear("FormInfo.{$this->FormName()}.errors");
1351
		Session::clear("FormInfo.{$this->FormName()}.data");
1352
	}
1353
1354
	/**
1355
	 * Returns the DataObject that has given this form its data
1356
	 * through {@link loadDataFrom()}.
1357
	 *
1358
	 * @return DataObject
1359
	 */
1360
	public function getRecord() {
1361
		return $this->record;
1362
	}
1363
1364
	/**
1365
	 * Get the legend value to be inserted into the
1366
	 * <legend> element in Form.ss
1367
	 *
1368
	 * @return string
1369
	 */
1370
	public function getLegend() {
1371
		return $this->legend;
1372
	}
1373
1374
	/**
1375
	 * Processing that occurs before a form is executed.
1376
	 *
1377
	 * This includes form validation, if it fails, we redirect back
1378
	 * to the form with appropriate error messages.
1379
	 * Always return true if the current form action is exempt from validation
1380
	 *
1381
	 * Triggered through {@link httpSubmission()}.
1382
	 *
1383
	 * Note that CSRF protection takes place in {@link httpSubmission()},
1384
	 * if it fails the form data will never reach this method.
1385
	 *
1386
	 * @return boolean
1387
	 */
1388
	public function validate(){
1389
		$action = $this->buttonClicked();
1390
		if($action && $this->actionIsValidationExempt($action)) {
1391
			return true;
1392
		}
1393
1394
		if($this->validator){
1395
			$errors = $this->validator->validate();
1396
1397
			if($errors){
1398
				// Load errors into session and post back
1399
				$data = $this->getData();
1400
1401
				// Encode validation messages as XML before saving into session state
1402
				// 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...
1403
				$errors = array_map(function($error) {
1404
					// Encode message as XML by default
1405
					if($error['message'] instanceof DBField) {
1406
						$error['message'] = $error['message']->forTemplate();;
1407
					} else {
1408
						$error['message'] = Convert::raw2xml($error['message']);
1409
					}
1410
					return $error;
1411
				}, $errors);
1412
1413
				Session::set("FormInfo.{$this->FormName()}.errors", $errors);
1414
				Session::set("FormInfo.{$this->FormName()}.data", $data);
1415
1416
				return false;
1417
			}
1418
		}
1419
1420
		return true;
1421
	}
1422
1423
	const MERGE_DEFAULT = 0;
1424
	const MERGE_CLEAR_MISSING = 1;
1425
	const MERGE_IGNORE_FALSEISH = 2;
1426
1427
	/**
1428
	 * Load data from the given DataObject or array.
1429
	 *
1430
	 * It will call $object->MyField to get the value of MyField.
1431
	 * If you passed an array, it will call $object[MyField].
1432
	 * Doesn't save into dataless FormFields ({@link DatalessField}),
1433
	 * as determined by {@link FieldList->dataFields()}.
1434
	 *
1435
	 * By default, if a field isn't set (as determined by isset()),
1436
	 * its value will not be saved to the field, retaining
1437
	 * potential existing values.
1438
	 *
1439
	 * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1440
	 * Escaping happens automatically on saving the data through {@link saveInto()}.
1441
	 *
1442
	 * Escaping happens automatically on saving the data through
1443
	 * {@link saveInto()}.
1444
	 *
1445
	 * @uses FieldList->dataFields()
1446
	 * @uses FormField->setValue()
1447
	 *
1448
	 * @param array|DataObject $data
1449
	 * @param int $mergeStrategy
1450
	 *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1451
	 *  what that property/key's value is.
1452
	 *
1453
	 *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1454
	 *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1455
	 *  "left alone", meaning they retain any previous value.
1456
	 *
1457
	 *  You can pass a bitmask here to change this behaviour.
1458
	 *
1459
	 *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1460
	 *  {@link $data} are cleared.
1461
	 *
1462
	 *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1463
	 *  a field's value.
1464
	 *
1465
	 *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1466
	 *  CLEAR_MISSING
1467
	 *
1468
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1469
	 * form that has some fields that save to one object, and some that save to another.
1470
	 * @return Form
1471
	 */
1472
	public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) {
1473
		if(!is_object($data) && !is_array($data)) {
1474
			user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1475
			return $this;
1476
		}
1477
1478
		// Handle the backwards compatible case of passing "true" as the second argument
1479
		if ($mergeStrategy === true) {
1480
			$mergeStrategy = self::MERGE_CLEAR_MISSING;
1481
		}
1482
		else if ($mergeStrategy === false) {
1483
			$mergeStrategy = 0;
1484
		}
1485
1486
		// if an object is passed, save it for historical reference through {@link getRecord()}
1487
		if(is_object($data)) $this->record = $data;
1488
1489
		// dont include fields without data
1490
		$dataFields = $this->Fields()->dataFields();
1491
		if($dataFields) foreach($dataFields as $field) {
1492
			$name = $field->getName();
1493
1494
			// Skip fields that have been excluded
1495
			if($fieldList && !in_array($name, $fieldList)) continue;
1496
1497
			// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1498
			if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
1499
1500
			// Does this property exist on $data?
1501
			$exists = false;
1502
			// The value from $data for this field
1503
			$val = null;
1504
1505
			if(is_object($data)) {
1506
				$exists = (
1507
					isset($data->$name) ||
1508
					$data->hasMethod($name) ||
1509
					($data->hasMethod('hasField') && $data->hasField($name))
1510
				);
1511
1512
				if ($exists) {
1513
					$val = $data->__get($name);
1514
				}
1515
			}
1516
			else if(is_array($data)){
1517
				if(array_key_exists($name, $data)) {
1518
					$exists = true;
1519
					$val = $data[$name];
1520
				}
1521
				// If field is in array-notation we need to access nested data
1522
				else if(strpos($name,'[')) {
1523
					// First encode data using PHP's method of converting nested arrays to form data
1524
					$flatData = urldecode(http_build_query($data));
1525
					// Then pull the value out from that flattened string
1526
					preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches);
1527
1528
					if (isset($matches[1])) {
1529
						$exists = true;
1530
						$val = $matches[1];
1531
					}
1532
				}
1533
			}
1534
1535
			// save to the field if either a value is given, or loading of blank/undefined values is forced
1536
			if($exists){
1537
				if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
1538
					// pass original data as well so composite fields can act on the additional information
1539
					$field->setValue($val, $data);
1540
				}
1541
			}
1542
			else if(($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING){
1543
				$field->setValue($val, $data);
1544
			}
1545
		}
1546
1547
		return $this;
1548
	}
1549
1550
	/**
1551
	 * Save the contents of this form into the given data object.
1552
	 * It will make use of setCastedField() to do this.
1553
	 *
1554
	 * @param DataObjectInterface $dataObject The object to save data into
1555
	 * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1556
	 * form that has some fields that save to one object, and some that save to another.
1557
	 */
1558
	public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
1559
		$dataFields = $this->fields->saveableFields();
1560
		$lastField = null;
1561
		if($dataFields) foreach($dataFields as $field) {
1562
			// Skip fields that have been excluded
1563
			if($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) continue;
1564
1565
1566
			$saveMethod = "save{$field->getName()}";
1567
1568
			if($field->getName() == "ClassName"){
1569
				$lastField = $field;
1570
			}else if( $dataObject->hasMethod( $saveMethod ) ){
1571
				$dataObject->$saveMethod( $field->dataValue());
1572
			} else if($field->getName() != "ID"){
1573
				$field->saveInto($dataObject);
1574
			}
1575
		}
1576
		if($lastField) $lastField->saveInto($dataObject);
1577
	}
1578
1579
	/**
1580
	 * Get the submitted data from this form through
1581
	 * {@link FieldList->dataFields()}, which filters out
1582
	 * any form-specific data like form-actions.
1583
	 * Calls {@link FormField->dataValue()} on each field,
1584
	 * which returns a value suitable for insertion into a DataObject
1585
	 * property.
1586
	 *
1587
	 * @return array
1588
	 */
1589
	public function getData() {
1590
		$dataFields = $this->fields->dataFields();
1591
		$data = array();
1592
1593
		if($dataFields){
1594
			foreach($dataFields as $field) {
1595
				if($field->getName()) {
1596
					$data[$field->getName()] = $field->dataValue();
1597
				}
1598
			}
1599
		}
1600
1601
		return $data;
1602
	}
1603
1604
	/**
1605
	 * Call the given method on the given field.
1606
	 *
1607
	 * @param array $data
1608
	 * @return mixed
1609
	 */
1610
	public function callfieldmethod($data) {
1611
		$fieldName = $data['fieldName'];
1612
		$methodName = $data['methodName'];
1613
		$fields = $this->fields->dataFields();
1614
1615
		// special treatment needed for TableField-class and TreeDropdownField
1616
		if(strpos($fieldName, '[')) {
1617
			preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
1618
			preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
1619
			$tableFieldName = $fieldNameMatches[1][0];
1620
			$subFieldName = $subFieldMatches[1][1];
1621
		}
1622
1623
		if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
1624
			$field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
1625
			return $field->$methodName();
1626
		} else if(isset($fields[$fieldName])) {
1627
			return $fields[$fieldName]->$methodName();
1628
		} else {
1629
			user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
1630
		}
1631
	}
1632
1633
	/**
1634
	 * Return a rendered version of this form.
1635
	 *
1636
	 * This is returned when you access a form as $FormObject rather
1637
	 * than <% with FormObject %>
1638
	 *
1639
	 * @return DBHTMLText
1640
	 */
1641
	public function forTemplate() {
1642
		$return = $this->renderWith(array_merge(
1643
			(array)$this->getTemplate(),
1644
			array('Includes/Form')
1645
		));
1646
1647
		// Now that we're rendered, clear message
1648
		$this->clearMessage();
1649
1650
		return $return;
1651
	}
1652
1653
	/**
1654
	 * Return a rendered version of this form, suitable for ajax post-back.
1655
	 *
1656
	 * It triggers slightly different behaviour, such as disabling the rewriting
1657
	 * of # links.
1658
	 *
1659
	 * @return DBHTMLText
1660
	 */
1661
	public function forAjaxTemplate() {
1662
		$view = new SSViewer(array(
1663
			$this->getTemplate(),
1664
			'Form'
1665
		));
1666
1667
		$return = $view->dontRewriteHashlinks()->process($this);
1668
1669
		// Now that we're rendered, clear message
1670
		$this->clearMessage();
1671
1672
		return $return;
1673
	}
1674
1675
	/**
1676
	 * Returns an HTML rendition of this form, without the <form> tag itself.
1677
	 *
1678
	 * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1679
	 * and _form_enctype.  These are the attributes of the form.  These fields
1680
	 * can be used to send the form to Ajax.
1681
	 *
1682
	 * @return DBHTMLText
1683
	 */
1684
	public function formHtmlContent() {
1685
		$this->IncludeFormTag = false;
1686
		$content = $this->forTemplate();
1687
		$this->IncludeFormTag = true;
1688
1689
		$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
1690
			. " value=\"" . $this->FormAction() . "\" />\n";
1691
		$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1692
		$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1693
		$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1694
1695
		return $content;
1696
	}
1697
1698
	/**
1699
	 * Render this form using the given template, and return the result as a string
1700
	 * You can pass either an SSViewer or a template name
1701
	 * @param string|array $template
1702
	 * @return DBHTMLText
1703
	 */
1704
	public function renderWithoutActionButton($template) {
1705
		$custom = $this->customise(array(
1706
			"Actions" => "",
1707
		));
1708
1709
		if(is_string($template)) {
1710
			$template = new SSViewer($template);
1711
		}
1712
1713
		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...
1714
	}
1715
1716
1717
	/**
1718
	 * Sets the button that was clicked.  This should only be called by the Controller.
1719
	 *
1720
	 * @param callable $funcName The name of the action method that will be called.
1721
	 * @return $this
1722
	 */
1723
	public function setButtonClicked($funcName) {
1724
		$this->buttonClickedFunc = $funcName;
1725
1726
		return $this;
1727
	}
1728
1729
	/**
1730
	 * @return FormAction
1731
	 */
1732
	public function buttonClicked() {
1733
		$actions = $this->getAllActions();
1734
 		foreach ($actions as $action) {
1735
			if ($this->buttonClickedFunc === $action->actionName()) {
1736
				return $action;
1737
			}
1738
		}
1739
1740
			return null;
1741
		}
1742
1743
	/**
1744
	 * Get a list of all actions, including those in the main "fields" FieldList
1745
	 * 
1746
	 * @return array
1747
	 */
1748
	protected function getAllActions() {
1749
		$fields = $this->fields->dataFields() ?: array();
1750
		$actions = $this->actions->dataFields() ?: array();
1751
1752
		$fieldsAndActions = array_merge($fields, $actions);
1753
		$actions = array_filter($fieldsAndActions, function($fieldOrAction) {
1754
			return $fieldOrAction instanceof FormAction;
1755
		});
1756
1757
		return $actions;
1758
	}
1759
1760
	/**
1761
	 * Return the default button that should be clicked when another one isn't
1762
	 * available.
1763
	 *
1764
	 * @return FormAction
1765
	 */
1766
	public function defaultAction() {
1767
		if($this->hasDefaultAction && $this->actions) {
1768
			return $this->actions->First();
1769
		}
1770
	}
1771
1772
	/**
1773
	 * Disable the default button.
1774
	 *
1775
	 * Ordinarily, when a form is processed and no action_XXX button is
1776
	 * available, then the first button in the actions list will be pressed.
1777
	 * However, if this is "delete", for example, this isn't such a good idea.
1778
	 *
1779
	 * @return Form
1780
	 */
1781
	public function disableDefaultAction() {
1782
		$this->hasDefaultAction = false;
1783
1784
		return $this;
1785
	}
1786
1787
	/**
1788
	 * Disable the requirement of a security token on this form instance. This
1789
	 * security protects against CSRF attacks, but you should disable this if
1790
	 * you don't want to tie a form to a session - eg a search form.
1791
	 *
1792
	 * Check for token state with {@link getSecurityToken()} and
1793
	 * {@link SecurityToken->isEnabled()}.
1794
	 *
1795
	 * @return Form
1796
	 */
1797
	public function disableSecurityToken() {
1798
		$this->securityToken = new NullSecurityToken();
1799
1800
		return $this;
1801
	}
1802
1803
	/**
1804
	 * Enable {@link SecurityToken} protection for this form instance.
1805
	 *
1806
	 * Check for token state with {@link getSecurityToken()} and
1807
	 * {@link SecurityToken->isEnabled()}.
1808
	 *
1809
	 * @return Form
1810
	 */
1811
	public function enableSecurityToken() {
1812
		$this->securityToken = new SecurityToken();
1813
1814
		return $this;
1815
	}
1816
1817
	/**
1818
	 * Returns the security token for this form (if any exists).
1819
	 *
1820
	 * Doesn't check for {@link securityTokenEnabled()}.
1821
	 *
1822
	 * Use {@link SecurityToken::inst()} to get a global token.
1823
	 *
1824
	 * @return SecurityToken|null
1825
	 */
1826
	public function getSecurityToken() {
1827
		return $this->securityToken;
1828
	}
1829
1830
	/**
1831
	 * Returns the name of a field, if that's the only field that the current
1832
	 * controller is interested in.
1833
	 *
1834
	 * It checks for a call to the callfieldmethod action.
1835
	 *
1836
	 * @return string
1837
	 */
1838
	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...
1839
		if(self::current_action() == 'callfieldmethod') {
1840
			return $_REQUEST['fieldName'];
1841
	}
1842
	}
1843
1844
	/**
1845
	 * Return the current form action being called, if available.
1846
	 *
1847
	 * @return string
1848
	 */
1849
	public static function current_action() {
1850
		return self::$current_action;
1851
	}
1852
1853
	/**
1854
	 * Set the current form action. Should only be called by {@link Controller}.
1855
	 *
1856
	 * @param string $action
1857
	 */
1858
	public static function set_current_action($action) {
1859
		self::$current_action = $action;
1860
	}
1861
1862
	/**
1863
	 * Compiles all CSS-classes.
1864
	 *
1865
	 * @return string
1866
	 */
1867
	public function extraClass() {
1868
		return implode(array_unique($this->extraClasses), ' ');
1869
	}
1870
1871
	/**
1872
	 * Add a CSS-class to the form-container. If needed, multiple classes can
1873
	 * be added by delimiting a string with spaces.
1874
	 *
1875
	 * @param string $class A string containing a classname or several class
1876
	 *                names delimited by a single space.
1877
	 * @return $this
1878
	 */
1879
	public function addExtraClass($class) {
1880
		//split at white space
1881
		$classes = preg_split('/\s+/', $class);
1882
		foreach($classes as $class) {
1883
			//add classes one by one
1884
			$this->extraClasses[$class] = $class;
1885
		}
1886
		return $this;
1887
	}
1888
1889
	/**
1890
	 * Remove a CSS-class from the form-container. Multiple class names can
1891
	 * be passed through as a space delimited string
1892
	 *
1893
	 * @param string $class
1894
	 * @return $this
1895
	 */
1896
	public function removeExtraClass($class) {
1897
		//split at white space
1898
		$classes = preg_split('/\s+/', $class);
1899
		foreach ($classes as $class) {
1900
			//unset one by one
1901
			unset($this->extraClasses[$class]);
1902
		}
1903
		return $this;
1904
	}
1905
1906
	public function debug() {
1907
		$result = "<h3>$this->class</h3><ul>";
1908
		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...
1909
			$result .= "<li>$field" . $field->debug() . "</li>";
1910
		}
1911
		$result .= "</ul>";
1912
1913
		if( $this->validator )
1914
			$result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1915
1916
		return $result;
1917
	}
1918
1919
1920
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1921
	// TESTING HELPERS
1922
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1923
1924
	/**
1925
	 * Test a submission of this form.
1926
	 * @param string $action
1927
	 * @param array $data
1928
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1929
	 * your unit test.
1930
	 * @throws SS_HTTPResponse_Exception
1931
	 */
1932
	public function testSubmission($action, $data) {
1933
		$data['action_' . $action] = true;
1934
1935
		return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
1936
	}
1937
1938
	/**
1939
	 * Test an ajax submission of this form.
1940
	 *
1941
	 * @param string $action
1942
	 * @param array $data
1943
	 * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in
1944
	 * your unit test.
1945
	 */
1946
	public function testAjaxSubmission($action, $data) {
1947
		$data['ajax'] = 1;
1948
		return $this->testSubmission($action, $data);
1949
	}
1950
}
1951
1952
/**
1953
 * @package forms
1954
 * @subpackage core
1955
 */
1956
class Form_FieldMap extends ViewableData {
1957
1958
	protected $form;
1959
1960
	public function __construct($form) {
1961
		$this->form = $form;
1962
		parent::__construct();
1963
	}
1964
1965
	/**
1966
	 * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
1967
	 * @param string $method
1968
	 * @return bool
1969
	 */
1970
	public function hasMethod($method) {
1971
		return true;
1972
	}
1973
1974
	public function __call($method, $args = null) {
1975
		return $this->form->Fields()->fieldByName($method);
1976
	}
1977
}
1978