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