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