CRM-10693 - AJAX - Standardize protocol for page, form, messages
[civicrm-core.git] / CRM / Core / Form.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This is our base form. It is part of the Form/Controller/StateMachine
30 * trifecta. Each form is associated with a specific state in the state
31 * machine. Each form can also operate in various modes
32 *
33 * @package CRM
34 * @copyright CiviCRM LLC (c) 2004-2013
35 * $Id$
36 *
37 */
38
39 require_once 'HTML/QuickForm/Page.php';
40 class CRM_Core_Form extends HTML_QuickForm_Page {
41
42 /**
43 * The state object that this form belongs to
44 * @var object
45 */
46 protected $_state;
47
48 /**
49 * The name of this form
50 * @var string
51 */
52 protected $_name;
53
54 /**
55 * The title of this form
56 * @var string
57 */
58 protected $_title = NULL;
59
60 /**
61 * The options passed into this form
62 * @var mixed
63 */
64 protected $_options = NULL;
65
66 /**
67 * The mode of operation for this form
68 * @var int
69 */
70 protected $_action;
71
72 /**
73 * the renderer used for this form
74 *
75 * @var object
76 */
77 protected $_renderer;
78
79 /**
80 * An array to hold a list of datefields on the form
81 * so that they can be converted to ISO in a consistent manner
82 *
83 * @var array
84 *
85 * e.g on a form declare $_dateFields = array(
86 * 'receive_date' => array('default' => 'now'),
87 * );
88 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
89 * to have the time field re-incorporated into the field & 'now' set if
90 * no value has been passed in
91 */
92 protected $_dateFields = array();
93
94 /**
95 * cache the smarty template for efficiency reasons
96 *
97 * @var CRM_Core_Smarty
98 */
99 static protected $_template;
100
101 /**
102 * What to return to the client if in ajax mode (snippet=6)
103 *
104 * @var array
105 */
106 public $ajaxResponse = array();
107
108 /**
109 * constants for attributes for various form elements
110 * attempt to standardize on the number of variations that we
111 * use of the below form elements
112 *
113 * @var const string
114 */
115 CONST ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
116
117 /**
118 * All checkboxes are defined with a common prefix. This allows us to
119 * have the same javascript to check / clear all the checkboxes etc
120 * If u have multiple groups of checkboxes, you will need to give them different
121 * ids to avoid potential name collision
122 *
123 * @var const string / int
124 */
125 CONST CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
126
127 /**
128 * Constructor for the basic form page
129 *
130 * We should not use QuickForm directly. This class provides a lot
131 * of default convenient functions, rules and buttons
132 *
133 * @param object $state State associated with this form
134 * @param enum $action The mode the form is operating in (None/Create/View/Update/Delete)
135 * @param string $method The type of http method used (GET/POST)
136 * @param string $name The name of the form if different from class name
137 *
138 * @return object
139 * @access public
140 */
141 function __construct(
142 $state = NULL,
143 $action = CRM_Core_Action::NONE,
144 $method = 'post',
145 $name = NULL
146 ) {
147
148 if ($name) {
149 $this->_name = $name;
150 }
151 else {
152 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
153 }
154
155 $this->HTML_QuickForm_Page($this->_name, $method);
156
157 $this->_state =& $state;
158 if ($this->_state) {
159 $this->_state->setName($this->_name);
160 }
161 $this->_action = (int) $action;
162
163 $this->registerRules();
164
165 // let the constructor initialize this, should happen only once
166 if (!isset(self::$_template)) {
167 self::$_template = CRM_Core_Smarty::singleton();
168 }
169
170 $this->assign('snippet', (int) CRM_Utils_Array::value('snippet', $_REQUEST));
171 }
172
173 static function generateID() {
174 }
175
176 /**
177 * register all the standard rules that most forms potentially use
178 *
179 * @return void
180 * @access private
181 *
182 */
183 function registerRules() {
184 static $rules = array(
185 'title', 'longTitle', 'variable', 'qfVariable',
186 'phone', 'integer', 'query',
187 'url', 'wikiURL',
188 'domain', 'numberOfDigit',
189 'date', 'currentDate',
190 'asciiFile', 'htmlFile', 'utf8File',
191 'objectExists', 'optionExists', 'postalCode', 'money', 'positiveInteger',
192 'xssString', 'fileExists', 'autocomplete', 'validContact',
193 );
194
195 foreach ($rules as $rule) {
196 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
197 }
198 }
199
200 /**
201 * Simple easy to use wrapper around addElement. Deal with
202 * simple validation rules
203 *
204 * @param string type of html element to be added
205 * @param string name of the html element
206 * @param string display label for the html element
207 * @param string attributes used for this element.
208 * These are not default values
209 * @param bool is this a required field
210 *
211 * @return object html element, could be an error object
212 * @access public
213 *
214 */
215 function &add($type, $name, $label = '',
216 $attributes = '', $required = FALSE, $javascript = NULL
217 ) {
218 $element = $this->addElement($type, $name, $label, $attributes, $javascript);
219 if (HTML_QuickForm::isError($element)) {
220 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
221 }
222
223 if ($required) {
224 if ($type == 'file') {
225 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
226 }
227 else {
228 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
229 }
230 if (HTML_QuickForm::isError($error)) {
231 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
232 }
233 }
234
235 return $element;
236 }
237
238 /**
239 * This function is called before buildForm. Any pre-processing that
240 * needs to be done for buildForm should be done here
241 *
242 * This is a virtual function and should be redefined if needed
243 *
244 * @access public
245 *
246 * @return void
247 *
248 */
249 function preProcess() {}
250
251 /**
252 * This function is called after the form is validated. Any
253 * processing of form state etc should be done in this function.
254 * Typically all processing associated with a form should be done
255 * here and relevant state should be stored in the session
256 *
257 * This is a virtual function and should be redefined if needed
258 *
259 * @access public
260 *
261 * @return void
262 *
263 */
264 function postProcess() {}
265
266 /**
267 * This function is just a wrapper, so that we can call all the hook functions
268 */
269 function mainProcess() {
270 $this->postProcess();
271 $this->postProcessHook();
272
273 // Respond with JSON if in AJAX context
274 if (!empty($_REQUEST['snippet']) && $_REQUEST['snippet'] == CRM_Core_Smarty::PRINT_JSON) {
275 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
276 $this->ajaxResponse['action'] = $this->_action;
277 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
278 }
279 }
280
281 /**
282 * The postProcess hook is typically called by the framework
283 * However in a few cases, the form exits or redirects early in which
284 * case it needs to call this function so other modules can do the needful
285 * Calling this function directly should be avoided if possible. In general a
286 * better way is to do setUserContext so the framework does the redirect
287 *
288 */
289 function postProcessHook() {
290 CRM_Utils_Hook::postProcess(get_class($this), $this);
291 }
292
293 /**
294 * This virtual function is used to build the form. It replaces the
295 * buildForm associated with QuickForm_Page. This allows us to put
296 * preProcess in front of the actual form building routine
297 *
298 * @access public
299 *
300 * @return void
301 *
302 */
303 function buildQuickForm() {}
304
305 /**
306 * This virtual function is used to set the default values of
307 * various form elements
308 *
309 * access public
310 *
311 * @return array reference to the array of default values
312 *
313 */
314 function setDefaultValues() {}
315
316 /**
317 * This is a virtual function that adds group and global rules to
318 * the form. Keeping it distinct from the form to keep code small
319 * and localized in the form building code
320 *
321 * @access public
322 *
323 * @return void
324 *
325 */
326 function addRules() {}
327
328 function validate() {
329 $error = parent::validate();
330
331 $hookErrors = CRM_Utils_Hook::validate(
332 get_class($this),
333 $this->_submitValues,
334 $this->_submitFiles,
335 $this
336 );
337
338 if (!is_array($hookErrors)) {
339 $hookErrors = array();
340 }
341
342 CRM_Utils_Hook::validateForm(
343 get_class($this),
344 $this->_submitValues,
345 $this->_submitFiles,
346 $this,
347 $hookErrors
348 );
349
350 if (!empty($hookErrors)) {
351 $this->_errors += $hookErrors;
352 }
353
354 return (0 == count($this->_errors));
355 }
356
357 /**
358 * Core function that builds the form. We redefine this function
359 * here and expect all CRM forms to build their form in the function
360 * buildQuickForm.
361 *
362 */
363 function buildForm() {
364 $this->_formBuilt = TRUE;
365
366 $this->preProcess();
367
368 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
369
370 if (
371 $this->controller->_key &&
372 $this->controller->_generateQFKey
373 ) {
374 $this->addElement('hidden', 'qfKey', $this->controller->_key);
375 $this->assign('qfKey', $this->controller->_key);
376
377 }
378
379 // _generateQFKey suppresses the qfKey generation on form snippets that
380 // are part of other forms, hence we use that to avoid adding entryURL
381 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
382 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
383 }
384
385 $this->buildQuickForm();
386
387 $defaults = $this->setDefaultValues();
388 unset($defaults['qfKey']);
389
390 if (!empty($defaults)) {
391 $this->setDefaults($defaults);
392 }
393
394 // call the form hook
395 // also call the hook function so any modules can set thier own custom defaults
396 // the user can do both the form and set default values with this hook
397 CRM_Utils_Hook::buildForm(get_class($this), $this);
398
399 $this->addRules();
400 }
401
402 /**
403 * Add default Next / Back buttons
404 *
405 * @param array array of associative arrays in the order in which the buttons should be
406 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
407 * The base form class will define a bunch of static arrays for commonly used
408 * formats
409 *
410 * @return void
411 *
412 * @access public
413 *
414 */
415 function addButtons($params) {
416 $prevnext = array();
417 $spacing = array();
418 foreach ($params as $button) {
419 $js = CRM_Utils_Array::value('js', $button);
420 $isDefault = CRM_Utils_Array::value('isDefault', $button, FALSE);
421 if ($isDefault) {
422 $attrs = array('class' => 'form-submit default');
423 }
424 else {
425 $attrs = array('class' => 'form-submit');
426 }
427
428 if ($js) {
429 $attrs = array_merge($js, $attrs);
430 }
431
432 if ($button['type'] === 'reset') {
433 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
434 }
435 else {
436 if (CRM_Utils_Array::value('subName', $button)) {
437 $buttonName = $this->getButtonName($button['type'], $button['subName']);
438 }
439 else {
440 $buttonName = $this->getButtonName($button['type']);
441 }
442
443 if (in_array($button['type'], array(
444 'next', 'upload')) && $button['name'] === 'Save') {
445 $attrs = array_merge($attrs, (array('accesskey' => 'S')));
446 }
447 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
448 }
449 if (CRM_Utils_Array::value('isDefault', $button)) {
450 $this->setDefaultAction($button['type']);
451 }
452
453 // if button type is upload, set the enctype
454 if ($button['type'] == 'upload') {
455 $this->updateAttributes(array('enctype' => 'multipart/form-data'));
456 $this->setMaxFileSize();
457 }
458
459 // hack - addGroup uses an array to express variable spacing, read from the last element
460 $spacing[] = CRM_Utils_Array::value('spacing', $button, self::ATTR_SPACING);
461 }
462 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
463 }
464
465 /**
466 * getter function for Name
467 *
468 * @return string
469 * @access public
470 */
471 function getName() {
472 return $this->_name;
473 }
474
475 /**
476 * getter function for State
477 *
478 * @return object
479 * @access public
480 */
481 function &getState() {
482 return $this->_state;
483 }
484
485 /**
486 * getter function for StateType
487 *
488 * @return int
489 * @access public
490 */
491 function getStateType() {
492 return $this->_state->getType();
493 }
494
495 /**
496 * getter function for title. Should be over-ridden by derived class
497 *
498 * @return string
499 * @access public
500 */
501 function getTitle() {
502 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
503 }
504
505 /**
506 * setter function for title.
507 *
508 * @param string $title the title of the form
509 *
510 * @return void
511 * @access public
512 */
513 function setTitle($title) {
514 $this->_title = $title;
515 }
516
517 /**
518 * Setter function for options
519 *
520 * @param mixed
521 *
522 * @return void
523 * @access public
524 */
525 function setOptions($options) {
526 $this->_options = $options;
527 }
528
529 /**
530 * getter function for link.
531 *
532 * @return string
533 * @access public
534 */
535 function getLink() {
536 $config = CRM_Core_Config::singleton();
537 return CRM_Utils_System::url($_GET[$config->userFrameworkURLVar],
538 '_qf_' . $this->_name . '_display=true'
539 );
540 }
541
542 /**
543 * boolean function to determine if this is a one form page
544 *
545 * @return boolean
546 * @access public
547 */
548 function isSimpleForm() {
549 return $this->_state->getType() & (CRM_Core_State::START | CRM_Core_State::FINISH);
550 }
551
552 /**
553 * getter function for Form Action
554 *
555 * @return string
556 * @access public
557 */
558 function getFormAction() {
559 return $this->_attributes['action'];
560 }
561
562 /**
563 * setter function for Form Action
564 *
565 * @param string
566 *
567 * @return void
568 * @access public
569 */
570 function setFormAction($action) {
571 $this->_attributes['action'] = $action;
572 }
573
574 /**
575 * render form and return contents
576 *
577 * @return string
578 * @access public
579 */
580 function toSmarty() {
581 $renderer = $this->getRenderer();
582 $this->accept($renderer);
583 $content = $renderer->toArray();
584 $content['formName'] = $this->getName();
585 return $content;
586 }
587
588 /**
589 * getter function for renderer. If renderer is not set
590 * create one and initialize it
591 *
592 * @return object
593 * @access public
594 */
595 function &getRenderer() {
596 if (!isset($this->_renderer)) {
597 $this->_renderer = CRM_Core_Form_Renderer::singleton();
598 }
599 return $this->_renderer;
600 }
601
602 /**
603 * Use the form name to create the tpl file name
604 *
605 * @return string
606 * @access public
607 */
608 function getTemplateFileName() {
609 $ext = CRM_Extension_System::singleton()->getMapper();
610 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
611 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
612 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
613 }
614 else {
615 $tplname = str_replace('_',
616 DIRECTORY_SEPARATOR,
617 CRM_Utils_System::getClassName($this)
618 ) . '.tpl';
619 }
620 return $tplname;
621 }
622
623 /**
624 * A wrapper for getTemplateFileName that includes calling the hook to
625 * prevent us from having to copy & paste the logic of calling the hook
626 */
627 function getHookedTemplateFileName() {
628 $pageTemplateFile = $this->getTemplateFileName();
629 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
630 return $pageTemplateFile;
631 }
632
633 /**
634 * Default extra tpl file basically just replaces .tpl with .extra.tpl
635 * i.e. we dont override
636 *
637 * @return string
638 * @access public
639 */
640 function overrideExtraTemplateFileName() {
641 return NULL;
642 }
643
644 /**
645 * Error reporting mechanism
646 *
647 * @param string $message Error Message
648 * @param int $code Error Code
649 * @param CRM_Core_DAO $dao A data access object on which we perform a rollback if non - empty
650 *
651 * @return void
652 * @access public
653 */
654 function error($message, $code = NULL, $dao = NULL) {
655 if ($dao) {
656 $dao->query('ROLLBACK');
657 }
658
659 $error = CRM_Core_Error::singleton();
660
661 $error->push($code, $message);
662 }
663
664 /**
665 * Store the variable with the value in the form scope
666 *
667 * @param string name : name of the variable
668 * @param mixed value : value of the variable
669 *
670 * @access public
671 *
672 * @return void
673 *
674 */
675 function set($name, $value) {
676 $this->controller->set($name, $value);
677 }
678
679 /**
680 * Get the variable from the form scope
681 *
682 * @param string name : name of the variable
683 *
684 * @access public
685 *
686 * @return mixed
687 *
688 */
689 function get($name) {
690 return $this->controller->get($name);
691 }
692
693 /**
694 * getter for action
695 *
696 * @return int
697 * @access public
698 */
699 function getAction() {
700 return $this->_action;
701 }
702
703 /**
704 * setter for action
705 *
706 * @param int $action the mode we want to set the form
707 *
708 * @return void
709 * @access public
710 */
711 function setAction($action) {
712 $this->_action = $action;
713 }
714
715 /**
716 * assign value to name in template
717 *
718 * @param array|string $name name of variable
719 * @param mixed $value value of varaible
720 *
721 * @return void
722 * @access public
723 */
724 function assign($var, $value = NULL) {
725 self::$_template->assign($var, $value);
726 }
727
728 /**
729 * assign value to name in template by reference
730 *
731 * @param array|string $name name of variable
732 * @param mixed $value value of varaible
733 *
734 * @return void
735 * @access public
736 */
737 function assign_by_ref($var, &$value) {
738 self::$_template->assign_by_ref($var, $value);
739 }
740
741 /**
742 * appends values to template variables
743 *
744 * @param array|string $tpl_var the template variable name(s)
745 * @param mixed $value the value to append
746 * @param bool $merge
747 */
748 function append($tpl_var, $value=NULL, $merge=FALSE) {
749 self::$_template->append($tpl_var, $value, $merge);
750 }
751
752 /**
753 * Returns an array containing template variables
754 *
755 * @param string $name
756 * @param string $type
757 * @return array
758 */
759 function get_template_vars($name=null) {
760 return self::$_template->get_template_vars($name);
761 }
762
763 function &addRadio($name, $title, &$values, $attributes = NULL, $separator = NULL, $required = FALSE) {
764 $options = array();
765 if (empty($attributes)) {
766 $attributes = array('id_suffix' => $name);
767 }
768 else {
769 $attributes = array_merge($attributes, array('id_suffix' => $name));
770 }
771 foreach ($values as $key => $var) {
772 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
773 }
774 $group = $this->addGroup($options, $name, $title, $separator);
775 if ($required) {
776 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
777 }
778 return $group;
779 }
780
781 function addYesNo($id, $title, $dontKnow = NULL, $required = NULL, $attribute = NULL) {
782 if (empty($attribute)) {
783 $attribute = array('id_suffix' => $id);
784 }
785 else {
786 $attribute = array_merge($attribute, array('id_suffix' => $id));
787 }
788 $choice = array();
789 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attribute);
790 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attribute);
791 if ($dontKnow) {
792 $choice[] = $this->createElement('radio', NULL, '22', ts("Don't Know"), '2', $attribute);
793 }
794 $this->addGroup($choice, $id, $title);
795
796 if ($required) {
797 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
798 }
799 }
800
801 function addCheckBox($id, $title, $values, $other = NULL,
802 $attributes = NULL, $required = NULL,
803 $javascriptMethod = NULL,
804 $separator = '<br />', $flipValues = FALSE
805 ) {
806 $options = array();
807
808 if ($javascriptMethod) {
809 foreach ($values as $key => $var) {
810 if (!$flipValues) {
811 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
812 }
813 else {
814 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
815 }
816 }
817 }
818 else {
819 foreach ($values as $key => $var) {
820 if (!$flipValues) {
821 $options[] = $this->createElement('checkbox', $var, NULL, $key);
822 }
823 else {
824 $options[] = $this->createElement('checkbox', $key, NULL, $var);
825 }
826 }
827 }
828
829 $this->addGroup($options, $id, $title, $separator);
830
831 if ($other) {
832 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
833 }
834
835 if ($required) {
836 $this->addRule($id,
837 ts('%1 is a required field.', array(1 => $title)),
838 'required'
839 );
840 }
841 }
842
843 function resetValues() {
844 $data = $this->controller->container();
845 $data['values'][$this->_name] = array();
846 }
847
848 /**
849 * simple shell that derived classes can call to add buttons to
850 * the form with a customized title for the main Submit
851 *
852 * @param string $title title of the main button
853 * @param string $type button type for the form after processing
854 * @param string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
855 *
856 * @return void
857 * @access public
858 */
859 function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
860 $buttons = array();
861 if ($backType != NULL) {
862 $buttons[] = array(
863 'type' => $backType,
864 'name' => ts('Previous'),
865 );
866 }
867 if ($nextType != NULL) {
868 $nextButton = array(
869 'type' => $nextType,
870 'name' => $title,
871 'isDefault' => TRUE,
872 );
873 if ($submitOnce) {
874 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
875 }
876 $buttons[] = $nextButton;
877 }
878 $this->addButtons($buttons);
879 }
880
881 function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
882 if ($displayTime) {
883 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
884 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
885 } else {
886 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
887 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
888 }
889 }
890
891 function addSelect($name, $label, $prefix = NULL, $required = NULL, $extra = NULL, $select = '- select -') {
892 if ($prefix) {
893 $this->addElement('select', $name . '_id' . $prefix, $label,
894 array(
895 '' => $select) + CRM_Core_OptionGroup::values($name), $extra
896 );
897 if ($required) {
898 $this->addRule($name . '_id' . $prefix, ts('Please select %1', array(1 => $label)), 'required');
899 }
900 }
901 else {
902 $this->addElement('select', $name . '_id', $label,
903 array(
904 '' => $select) + CRM_Core_OptionGroup::values($name), $extra
905 );
906 if ($required) {
907 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
908 }
909 }
910 }
911
912 /**
913 * Add a widget for selecting/editing/creating/copying a profile form
914 *
915 * @param string $name HTML form-element name
916 * @param string $label Printable label
917 * @param string $allowCoreTypes only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'
918 * @param string $allowSubTypes only present a UFGroup if its group_type is compatible with $allowSubypes
919 * @param array $entities
920 */
921 function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities) {
922 // Output widget
923 // FIXME: Instead of adhoc serialization, use a single json_encode()
924 CRM_UF_Page_ProfileEditor::registerProfileScripts();
925 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
926 $this->add('text', $name, $label, array(
927 'class' => 'crm-profile-selector',
928 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
929 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
930 'data-entities' => json_encode($entities),
931 ));
932 }
933
934 function addWysiwyg($name, $label, $attributes, $forceTextarea = FALSE) {
935 // 1. Get configuration option for editor (tinymce, ckeditor, pure textarea)
936 // 2. Based on the option, initialise proper editor
937 $editorID = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
938 'editor_id'
939 );
940 $editor = strtolower(CRM_Utils_Array::value($editorID,
941 CRM_Core_OptionGroup::values('wysiwyg_editor')
942 ));
943 if (!$editor || $forceTextarea) {
944 $editor = 'textarea';
945 }
946 if ($editor == 'joomla default editor') {
947 $editor = 'joomlaeditor';
948 }
949
950 if ($editor == 'drupal default editor') {
951 $editor = 'drupalwysiwyg';
952 }
953
954 //lets add the editor as a attribute
955 $attributes['editor'] = $editor;
956
957 $this->addElement($editor, $name, $label, $attributes);
958 $this->assign('editor', $editor);
959
960 // include wysiwyg editor js files
961 $includeWysiwygEditor = FALSE;
962 $includeWysiwygEditor = $this->get('includeWysiwygEditor');
963 if (!$includeWysiwygEditor) {
964 $includeWysiwygEditor = TRUE;
965 $this->set('includeWysiwygEditor', $includeWysiwygEditor);
966 }
967
968 $this->assign('includeWysiwygEditor', $includeWysiwygEditor);
969 }
970
971 function addCountry($id, $title, $required = NULL, $extra = NULL) {
972 $this->addElement('select', $id, $title,
973 array(
974 '' => ts('- select -')) + CRM_Core_PseudoConstant::country(), $extra
975 );
976 if ($required) {
977 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
978 }
979 }
980
981 function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
982
983 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
984
985 if ($required) {
986 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
987 }
988 }
989
990 function buildAddressBlock($locationId, $title, $phone,
991 $alternatePhone = NULL, $addressRequired = NULL,
992 $phoneRequired = NULL, $altPhoneRequired = NULL,
993 $locationName = NULL
994 ) {
995 if (!$locationName) {
996 $locationName = "location";
997 }
998
999 $config = CRM_Core_Config::singleton();
1000 $attributes = CRM_Core_DAO::getAttribute('CRM_Core_DAO_Address');
1001
1002 $location[$locationId]['address']['street_address'] = $this->addElement('text', "{$locationName}[$locationId][address][street_address]", $title,
1003 $attributes['street_address']
1004 );
1005 if ($addressRequired) {
1006 $this->addRule("{$locationName}[$locationId][address][street_address]", ts("Please enter the Street Address for %1.", array(1 => $title)), 'required');
1007 }
1008
1009 $location[$locationId]['address']['supplemental_address_1'] = $this->addElement('text', "{$locationName}[$locationId][address][supplemental_address_1]", ts('Supplemental Address 1'),
1010 $attributes['supplemental_address_1']
1011 );
1012 $location[$locationId]['address']['supplemental_address_2'] = $this->addElement('text', "{$locationName}[$locationId][address][supplemental_address_2]", ts('Supplemental Address 2'),
1013 $attributes['supplemental_address_2']
1014 );
1015
1016 $location[$locationId]['address']['city'] = $this->addElement('text', "{$locationName}[$locationId][address][city]", ts('City'),
1017 $attributes['city']
1018 );
1019 if ($addressRequired) {
1020 $this->addRule("{$locationName}[$locationId][address][city]", ts("Please enter the City for %1.", array(1 => $title)), 'required');
1021 }
1022
1023 $location[$locationId]['address']['postal_code'] = $this->addElement('text', "{$locationName}[$locationId][address][postal_code]", ts('Zip / Postal Code'),
1024 $attributes['postal_code']
1025 );
1026 if ($addressRequired) {
1027 $this->addRule("{$locationName}[$locationId][address][postal_code]", ts("Please enter the Zip/Postal Code for %1.", array(1 => $title)), 'required');
1028 }
1029
1030 $location[$locationId]['address']['postal_code_suffix'] = $this->addElement('text', "{$locationName}[$locationId][address][postal_code_suffix]", ts('Add-on Code'),
1031 array('size' => 4, 'maxlength' => 12)
1032 );
1033 $this->addRule("{$locationName}[$locationId][address][postal_code_suffix]", ts('Zip-Plus not valid.'), 'positiveInteger');
1034
1035 if ($config->includeCounty) {
1036 $location[$locationId]['address']['county_id'] = $this->addElement('select', "{$locationName}[$locationId][address][county_id]", ts('County'),
1037 array('' => ts('- select -')) + CRM_Core_PseudoConstant::county()
1038 );
1039 }
1040
1041 $location[$locationId]['address']['state_province_id'] = $this->addElement('select', "{$locationName}[$locationId][address][state_province_id]", ts('State / Province'),
1042 array('' => ts('- select -')) + CRM_Core_PseudoConstant::stateProvince()
1043 );
1044
1045 $location[$locationId]['address']['country_id'] = $this->addElement('select', "{$locationName}[$locationId][address][country_id]", ts('Country'),
1046 array('' => ts('- select -')) + CRM_Core_PseudoConstant::country()
1047 );
1048 if ($addressRequired) {
1049 $this->addRule("{$locationName}[$locationId][address][country_id]", ts("Please select the Country for %1.", array(1 => $title)), 'required');
1050 }
1051
1052
1053 if ($phone) {
1054 $location[$locationId]['phone'][1]['phone'] = $this->addElement('text',
1055 "{$locationName}[$locationId][phone][1][phone]",
1056 $phone,
1057 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Phone',
1058 'phone'
1059 )
1060 );
1061 if ($phoneRequired) {
1062 $this->addRule("{$locationName}[$locationId][phone][1][phone]", ts('Please enter a value for %1', array(1 => $phone)), 'required');
1063 }
1064 $this->addRule("{$locationName}[$locationId][phone][1][phone]", ts('Please enter a valid number for %1', array(1 => $phone)), 'phone');
1065 }
1066
1067 if ($alternatePhone) {
1068 $location[$locationId]['phone'][2]['phone'] = $this->addElement('text',
1069 "{$locationName}[$locationId][phone][2][phone]",
1070 $alternatePhone,
1071 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Phone',
1072 'phone'
1073 )
1074 );
1075 if ($alternatePhoneRequired) {
1076 $this->addRule("{$locationName}[$locationId][phone][2][phone]", ts('Please enter a value for %1', array(1 => $alternatePhone)), 'required');
1077 }
1078 $this->addRule("{$locationName}[$locationId][phone][2][phone]", ts('Please enter a valid number for %1', array(1 => $alternatePhone)), 'phone');
1079 }
1080 }
1081
1082 public function getRootTitle() {
1083 return NULL;
1084 }
1085
1086 public function getCompleteTitle() {
1087 return $this->getRootTitle() . $this->getTitle();
1088 }
1089
1090 static function &getTemplate() {
1091 return self::$_template;
1092 }
1093
1094 function addUploadElement($elementName) {
1095 $uploadNames = $this->get('uploadNames');
1096 if (!$uploadNames) {
1097 $uploadNames = array();
1098 }
1099 if (is_array($elementName)) {
1100 foreach ($elementName as $name) {
1101 if (!in_array($name, $uploadNames)) {
1102 $uploadNames[] = $name;
1103 }
1104 }
1105 }
1106 else {
1107 if (!in_array($elementName, $uploadNames)) {
1108 $uploadNames[] = $elementName;
1109 }
1110 }
1111 $this->set('uploadNames', $uploadNames);
1112
1113 $config = CRM_Core_Config::singleton();
1114 if (!empty($uploadNames)) {
1115 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1116 }
1117 }
1118
1119 function buttonType() {
1120 $uploadNames = $this->get('uploadNames');
1121 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
1122 $this->assign('buttonType', $buttonType);
1123 return $buttonType;
1124 }
1125
1126 function getVar($name) {
1127 return isset($this->$name) ? $this->$name : NULL;
1128 }
1129
1130 function setVar($name, $value) {
1131 $this->$name = $value;
1132 }
1133
1134 /**
1135 * Function to add date
1136 * @param string $name name of the element
1137 * @param string $label label of the element
1138 * @param array $attributes key / value pair
1139 *
1140 // if you need time
1141 * $attributes = array ( 'addTime' => true,
1142 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1143 * );
1144 * @param boolean $required true if required
1145 *
1146 */
1147 function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1148 if (CRM_Utils_Array::value('formatType', $attributes)) {
1149 // get actual format
1150 $params = array('name' => $attributes['formatType']);
1151 $values = array();
1152
1153 // cache date information
1154 static $dateFormat;
1155 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1156 if (!CRM_Utils_Array::value($key, $dateFormat)) {
1157 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1158 $dateFormat[$key] = $values;
1159 }
1160 else {
1161 $values = $dateFormat[$key];
1162 }
1163
1164 if ($values['date_format']) {
1165 $attributes['format'] = $values['date_format'];
1166 }
1167
1168 if (CRM_Utils_Array::value('time_format', $values)) {
1169 $attributes['timeFormat'] = $values['time_format'];
1170 }
1171 $attributes['startOffset'] = $values['start'];
1172 $attributes['endOffset'] = $values['end'];
1173 }
1174
1175 $config = CRM_Core_Config::singleton();
1176 if (!CRM_Utils_Array::value('format', $attributes)) {
1177 $attributes['format'] = $config->dateInputFormat;
1178 }
1179
1180 if (!isset($attributes['startOffset'])) {
1181 $attributes['startOffset'] = 10;
1182 }
1183
1184 if (!isset($attributes['endOffset'])) {
1185 $attributes['endOffset'] = 10;
1186 }
1187
1188 $this->add('text', $name, $label, $attributes);
1189
1190 if (CRM_Utils_Array::value('addTime', $attributes) ||
1191 CRM_Utils_Array::value('timeFormat', $attributes)
1192 ) {
1193
1194 if (!isset($attributes['timeFormat'])) {
1195 $timeFormat = $config->timeInputFormat;
1196 }
1197 else {
1198 $timeFormat = $attributes['timeFormat'];
1199 }
1200
1201 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1202 if ($timeFormat) {
1203 $show24Hours = TRUE;
1204 if ($timeFormat == 1) {
1205 $show24Hours = FALSE;
1206 }
1207
1208 //CRM-6664 -we are having time element name
1209 //in either flat string or an array format.
1210 $elementName = $name . '_time';
1211 if (substr($name, -1) == ']') {
1212 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1213 }
1214
1215 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1216 }
1217 }
1218
1219 if ($required) {
1220 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1221 if (CRM_Utils_Array::value('addTime', $attributes) && CRM_Utils_Array::value('addTimeRequired', $attributes)) {
1222 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1223 }
1224 }
1225 }
1226
1227 /**
1228 * Function that will add date and time
1229 */
1230 function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1231 $addTime = array('addTime' => TRUE);
1232 if (is_array($attributes)) {
1233 $attributes = array_merge($attributes, $addTime);
1234 }
1235 else {
1236 $attributes = $addTime;
1237 }
1238
1239 $this->addDate($name, $label, $required, $attributes);
1240 }
1241
1242 /**
1243 * add a currency and money element to the form
1244 */
1245 function addMoney($name,
1246 $label,
1247 $required = FALSE,
1248 $attributes = NULL,
1249 $addCurrency = TRUE,
1250 $currencyName = 'currency',
1251 $defaultCurrency = NULL,
1252 $freezeCurrency = FALSE
1253 ) {
1254 $element = $this->add('text', $name, $label, $attributes, $required);
1255 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1256
1257 if ($addCurrency) {
1258 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1259 }
1260
1261 return $element;
1262 }
1263
1264 /**
1265 * add currency element to the form
1266 */
1267 function addCurrency($name = 'currency',
1268 $label = NULL,
1269 $required = TRUE,
1270 $defaultCurrency = NULL,
1271 $freezeCurrency = FALSE
1272 ) {
1273 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1274 if (!$required) {
1275 $currencies = array(
1276 '' => ts('- select -')) + $currencies;
1277 }
1278 $ele = $this->add('select', $name, $label, $currencies, $required);
1279 if ($freezeCurrency) {
1280 $ele->freeze();
1281 }
1282 if (!$defaultCurrency) {
1283 $config = CRM_Core_Config::singleton();
1284 $defaultCurrency = $config->defaultCurrency;
1285 }
1286 $this->setDefaults(array($name => $defaultCurrency));
1287 }
1288
1289 /**
1290 * Convert all date fields within the params to mysql date ready for the
1291 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1292 * and if time is defined it is incorporated
1293 *
1294 * @param array $params input params from the form
1295 *
1296 * @todo it would probably be better to work on $this->_params than a passed array
1297 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1298 * handling from BAO
1299 */
1300 function convertDateFieldsToMySQL(&$params){
1301 foreach ($this->_dateFields as $fieldName => $specs){
1302 if(!empty($params[$fieldName])){
1303 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1304 CRM_Utils_Date::processDate(
1305 $params[$fieldName],
1306 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1307 );
1308 }
1309 else{
1310 if(isset($specs['default'])){
1311 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1312 }
1313 }
1314 }
1315 }
1316
1317 function removeFileRequiredRules($elementName) {
1318 $this->_required = array_diff($this->_required, array($elementName));
1319 if (isset($this->_rules[$elementName])) {
1320 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1321 if ($ruleInfo['type'] == 'uploadedfile') {
1322 unset($this->_rules[$elementName][$index]);
1323 }
1324 }
1325 if (empty($this->_rules[$elementName])) {
1326 unset($this->_rules[$elementName]);
1327 }
1328 }
1329 }
1330
1331 /**
1332 * Function that can be defined in Form to override or
1333 * perform specific action on cancel action
1334 *
1335 * @access public
1336 */
1337 function cancelAction() {}
1338
1339 /**
1340 * Helper function to verify that required fields have been filled
1341 * Typically called within the scope of a FormRule function
1342 */
1343 static function validateMandatoryFields($fields, $values, &$errors) {
1344 foreach ($fields as $name => $fld) {
1345 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1346 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1347 }
1348 }
1349 }
1350
1351 /**
1352 * Get contact if for a form object. Prioritise
1353 * - cid in URL if 0 (on behalf on someoneelse)
1354 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1355 * - logged in user id if it matches the one in the cid in the URL
1356 * - contact id validated from a checksum from a checksum
1357 * - cid from the url if the caller has ACL permission to view
1358 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1359 *
1360 * @return Ambigous <mixed, NULL, value, unknown, array, number>|unknown
1361 */
1362 function getContactID() {
1363 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1364 if(isset($this->_params) && isset($this->_params['select_contact_id'])) {
1365 $tempID = $this->_params['select_contact_id'];
1366 }
1367 if(isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1368 // event form stores as an indexed array, contribution form not so much...
1369 $tempID = $this->_params[0]['select_contact_id'];
1370 }
1371 // force to ignore the authenticated user
1372 if ($tempID === '0') {
1373 return $tempID;
1374 }
1375
1376 $userID = $this->getLoggedInUserContactID();
1377
1378 if ($tempID == $userID) {
1379 return $userID;
1380 }
1381
1382 //check if this is a checksum authentication
1383 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1384 if ($userChecksum) {
1385 //check for anonymous user.
1386 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1387 if ($validUser) {
1388 return $tempID;
1389 }
1390 }
1391 // check if user has permission, CRM-12062
1392 else if ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1393 return $tempID;
1394 }
1395
1396 return $userID;
1397 }
1398
1399 /**
1400 * Get the contact id of the logged in user
1401 */
1402 function getLoggedInUserContactID() {
1403 // check if the user is logged in and has a contact ID
1404 $session = CRM_Core_Session::singleton();
1405 return $session->get('userID');
1406 }
1407
1408 /**
1409 * add autoselector field -if user has permission to view contacts
1410 * If adding this to a form you also need to add to the tpl e.g
1411 *
1412 * {if !empty($selectable)}
1413 * <div class="crm-summary-row">
1414 * <div class="crm-label">{$form.select_contact.label}</div>
1415 * <div class="crm-content">
1416 * {$form.select_contact.html}
1417 * </div>
1418 * </div>
1419 * {/if}
1420 * @param array $profiles ids of profiles that are on the form (to be autofilled)
1421 * @param array $field metadata of field to use as selector including
1422 * - name_field
1423 * - id_field
1424 * - url (for ajax lookup)
1425 *
1426 * @todo add data attributes so we can deal with multiple instances on a form
1427 */
1428 function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1429 $autoCompleteField = array_merge(array(
1430 'name_field' => 'select_contact',
1431 'id_field' => 'select_contact_id',
1432 'field_text' => ts('Select Contact'),
1433 'show_hide' => TRUE,
1434 'show_text' => ts('to select someone already in our database.'),
1435 'hide_text' => ts('to clear this person\'s information, and fill the form in for someone else'),
1436 'url' => array('civicrm/ajax/rest', 'className=CRM_Contact_Page_AJAX&fnName=getContactList&json=1'),
1437 'max' => civicrm_api3('setting', 'getvalue', array(
1438 'name' => 'search_autocomplete_count',
1439 'group' => 'Search Preferences',
1440 ))
1441 ), $autoCompleteField);
1442
1443 if(0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1)))) {
1444 $this->addElement('text', $autoCompleteField['name_field'] , $autoCompleteField['field_text']);
1445 $this->addElement('hidden', $autoCompleteField['id_field'], '', array('id' => $autoCompleteField['id_field']));
1446 $this->assign('selectable', $autoCompleteField['id_field']);
1447
1448 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AutoComplete.js')
1449 ->addSetting(array(
1450 'form' => array('autocompletes' => $autoCompleteField),
1451 'ids' => array('profile' => $profiles),
1452 ));
1453 }
1454 }
1455
1456 /**
1457 * Add the options appropriate to cid = zero - ie. autocomplete
1458 *
1459 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1460 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1461 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1462 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1463 */
1464 function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1465 $this->assign('nocid', TRUE);
1466 $profiles = array();
1467 if($this->_values['custom_pre_id']) {
1468 $profiles[] = $this->_values['custom_pre_id'];
1469 }
1470 if($this->_values['custom_post_id']) {
1471 $profiles[] = $this->_values['custom_post_id'];
1472 }
1473 if($onlinePaymentProcessorEnabled) {
1474 $profiles[] = 'billing';
1475 }
1476 if(!empty($this->_values)) {
1477 $this->addAutoSelector($profiles);
1478 }
1479 }
1480
1481 /**
1482 * Set default values on form for given contact (or no contact defaults)
1483 * @param mixed $profile_id (can be id, or profile name)
1484 * @param integer $contactID
1485 */
1486 function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
1487 try{
1488 $defaults = civicrm_api3('profile', 'getsingle', array(
1489 'profile_id' => (array) $profile_id,
1490 'contact_id' => $contactID,
1491 ));
1492 return $defaults;
1493 }
1494 catch (Exception $e) {
1495 // the try catch block gives us silent failure -not 100% sure this is a good idea
1496 // as silent failures are often worse than noisy ones
1497 return array();
1498 }
1499 }
1500 }
1501