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