validate()} methods for each field.
* You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads depend on it.
* The default validator is an instance of {@link RequiredFields}.
* If you want to enforce serverside-validation to be ignored for a specific {@link FormField},
* you need to subclass it.
*
* @package forms
* @subpackage core
*/
class Form extends RequestHandler {
/**
* @var boolean $includeFormTag Accessed by Form.ss; modified by {@link formHtmlContent()}.
* A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
*/
public $IncludeFormTag = true;
protected $fields;
protected $actions;
protected $controller;
protected $name;
protected $validator;
protected $formMethod = "post";
protected static $current_action;
/**
* @var Dataobject $record Populated by {@link loadDataFrom()}.
*/
protected $record;
/**
* Keeps track of whether this form has a default action or not.
* Set to false by $this->disableDefaultAction();
*/
protected $hasDefaultAction = true;
/**
* Target attribute of form-tag.
* Useful to open a new window upon
* form submission.
*
* @var string
*/
protected $target;
/**
* Legend value, to be inserted into the
* element before the
* in Form.ss template.
*
* @var string
*/
protected $legend;
/**
* The SS template to render this form HTML into.
* Default is "Form", but this can be changed to
* another template for customisation.
*
* @see Form->setTemplate()
* @var string
*/
protected $template;
protected $buttonClickedFunc;
protected $message;
protected $messageType;
/**
* Should we redirect the user back down to the
* the form on validation errors rather then just the page
*
* @var bool
*/
protected $redirectToFormOnValidationError = false;
protected $security = true;
/**
* HACK This is a temporary hack to allow multiple calls to includeJavascriptValidation on
* the validator (if one is present).
*
* @var boolean
*/
public $jsValidationIncluded = false;
/**
* @var $extraClasses array Extra CSS-classes for the formfield-container
*/
protected $extraClasses = array();
/**
* Create a new form, with the given fields an action buttons.
*
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
* @param String $name The method on the controller that will return this form object.
* @param FieldSet $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects.
* @param FieldSet $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects
* @param Validator $validator Override the default validator instance (Default: {@link RequiredFields})
*/
function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $validator = null) {
parent::__construct();
foreach($fields as $field) $field->setForm($this);
foreach($actions as $action) $actions->setForm($this);
$this->fields = $fields;
$this->actions = $actions;
$this->controller = $controller;
$this->name = $name;
if(!$this->controller) user_error("$this->class form created without a controller", E_USER_ERROR);
// Form validation
$this->validator = ($validator) ? $validator : new RequiredFields();
$this->validator->setForm($this);
// Form error controls
$this->setupFormErrors();
// Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
// method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
if(method_exists($controller, 'securityTokenEnabled')) {
$this->security = $controller->securityTokenEnabled();
} else {
$this->security = self::$default_security;
}
}
static $url_handlers = array(
'field/$FieldName!' => 'handleField',
'$Action!' => 'handleAction',
'POST ' => 'httpSubmission',
'GET ' => 'httpSubmission',
);
/**
* Set up current form errors in session to
* the current form if appropriate.
*/
function setupFormErrors() {
$errorInfo = Session::get("FormInfo.{$this->FormName()}");
if(isset($errorInfo['errors']) && is_array($errorInfo['errors'])) {
foreach($errorInfo['errors'] as $error) {
$field = $this->fields->dataFieldByName($error['fieldName']);
if(!$field) {
$errorInfo['message'] = $error['message'];
$errorInfo['type'] = $error['messageType'];
} else {
$field->setError($error['message'], $error['messageType']);
}
}
// load data in from previous submission upon error
if(isset($errorInfo['data'])) $this->loadDataFrom($errorInfo['data']);
}
if(isset($errorInfo['message']) && isset($errorInfo['type'])) {
$this->setMessage($errorInfo['message'], $errorInfo['type']);
}
}
/**
* Handle a form submission. GET and POST requests behave identically.
* Populates the form with {@link loadDataFrom()}, calls {@link validate()},
* and only triggers the requested form action/method
* if the form is valid.
*/
function httpSubmission($request) {
$vars = $request->requestVars();
if(isset($funcName)) {
Form::set_current_action($funcName);
}
// Populate the form
$this->loadDataFrom($vars, true);
// Protection against CSRF attacks
if($this->securityTokenEnabled()) {
$securityID = Session::get('SecurityID');
if(!$securityID || !isset($vars['SecurityID']) || $securityID != $vars['SecurityID']) {
$this->httpError(400, "SecurityID doesn't match, possible CRSF attack.");
}
}
// Determine the action button clicked
$funcName = null;
foreach($vars as $paramName => $paramVal) {
if(substr($paramName,0,7) == 'action_') {
// Break off querystring arguments included in the action
if(strpos($paramName,'?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array();
parse_str($paramVars, $newRequestParams);
$vars = array_merge((array)$vars, (array)$newRequestParams);
}
// Cleanup action_, _x and _y from image fields
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
break;
}
}
// If the action wasnt' set, choose the default on the form.
if(!isset($funcName) && $defaultAction = $this->defaultAction()){
$funcName = $defaultAction->actionName();
}
if(isset($funcName)) {
$this->setButtonClicked($funcName);
}
// Validate the form
if(!$this->validate()) {
if(Director::is_ajax()) {
// Special case for legacy Validator.js implementation (assumes eval'ed javascript collected through FormResponse)
if($this->validator->getJavascriptValidationHandler() == 'prototype') {
return FormResponse::respond();
} else {
$acceptType = $request->getHeader('Accept');
if(strpos($acceptType, 'application/json') !== FALSE) {
// Send validation errors back as JSON with a flag at the start
$response = new HTTPResponse(Convert::array2json($this->validator->getErrors()));
$response->addHeader('Content-Type', 'application/json');
} else {
$this->setupFormErrors();
// Send the newly rendered form tag as HTML
$response = new HTTPResponse($this->forTemplate());
$response->addHeader('Content-Type', 'text/html');
}
return $response;
}
} else {
if($this->getRedirectToFormOnValidationError()) {
if($pageURL = $request->getHeader('Referer')) {
return Director::redirect($pageURL . '#' . $this->FormName());
}
}
return Director::redirectBack();
}
}
// First, try a handler method on the controller
if($this->controller->hasMethod($funcName)) {
return $this->controller->$funcName($vars, $this, $request);
// Otherwise, try a handler method on the form object
} else {
return $this->$funcName($vars, $this, $request);
}
}
/**
* Handle a field request
*/
function handleField($request) {
return $this->dataFieldByName($request->param('FieldName'));
}
/**
* Convert this form into a readonly form
*/
function makeReadonly() {
$this->transform(new ReadonlyTransformation());
}
/**
* Set whether the user should be redirected back down to the
* form on the page upon validation errors in the form or if
* they just need to redirect back to the page
*
* @param bool Redirect to the form
*/
public function setRedirectToFormOnValidationError($bool) {
$this->redirectToFormOnValidationError = $bool;
}
/**
* Get whether the user should be redirected back down to the
* form on the page upon validation errors
*
* @return bool
*/
public function getRedirectToFormOnValidationError() {
return $this->redirectToFormOnValidationError;
}
/**
* Add an error message to a field on this form. It will be saved into the session
* and used the next time this form is displayed.
*/
function addErrorMessage($fieldName, $message, $messageType) {
Session::addToArray("FormInfo.{$this->FormName()}.errors", array(
'fieldName' => $fieldName,
'message' => $message,
'messageType' => $messageType,
));
}
function transform(FormTransformation $trans) {
$newFields = new FieldSet();
foreach($this->fields as $field) {
$newFields->push($field->transform($trans));
}
$this->fields = $newFields;
$newActions = new FieldSet();
foreach($this->actions as $action) {
$newActions->push($action->transform($trans));
}
$this->actions = $newActions;
// We have to remove validation, if the fields are not editable ;-)
if($this->validator)
$this->validator->removeValidation();
}
/**
* Get the {@link Validator} attached to this form.
* @return Validator
*/
function getValidator() {
return $this->validator;
}
/**
* Set the {@link Validator} on this form.
*/
function setValidator( Validator $validator ) {
if($validator) {
$this->validator = $validator;
$this->validator->setForm($this);
}
}
/**
* Remove the {@link Validator} from this from.
*/
function unsetValidator(){
$this->validator = null;
}
/**
* Convert this form to another format.
*/
function transformTo(FormTransformation $format) {
$newFields = new FieldSet();
foreach($this->fields as $field) {
$newFields->push($field->transformTo($format));
}
$this->fields = $newFields;
// We have to remove validation, if the fields are not editable ;-)
if($this->validator)
$this->validator->removeValidation();
}
/**
* Generate extra special fields - namely the SecurityID field
*
* @return FieldSet
*/
public function getExtraFields() {
$extraFields = new FieldSet();
if(!$this->fields->fieldByName('SecurityID') && $this->securityTokenEnabled()) {
if(Session::get('SecurityID')) {
$securityID = Session::get('SecurityID');
} else {
$securityID = rand();
Session::set('SecurityID', $securityID);
}
$securityField = new HiddenField('SecurityID', '', $securityID);
$securityField->setForm($this);
$extraFields->push($securityField);
$this->securityTokenAdded = true;
}
// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
if($this->FormMethod() != $this->FormHttpMethod()) {
$methodField = new HiddenField('_method', '', $this->FormHttpMethod());
$methodField->setForm($this);
$extraFields->push($methodField);
}
return $extraFields;
}
/**
* Return the form's fields - used by the templates
*
* @return FieldSet The form fields
*/
function Fields() {
foreach($this->getExtraFields() as $field) {
if(!$this->fields->fieldByName($field->Name())) $this->fields->push($field);
}
return $this->fields;
}
/**
* Return all fields
* in a form - including fields nested in {@link CompositeFields}.
* Useful when doing custom field layouts.
*
* @return FieldSet
*/
function HiddenFields() {
return $this->fields->HiddenFields();
}
/**
* Setter for the form fields.
*
* @param FieldSet $fields
*/
function setFields($fields) {
$this->fields = $fields;
}
/**
* Get a named field from this form's fields.
* It will traverse into composite fields for you, to find the field you want.
* It will only return a data field.
*
* @return FormField
*/
function dataFieldByName($name) {
foreach($this->getExtraFields() as $field) {
if(!$this->fields->dataFieldByName($field->Name())) $this->fields->push($field);
}
return $this->fields->dataFieldByName($name);
}
/**
* Return the form's action buttons - used by the templates
*
* @return FieldSet The action list
*/
function Actions() {
return $this->actions;
}
/**
* Setter for the form actions.
*
* @param FieldSet $actions
*/
function setActions($actions) {
$this->actions = $actions;
}
/**
* Unset all form actions
*/
function unsetAllActions(){
$this->actions = new FieldSet();
}
/**
* Unset the form's action button by its name.
*
* @param string $name
*/
function unsetActionByName($name) {
$this->actions->removeByName($name);
}
/**
* Unset the form's dataField by its name
*/
function unsetDataFieldByName($fieldName){
foreach($this->Fields()->dataFields() as $child) {
if(is_object($child) && ($child->Name() == $fieldName || $child->Title() == $fieldName)) {
$child = null;
}
}
}
/**
* Remove a field from the given tab.
*/
public function unsetFieldFromTab($tabName, $fieldName) {
// Find the tab
$tab = $this->Fields()->findOrMakeTab($tabName);
$tab->removeByName($fieldName);
}
/**
* Return the attributes of the form tag - used by the templates.
*
* @return string The attribute string
*/
function FormAttributes() {
$attributes = array();
// Forms shouldn't be cached, cos their error messages won't be shown
HTTP::set_cache_age(0);
// workaround to include javascript validation
if($this->validator && !$this->jsValidationIncluded) $this->validator->includeJavascriptValidation();
// compile attributes
$attributes['id'] = $this->FormName();
$attributes['action'] = $this->FormAction();
$attributes['method'] = $this->FormMethod();
$attributes['enctype'] = $this->FormEncType();
if($this->target) $attributes['target'] = $this->target;
if($this->extraClass()) $attributes['class'] = $this->extraClass();
if($this->validator && $this->validator->getErrors()) {
if(!isset($attributes['class'])) $attributes['class'] = '';
$attributes['class'] .= ' validationerror';
}
// implode attributes into string
$preparedAttributes = '';
foreach($attributes as $k => $v) {
// Note: as indicated by the $k == value item here; the decisions over what to include in the attributes can sometimes get finicky
if(!empty($v) || $v === '0' || $k == 'value') $preparedAttributes .= " $k=\"" . Convert::raw2att($v) . "\"";
}
return $preparedAttributes;
}
/**
* Set the target of this form to any value - useful for opening the form contents in a new window or refreshing another frame
*
* @param target The value of the target
*/
function setTarget($target) {
$this->target = $target;
}
/**
* Set the legend value to be inserted into
* the element in the Form.ss template.
*/
function setLegend($legend) {
$this->legend = $legend;
}
/**
* Set the SS template that this form should use
* to render with. The default is "Form".
*
* @param string $template The name of the template (without the .ss extension)
*/
function setTemplate($template) {
$this->template = $template;
}
/**
* Return the template to render this form with.
* If the template isn't set, then default to the
* form class name e.g "Form".
*
* @return string
*/
function getTemplate() {
if($this->template) return $this->template;
else return $this->class;
}
/**
* Returns the encoding type of the form.
* This will be either "multipart/form-data"" if there are any {@link FileField} instances,
* otherwise "application/x-www-form-urlencoded"
*
* @return string The encoding mime type
*/
function FormEncType() {
if(is_array($this->fields->dataFields())){
foreach($this->fields->dataFields() as $field) {
if(is_a($field, "FileField")) return "multipart/form-data";
}
}
return "application/x-www-form-urlencoded";
}
/**
* Returns the real HTTP method for the form:
* GET, POST, PUT, DELETE or HEAD.
* As most browsers only support GET and POST in
* form submissions, all other HTTP methods are
* added as a hidden field "_method" that
* gets evaluated in {@link Director::direct()}.
* See {@link FormMethod()} to get a HTTP method
* for safe insertion into a