Merge remote-tracking branch 'upstream/4.3' into 4.3-4.4-2013-10-28-14-52-15
[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 function &addRadio($name, $title, &$values, $attributes = NULL, $separator = NULL, $required = FALSE) {
724 $options = array();
725 if (empty($attributes)) {
726 $attributes = array('id_suffix' => $name);
727 }
728 else {
729 $attributes = array_merge($attributes, array('id_suffix' => $name));
730 }
731 foreach ($values as $key => $var) {
732 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
733 }
734 $group = $this->addGroup($options, $name, $title, $separator);
735 if ($required) {
736 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
737 }
738 return $group;
739 }
740
741 function addYesNo($id, $title, $dontKnow = NULL, $required = NULL, $attribute = NULL) {
742 if (empty($attribute)) {
743 $attribute = array('id_suffix' => $id);
744 }
745 else {
746 $attribute = array_merge($attribute, array('id_suffix' => $id));
747 }
748 $choice = array();
749 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attribute);
750 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attribute);
751 if ($dontKnow) {
752 $choice[] = $this->createElement('radio', NULL, '22', ts("Don't Know"), '2', $attribute);
753 }
754 $this->addGroup($choice, $id, $title);
755
756 if ($required) {
757 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
758 }
759 }
760
761 function addCheckBox($id, $title, $values, $other = NULL,
762 $attributes = NULL, $required = NULL,
763 $javascriptMethod = NULL,
764 $separator = '<br />', $flipValues = FALSE
765 ) {
766 $options = array();
767
768 if ($javascriptMethod) {
769 foreach ($values as $key => $var) {
770 if (!$flipValues) {
771 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
772 }
773 else {
774 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
775 }
776 }
777 }
778 else {
779 foreach ($values as $key => $var) {
780 if (!$flipValues) {
781 $options[] = $this->createElement('checkbox', $var, NULL, $key);
782 }
783 else {
784 $options[] = $this->createElement('checkbox', $key, NULL, $var);
785 }
786 }
787 }
788
789 $this->addGroup($options, $id, $title, $separator);
790
791 if ($other) {
792 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
793 }
794
795 if ($required) {
796 $this->addRule($id,
797 ts('%1 is a required field.', array(1 => $title)),
798 'required'
799 );
800 }
801 }
802
803 function resetValues() {
804 $data = $this->controller->container();
805 $data['values'][$this->_name] = array();
806 }
807
808 /**
809 * simple shell that derived classes can call to add buttons to
810 * the form with a customized title for the main Submit
811 *
812 * @param string $title title of the main button
813 * @param string $type button type for the form after processing
814 * @param string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
815 *
816 * @return void
817 * @access public
818 */
819 function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
820 $buttons = array();
821 if ($backType != NULL) {
822 $buttons[] = array(
823 'type' => $backType,
824 'name' => ts('Previous'),
825 );
826 }
827 if ($nextType != NULL) {
828 $nextButton = array(
829 'type' => $nextType,
830 'name' => $title,
831 'isDefault' => TRUE,
832 );
833 if ($submitOnce) {
834 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
835 }
836 $buttons[] = $nextButton;
837 }
838 $this->addButtons($buttons);
839 }
840
841 function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
842 if ($displayTime) {
843 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
844 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
845 } else {
846 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
847 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
848 }
849 }
850
851 function addSelect($name, $label, $prefix = NULL, $required = NULL, $extra = NULL, $select = '- select -') {
852 if ($prefix) {
853 $this->addElement('select', $name . '_id' . $prefix, $label,
854 array(
855 '' => $select) + CRM_Core_OptionGroup::values($name), $extra
856 );
857 if ($required) {
858 $this->addRule($name . '_id' . $prefix, ts('Please select %1', array(1 => $label)), 'required');
859 }
860 }
861 else {
862 $this->addElement('select', $name . '_id', $label,
863 array(
864 '' => $select) + CRM_Core_OptionGroup::values($name), $extra
865 );
866 if ($required) {
867 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
868 }
869 }
870 }
871
872 /**
873 * Add a widget for selecting/editing/creating/copying a profile form
874 *
875 * @param string $name HTML form-element name
876 * @param string $label Printable label
877 * @param string $allowCoreTypes only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'
878 * @param string $allowSubTypes only present a UFGroup if its group_type is compatible with $allowSubypes
879 * @param array $entities
880 */
881 function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities) {
882 // Output widget
883 // FIXME: Instead of adhoc serialization, use a single json_encode()
884 CRM_UF_Page_ProfileEditor::registerProfileScripts();
885 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
886 $this->add('text', $name, $label, array(
887 'class' => 'crm-profile-selector',
888 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
889 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
890 'data-entities' => json_encode($entities),
891 ));
892 }
893
894 function addWysiwyg($name, $label, $attributes, $forceTextarea = FALSE) {
895 // 1. Get configuration option for editor (tinymce, ckeditor, pure textarea)
896 // 2. Based on the option, initialise proper editor
897 $editorID = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
898 'editor_id'
899 );
900 $editor = strtolower(CRM_Utils_Array::value($editorID,
901 CRM_Core_OptionGroup::values('wysiwyg_editor')
902 ));
903 if (!$editor || $forceTextarea) {
904 $editor = 'textarea';
905 }
906 if ($editor == 'joomla default editor') {
907 $editor = 'joomlaeditor';
908 }
909
910 if ($editor == 'drupal default editor') {
911 $editor = 'drupalwysiwyg';
912 }
913
914 //lets add the editor as a attribute
915 $attributes['editor'] = $editor;
916
917 $this->addElement($editor, $name, $label, $attributes);
918 $this->assign('editor', $editor);
919
920 // include wysiwyg editor js files
921 $includeWysiwygEditor = FALSE;
922 $includeWysiwygEditor = $this->get('includeWysiwygEditor');
923 if (!$includeWysiwygEditor) {
924 $includeWysiwygEditor = TRUE;
925 $this->set('includeWysiwygEditor', $includeWysiwygEditor);
926 }
927
928 $this->assign('includeWysiwygEditor', $includeWysiwygEditor);
929 }
930
931 function addCountry($id, $title, $required = NULL, $extra = NULL) {
932 $this->addElement('select', $id, $title,
933 array(
934 '' => ts('- select -')) + CRM_Core_PseudoConstant::country(), $extra
935 );
936 if ($required) {
937 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
938 }
939 }
940
941 function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
942
943 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
944
945 if ($required) {
946 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
947 }
948 }
949
950 function buildAddressBlock($locationId, $title, $phone,
951 $alternatePhone = NULL, $addressRequired = NULL,
952 $phoneRequired = NULL, $altPhoneRequired = NULL,
953 $locationName = NULL
954 ) {
955 if (!$locationName) {
956 $locationName = "location";
957 }
958
959 $config = CRM_Core_Config::singleton();
960 $attributes = CRM_Core_DAO::getAttribute('CRM_Core_DAO_Address');
961
962 $location[$locationId]['address']['street_address'] = $this->addElement('text', "{$locationName}[$locationId][address][street_address]", $title,
963 $attributes['street_address']
964 );
965 if ($addressRequired) {
966 $this->addRule("{$locationName}[$locationId][address][street_address]", ts("Please enter the Street Address for %1.", array(1 => $title)), 'required');
967 }
968
969 $location[$locationId]['address']['supplemental_address_1'] = $this->addElement('text', "{$locationName}[$locationId][address][supplemental_address_1]", ts('Supplemental Address 1'),
970 $attributes['supplemental_address_1']
971 );
972 $location[$locationId]['address']['supplemental_address_2'] = $this->addElement('text', "{$locationName}[$locationId][address][supplemental_address_2]", ts('Supplemental Address 2'),
973 $attributes['supplemental_address_2']
974 );
975
976 $location[$locationId]['address']['city'] = $this->addElement('text', "{$locationName}[$locationId][address][city]", ts('City'),
977 $attributes['city']
978 );
979 if ($addressRequired) {
980 $this->addRule("{$locationName}[$locationId][address][city]", ts("Please enter the City for %1.", array(1 => $title)), 'required');
981 }
982
983 $location[$locationId]['address']['postal_code'] = $this->addElement('text', "{$locationName}[$locationId][address][postal_code]", ts('Zip / Postal Code'),
984 $attributes['postal_code']
985 );
986 if ($addressRequired) {
987 $this->addRule("{$locationName}[$locationId][address][postal_code]", ts("Please enter the Zip/Postal Code for %1.", array(1 => $title)), 'required');
988 }
989
990 $location[$locationId]['address']['postal_code_suffix'] = $this->addElement('text', "{$locationName}[$locationId][address][postal_code_suffix]", ts('Add-on Code'),
991 array('size' => 4, 'maxlength' => 12)
992 );
993 $this->addRule("{$locationName}[$locationId][address][postal_code_suffix]", ts('Zip-Plus not valid.'), 'positiveInteger');
994
995 if ($config->includeCounty) {
996 $location[$locationId]['address']['county_id'] = $this->addElement('select', "{$locationName}[$locationId][address][county_id]", ts('County'),
997 array('' => ts('- select -')) + CRM_Core_PseudoConstant::county()
998 );
999 }
1000
1001 $location[$locationId]['address']['state_province_id'] = $this->addElement('select', "{$locationName}[$locationId][address][state_province_id]", ts('State / Province'),
1002 array('' => ts('- select -')) + CRM_Core_PseudoConstant::stateProvince()
1003 );
1004
1005 $location[$locationId]['address']['country_id'] = $this->addElement('select', "{$locationName}[$locationId][address][country_id]", ts('Country'),
1006 array('' => ts('- select -')) + CRM_Core_PseudoConstant::country()
1007 );
1008 if ($addressRequired) {
1009 $this->addRule("{$locationName}[$locationId][address][country_id]", ts("Please select the Country for %1.", array(1 => $title)), 'required');
1010 }
1011
1012
1013 if ($phone) {
1014 $location[$locationId]['phone'][1]['phone'] = $this->addElement('text',
1015 "{$locationName}[$locationId][phone][1][phone]",
1016 $phone,
1017 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Phone',
1018 'phone'
1019 )
1020 );
1021 if ($phoneRequired) {
1022 $this->addRule("{$locationName}[$locationId][phone][1][phone]", ts('Please enter a value for %1', array(1 => $phone)), 'required');
1023 }
1024 $this->addRule("{$locationName}[$locationId][phone][1][phone]", ts('Please enter a valid number for %1', array(1 => $phone)), 'phone');
1025 }
1026
1027 if ($alternatePhone) {
1028 $location[$locationId]['phone'][2]['phone'] = $this->addElement('text',
1029 "{$locationName}[$locationId][phone][2][phone]",
1030 $alternatePhone,
1031 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Phone',
1032 'phone'
1033 )
1034 );
1035 if ($alternatePhoneRequired) {
1036 $this->addRule("{$locationName}[$locationId][phone][2][phone]", ts('Please enter a value for %1', array(1 => $alternatePhone)), 'required');
1037 }
1038 $this->addRule("{$locationName}[$locationId][phone][2][phone]", ts('Please enter a valid number for %1', array(1 => $alternatePhone)), 'phone');
1039 }
1040 }
1041
1042 public function getRootTitle() {
1043 return NULL;
1044 }
1045
1046 public function getCompleteTitle() {
1047 return $this->getRootTitle() . $this->getTitle();
1048 }
1049
1050 static function &getTemplate() {
1051 return self::$_template;
1052 }
1053
1054 function addUploadElement($elementName) {
1055 $uploadNames = $this->get('uploadNames');
1056 if (!$uploadNames) {
1057 $uploadNames = array();
1058 }
1059 if (is_array($elementName)) {
1060 foreach ($elementName as $name) {
1061 if (!in_array($name, $uploadNames)) {
1062 $uploadNames[] = $name;
1063 }
1064 }
1065 }
1066 else {
1067 if (!in_array($elementName, $uploadNames)) {
1068 $uploadNames[] = $elementName;
1069 }
1070 }
1071 $this->set('uploadNames', $uploadNames);
1072
1073 $config = CRM_Core_Config::singleton();
1074 if (!empty($uploadNames)) {
1075 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1076 }
1077 }
1078
1079 function buttonType() {
1080 $uploadNames = $this->get('uploadNames');
1081 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
1082 $this->assign('buttonType', $buttonType);
1083 return $buttonType;
1084 }
1085
1086 function getVar($name) {
1087 return isset($this->$name) ? $this->$name : NULL;
1088 }
1089
1090 function setVar($name, $value) {
1091 $this->$name = $value;
1092 }
1093
1094 /**
1095 * Function to add date
1096 * @param string $name name of the element
1097 * @param string $label label of the element
1098 * @param array $attributes key / value pair
1099 *
1100 // if you need time
1101 * $attributes = array ( 'addTime' => true,
1102 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1103 * );
1104 * @param boolean $required true if required
1105 *
1106 */
1107 function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1108 if (CRM_Utils_Array::value('formatType', $attributes)) {
1109 // get actual format
1110 $params = array('name' => $attributes['formatType']);
1111 $values = array();
1112
1113 // cache date information
1114 static $dateFormat;
1115 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1116 if (!CRM_Utils_Array::value($key, $dateFormat)) {
1117 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1118 $dateFormat[$key] = $values;
1119 }
1120 else {
1121 $values = $dateFormat[$key];
1122 }
1123
1124 if ($values['date_format']) {
1125 $attributes['format'] = $values['date_format'];
1126 }
1127
1128 if (CRM_Utils_Array::value('time_format', $values)) {
1129 $attributes['timeFormat'] = $values['time_format'];
1130 }
1131 $attributes['startOffset'] = $values['start'];
1132 $attributes['endOffset'] = $values['end'];
1133 }
1134
1135 $config = CRM_Core_Config::singleton();
1136 if (!CRM_Utils_Array::value('format', $attributes)) {
1137 $attributes['format'] = $config->dateInputFormat;
1138 }
1139
1140 if (!isset($attributes['startOffset'])) {
1141 $attributes['startOffset'] = 10;
1142 }
1143
1144 if (!isset($attributes['endOffset'])) {
1145 $attributes['endOffset'] = 10;
1146 }
1147
1148 $this->add('text', $name, $label, $attributes);
1149
1150 if (CRM_Utils_Array::value('addTime', $attributes) ||
1151 CRM_Utils_Array::value('timeFormat', $attributes)
1152 ) {
1153
1154 if (!isset($attributes['timeFormat'])) {
1155 $timeFormat = $config->timeInputFormat;
1156 }
1157 else {
1158 $timeFormat = $attributes['timeFormat'];
1159 }
1160
1161 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1162 if ($timeFormat) {
1163 $show24Hours = TRUE;
1164 if ($timeFormat == 1) {
1165 $show24Hours = FALSE;
1166 }
1167
1168 //CRM-6664 -we are having time element name
1169 //in either flat string or an array format.
1170 $elementName = $name . '_time';
1171 if (substr($name, -1) == ']') {
1172 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1173 }
1174
1175 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1176 }
1177 }
1178
1179 if ($required) {
1180 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1181 if (CRM_Utils_Array::value('addTime', $attributes) && CRM_Utils_Array::value('addTimeRequired', $attributes)) {
1182 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1183 }
1184 }
1185 }
1186
1187 /**
1188 * Function that will add date and time
1189 */
1190 function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1191 $addTime = array('addTime' => TRUE);
1192 if (is_array($attributes)) {
1193 $attributes = array_merge($attributes, $addTime);
1194 }
1195 else {
1196 $attributes = $addTime;
1197 }
1198
1199 $this->addDate($name, $label, $required, $attributes);
1200 }
1201
1202 /**
1203 * add a currency and money element to the form
1204 */
1205 function addMoney($name,
1206 $label,
1207 $required = FALSE,
1208 $attributes = NULL,
1209 $addCurrency = TRUE,
1210 $currencyName = 'currency',
1211 $defaultCurrency = NULL,
1212 $freezeCurrency = FALSE
1213 ) {
1214 $element = $this->add('text', $name, $label, $attributes, $required);
1215 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1216
1217 if ($addCurrency) {
1218 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1219 }
1220
1221 return $element;
1222 }
1223
1224 /**
1225 * add currency element to the form
1226 */
1227 function addCurrency($name = 'currency',
1228 $label = NULL,
1229 $required = TRUE,
1230 $defaultCurrency = NULL,
1231 $freezeCurrency = FALSE
1232 ) {
1233 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1234 if (!$required) {
1235 $currencies = array(
1236 '' => ts('- select -')) + $currencies;
1237 }
1238 $ele = $this->add('select', $name, $label, $currencies, $required);
1239 if ($freezeCurrency) {
1240 $ele->freeze();
1241 }
1242 if (!$defaultCurrency) {
1243 $config = CRM_Core_Config::singleton();
1244 $defaultCurrency = $config->defaultCurrency;
1245 }
1246 $this->setDefaults(array($name => $defaultCurrency));
1247 }
1248
1249 /**
1250 * Convert all date fields within the params to mysql date ready for the
1251 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1252 * and if time is defined it is incorporated
1253 *
1254 * @param array $params input params from the form
1255 *
1256 * @todo it would probably be better to work on $this->_params than a passed array
1257 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1258 * handling from BAO
1259 */
1260 function convertDateFieldsToMySQL(&$params){
1261 foreach ($this->_dateFields as $fieldName => $specs){
1262 if(!empty($params[$fieldName])){
1263 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1264 CRM_Utils_Date::processDate(
1265 $params[$fieldName],
1266 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1267 );
1268 }
1269 else{
1270 if(isset($specs['default'])){
1271 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1272 }
1273 }
1274 }
1275 }
1276
1277 function removeFileRequiredRules($elementName) {
1278 $this->_required = array_diff($this->_required, array($elementName));
1279 if (isset($this->_rules[$elementName])) {
1280 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1281 if ($ruleInfo['type'] == 'uploadedfile') {
1282 unset($this->_rules[$elementName][$index]);
1283 }
1284 }
1285 if (empty($this->_rules[$elementName])) {
1286 unset($this->_rules[$elementName]);
1287 }
1288 }
1289 }
1290
1291 /**
1292 * Function that can be defined in Form to override or
1293 * perform specific action on cancel action
1294 *
1295 * @access public
1296 */
1297 function cancelAction() {}
1298
1299 /**
1300 * Helper function to verify that required fields have been filled
1301 * Typically called within the scope of a FormRule function
1302 */
1303 static function validateMandatoryFields($fields, $values, &$errors) {
1304 foreach ($fields as $name => $fld) {
1305 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1306 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1307 }
1308 }
1309 }
1310
1311 /**
1312 * Get contact if for a form object. Prioritise
1313 * - cid in URL if 0 (on behalf on someoneelse)
1314 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1315 * - logged in user id if it matches the one in the cid in the URL
1316 * - contact id validated from a checksum from a checksum
1317 * - cid from the url if the caller has ACL permission to view
1318 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1319 *
1320 * @return Ambigous <mixed, NULL, value, unknown, array, number>|unknown
1321 */
1322 function getContactID() {
1323 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1324 if(isset($this->_params) && isset($this->_params['select_contact_id'])) {
1325 $tempID = $this->_params['select_contact_id'];
1326 }
1327 if(isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1328 // event form stores as an indexed array, contribution form not so much...
1329 $tempID = $this->_params[0]['select_contact_id'];
1330 }
1331 // force to ignore the authenticated user
1332 if ($tempID === '0') {
1333 return $tempID;
1334 }
1335
1336 $userID = $this->getLoggedInUserContactID();
1337
1338 if ($tempID == $userID) {
1339 return $userID;
1340 }
1341
1342 //check if this is a checksum authentication
1343 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1344 if ($userChecksum) {
1345 //check for anonymous user.
1346 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1347 if ($validUser) {
1348 return $tempID;
1349 }
1350 }
1351 // check if user has permission, CRM-12062
1352 else if ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1353 return $tempID;
1354 }
1355
1356 return $userID;
1357 }
1358
1359 /**
1360 * Get the contact id of the logged in user
1361 */
1362 function getLoggedInUserContactID() {
1363 // check if the user is logged in and has a contact ID
1364 $session = CRM_Core_Session::singleton();
1365 return $session->get('userID');
1366 }
1367
1368 /**
1369 * add autoselector field -if user has permission to view contacts
1370 * If adding this to a form you also need to add to the tpl e.g
1371 *
1372 * {if !empty($selectable)}
1373 * <div class="crm-summary-row">
1374 * <div class="crm-label">{$form.select_contact.label}</div>
1375 * <div class="crm-content">
1376 * {$form.select_contact.html}
1377 * </div>
1378 * </div>
1379 * {/if}
1380 * @param array $profiles ids of profiles that are on the form (to be autofilled)
1381 * @param array $field metadata of field to use as selector including
1382 * - name_field
1383 * - id_field
1384 * - url (for ajax lookup)
1385 *
1386 * @todo add data attributes so we can deal with multiple instances on a form
1387 */
1388 function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1389 $autoCompleteField = array_merge(array(
1390 'name_field' => 'select_contact',
1391 'id_field' => 'select_contact_id',
1392 'field_text' => ts('Select Contact'),
1393 'show_hide' => TRUE,
1394 'show_text' => ts('to select someone already in our database.'),
1395 'hide_text' => ts('to clear this person\'s information, and fill the form in for someone else'),
1396 'url' => array('civicrm/ajax/rest', 'className=CRM_Contact_Page_AJAX&fnName=getContactList&json=1'),
1397 'max' => civicrm_api3('setting', 'getvalue', array(
1398 'name' => 'search_autocomplete_count',
1399 'group' => 'Search Preferences',
1400 ))
1401 ), $autoCompleteField);
1402
1403 if(0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1)))) {
1404 $this->addElement('text', $autoCompleteField['name_field'] , $autoCompleteField['field_text']);
1405 $this->addElement('hidden', $autoCompleteField['id_field'], '', array('id' => $autoCompleteField['id_field']));
1406 $this->assign('selectable', $autoCompleteField['id_field']);
1407
1408 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AutoComplete.js')
1409 ->addSetting(array(
1410 'form' => array('autocompletes' => $autoCompleteField),
1411 'ids' => array('profile' => $profiles),
1412 ));
1413 }
1414 }
1415
1416 /**
1417 * Add the options appropriate to cid = zero - ie. autocomplete
1418 *
1419 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1420 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1421 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1422 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1423 */
1424 function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1425 $this->assign('nocid', TRUE);
1426 $profiles = array();
1427 if($this->_values['custom_pre_id']) {
1428 $profiles[] = $this->_values['custom_pre_id'];
1429 }
1430 if($this->_values['custom_post_id']) {
1431 $profiles[] = $this->_values['custom_post_id'];
1432 }
1433 if($onlinePaymentProcessorEnabled) {
1434 $profiles[] = 'billing';
1435 }
1436 if(!empty($this->_values)) {
1437 $this->addAutoSelector($profiles);
1438 }
1439 }
1440
1441 /**
1442 * Set default values on form for given contact (or no contact defaults)
1443 * @param mixed $profile_id (can be id, or profile name)
1444 * @param integer $contactID
1445 */
1446 function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
1447 try{
1448 $defaults = civicrm_api3('profile', 'getsingle', array(
1449 'profile_id' => (array) $profile_id,
1450 'contact_id' => $contactID,
1451 ));
1452 return $defaults;
1453 }
1454 catch (Exception $e) {
1455 // the try catch block gives us silent failure -not 100% sure this is a good idea
1456 // as silent failures are often worse than noisy ones
1457 return array();
1458 }
1459 }
1460 }
1461