Version: current

       1 <?php
       2 /**
       3  * Base class for all forms.
       4  * The form class is an extensible base for all forms on a sapphire application.  It can be used
       5  * either by extending it, and creating processor methods on the subclass, or by creating instances
       6  * of form whose actions are handled by the parent controller.
       7  *
       8  * In either case, if you want to get a form to do anything, it must be inextricably tied to a
       9  * controller.  The constructor is passed a controller and a method on that controller.  This method
      10  * should return the form object, and it shouldn't require any arguments.  Parameters, if necessary,
      11  * can be passed using the URL or get variables.  These restrictions are in place so that we can
      12  * recreate the form object upon form submission, without the use of a session, which would be too
      13  * resource-intensive.
      14  *
      15  * You will need to create at least one method for processing the submission (through {@link FormAction}).
      16  * This method will be passed two parameters: the raw request data, and the form object.
      17  * Usually you want to save data into a {@link DataObject} by using {@link saveInto()}.
      18  * If you want to process the submitted data in any way, please use {@link getData()} rather than
      19  * the raw request data.
      20  *
      21  * <h2>Validation</h2>
      22  * Each form needs some form of {@link Validator} to trigger the {@link FormField->validate()} methods for each field.
      23  * You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads depend on it.
      24  * The default validator is an instance of {@link RequiredFields}.
      25  * If you want to enforce serverside-validation to be ignored for a specific {@link FormField},
      26  * you need to subclass it.
      27  *
      28  * <h2>URL Handling</h2>
      29  * The form class extends {@link RequestHandler}, which means it can
      30  * be accessed directly through a URL. This can be handy for refreshing
      31  * a form by ajax, or even just displaying a single form field.
      32  * You can find out the base URL for your form by looking at the
      33  * <form action="..."> value. For example, the edit form in the CMS would be located at
      34  * "admin/EditForm". This URL will render the form without its surrounding
      35  * template when called through GET instead of POST.
      36  *
      37  * By appending to this URL, you can render invidual form elements
      38  * through the {@link FormField->FieldHolder()} method.
      39  * For example, the "URLSegment" field in a standard CMS form would be
      40  * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
      41  *
      42  * @package forms
      43  * @subpackage core
      44  */
      45 class Form extends RequestHandler {
      46
      47     /**
      48      * @var boolean $includeFormTag Accessed by Form.ss; modified by {@link formHtmlContent()}.
      49      * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
      50      */
      51     public $IncludeFormTag = true;
      52
      53     protected $fields;
      54
      55     protected $actions;
      56
      57     protected $controller;
      58
      59     protected $name;
      60
      61     protected $validator;
      62
      63     protected $formMethod = "post";
      64
      65     protected static $current_action;
      66
      67     /**
      68      * @var Dataobject $record Populated by {@link loadDataFrom()}.
      69      */
      70     protected $record;
      71
      72     /**
      73      * Keeps track of whether this form has a default action or not.
      74      * Set to false by $this->disableDefaultAction();
      75      */
      76     protected $hasDefaultAction = true;
      77
      78     /**
      79      * Target attribute of form-tag.
      80      * Useful to open a new window upon
      81      * form submission.
      82      *
      83      * @var string
      84      */
      85     protected $target;
      86
      87     /**
      88      * Legend value, to be inserted into the
      89      * <legend> element before the <fieldset>
      90      * in Form.ss template.
      91      *
      92      * @var string
      93      */
      94     protected $legend;
      95
      96     /**
      97      * The SS template to render this form HTML into.
      98      * Default is "Form", but this can be changed to
      99      * another template for customisation.
     100      *
     101      * @see Form->setTemplate()
     102      * @var string
     103      */
     104     protected $template;
     105
     106     protected $buttonClickedFunc;
     107
     108     protected $message;
     109
     110     protected $messageType;
     111
     112     /**
     113      * Should we redirect the user back down to the
     114      * the form on validation errors rather then just the page
     115      *
     116      * @var bool
     117      */
     118     protected $redirectToFormOnValidationError = false;
     119
     120     protected $security = true;
     121
     122     /**
     123      * HACK This is a temporary hack to allow multiple calls to includeJavascriptValidation on
     124      * the validator (if one is present).
     125      *
     126      * @var boolean
     127      */
     128     public $jsValidationIncluded = false;
     129
     130     /**
     131      * @var $extraClasses array Extra CSS-classes for the formfield-container
     132      */
     133     protected $extraClasses = array();
     134
     135     /**
     136      * Create a new form, with the given fields an action buttons.
     137      *
     138      * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
     139      * @param String $name The method on the controller that will return this form object.
     140      * @param FieldSet $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects.
     141      * @param FieldSet $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects
     142      * @param Validator $validator Override the default validator instance (Default: {@link RequiredFields})
     143      */
     144     function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $validator = null) {
     145         parent::__construct();
     146
     147         $fields->setForm($this);
     148         $actions->setForm($this);
     149
     150         $this->fields = $fields;
     151         $this->actions = $actions;
     152         $this->controller = $controller;
     153         $this->name = $name;
     154
     155         if(!$this->controller) user_error("$this->class form created without a controller", E_USER_ERROR);
     156
     157         // Form validation
     158         $this->validator = ($validator) ? $validator : new RequiredFields();
     159         $this->validator->setForm($this);
     160
     161         // Form error controls
     162         $this->setupFormErrors();
     163
     164         // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
     165         // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
     166         if(method_exists($controller, 'securityTokenEnabled')) {
     167             $this->security = $controller->securityTokenEnabled();
     168         } else {
     169             $this->security = self::$default_security;
     170         }
     171     }
     172
     173     static $url_handlers = array(
     174         'field/$FieldName!' => 'handleField',
     175         '$Action!' => 'handleAction',
     176         'POST ' => 'httpSubmission',
     177         'GET ' => 'httpSubmission',
     178     );
     179
     180     /**
     181      * Set up current form errors in session to
     182      * the current form if appropriate.
     183      */
     184     function setupFormErrors() {
     185         $errorInfo = Session::get("FormInfo.{$this->FormName()}");
     186
     187         if(isset($errorInfo['errors']) && is_array($errorInfo['errors'])) {
     188             foreach($errorInfo['errors'] as $error) {
     189                 $field = $this->fields->dataFieldByName($error['fieldName']);
     190
     191                 if(!$field) {
     192                     $errorInfo['message'] = $error['message'];
     193                     $errorInfo['type'] = $error['messageType'];
     194                 } else {
     195                     $field->setError($error['message'], $error['messageType']);
     196                 }
     197             }
     198
     199             // load data in from previous submission upon error
     200             if(isset($errorInfo['data'])) $this->loadDataFrom($errorInfo['data']);
     201         }
     202
     203         if(isset($errorInfo['message']) && isset($errorInfo['type'])) {
     204             $this->setMessage($errorInfo['message'], $errorInfo['type']);
     205         }
     206     }
     207
     208     /**
     209      * Handle a form submission.  GET and POST requests behave identically.
     210      * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
     211      * and only triggers the requested form action/method
     212      * if the form is valid.
     213      */
     214     function httpSubmission($request) {
     215         $vars = $request->requestVars();
     216         if(isset($funcName)) {
     217             Form::set_current_action($funcName);
     218         }
     219
     220         // Populate the form
     221         $this->loadDataFrom($vars, true);
     222
     223         // Protection against CSRF attacks
     224         if($this->securityTokenEnabled()) {
     225             $securityID = Session::get('SecurityID');
     226
     227             if(!$securityID || !isset($vars['SecurityID']) || $securityID != $vars['SecurityID']) {
     228                 $this->httpError(400, "SecurityID doesn't match, possible CSRF attack.");
     229             }
     230         }
     231
     232         // Determine the action button clicked
     233         $funcName = null;
     234         foreach($vars as $paramName => $paramVal) {
     235             if(substr($paramName,0,7) == 'action_') {
     236                 // Break off querystring arguments included in the action
     237                 if(strpos($paramName,'?') !== false) {
     238                     list($paramName, $paramVars) = explode('?', $paramName, 2);
     239                     $newRequestParams = array();
     240                     parse_str($paramVars, $newRequestParams);
     241                     $vars = array_merge((array)$vars, (array)$newRequestParams);
     242                 }
     243
     244                 // Cleanup action_, _x and _y from image fields
     245                 $funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
     246                 break;
     247             }
     248         }
     249
     250         // If the action wasnt' set, choose the default on the form.
     251         if(!isset($funcName) && $defaultAction = $this->defaultAction()){
     252             $funcName = $defaultAction->actionName();
     253         }
     254
     255         if(isset($funcName)) {
     256             $this->setButtonClicked($funcName);
     257         }
     258
     259         // Validate the form
     260         if(!$this->validate()) {
     261             if(Director::is_ajax()) {
     262                 // Special case for legacy Validator.js implementation (assumes eval'ed javascript collected through FormResponse)
     263                 if($this->validator->getJavascriptValidationHandler() == 'prototype') {
     264                     return FormResponse::respond();
     265                 } else {
     266                     $acceptType = $request->getHeader('Accept');
     267                     if(strpos($acceptType, 'application/json') !== FALSE) {
     268                         // Send validation errors back as JSON with a flag at the start
     269                         $response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors()));
     270                         $response->addHeader('Content-Type', 'application/json');
     271                     } else {
     272                         $this->setupFormErrors();
     273                         // Send the newly rendered form tag as HTML
     274                         $response = new SS_HTTPResponse($this->forAjaxTemplate());
     275                         $response->addHeader('Content-Type', 'text/html');
     276                     }
     277
     278                     return $response;
     279                 }
     280             } else {
     281                 if($this->getRedirectToFormOnValidationError()) {
     282                     if($pageURL = $request->getHeader('Referer')) {
     283                         if(Director::is_site_url($pageURL)) {
     284                             // Remove existing pragmas
     285                             $pageURL = preg_replace('/(#.*)/', '', $pageURL);
     286                             return Director::redirect($pageURL . '#' . $this->FormName());
     287                         }
     288                     }
     289                 }
     290                 return Director::redirectBack();
     291             }
     292         }
     293
     294         // First, try a handler method on the controller
     295         if($this->controller->hasMethod($funcName)) {
     296             return $this->controller->$funcName($vars, $this, $request);
     297
     298         // Otherwise, try a handler method on the form object
     299         } else {
     300             if($this->hasMethod($funcName)) {
     301                 return $this->$funcName($vars, $this, $request);
     302             }
     303         }
     304     }
     305
     306     /**
     307      * Handle a field request.
     308      * Uses {@link Form->dataFieldByName()} to find a matching field,
     309      * and falls back to {@link FieldSet->fieldByName()} to look
     310      * for tabs instead. This means that if you have a tab and a
     311      * formfield with the same name, this method gives priority
     312      * to the formfield.
     313      *
     314      * @param SS_HTTPRequest $request
     315      * @return FormField
     316      */
     317     function handleField($request) {
     318         $field = $this->dataFieldByName($request->param('FieldName'));
     319
     320         if($field) {
     321             return $field;
     322         } else {
     323             // falling back to fieldByName, e.g. for getting tabs
     324             return $this->Fields()->fieldByName($request->param('FieldName'));
     325         }
     326     }
     327
     328     /**
     329      * Convert this form into a readonly form
     330      */
     331     function makeReadonly() {
     332         $this->transform(new ReadonlyTransformation());
     333     }
     334
     335     /**
     336      * Set whether the user should be redirected back down to the
     337      * form on the page upon validation errors in the form or if
     338      * they just need to redirect back to the page
     339      *
     340      * @param bool Redirect to the form
     341      */
     342     public function setRedirectToFormOnValidationError($bool) {
     343         $this->redirectToFormOnValidationError = $bool;
     344     }
     345
     346     /**
     347      * Get whether the user should be redirected back down to the
     348      * form on the page upon validation errors
     349      *
     350      * @return bool
     351      */
     352     public function getRedirectToFormOnValidationError() {
     353         return $this->redirectToFormOnValidationError;
     354     }
     355
     356     /**
     357      * Add an error message to a field on this form.  It will be saved into the session
     358      * and used the next time this form is displayed.
     359      */
     360     function addErrorMessage($fieldName, $message, $messageType) {
     361         Session::add_to_array("FormInfo.{$this->FormName()}.errors",  array(
     362             'fieldName' => $fieldName,
     363             'message' => $message,
     364             'messageType' => $messageType,
     365         ));
     366     }
     367
     368     function transform(FormTransformation $trans) {
     369         $newFields = new FieldSet();
     370         foreach($this->fields as $field) {
     371             $newFields->push($field->transform($trans));
     372         }
     373         $this->fields = $newFields;
     374
     375         $newActions = new FieldSet();
     376         foreach($this->actions as $action) {
     377             $newActions->push($action->transform($trans));
     378         }
     379         $this->actions = $newActions;
     380
     381
     382         // We have to remove validation, if the fields are not editable ;-)
     383         if($this->validator)
     384             $this->validator->removeValidation();
     385     }
     386
     387     /**
     388      * Get the {@link Validator} attached to this form.
     389      * @return Validator
     390      */
     391     function getValidator() {
     392         return $this->validator;
     393     }
     394
     395     /**
     396      * Set the {@link Validator} on this form.
     397      */
     398     function setValidator( Validator $validator ) {
     399         if($validator) {
     400             $this->validator = $validator;
     401             $this->validator->setForm($this);
     402         }
     403     }
     404
     405     /**
     406      * Remove the {@link Validator} from this from.
     407      */
     408     function unsetValidator(){
     409         $this->validator = null;
     410     }
     411
     412     /**
     413      * Convert this form to another format.
     414      */
     415     function transformTo(FormTransformation $format) {
     416         $newFields = new FieldSet();
     417         foreach($this->fields as $field) {
     418             $newFields->push($field->transformTo($format));
     419         }
     420         $this->fields = $newFields;
     421
     422         // We have to remove validation, if the fields are not editable ;-)
     423         if($this->validator)
     424             $this->validator->removeValidation();
     425     }
     426
     427
     428     /**
     429      * Generate extra special fields - namely the SecurityID field
     430      *
     431      * @return FieldSet
     432      */
     433     public function getExtraFields() {
     434         $extraFields = new FieldSet();
     435
     436         if(!$this->fields->fieldByName('SecurityID') && $this->securityTokenEnabled()) {
     437             if(Session::get('SecurityID')) {
     438                 $securityID = Session::get('SecurityID');
     439             } else {
     440                 $securityID = rand();
     441                 Session::set('SecurityID', $securityID);
     442             }
     443
     444             $securityField = new HiddenField('SecurityID', '', $securityID);
     445             $securityField->setForm($this);
     446             $extraFields->push($securityField);
     447             $this->securityTokenAdded = true;
     448         }
     449
     450         // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
     451         if($this->FormMethod() != $this->FormHttpMethod()) {
     452             $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
     453             $methodField->setForm($this);
     454             $extraFields->push($methodField);
     455         }
     456
     457         return $extraFields;
     458     }
     459
     460     /**
     461      * Return the form's fields - used by the templates
     462      *
     463      * @return FieldSet The form fields
     464      */
     465     function Fields() {
     466         foreach($this->getExtraFields() as $field) {
     467             if(!$this->fields->fieldByName($field->Name())) $this->fields->push($field);
     468         }
     469
     470         return $this->fields;
     471     }
     472
     473     /**
     474      * Return all <input type="hidden"> fields
     475      * in a form - including fields nested in {@link CompositeFields}.
     476      * Useful when doing custom field layouts.
     477      *
     478      * @return FieldSet
     479      */
     480     function HiddenFields() {
     481         return $this->fields->HiddenFields();
     482     }
     483
     484     /**
     485      * Setter for the form fields.
     486      *
     487      * @param FieldSet $fields
     488      */
     489     function setFields($fields) {
     490         $this->fields = $fields;
     491     }
     492
     493     /**
     494      * Get a named field from this form's fields.
     495      * It will traverse into composite fields for you, to find the field you want.
     496      * It will only return a data field.
     497      *
     498      * @return FormField
     499      */
     500     function dataFieldByName($name) {
     501         foreach($this->getExtraFields() as $field) {
     502             if(!$this->fields->dataFieldByName($field->Name())) $this->fields->push($field);
     503         }
     504
     505         return $this->fields->dataFieldByName($name);
     506     }
     507
     508     /**
     509      * Return the form's action buttons - used by the templates
     510      *
     511      * @return FieldSet The action list
     512      */
     513     function Actions() {
     514         return $this->actions;
     515     }
     516
     517     /**
     518      * Setter for the form actions.
     519      *
     520      * @param FieldSet $actions
     521      */
     522     function setActions($actions) {
     523         $this->actions = $actions;
     524     }
     525
     526     /**
     527      * Unset all form actions
     528      */
     529     function unsetAllActions(){
     530         $this->actions = new FieldSet();
     531     }
     532
     533     /**
     534      * Unset the form's action button by its name.
     535      *
     536      * @param string $name
     537      */
     538     function unsetActionByName($name) {
     539         $this->actions->removeByName($name);
     540     }
     541
     542     /**
     543      * Unset the form's dataField by its name
     544      */
     545     function unsetDataFieldByName($fieldName){
     546         foreach($this->Fields()->dataFields() as $child) {
     547             if(is_object($child) && ($child->Name() == $fieldName || $child->Title() == $fieldName)) {
     548                 $child = null;
     549             }
     550         }
     551     }
     552
     553     /**
     554      * Remove a field from the given tab.
     555      */
     556     public function unsetFieldFromTab($tabName, $fieldName) {
     557         // Find the tab
     558         $tab = $this->Fields()->findOrMakeTab($tabName);
     559         $tab->removeByName($fieldName);
     560     }
     561
     562     /**
     563      * Return the attributes of the form tag - used by the templates.
     564      *
     565      * @return string The attribute string
     566      */
     567     function FormAttributes() {
     568         $attributes = array();
     569
     570         // Forms shouldn't be cached, cos their error messages won't be shown
     571         HTTP::set_cache_age(0);
     572
     573         // workaround to include javascript validation
     574         if($this->validator && !$this->jsValidationIncluded) {
     575             $this->validator->includeJavascriptValidation();
     576             if ($this->validator->getJavascriptValidationHandler() == 'prototype')
     577                 $attributes['novalidate'] = 'novalidate';
     578         }
     579
     580         // compile attributes
     581         $attributes['id'] = $this->FormName();
     582         $attributes['action'] = $this->FormAction();
     583         $attributes['method'] = $this->FormMethod();
     584         $attributes['enctype'] = $this->FormEncType();
     585         if($this->target) $attributes['target'] = $this->target;
     586         if($this->extraClass()) $attributes['class'] = $this->extraClass();
     587         if($this->validator && $this->validator->getErrors()) {
     588             if(!isset($attributes['class'])) $attributes['class'] = '';
     589             $attributes['class'] .= ' validationerror';
     590         }
     591
     592         // implode attributes into string
     593         $preparedAttributes = '';
     594         foreach($attributes as $k => $v) {
     595             // Note: as indicated by the $k == value item here; the decisions over what to include in the attributes can sometimes get finicky
     596             if(!empty($v) || $v === '0' || $k == 'value') $preparedAttributes .= " $k=\"" . Convert::raw2att($v) . "\"";
     597         }
     598
     599         return $preparedAttributes;
     600     }
     601
     602     /**
     603     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing another frame
     604     *
     605     * @param target The value of the target
     606     */
     607     function setTarget($target) {
     608         $this->target = $target;
     609     }
     610
     611     /**
     612      * Set the legend value to be inserted into
     613      * the <legend> element in the Form.ss template.
     614      */
     615     function setLegend($legend) {
     616         $this->legend = $legend;
     617     }
     618
     619     /**
     620      * Set the SS template that this form should use
     621      * to render with. The default is "Form".
     622      *
     623      * @param string $template The name of the template (without the .ss extension)
     624      */
     625     function setTemplate($template) {
     626         $this->template = $template;
     627     }
     628
     629     /**
     630      * Return the template to render this form with.
     631      * If the template isn't set, then default to the
     632      * form class name e.g "Form".
     633      *
     634      * @return string
     635      */
     636     function getTemplate() {
     637         if($this->template) return $this->template;
     638         else return $this->class;
     639     }
     640
     641     /**
     642      * Returns the encoding type of the form.
     643      * This will be either "multipart/form-data"" if there are any {@link FileField} instances,
     644      * otherwise "application/x-www-form-urlencoded"
     645      *
     646      * @return string The encoding mime type
     647      */
     648     function FormEncType() {
     649         if(is_array($this->fields->dataFields())){
     650             foreach($this->fields->dataFields() as $field) {
     651                 if(is_a($field, "FileField")) return "multipart/form-data";
     652             }
     653         }
     654         return "application/x-www-form-urlencoded";
     655     }
     656
     657     /**
     658      * Returns the real HTTP method for the form:
     659      * GET, POST, PUT, DELETE or HEAD.
     660      * As most browsers only support GET and POST in
     661      * form submissions, all other HTTP methods are
     662      * added as a hidden field "_method" that
     663      * gets evaluated in {@link Director::direct()}.
     664      * See {@link FormMethod()} to get a HTTP method
     665      * for safe insertion into a <form> tag.
     666      *
     667      * @return string HTTP method
     668      */
     669     function FormHttpMethod() {
     670         return $this->formMethod;
     671     }
     672
     673     /**
     674      * Returns the form method to be used in the <form> tag.
     675      * See {@link FormHttpMethod()} to get the "real" method.
     676      *
     677      * @return string Form tag compatbile HTTP method: 'get' or 'post'
     678      */
     679     function FormMethod() {
     680         if(in_array($this->formMethod,array('get','post'))) {
     681             return $this->formMethod;
     682         } else {
     683             return 'post';
     684         }
     685     }
     686
     687     /**
     688      * Set the form method: GET, POST, PUT, DELETE.
     689      *
     690      * @param $method string
     691      */
     692     function setFormMethod($method) {
     693         $this->formMethod = strtolower($method);
     694     }
     695
     696     /**
     697      * Return the form's action attribute.
     698      * This is build by adding an executeForm get variable to the parent controller's Link() value
     699      *
     700      * @return string
     701      */
     702     function FormAction() {
     703         if ($this->formActionPath) {
     704             return $this->formActionPath;
     705         } elseif($this->controller->hasMethod("FormObjectLink")) {
     706             return $this->controller->FormObjectLink($this->name);
     707         } else {
     708             return Controller::join_links($this->controller->Link(), $this->name);
     709         }
     710     }
     711
     712     /** @ignore */
     713     private $formActionPath = false;
     714
     715     /**
     716      * Set the form action attribute to a custom URL.
     717      *
     718      * Note: For "normal" forms, you shouldn't need to use this method.  It is recommended only for situations where you have
     719      * two relatively distinct parts of the system trying to communicate via a form post.
     720      */
     721     function setFormAction($path) {
     722         $this->formActionPath = $path;
     723     }
     724
     725     /**
     726      * @ignore
     727      */
     728     private $htmlID = null;
     729
     730     /**
     731      * Returns the name of the form
     732      */
     733     function FormName() {
     734         if($this->htmlID) return $this->htmlID;
     735         else return $this->class . '_' . str_replace(array('.','/'),'',$this->name);
     736     }
     737
     738     /**
     739      * Set the HTML ID attribute of the form
     740      */
     741     function setHTMLID($id) {
     742         $this->htmlID = $id;
     743     }
     744
     745     /**
     746      * Returns this form's controller
     747      */
     748     function Controller() {
     749         return $this->controller;
     750     }
     751
     752     /**
     753      * @return string
     754      */
     755     function Name() {
     756         return $this->name;
     757     }
     758
     759     /**
     760      * Returns an object where there is a method with the same name as each data field on the form.
     761      * That method will return the field itself.
     762      * It means that you can execute $firstNameField = $form->FieldMap()->FirstName(), which can be handy
     763      */
     764     function FieldMap() {
     765         return new Form_FieldMap($this);
     766     }
     767
     768     /**
     769      * The next functions store and modify the forms
     770      * message attributes. messages are stored in session under
     771      * $_SESSION[formname][message];
     772      *
     773      * @return string
     774      */
     775     function Message() {
     776         $this->getMessageFromSession();
     777         $message = $this->message;
     778         $this->clearMessage();
     779         return $message;
     780     }
     781
     782     /**
     783      * @return string
     784      */
     785     function MessageType() {
     786         $this->getMessageFromSession();
     787         return $this->messageType;
     788     }
     789
     790     protected function getMessageFromSession() {
     791         if($this->message || $this->messageType) {
     792             return $this->message;
     793         }else{
     794             $this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
     795             $this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
     796
     797             Session::clear("FormInfo.{$this->FormName()}");
     798         }
     799     }
     800
     801     /**
     802      * Set a status message for the form.
     803      *
     804      * @param message the text of the message
     805      * @param type Should be set to good, bad, or warning.
     806      */
     807     function setMessage($message, $type) {
     808         $this->message = $message;
     809         $this->messageType = $type;
     810     }
     811
     812     /**
     813      * Set a message to the session, for display next time this form is shown.
     814      *
     815      * @param message the text of the message
     816      * @param type Should be set to good, bad, or warning.
     817      */
     818     function sessionMessage($message, $type) {
     819         Session::set("FormInfo.{$this->FormName()}.formError.message", $message);
     820         Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
     821     }
     822
     823     static function messageForForm( $formName, $message, $type ) {
     824         Session::set("FormInfo.{$formName}.formError.message", $message);
     825         Session::set("FormInfo.{$formName}.formError.type", $type);
     826     }
     827
     828     function clearMessage() {
     829         $this->message  = null;
     830         Session::clear("FormInfo.{$this->FormName()}.errors");
     831         Session::clear("FormInfo.{$this->FormName()}.formError");
     832     }
     833     function resetValidation() {
     834         Session::clear("FormInfo.{$this->FormName()}.errors");
     835     }
     836
     837     /**
     838      * Returns the DataObject that has given this form its data
     839      * through {@link loadDataFrom()}.
     840      *
     841      * @return DataObject
     842      */
     843     function getRecord() {
     844         return $this->record;
     845     }
     846
     847     /**
     848      * Get the legend value to be inserted into the
     849      * <legend> element in Form.ss
     850      *
     851      * @return string
     852      */
     853     function getLegend() {
     854         return $this->legend;
     855     }
     856
     857     /**
     858      * Processing that occurs before a form is executed.
     859      * This includes form validation, if it fails, we redirect back
     860      * to the form with appropriate error messages.
     861      * Triggered through {@link httpSubmission()} which is triggered
     862      */
     863      function validate(){
     864         if($this->validator){
     865             $errors = $this->validator->validate();
     866
     867             if($errors){
     868                 if(Director::is_ajax() && $this->validator->getJavascriptValidationHandler() == 'prototype') {
     869                     FormResponse::status_message(_t('Form.VALIDATIONFAILED', 'Validation failed'), 'bad');
     870                     foreach($errors as $error) {
     871                         FormResponse::add(sprintf(
     872                             "validationError('%s', '%s', '%s');\n",
     873                             Convert::raw2js($error['fieldName']),
     874                             Convert::raw2js($error['message']),
     875                             Convert::raw2js($error['messageType'])
     876                         ));
     877                     }
     878                 } else {
     879                     $data = $this->getData();
     880
     881                     // Load errors into session and post back
     882                     Session::set("FormInfo.{$this->FormName()}", array(
     883                         'errors' => $errors,
     884                         'data' => $data,
     885                     ));
     886
     887                 }
     888                 return false;
     889             }
     890         }
     891         return true;
     892     }
     893
     894     /**
     895      * Load data from the given DataObject or array.
     896      * It will call $object->MyField to get the value of MyField.
     897      * If you passed an array, it will call $object[MyField].
     898      * Doesn't save into dataless FormFields ({@link DatalessField}),
     899      * as determined by {@link FieldSet->dataFields()}.
     900      *
     901      * By default, if a field isn't set (as determined by isset()),
     902      * its value will not be saved to the field, retaining
     903      * potential existing values.
     904      *
     905      * Passed data should not be escaped, and is saved to the FormField instances unescaped.
     906      * Escaping happens automatically on saving the data through {@link saveInto()}.
     907      *
     908      * @uses FieldSet->dataFields()
     909      * @uses FormField->setValue()
     910      *
     911      * @param array|DataObject $data
     912      * @param boolean $clearMissingFields By default, fields which don't match
     913      *  a property or array-key of the passed {@link $data} argument are "left alone",
     914      *  meaning they retain any previous values (if present). If this flag is set to true,
     915      *  those fields are overwritten with null regardless if they have a match in {@link $data}.
     916      * @param $fieldList An optional list of fields to process.  This can be useful when you have a
     917      * form that has some fields that save to one object, and some that save to another.
     918      */
     919     function loadDataFrom($data, $clearMissingFields = false, $fieldList = null) {
     920         if(!is_object($data) && !is_array($data)) {
     921             user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
     922             return false;
     923         }
     924
     925         // if an object is passed, save it for historical reference through {@link getRecord()}
     926         if(is_object($data)) $this->record = $data;
     927
     928         // dont include fields without data
     929         $dataFields = $this->fields->dataFields();
     930         if($dataFields) foreach($dataFields as $field) {
     931             $name = $field->Name();
     932
     933             // Skip fields that have been exlcuded
     934             if($fieldList && !in_array($name, $fieldList)) continue;
     935
     936             // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
     937             if(is_array($data) && isset($data[$name . '_unchanged'])) continue;
     938
     939             // get value in different formats
     940             $hasObjectValue = false;
     941             if(
     942                 is_object($data)
     943                 && (
     944                     isset($data->$name)
     945                     || $data->hasMethod($name)
     946                     || ($data->hasMethod('hasField') && $data->hasField($name))
     947                 )
     948             ) {
     949                 // We don't actually call the method because it might be slow.
     950                 // In a later release, relation methods will just return references to the query that should be executed,
     951                 // and so we will be able to safely pass the return value of the
     952                 // relation method to the first argument of setValue
     953                 $val = $data->__get($name);
     954                 $hasObjectValue = true;
     955             } else if(strpos($name,'[') && is_array($data) && !isset($data[$name])) {
     956                 // if field is in array-notation, we need to resolve the array-structure PHP creates from query-strings
     957                 preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', urldecode(http_build_query($data)), $matches);
     958                 $val = isset($matches[1]) ? $matches[1] : null;
     959             } elseif(is_array($data) && array_key_exists($name, $data)) {
     960                 // else we assume its a simple keyed array
     961                 $val = $data[$name];
     962             } else {
     963                 $val = null;
     964             }
     965
     966             // save to the field if either a value is given, or loading of blank/undefined values is forced
     967             if(isset($val) || $hasObjectValue || $clearMissingFields) {
     968                 // pass original data as well so composite fields can act on the additional information
     969                 $field->setValue($val, $data);
     970             }
     971         }
     972     }
     973
     974     /**
     975      * Save the contents of this form into the given data object.
     976      * It will make use of setCastedField() to do this.
     977      *
     978      * @param $dataObject The object to save data into
     979      * @param $fieldList An optional list of fields to process.  This can be useful when you have a
     980      * form that has some fields that save to one object, and some that save to another.
     981      */
     982     function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
     983         $dataFields = $this->fields->saveableFields();
     984         $lastField = null;
     985
     986         if($dataFields) foreach($dataFields as $field) {
     987             // Skip fields that have been exlcuded
     988             if($fieldList && is_array($fieldList) && !in_array($field->Name(), $fieldList)) continue;
     989
     990
     991             $saveMethod = "save{$field->Name()}";
     992
     993             if($field->Name() == "ClassName"){
     994                 $lastField = $field;
     995             }else if( $dataObject->hasMethod( $saveMethod ) ){
     996                 $dataObject->$saveMethod( $field->dataValue());
     997             } else if($field->Name() != "ID"){
     998                 $field->saveInto($dataObject);
     999             }
    1000         }
    1001         if($lastField) $lastField->saveInto($dataObject);
    1002     }
    1003
    1004     /**
    1005      * Get the submitted data from this form through
    1006      * {@link FieldSet->dataFields()}, which filters out
    1007      * any form-specific data like form-actions.
    1008      * Calls {@link FormField->dataValue()} on each field,
    1009      * which returns a value suitable for insertion into a DataObject
    1010      * property.
    1011      *
    1012      * @return array
    1013      */
    1014     function getData() {
    1015         $dataFields = $this->fields->dataFields();
    1016         $data = array();
    1017
    1018         if($dataFields){
    1019             foreach($dataFields as $field) {
    1020                 if($field->Name()) {
    1021                     $data[$field->Name()] = $field->dataValue();
    1022                 }
    1023             }
    1024         }
    1025         return $data;
    1026     }
    1027
    1028     /**
    1029      * Resets a specific field to its passed default value.
    1030      * Does NOT clear out all submitted data in the form.
    1031      *
    1032      * @param string $fieldName
    1033      * @param mixed $fieldValue
    1034      */
    1035     function resetField($fieldName, $fieldValue = null) {
    1036         $dataFields = $this->fields->dataFields();
    1037         if($dataFields) foreach($dataFields as $field) {
    1038             if($field->Name()==$fieldName) {
    1039                 $field = $field->setValue($fieldValue);
    1040             }
    1041         }
    1042     }
    1043
    1044     /**
    1045      * Call the given method on the given field.
    1046      * This is used by Ajax-savvy form fields.  By putting '&action=callfieldmethod' to the end
    1047      * of the form action, they can access server-side data.
    1048      * @param fieldName The name of the field.  Can be overridden by $_REQUEST[fieldName]
    1049      * @param methodName The name of the field.  Can be overridden by $_REQUEST[methodName]
    1050      */
    1051     function callfieldmethod($data) {
    1052         $fieldName = $data['fieldName'];
    1053         $methodName = $data['methodName'];
    1054         $fields = $this->fields->dataFields();
    1055
    1056         // special treatment needed for TableField-class and TreeDropdownField
    1057         if(strpos($fieldName, '[')) {
    1058             preg_match_all('/([^\[]*)/',$fieldName, $fieldNameMatches);
    1059             preg_match_all('/\[([^\]]*)\]/',$fieldName, $subFieldMatches);
    1060             $tableFieldName = $fieldNameMatches[1][0];
    1061             $subFieldName = $subFieldMatches[1][1];
    1062         }
    1063
    1064         if(isset($tableFieldName) && isset($subFieldName) && is_a($fields[$tableFieldName], 'TableField')) {
    1065             $field = $fields[$tableFieldName]->getField($subFieldName, $fieldName);
    1066             return $field->$methodName();
    1067         } else if(isset($fields[$fieldName])) {
    1068             return $fields[$fieldName]->$methodName();
    1069         } else {
    1070             user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
    1071         }
    1072
    1073     }
    1074
    1075     /**
    1076      * Return a rendered version of this form.
    1077      *
    1078      * This is returned when you access a form as $FormObject rather
    1079      * than <% control FormObject %>
    1080      */
    1081     function forTemplate() {
    1082         return $this->renderWith(array(
    1083             $this->getTemplate(),
    1084             $this->class,
    1085             'Form'
    1086         ));
    1087     }
    1088
    1089     /**
    1090      * Return a rendered version of this form, suitable for ajax post-back.
    1091      * It triggers slightly different behaviour, such as disabling the rewriting of # links
    1092      */
    1093     function forAjaxTemplate() {
    1094         $view = new SSViewer(array(
    1095             $this->getTemplate(),
    1096             $this->class,
    1097             'Form'
    1098         ));
    1099
    1100         return $view->dontRewriteHashlinks()->process($this);
    1101     }
    1102
    1103     /**
    1104      * Returns an HTML rendition of this form, without the <form> tag itself.
    1105      * Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype.  These are
    1106      * the attributes of the form.  These fields can be used to send the form to Ajax.
    1107      */
    1108     function formHtmlContent() {
    1109         // Call FormAttributes to force inclusion of custom client-side validation of fields
    1110         // because it won't be included by the template
    1111         if($this->validator && !$this->jsValidationIncluded) $this->validator->includeJavascriptValidation();
    1112
    1113         $this->IncludeFormTag = false;
    1114         $content = $this->forTemplate();
    1115         $this->IncludeFormTag = true;
    1116
    1117         $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\" value=\"" . $this->FormAction() . "\" />\n";
    1118         $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
    1119         $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
    1120         $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->FormEncType() . "\" />\n";
    1121
    1122         return $content;
    1123     }
    1124
    1125     /**
    1126      * Render this form using the given template, and return the result as a string
    1127      * You can pass either an SSViewer or a template name
    1128      */
    1129     function renderWithoutActionButton($template) {
    1130         $custom = $this->customise(array(
    1131             "Actions" => "",
    1132         ));
    1133
    1134         if(is_string($template)) $template = new SSViewer($template);
    1135         return $template->process($custom);
    1136     }
    1137
    1138
    1139     /**
    1140      * Sets the button that was clicked.  This should only be called by the Controller.
    1141      * @param funcName The name of the action method that will be called.
    1142      */
    1143     function setButtonClicked($funcName) {
    1144         $this->buttonClickedFunc = $funcName;
    1145     }
    1146
    1147     function buttonClicked() {
    1148         foreach($this->actions as $action) {
    1149             if($this->buttonClickedFunc == $action->actionName()) return $action;
    1150         }
    1151     }
    1152
    1153     /**
    1154      * Return the default button that should be clicked when another one isn't available
    1155      */
    1156     function defaultAction() {
    1157         if($this->hasDefaultAction && $this->actions)
    1158             return $this->actions->First();
    1159     }
    1160
    1161     /**
    1162      * Disable the default button.
    1163      * Ordinarily, when a form is processed and no action_XXX button is available, then the first button in the actions list
    1164      * will be pressed.  However, if this is "delete", for example, this isn't such a good idea.
    1165      */
    1166     function disableDefaultAction() {
    1167         $this->hasDefaultAction = false;
    1168     }
    1169
    1170     /**
    1171      * Disable the requirement of a SecurityID in the Form. This security protects
    1172      * against CSRF attacks, but you should disable this if you don't want to tie
    1173      * a form to a session - eg a search form.
    1174      */
    1175     function disableSecurityToken() {
    1176         $this->security = false;
    1177     }
    1178
    1179
    1180     private static $default_security = true;
    1181
    1182     /**
    1183      * Disable security tokens for every form on this site.
    1184      */
    1185     static function disable_all_security_tokens() {
    1186         self::$default_security = false;
    1187     }
    1188
    1189     /**
    1190      * Returns true if security is enabled - that is if the SecurityID
    1191      * should be included and checked on this form.
    1192      *
    1193      * @return bool
    1194      */
    1195     function securityTokenEnabled() {
    1196         return $this->security;
    1197     }
    1198
    1199     /**
    1200      * Returns the name of a field, if that's the only field that the current controller is interested in.
    1201      * It checks for a call to the callfieldmethod action.
    1202      * This is useful for optimising your forms
    1203      *
    1204      * @return string
    1205      */
    1206     static function single_field_required() {
    1207         if(self::current_action() == 'callfieldmethod') return $_REQUEST['fieldName'];
    1208     }
    1209
    1210     /**
    1211      * Return the current form action being called, if available.
    1212      * This is useful for optimising your forms
    1213      */
    1214     static function current_action() {
    1215         return self::$current_action;
    1216     }
    1217
    1218     /**
    1219      * Set the current form action.  Should only be called by Controller.
    1220      */
    1221     static function set_current_action($action) {
    1222         self::$current_action = $action;
    1223     }
    1224
    1225     /**
    1226      * Compiles all CSS-classes.
    1227      *
    1228      * @return String CSS-classnames, separated by a space
    1229      */
    1230     function extraClass() {
    1231         return implode($this->extraClasses, " ");
    1232     }
    1233
    1234     /**
    1235      * Add a CSS-class to the form-container.
    1236      *
    1237      * @param $class String
    1238      */
    1239     function addExtraClass($class) {
    1240         $this->extraClasses[$class] = $class;
    1241     }
    1242
    1243     /**
    1244      * Remove a CSS-class from the form-container.
    1245      *
    1246      * @param $class String
    1247      */
    1248     function removeExtraClass($class) {
    1249         if(array_key_exists($class, $this->extraClasses)) unset($this->extraClasses[$class]);
    1250     }
    1251
    1252     /**
    1253      * Return Form extraClasses as DataObjectSet
    1254      *
    1255      * @return DataObjectSet
    1256      */
    1257     function getExtraClasses() {
    1258         $classes = new DataObjectSet();
    1259         if (count($this->extraClasses)) {
    1260             foreach($this->extraClasses as $class) {
    1261                 $classes->push(new ArrayData(array(
    1262                     'Class' => $class
    1263                 )));
    1264             }
    1265         }
    1266         return $classes;
    1267     }
    1268
    1269     function debug() {
    1270         $result = "<h3>$this->class</h3><ul>";
    1271         foreach($this->fields as $field) {
    1272             $result .= "<li>$field" . $field->debug() . "</li>";
    1273         }
    1274         $result .= "</ul>";
    1275
    1276         if( $this->validator )
    1277                 $result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
    1278
    1279         return $result;
    1280     }
    1281
    1282
    1283     //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    1284     // TESTING HELPERS
    1285     //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    1286
    1287     /**
    1288      * Test a submission of this form.
    1289      * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in your unit test.
    1290      */
    1291     function testSubmission($action, $data) {
    1292         $data['action_' . $action] = true;
    1293
    1294         return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
    1295
    1296         //$response = $this->controller->run($data);
    1297         //return $response;
    1298     }
    1299
    1300     /**
    1301      * Test an ajax submission of this form.
    1302      * @return SS_HTTPResponse the response object that the handling controller produces.  You can interrogate this in your unit test.
    1303      */
    1304     function testAjaxSubmission($action, $data) {
    1305         $data['ajax'] = 1;
    1306         return $this->testSubmission($action, $data);
    1307     }
    1308 }
    1309
    1310 /**
    1311  * @package forms
    1312  * @subpackage core
    1313  */
    1314 class Form_FieldMap extends ViewableData {
    1315     protected $form;
    1316
    1317     function __construct($form) {
    1318         $this->form = $form;
    1319         parent::__construct();
    1320     }
    1321
    1322     /**
    1323      * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
    1324      */
    1325     function hasMethod($method) {
    1326         return true;
    1327     }
    1328
    1329     function __call($method, $args = null) {
    1330         return $this->form->Fields()->fieldByName($method);
    1331     }
    1332 }
    1333

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -