Merge pull request #5760 from sudhabisht/SMSissue
[civicrm-core.git] / CRM / Core / Form.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
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-2015
35 * $Id$
36 *
37 */
38
39 require_once 'HTML/QuickForm/Page.php';
40
41 /**
42 * Class CRM_Core_Form
43 */
44 class CRM_Core_Form extends HTML_QuickForm_Page {
45
46 /**
47 * The state object that this form belongs to
48 * @var object
49 */
50 protected $_state;
51
52 /**
53 * The name of this form
54 * @var string
55 */
56 protected $_name;
57
58 /**
59 * The title of this form
60 * @var string
61 */
62 protected $_title = NULL;
63
64 /**
65 * The options passed into this form
66 * @var mixed
67 */
68 protected $_options = NULL;
69
70 /**
71 * The mode of operation for this form
72 * @var int
73 */
74 protected $_action;
75
76 /**
77 * The renderer used for this form
78 *
79 * @var object
80 */
81 protected $_renderer;
82
83 /**
84 * An array to hold a list of datefields on the form
85 * so that they can be converted to ISO in a consistent manner
86 *
87 * @var array
88 *
89 * e.g on a form declare $_dateFields = array(
90 * 'receive_date' => array('default' => 'now'),
91 * );
92 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
93 * to have the time field re-incorporated into the field & 'now' set if
94 * no value has been passed in
95 */
96 protected $_dateFields = array();
97
98 /**
99 * Cache the smarty template for efficiency reasons
100 *
101 * @var CRM_Core_Smarty
102 */
103 static protected $_template;
104
105 /**
106 * Indicate if this form should warn users of unsaved changes
107 */
108 protected $unsavedChangesWarn;
109
110 /**
111 * What to return to the client if in ajax mode (snippet=json)
112 *
113 * @var array
114 */
115 public $ajaxResponse = array();
116
117 /**
118 * Url path used to reach this page
119 *
120 * @var array
121 */
122 public $urlPath = array();
123
124 /**
125 * @var CRM_Core_Controller
126 */
127 public $controller;
128
129 /**
130 * Constants for attributes for various form elements
131 * attempt to standardize on the number of variations that we
132 * use of the below form elements
133 *
134 * @var const string
135 */
136 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
137
138 /**
139 * All checkboxes are defined with a common prefix. This allows us to
140 * have the same javascript to check / clear all the checkboxes etc
141 * If u have multiple groups of checkboxes, you will need to give them different
142 * ids to avoid potential name collision
143 *
144 * @var string|int
145 */
146 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
147
148 /**
149 * @internal to keep track of chain-select fields
150 * @var array
151 */
152 private $_chainSelectFields = array();
153
154 /**
155 * Constructor for the basic form page.
156 *
157 * We should not use QuickForm directly. This class provides a lot
158 * of default convenient functions, rules and buttons
159 *
160 * @param object $state
161 * State associated with this form.
162 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
163 * @param string $method
164 * The type of http method used (GET/POST).
165 * @param string $name
166 * The name of the form if different from class name.
167 *
168 * @return \CRM_Core_Form
169 */
170 public function __construct(
171 $state = NULL,
172 $action = CRM_Core_Action::NONE,
173 $method = 'post',
174 $name = NULL
175 ) {
176
177 if ($name) {
178 $this->_name = $name;
179 }
180 else {
181 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
182 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
183 }
184
185 $this->HTML_QuickForm_Page($this->_name, $method);
186
187 $this->_state =& $state;
188 if ($this->_state) {
189 $this->_state->setName($this->_name);
190 }
191 $this->_action = (int) $action;
192
193 $this->registerRules();
194
195 // let the constructor initialize this, should happen only once
196 if (!isset(self::$_template)) {
197 self::$_template = CRM_Core_Smarty::singleton();
198 }
199 // Workaround for CRM-15153 - give each form a reasonably unique css class
200 $this->addClass(CRM_Utils_System::getClassName($this));
201
202 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
203 }
204
205 public static function generateID() {
206 }
207
208 /**
209 * Add one or more css classes to the form
210 * @param string $className
211 */
212 public function addClass($className) {
213 $classes = $this->getAttribute('class');
214 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
215 }
216
217 /**
218 * Register all the standard rules that most forms potentially use.
219 *
220 * @return void
221 */
222 public function registerRules() {
223 static $rules = array(
224 'title',
225 'longTitle',
226 'variable',
227 'qfVariable',
228 'phone',
229 'integer',
230 'query',
231 'url',
232 'wikiURL',
233 'domain',
234 'numberOfDigit',
235 'date',
236 'currentDate',
237 'asciiFile',
238 'htmlFile',
239 'utf8File',
240 'objectExists',
241 'optionExists',
242 'postalCode',
243 'money',
244 'positiveInteger',
245 'xssString',
246 'fileExists',
247 'autocomplete',
248 'validContact',
249 );
250
251 foreach ($rules as $rule) {
252 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
253 }
254 }
255
256 /**
257 * Simple easy to use wrapper around addElement. Deal with
258 * simple validation rules
259 *
260 * @param string $type
261 * @param string $name
262 * @param string $label
263 * @param string|array $attributes (options for select elements)
264 * @param bool $required
265 * @param array $extra
266 * (attributes for select elements).
267 *
268 * @return HTML_QuickForm_Element could be an error object
269 */
270 public function &add(
271 $type, $name, $label = '',
272 $attributes = '', $required = FALSE, $extra = NULL
273 ) {
274 if ($type == 'wysiwyg') {
275 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
276 $attributes['class'] .= ' crm-form-wysiwyg';
277 $type = "textarea";
278 }
279 if ($type == 'select' && is_array($extra)) {
280 // Normalize this property
281 if (!empty($extra['multiple'])) {
282 $extra['multiple'] = 'multiple';
283 }
284 else {
285 unset($extra['multiple']);
286 }
287 // Add placeholder option for select
288 if (isset($extra['placeholder'])) {
289 if ($extra['placeholder'] === TRUE) {
290 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
291 }
292 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
293 $attributes = array('' => $extra['placeholder']) + $attributes;
294 }
295 }
296 }
297 $element = $this->addElement($type, $name, $label, $attributes, $extra);
298 if (HTML_QuickForm::isError($element)) {
299 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
300 }
301
302 if ($required) {
303 if ($type == 'file') {
304 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
305 }
306 else {
307 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
308 }
309 if (HTML_QuickForm::isError($error)) {
310 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
311 }
312 }
313
314 return $element;
315 }
316
317 /**
318 * called before buildForm. Any pre-processing that
319 * needs to be done for buildForm should be done here
320 *
321 * This is a virtual function and should be redefined if needed
322 *
323 *
324 * @return void
325 */
326 public function preProcess() {
327 }
328
329 /**
330 * called after the form is validated. Any
331 * processing of form state etc should be done in this function.
332 * Typically all processing associated with a form should be done
333 * here and relevant state should be stored in the session
334 *
335 * This is a virtual function and should be redefined if needed
336 *
337 *
338 * @return void
339 */
340 public function postProcess() {
341 }
342
343 /**
344 * just a wrapper, so that we can call all the hook functions
345 * @param bool $allowAjax
346 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
347 * and bury it deeper down in the controller
348 */
349 public function mainProcess($allowAjax = TRUE) {
350 $this->postProcess();
351 $this->postProcessHook();
352
353 // Respond with JSON if in AJAX context (also support legacy value '6')
354 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(
355 CRM_Core_Smarty::PRINT_JSON,
356 6,
357 ))
358 ) {
359 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
360 $this->ajaxResponse['action'] = $this->_action;
361 if (isset($this->_id) || isset($this->id)) {
362 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
363 }
364 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
365 }
366 }
367
368 /**
369 * The postProcess hook is typically called by the framework.
370 * However in a few cases, the form exits or redirects early in which
371 * case it needs to call this function so other modules can do the needful
372 * Calling this function directly should be avoided if possible. In general a
373 * better way is to do setUserContext so the framework does the redirect
374 */
375 public function postProcessHook() {
376 CRM_Utils_Hook::postProcess(get_class($this), $this);
377 }
378
379 /**
380 * This virtual function is used to build the form. It replaces the
381 * buildForm associated with QuickForm_Page. This allows us to put
382 * preProcess in front of the actual form building routine
383 *
384 *
385 * @return void
386 */
387 public function buildQuickForm() {
388 }
389
390 /**
391 * This virtual function is used to set the default values of
392 * various form elements
393 *
394 * access public
395 *
396 * @return array|NULL
397 * reference to the array of default values
398 */
399 public function setDefaultValues() {
400 return NULL;
401 }
402
403 /**
404 * This is a virtual function that adds group and global rules to
405 * the form. Keeping it distinct from the form to keep code small
406 * and localized in the form building code
407 *
408 *
409 * @return void
410 */
411 public function addRules() {
412 }
413
414 /**
415 * Performs the server side validation.
416 * @since 1.0
417 * @return bool
418 * true if no error found
419 * @throws HTML_QuickForm_Error
420 */
421 public function validate() {
422 $error = parent::validate();
423
424 $this->validateChainSelectFields();
425
426 $hookErrors = CRM_Utils_Hook::validate(
427 get_class($this),
428 $this->_submitValues,
429 $this->_submitFiles,
430 $this
431 );
432
433 if (!is_array($hookErrors)) {
434 $hookErrors = array();
435 }
436
437 CRM_Utils_Hook::validateForm(
438 get_class($this),
439 $this->_submitValues,
440 $this->_submitFiles,
441 $this,
442 $hookErrors
443 );
444
445 if (!empty($hookErrors)) {
446 $this->_errors += $hookErrors;
447 }
448
449 return (0 == count($this->_errors));
450 }
451
452 /**
453 * Core function that builds the form. We redefine this function
454 * here and expect all CRM forms to build their form in the function
455 * buildQuickForm.
456 */
457 public function buildForm() {
458 $this->_formBuilt = TRUE;
459
460 $this->preProcess();
461
462 CRM_Utils_Hook::preProcess(get_class($this), $this);
463
464 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
465
466 if (
467 $this->controller->_key &&
468 $this->controller->_generateQFKey
469 ) {
470 $this->addElement('hidden', 'qfKey', $this->controller->_key);
471 $this->assign('qfKey', $this->controller->_key);
472
473 }
474
475 // _generateQFKey suppresses the qfKey generation on form snippets that
476 // are part of other forms, hence we use that to avoid adding entryURL
477 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
478 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
479 }
480
481 $this->buildQuickForm();
482
483 $defaults = $this->setDefaultValues();
484 unset($defaults['qfKey']);
485
486 if (!empty($defaults)) {
487 $this->setDefaults($defaults);
488 }
489
490 // call the form hook
491 // also call the hook function so any modules can set their own custom defaults
492 // the user can do both the form and set default values with this hook
493 CRM_Utils_Hook::buildForm(get_class($this), $this);
494
495 $this->addRules();
496
497 //Set html data-attribute to enable warning user of unsaved changes
498 if ($this->unsavedChangesWarn === TRUE
499 || (!isset($this->unsavedChangesWarn)
500 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
501 )
502 ) {
503 $this->setAttribute('data-warn-changes', 'true');
504 }
505 }
506
507 /**
508 * Add default Next / Back buttons
509 *
510 * @param array $params
511 * Array of associative arrays in the order in which the buttons should be
512 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
513 * The base form class will define a bunch of static arrays for commonly used
514 * formats.
515 *
516 * @return void
517 */
518 public function addButtons($params) {
519 $prevnext = $spacing = array();
520 foreach ($params as $button) {
521 $attrs = array('class' => 'crm-form-submit') + (array) CRM_Utils_Array::value('js', $button);
522
523 if (!empty($button['class'])) {
524 $attrs['class'] .= ' ' . $button['class'];
525 }
526
527 if (!empty($button['isDefault'])) {
528 $attrs['class'] .= ' default';
529 }
530
531 if (in_array($button['type'], array('upload', 'next', 'submit', 'done', 'process', 'refresh'))) {
532 $attrs['class'] .= ' validate';
533 $defaultIcon = 'check';
534 }
535 else {
536 $attrs['class'] .= ' cancel';
537 $defaultIcon = $button['type'] == 'back' ? 'triangle-1-w' : 'close';
538 }
539
540 if ($button['type'] === 'reset') {
541 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
542 }
543 else {
544 if (!empty($button['subName'])) {
545 if ($button['subName'] == 'new') {
546 $defaultIcon = 'plus';
547 }
548 if ($button['subName'] == 'done') {
549 $defaultIcon = 'circle-check';
550 }
551 if ($button['subName'] == 'next') {
552 $defaultIcon = 'circle-triangle-e';
553 }
554 }
555
556 if (in_array($button['type'], array('next', 'upload', 'done')) && $button['name'] === ts('Save')) {
557 $attrs['accesskey'] = 'S';
558 }
559 $icon = CRM_Utils_Array::value('icon', $button, $defaultIcon);
560 if ($icon) {
561 $attrs['crm-icon'] = $icon;
562 }
563 $buttonName = $this->getButtonName($button['type'], CRM_Utils_Array::value('subName', $button));
564 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
565 }
566 if (!empty($button['isDefault'])) {
567 $this->setDefaultAction($button['type']);
568 }
569
570 // if button type is upload, set the enctype
571 if ($button['type'] == 'upload') {
572 $this->updateAttributes(array('enctype' => 'multipart/form-data'));
573 $this->setMaxFileSize();
574 }
575
576 // hack - addGroup uses an array to express variable spacing, read from the last element
577 $spacing[] = CRM_Utils_Array::value('spacing', $button, self::ATTR_SPACING);
578 }
579 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
580 }
581
582 /**
583 * Getter function for Name.
584 *
585 * @return string
586 */
587 public function getName() {
588 return $this->_name;
589 }
590
591 /**
592 * Getter function for State.
593 *
594 * @return object
595 */
596 public function &getState() {
597 return $this->_state;
598 }
599
600 /**
601 * Getter function for StateType.
602 *
603 * @return int
604 */
605 public function getStateType() {
606 return $this->_state->getType();
607 }
608
609 /**
610 * Getter function for title. Should be over-ridden by derived class
611 *
612 * @return string
613 */
614 public function getTitle() {
615 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
616 }
617
618 /**
619 * Setter function for title.
620 *
621 * @param string $title
622 * The title of the form.
623 *
624 * @return void
625 */
626 public function setTitle($title) {
627 $this->_title = $title;
628 }
629
630 /**
631 * Setter function for options.
632 *
633 * @param mixed $options
634 *
635 * @return void
636 */
637 public function setOptions($options) {
638 $this->_options = $options;
639 }
640
641 /**
642 * Getter function for link.
643 *
644 * @return string
645 */
646 public function getLink() {
647 $config = CRM_Core_Config::singleton();
648 return CRM_Utils_System::url($_GET[$config->userFrameworkURLVar],
649 '_qf_' . $this->_name . '_display=true'
650 );
651 }
652
653 /**
654 * Boolean function to determine if this is a one form page.
655 *
656 * @return bool
657 */
658 public function isSimpleForm() {
659 return $this->_state->getType() & (CRM_Core_State::START | CRM_Core_State::FINISH);
660 }
661
662 /**
663 * Getter function for Form Action.
664 *
665 * @return string
666 */
667 public function getFormAction() {
668 return $this->_attributes['action'];
669 }
670
671 /**
672 * Setter function for Form Action.
673 *
674 * @param string $action
675 *
676 * @return void
677 */
678 public function setFormAction($action) {
679 $this->_attributes['action'] = $action;
680 }
681
682 /**
683 * Render form and return contents.
684 *
685 * @return string
686 */
687 public function toSmarty() {
688 $this->preProcessChainSelectFields();
689 $renderer = $this->getRenderer();
690 $this->accept($renderer);
691 $content = $renderer->toArray();
692 $content['formName'] = $this->getName();
693 // CRM-15153
694 $content['formClass'] = CRM_Utils_System::getClassName($this);
695 return $content;
696 }
697
698 /**
699 * Getter function for renderer. If renderer is not set
700 * create one and initialize it
701 *
702 * @return object
703 */
704 public function &getRenderer() {
705 if (!isset($this->_renderer)) {
706 $this->_renderer = CRM_Core_Form_Renderer::singleton();
707 }
708 return $this->_renderer;
709 }
710
711 /**
712 * Use the form name to create the tpl file name.
713 *
714 * @return string
715 */
716 public function getTemplateFileName() {
717 $ext = CRM_Extension_System::singleton()->getMapper();
718 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
719 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
720 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
721 }
722 else {
723 $tplname = strtr(
724 CRM_Utils_System::getClassName($this),
725 array(
726 '_' => DIRECTORY_SEPARATOR,
727 '\\' => DIRECTORY_SEPARATOR,
728 )
729 ) . '.tpl';
730 }
731 return $tplname;
732 }
733
734 /**
735 * A wrapper for getTemplateFileName that includes calling the hook to
736 * prevent us from having to copy & paste the logic of calling the hook
737 */
738 public function getHookedTemplateFileName() {
739 $pageTemplateFile = $this->getTemplateFileName();
740 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
741 return $pageTemplateFile;
742 }
743
744 /**
745 * Default extra tpl file basically just replaces .tpl with .extra.tpl
746 * i.e. we dont override
747 *
748 * @return string
749 */
750 public function overrideExtraTemplateFileName() {
751 return NULL;
752 }
753
754 /**
755 * Error reporting mechanism.
756 *
757 * @param string $message
758 * Error Message.
759 * @param int $code
760 * Error Code.
761 * @param CRM_Core_DAO $dao
762 * A data access object on which we perform a rollback if non - empty.
763 *
764 * @return void
765 */
766 public function error($message, $code = NULL, $dao = NULL) {
767 if ($dao) {
768 $dao->query('ROLLBACK');
769 }
770
771 $error = CRM_Core_Error::singleton();
772
773 $error->push($code, $message);
774 }
775
776 /**
777 * Store the variable with the value in the form scope.
778 *
779 * @param string $name
780 * Name of the variable.
781 * @param mixed $value
782 * Value of the variable.
783 *
784 * @return void
785 */
786 public function set($name, $value) {
787 $this->controller->set($name, $value);
788 }
789
790 /**
791 * Get the variable from the form scope.
792 *
793 * @param string $name
794 * Name of the variable
795 *
796 * @return mixed
797 */
798 public function get($name) {
799 return $this->controller->get($name);
800 }
801
802 /**
803 * Getter for action.
804 *
805 * @return int
806 */
807 public function getAction() {
808 return $this->_action;
809 }
810
811 /**
812 * Setter for action.
813 *
814 * @param int $action
815 * The mode we want to set the form.
816 *
817 * @return void
818 */
819 public function setAction($action) {
820 $this->_action = $action;
821 }
822
823 /**
824 * Assign value to name in template.
825 *
826 * @param string $var
827 * Name of variable.
828 * @param mixed $value
829 * Value of variable.
830 *
831 * @return void
832 */
833 public function assign($var, $value = NULL) {
834 self::$_template->assign($var, $value);
835 }
836
837 /**
838 * Assign value to name in template by reference.
839 *
840 * @param string $var
841 * Name of variable.
842 * @param mixed $value
843 * Value of varaible.
844 *
845 * @return void
846 */
847 public function assign_by_ref($var, &$value) {
848 self::$_template->assign_by_ref($var, $value);
849 }
850
851 /**
852 * Appends values to template variables.
853 *
854 * @param array|string $tpl_var the template variable name(s)
855 * @param mixed $value
856 * The value to append.
857 * @param bool $merge
858 */
859 public function append($tpl_var, $value = NULL, $merge = FALSE) {
860 self::$_template->append($tpl_var, $value, $merge);
861 }
862
863 /**
864 * Returns an array containing template variables.
865 *
866 * @param string $name
867 *
868 * @return array
869 */
870 public function get_template_vars($name = NULL) {
871 return self::$_template->get_template_vars($name);
872 }
873
874 /**
875 * @param string $name
876 * @param $title
877 * @param $values
878 * @param array $attributes
879 * @param null $separator
880 * @param bool $required
881 *
882 * @return HTML_QuickForm_group
883 */
884 public function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
885 $options = array();
886 $attributes = $attributes ? $attributes : array();
887 $allowClear = !empty($attributes['allowClear']);
888 unset($attributes['allowClear']);
889 $attributes['id_suffix'] = $name;
890 foreach ($values as $key => $var) {
891 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
892 }
893 $group = $this->addGroup($options, $name, $title, $separator);
894 if ($required) {
895 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
896 }
897 if ($allowClear) {
898 $group->setAttribute('allowClear', TRUE);
899 }
900 return $group;
901 }
902
903 /**
904 * @param int $id
905 * @param $title
906 * @param bool $allowClear
907 * @param null $required
908 * @param array $attributes
909 */
910 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
911 $attributes += array('id_suffix' => $id);
912 $choice = array();
913 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
914 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
915
916 $group = $this->addGroup($choice, $id, $title);
917 if ($allowClear) {
918 $group->setAttribute('allowClear', TRUE);
919 }
920 if ($required) {
921 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
922 }
923 }
924
925 /**
926 * @param int $id
927 * @param $title
928 * @param $values
929 * @param null $other
930 * @param null $attributes
931 * @param null $required
932 * @param null $javascriptMethod
933 * @param string $separator
934 * @param bool $flipValues
935 */
936 public function addCheckBox(
937 $id, $title, $values, $other = NULL,
938 $attributes = NULL, $required = NULL,
939 $javascriptMethod = NULL,
940 $separator = '<br />', $flipValues = FALSE
941 ) {
942 $options = array();
943
944 if ($javascriptMethod) {
945 foreach ($values as $key => $var) {
946 if (!$flipValues) {
947 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
948 }
949 else {
950 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
951 }
952 }
953 }
954 else {
955 foreach ($values as $key => $var) {
956 if (!$flipValues) {
957 $options[] = $this->createElement('checkbox', $var, NULL, $key);
958 }
959 else {
960 $options[] = $this->createElement('checkbox', $key, NULL, $var);
961 }
962 }
963 }
964
965 $this->addGroup($options, $id, $title, $separator);
966
967 if ($other) {
968 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
969 }
970
971 if ($required) {
972 $this->addRule($id,
973 ts('%1 is a required field.', array(1 => $title)),
974 'required'
975 );
976 }
977 }
978
979 public function resetValues() {
980 $data = $this->controller->container();
981 $data['values'][$this->_name] = array();
982 }
983
984 /**
985 * Simple shell that derived classes can call to add buttons to
986 * the form with a customized title for the main Submit
987 *
988 * @param string $title
989 * Title of the main button.
990 * @param string $nextType
991 * Button type for the form after processing.
992 * @param string $backType
993 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
994 *
995 * @return void
996 */
997 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
998 $buttons = array();
999 if ($backType != NULL) {
1000 $buttons[] = array(
1001 'type' => $backType,
1002 'name' => ts('Previous'),
1003 );
1004 }
1005 if ($nextType != NULL) {
1006 $nextButton = array(
1007 'type' => $nextType,
1008 'name' => $title,
1009 'isDefault' => TRUE,
1010 );
1011 if ($submitOnce) {
1012 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
1013 }
1014 $buttons[] = $nextButton;
1015 }
1016 $this->addButtons($buttons);
1017 }
1018
1019 /**
1020 * @param string $name
1021 * @param string $from
1022 * @param string $to
1023 * @param string $label
1024 * @param string $dateFormat
1025 * @param bool $required
1026 * @param bool $displayTime
1027 */
1028 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1029 if ($displayTime) {
1030 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1031 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1032 }
1033 else {
1034 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1035 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1036 }
1037 }
1038
1039 /**
1040 * Based on form action, return a string representing the api action.
1041 * Used by addField method.
1042 *
1043 * Return string
1044 */
1045 private function getApiAction() {
1046 $action = $this->getAction();
1047 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1048 return 'create';
1049 }
1050 if ($action & (CRM_Core_Action::BROWSE)) {
1051 return 'get';
1052 }
1053 // If you get this exception try adding more cases above.
1054 throw new Exception("Cannot determine api action for " . __CLASS__);
1055 }
1056
1057 /**
1058 * Classes extending CRM_Core_Form should implement this method.
1059 * @throws Exception
1060 */
1061 public function getDefaultEntity() {
1062 throw new Exception("Cannot determine default entity. The form class should implement getDefaultEntity().");
1063 }
1064
1065 /**
1066 * Classes extending CRM_Core_Form should implement this method.
1067 *
1068 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1069 * @throws Exception
1070 */
1071 public function getDefaultContext() {
1072 throw new Exception("Cannot determine default context. The form class should implement getDefaultContext().");
1073 }
1074
1075 /**
1076 * Adds a select based on field metadata.
1077 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1078 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1079 * @param $name
1080 * Field name to go on the form.
1081 * @param array $props
1082 * Mix of html attributes and special properties, namely.
1083 * - entity (api entity name, can usually be inferred automatically from the form class)
1084 * - field (field name - only needed if different from name used on the form)
1085 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1086 * - placeholder - set to NULL to disable
1087 * - multiple - bool
1088 * - context - @see CRM_Core_DAO::buildOptionsContext
1089 * @param bool $required
1090 * @throws CRM_Core_Exception
1091 * @return HTML_QuickForm_Element
1092 */
1093 public function addSelect($name, $props = array(), $required = FALSE) {
1094 if (!isset($props['entity'])) {
1095 $props['entity'] = $this->getDefaultEntity();
1096 }
1097 if (!isset($props['field'])) {
1098 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1099 }
1100 // Fetch options from the api unless passed explicitly
1101 if (isset($props['options'])) {
1102 $options = $props['options'];
1103 }
1104 else {
1105 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1106 $options = $info['values'];
1107 }
1108 if (!array_key_exists('placeholder', $props)) {
1109 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1110 }
1111 // Handle custom field
1112 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1113 list(, $id) = explode('_', $name);
1114 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1115 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1116 if (CRM_Utils_Array::value('context', $props) != 'search') {
1117 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : 'civicrm/admin/options/' . CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $gid);
1118 }
1119 }
1120 // Core field
1121 else {
1122 $info = civicrm_api3($props['entity'], 'getfields');
1123 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1124 if (
1125 $uniqueName === $props['field'] ||
1126 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1127 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
1128 ) {
1129 break;
1130 }
1131 }
1132 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1133 if (CRM_Utils_Array::value('context', $props) != 'search') {
1134 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1135 }
1136 }
1137 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1138 $props['data-api-entity'] = $props['entity'];
1139 $props['data-api-field'] = $props['field'];
1140 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1141 return $this->add('select', $name, $label, $options, $required, $props);
1142 }
1143
1144 /**
1145 * Adds a field based on metadata.
1146 *
1147 * @param $name
1148 * Field name to go on the form.
1149 * @param array $props
1150 * Mix of html attributes and special properties, namely.
1151 * - entity (api entity name, can usually be inferred automatically from the form class)
1152 * - name (field name - only needed if different from name used on the form)
1153 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1154 * - placeholder - set to NULL to disable
1155 * - multiple - bool
1156 * - context - @see CRM_Core_DAO::buildOptionsContext
1157 * @param bool $required
1158 * @throws \CiviCRM_API3_Exception
1159 * @throws \Exception
1160 */
1161 public function addField($name, $props = array(), $required = FALSE) {
1162 // TODO: Handle custom field
1163 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1164 throw new Exception("Custom fields are not supported by the addField method. ");
1165 }
1166 // Resolve context.
1167 if (!isset($props['context'])) {
1168 $props['context'] = $this->getDefaultContext();
1169 }
1170 // Resolve entity.
1171 if (!isset($props['entity'])) {
1172 $props['entity'] = $this->getDefaultEntity();
1173 }
1174 // Resolve field.
1175 if (!isset($props['name'])) {
1176 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1177 }
1178 // Resolve action.
1179 if (!isset($props['action'])) {
1180 $props['action'] = $this->getApiAction();
1181 }
1182 // Get field metadata.
1183 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1184 $fieldSpec = $fieldSpec['values'];
1185 $label = CRM_Utils_Array::value('label', $props, isset($fieldSpec['title']) ? $fieldSpec['title'] : NULL);
1186
1187 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1188 if ($widget == 'TextArea' && $props['context'] == 'search') {
1189 $widget = 'Text';
1190 }
1191
1192 $isSelect = (in_array($widget, array(
1193 'Select',
1194 'Multi-Select',
1195 'Select State/Province',
1196 'Multi-Select State/Province',
1197 'Select Country',
1198 'Multi-Select Country',
1199 'AdvMulti-Select',
1200 'CheckBox',
1201 'Radio',
1202 )));
1203
1204 if ($isSelect) {
1205 // Fetch options from the api unless passed explicitly.
1206 if (isset($props['options'])) {
1207 $options = $props['options'];
1208 // Else this get passed to the form->add method.
1209 unset($props['options']);
1210 }
1211 else {
1212 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1213 }
1214 //@TODO AdvMulti-Select is deprecated, drop support.
1215 if ($props['context'] == 'search' || ($widget !== 'AdvMulti-Select' && strpos($widget, 'Select') !== FALSE)) {
1216 $widget = 'Select';
1217 }
1218 // Set default options-url value.
1219 if ((!isset($props['options-url']))) {
1220 $props['options-url'] = TRUE;
1221 }
1222
1223 // Add data for popup link.
1224 if ((isset($props['options-url']) && $props['options-url']) && ($props['context'] != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) {
1225 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1226 $props['data-api-entity'] = $props['entity'];
1227 $props['data-api-field'] = $props['name'];
1228 if (isset($props['options-url'])) {
1229 unset($props['options-url']);
1230 }
1231 }
1232 }
1233 //Use select2 library for following widgets.
1234 $isSelect2 = (in_array($widget, array(
1235 'Select',
1236 'Multi-Select',
1237 'Select State/Province',
1238 'Multi-Select State/Province',
1239 'Select Country',
1240 'Multi-Select Country',
1241 )));
1242 if ($isSelect2) {
1243 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1244 if ($props['context'] == 'search' || strpos($widget, 'Multi') !== FALSE) {
1245 $props['class'] .= ' huge';
1246 $props['multiple'] = 'multiple';
1247 }
1248 // The placeholder is only used for select-elements.
1249 if (!array_key_exists('placeholder', $props)) {
1250 $props['placeholder'] = $required ? ts('- select -') : $props['context'] == 'search' ? ts('- any -') : ts('- none -');
1251 }
1252 }
1253 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
1254 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type');
1255 // TODO: refactor switch statement, to separate methods.
1256 switch ($widget) {
1257 case 'Text':
1258 case 'Link':
1259 //TODO: Autodetect ranges
1260 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1261 $this->add('text', $name, $label, $props, $required);
1262 break;
1263
1264 case 'hidden':
1265 $this->add('hidden', $name, $label, $props, $required);
1266 break;
1267
1268 case 'TextArea':
1269 //Set default columns and rows for textarea.
1270 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1271 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1272 $this->addElement('textarea', $name, $label, $props, $required);
1273 break;
1274
1275 //case 'Select Date':
1276 //TODO: Add date formats
1277 //TODO: Add javascript template for dates.
1278 case 'Radio':
1279 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1280 unset($props['separator']);
1281 if (!isset($props['allowClear'])) {
1282 $props['allowClear'] = !$required;
1283 }
1284 $this->addRadio($name, $label, $options, $props, $separator, $required);
1285 break;
1286
1287 case 'Select':
1288 if (empty($props['multiple'])) {
1289 $options = array('' => $props['placeholder']) + $options;
1290 }
1291 $this->add('select', $name, $label, $options, $required, $props);
1292 // TODO: Add and/or option for fields that store multiple values
1293 break;
1294
1295 //case 'AdvMulti-Select':
1296 case 'CheckBox':
1297 $this->add('checkbox', $name, $label, NULL, $required);
1298 break;
1299
1300 case 'File':
1301 // We should not build upload file in search mode.
1302 if (isset($props['context']) && $props['context'] == 'search') {
1303 return;
1304 }
1305 $this->add('file', $name, $label, $props, $required);
1306 $this->addUploadElement($name);
1307 break;
1308
1309 //case 'RichTextEditor':
1310 //TODO: Add javascript template for wysiwyg.
1311 case 'Autocomplete-Select':
1312 case 'EntityRef':
1313 $this->addEntityRef($name, $label, $props, $required);
1314 break;
1315
1316 // Check datatypes of fields
1317 // case 'Int':
1318 //case 'Float':
1319 //case 'Money':
1320 //case 'Link':
1321 //case read only fields
1322 default:
1323 throw new Exception("Unsupported html-element " . $widget);
1324 }
1325 }
1326
1327 /**
1328 * Add a widget for selecting/editing/creating/copying a profile form
1329 *
1330 * @param string $name
1331 * HTML form-element name.
1332 * @param string $label
1333 * Printable label.
1334 * @param string $allowCoreTypes
1335 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1336 * @param string $allowSubTypes
1337 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1338 * @param array $entities
1339 * @param bool $default
1340 * //CRM-15427.
1341 */
1342 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE) {
1343 // Output widget
1344 // FIXME: Instead of adhoc serialization, use a single json_encode()
1345 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1346 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1347 $this->add('text', $name, $label, array(
1348 'class' => 'crm-profile-selector',
1349 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1350 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1351 'data-entities' => json_encode($entities),
1352 //CRM-15427
1353 'data-default' => $default,
1354 ));
1355 }
1356
1357 /**
1358 * @param int $id
1359 * @param $title
1360 * @param null $required
1361 * @param null $extra
1362 */
1363 public function addCountry($id, $title, $required = NULL, $extra = NULL) {
1364 $this->addElement('select', $id, $title,
1365 array(
1366 '' => ts('- select -'),
1367 ) + CRM_Core_PseudoConstant::country(), $extra
1368 );
1369 if ($required) {
1370 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
1371 }
1372 }
1373
1374 /**
1375 * @param string $name
1376 * @param $label
1377 * @param $options
1378 * @param $attributes
1379 * @param null $required
1380 * @param null $javascriptMethod
1381 */
1382 public function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
1383
1384 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
1385
1386 if ($required) {
1387 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
1388 }
1389 }
1390
1391 /**
1392 * @return null
1393 */
1394 public function getRootTitle() {
1395 return NULL;
1396 }
1397
1398 /**
1399 * @return string
1400 */
1401 public function getCompleteTitle() {
1402 return $this->getRootTitle() . $this->getTitle();
1403 }
1404
1405 /**
1406 * @return CRM_Core_Smarty
1407 */
1408 public static function &getTemplate() {
1409 return self::$_template;
1410 }
1411
1412 /**
1413 * @param $elementName
1414 */
1415 public function addUploadElement($elementName) {
1416 $uploadNames = $this->get('uploadNames');
1417 if (!$uploadNames) {
1418 $uploadNames = array();
1419 }
1420 if (is_array($elementName)) {
1421 foreach ($elementName as $name) {
1422 if (!in_array($name, $uploadNames)) {
1423 $uploadNames[] = $name;
1424 }
1425 }
1426 }
1427 else {
1428 if (!in_array($elementName, $uploadNames)) {
1429 $uploadNames[] = $elementName;
1430 }
1431 }
1432 $this->set('uploadNames', $uploadNames);
1433
1434 $config = CRM_Core_Config::singleton();
1435 if (!empty($uploadNames)) {
1436 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1437 }
1438 }
1439
1440 /**
1441 * @return string
1442 */
1443 public function buttonType() {
1444 $uploadNames = $this->get('uploadNames');
1445 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
1446 $this->assign('buttonType', $buttonType);
1447 return $buttonType;
1448 }
1449
1450 /**
1451 * @param $name
1452 *
1453 * @return null
1454 */
1455 public function getVar($name) {
1456 return isset($this->$name) ? $this->$name : NULL;
1457 }
1458
1459 /**
1460 * @param $name
1461 * @param $value
1462 */
1463 public function setVar($name, $value) {
1464 $this->$name = $value;
1465 }
1466
1467 /**
1468 * Add date.
1469 *
1470 * @code
1471 * // if you need time
1472 * $attributes = array(
1473 * 'addTime' => true,
1474 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1475 * );
1476 * @endcode
1477 *
1478 * @param string $name
1479 * Name of the element.
1480 * @param string $label
1481 * Label of the element.
1482 * @param bool $required
1483 * True if required.
1484 * @param array $attributes
1485 * Key / value pair.
1486 */
1487 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1488 if (!empty($attributes['formatType'])) {
1489 // get actual format
1490 $params = array('name' => $attributes['formatType']);
1491 $values = array();
1492
1493 // cache date information
1494 static $dateFormat;
1495 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1496 if (empty($dateFormat[$key])) {
1497 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1498 $dateFormat[$key] = $values;
1499 }
1500 else {
1501 $values = $dateFormat[$key];
1502 }
1503
1504 if ($values['date_format']) {
1505 $attributes['format'] = $values['date_format'];
1506 }
1507
1508 if (!empty($values['time_format'])) {
1509 $attributes['timeFormat'] = $values['time_format'];
1510 }
1511 $attributes['startOffset'] = $values['start'];
1512 $attributes['endOffset'] = $values['end'];
1513 }
1514
1515 $config = CRM_Core_Config::singleton();
1516 if (empty($attributes['format'])) {
1517 $attributes['format'] = $config->dateInputFormat;
1518 }
1519
1520 if (!isset($attributes['startOffset'])) {
1521 $attributes['startOffset'] = 10;
1522 }
1523
1524 if (!isset($attributes['endOffset'])) {
1525 $attributes['endOffset'] = 10;
1526 }
1527
1528 $this->add('text', $name, $label, $attributes);
1529
1530 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1531
1532 if (!isset($attributes['timeFormat'])) {
1533 $timeFormat = $config->timeInputFormat;
1534 }
1535 else {
1536 $timeFormat = $attributes['timeFormat'];
1537 }
1538
1539 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1540 if ($timeFormat) {
1541 $show24Hours = TRUE;
1542 if ($timeFormat == 1) {
1543 $show24Hours = FALSE;
1544 }
1545
1546 //CRM-6664 -we are having time element name
1547 //in either flat string or an array format.
1548 $elementName = $name . '_time';
1549 if (substr($name, -1) == ']') {
1550 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1551 }
1552
1553 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1554 }
1555 }
1556
1557 if ($required) {
1558 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1559 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1560 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1561 }
1562 }
1563 }
1564
1565 /**
1566 * Function that will add date and time.
1567 */
1568 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1569 $addTime = array('addTime' => TRUE);
1570 if (is_array($attributes)) {
1571 $attributes = array_merge($attributes, $addTime);
1572 }
1573 else {
1574 $attributes = $addTime;
1575 }
1576
1577 $this->addDate($name, $label, $required, $attributes);
1578 }
1579
1580 /**
1581 * Add a currency and money element to the form.
1582 */
1583 public function addMoney(
1584 $name,
1585 $label,
1586 $required = FALSE,
1587 $attributes = NULL,
1588 $addCurrency = TRUE,
1589 $currencyName = 'currency',
1590 $defaultCurrency = NULL,
1591 $freezeCurrency = FALSE
1592 ) {
1593 $element = $this->add('text', $name, $label, $attributes, $required);
1594 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1595
1596 if ($addCurrency) {
1597 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1598 }
1599
1600 return $element;
1601 }
1602
1603 /**
1604 * Add currency element to the form.
1605 */
1606 public function addCurrency(
1607 $name = 'currency',
1608 $label = NULL,
1609 $required = TRUE,
1610 $defaultCurrency = NULL,
1611 $freezeCurrency = FALSE
1612 ) {
1613 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1614 $options = array('class' => 'crm-select2 eight');
1615 if (!$required) {
1616 $currencies = array('' => '') + $currencies;
1617 $options['placeholder'] = ts('- none -');
1618 }
1619 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1620 if ($freezeCurrency) {
1621 $ele->freeze();
1622 }
1623 if (!$defaultCurrency) {
1624 $config = CRM_Core_Config::singleton();
1625 $defaultCurrency = $config->defaultCurrency;
1626 }
1627 $this->setDefaults(array($name => $defaultCurrency));
1628 }
1629
1630 /**
1631 * Create a single or multiple entity ref field.
1632 * @param string $name
1633 * @param string $label
1634 * @param array $props
1635 * Mix of html and widget properties, including:.
1636 * - select - params to give to select2 widget
1637 * - entity - defaults to contact
1638 * - create - can the user create a new entity on-the-fly?
1639 * Set to TRUE if entity is contact and you want the default profiles,
1640 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1641 * note that permissions are checked automatically
1642 * - api - array of settings for the getlist api wrapper
1643 * note that it accepts a 'params' setting which will be passed to the underlying api
1644 * - placeholder - string
1645 * - multiple - bool
1646 * - class, etc. - other html properties
1647 * @param bool $required
1648 *
1649 * @return HTML_QuickForm_Element
1650 */
1651 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1652 require_once "api/api.php";
1653 $config = CRM_Core_Config::singleton();
1654 // Default properties
1655 $props['api'] = CRM_Utils_Array::value('api', $props, array());
1656 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1657 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1658
1659 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission::check('edit all contacts') || CRM_Core_Permission::check('add contacts'))) {
1660 unset($props['create']);
1661 }
1662
1663 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1664
1665 $defaults = array();
1666 if (!empty($props['multiple'])) {
1667 $defaults['multiple'] = TRUE;
1668 }
1669 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
1670
1671 $this->formatReferenceFieldAttributes($props);
1672 return $this->add('text', $name, $label, $props, $required);
1673 }
1674
1675 /**
1676 * @param $props
1677 */
1678 private function formatReferenceFieldAttributes(&$props) {
1679 $props['data-select-params'] = json_encode($props['select']);
1680 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1681 $props['data-api-entity'] = $props['entity'];
1682 if (!empty($props['create'])) {
1683 $props['data-create-links'] = json_encode($props['create']);
1684 }
1685 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1686 }
1687
1688 /**
1689 * Convert all date fields within the params to mysql date ready for the
1690 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1691 * and if time is defined it is incorporated
1692 *
1693 * @param array $params
1694 * Input params from the form.
1695 *
1696 * @todo it would probably be better to work on $this->_params than a passed array
1697 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1698 * handling from BAO
1699 */
1700 public function convertDateFieldsToMySQL(&$params) {
1701 foreach ($this->_dateFields as $fieldName => $specs) {
1702 if (!empty($params[$fieldName])) {
1703 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1704 CRM_Utils_Date::processDate(
1705 $params[$fieldName],
1706 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1707 );
1708 }
1709 else {
1710 if (isset($specs['default'])) {
1711 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1712 }
1713 }
1714 }
1715 }
1716
1717 /**
1718 * @param $elementName
1719 */
1720 public function removeFileRequiredRules($elementName) {
1721 $this->_required = array_diff($this->_required, array($elementName));
1722 if (isset($this->_rules[$elementName])) {
1723 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1724 if ($ruleInfo['type'] == 'uploadedfile') {
1725 unset($this->_rules[$elementName][$index]);
1726 }
1727 }
1728 if (empty($this->_rules[$elementName])) {
1729 unset($this->_rules[$elementName]);
1730 }
1731 }
1732 }
1733
1734 /**
1735 * Function that can be defined in Form to override or.
1736 * perform specific action on cancel action
1737 */
1738 public function cancelAction() {
1739 }
1740
1741 /**
1742 * Helper function to verify that required fields have been filled.
1743 * Typically called within the scope of a FormRule function
1744 */
1745 public static function validateMandatoryFields($fields, $values, &$errors) {
1746 foreach ($fields as $name => $fld) {
1747 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1748 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1749 }
1750 }
1751 }
1752
1753 /**
1754 * Get contact if for a form object. Prioritise
1755 * - cid in URL if 0 (on behalf on someoneelse)
1756 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1757 * - logged in user id if it matches the one in the cid in the URL
1758 * - contact id validated from a checksum from a checksum
1759 * - cid from the url if the caller has ACL permission to view
1760 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1761 *
1762 * @return NULL|int
1763 */
1764 public function getContactID() {
1765 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1766 if (isset($this->_params) && isset($this->_params['select_contact_id'])) {
1767 $tempID = $this->_params['select_contact_id'];
1768 }
1769 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1770 // event form stores as an indexed array, contribution form not so much...
1771 $tempID = $this->_params[0]['select_contact_id'];
1772 }
1773
1774 // force to ignore the authenticated user
1775 if ($tempID === '0' || $tempID === 0) {
1776 // we set the cid on the form so that this will be retained for the Confirm page
1777 // in the multi-page form & prevent us returning the $userID when this is called
1778 // from that page
1779 // we don't really need to set it when $tempID is set because the params have that stored
1780 $this->set('cid', 0);
1781 return $tempID;
1782 }
1783
1784 $userID = $this->getLoggedInUserContactID();
1785
1786 if ($tempID == $userID) {
1787 return $userID;
1788 }
1789
1790 //check if this is a checksum authentication
1791 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1792 if ($userChecksum) {
1793 //check for anonymous user.
1794 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1795 if ($validUser) {
1796 return $tempID;
1797 }
1798 }
1799 // check if user has permission, CRM-12062
1800 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1801 return $tempID;
1802 }
1803
1804 return $userID;
1805 }
1806
1807 /**
1808 * Get the contact id of the logged in user.
1809 */
1810 public function getLoggedInUserContactID() {
1811 // check if the user is logged in and has a contact ID
1812 $session = CRM_Core_Session::singleton();
1813 return $session->get('userID');
1814 }
1815
1816 /**
1817 * Add autoselector field -if user has permission to view contacts
1818 * If adding this to a form you also need to add to the tpl e.g
1819 *
1820 * {if !empty($selectable)}
1821 * <div class="crm-summary-row">
1822 * <div class="crm-label">{$form.select_contact.label}</div>
1823 * <div class="crm-content">
1824 * {$form.select_contact.html}
1825 * </div>
1826 * </div>
1827 * {/if}
1828 *
1829 * @param array $profiles
1830 * Ids of profiles that are on the form (to be autofilled).
1831 * @param array $autoCompleteField
1832 *
1833 * - name_field
1834 * - id_field
1835 * - url (for ajax lookup)
1836 *
1837 * @todo add data attributes so we can deal with multiple instances on a form
1838 */
1839 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1840 $autoCompleteField = array_merge(array(
1841 'id_field' => 'select_contact_id',
1842 'placeholder' => ts('Select someone else ...'),
1843 'show_hide' => TRUE,
1844 'api' => array('params' => array('contact_type' => 'Individual')),
1845 ), $autoCompleteField);
1846
1847 if ($this->canUseAjaxContactLookups()) {
1848 $this->assign('selectable', $autoCompleteField['id_field']);
1849 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
1850 'placeholder' => $autoCompleteField['placeholder'],
1851 'api' => $autoCompleteField['api'],
1852 ));
1853
1854 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
1855 ->addSetting(array(
1856 'form' => array('autocompletes' => $autoCompleteField),
1857 'ids' => array('profile' => $profiles),
1858 ));
1859 }
1860 }
1861
1862 /**
1863 */
1864 public function canUseAjaxContactLookups() {
1865 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
1866 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
1867 ) {
1868 return TRUE;
1869 }
1870 }
1871
1872 /**
1873 * Add the options appropriate to cid = zero - ie. autocomplete
1874 *
1875 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1876 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1877 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1878 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1879 *
1880 * @param $onlinePaymentProcessorEnabled
1881 */
1882 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1883 $this->assign('nocid', TRUE);
1884 $profiles = array();
1885 if ($this->_values['custom_pre_id']) {
1886 $profiles[] = $this->_values['custom_pre_id'];
1887 }
1888 if ($this->_values['custom_post_id']) {
1889 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
1890 }
1891 if ($onlinePaymentProcessorEnabled) {
1892 $profiles[] = 'billing';
1893 }
1894 if (!empty($this->_values)) {
1895 $this->addAutoSelector($profiles);
1896 }
1897 }
1898
1899 /**
1900 * Set default values on form for given contact (or no contact defaults)
1901 *
1902 * @param mixed $profile_id
1903 * (can be id, or profile name).
1904 * @param int $contactID
1905 *
1906 * @return array
1907 */
1908 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
1909 try {
1910 $defaults = civicrm_api3('profile', 'getsingle', array(
1911 'profile_id' => (array) $profile_id,
1912 'contact_id' => $contactID,
1913 ));
1914 return $defaults;
1915 }
1916 catch (Exception $e) {
1917 // the try catch block gives us silent failure -not 100% sure this is a good idea
1918 // as silent failures are often worse than noisy ones
1919 return array();
1920 }
1921 }
1922
1923 /**
1924 * Sets form attribute.
1925 * @see CRM.loadForm
1926 */
1927 public function preventAjaxSubmit() {
1928 $this->setAttribute('data-no-ajax-submit', 'true');
1929 }
1930
1931 /**
1932 * Sets form attribute.
1933 * @see CRM.loadForm
1934 */
1935 public function allowAjaxSubmit() {
1936 $this->removeAttribute('data-no-ajax-submit');
1937 }
1938
1939 /**
1940 * Sets page title based on entity and action.
1941 * @param string $entityLabel
1942 */
1943 public function setPageTitle($entityLabel) {
1944 switch ($this->_action) {
1945 case CRM_Core_Action::ADD:
1946 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
1947 break;
1948
1949 case CRM_Core_Action::UPDATE:
1950 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
1951 break;
1952
1953 case CRM_Core_Action::VIEW:
1954 case CRM_Core_Action::PREVIEW:
1955 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
1956 break;
1957
1958 case CRM_Core_Action::DELETE:
1959 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
1960 break;
1961 }
1962 }
1963
1964 /**
1965 * Create a chain-select target field. All settings are optional; the defaults usually work.
1966 *
1967 * @param string $elementName
1968 * @param array $settings
1969 *
1970 * @return HTML_QuickForm_Element
1971 */
1972 public function addChainSelect($elementName, $settings = array()) {
1973 $props = $settings += array(
1974 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
1975 'country',
1976 'Country',
1977 'state_province',
1978 'StateProvince',
1979 ), $elementName),
1980 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
1981 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
1982 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
1983 'data-none-prompt' => ts('- N/A -'),
1984 'multiple' => FALSE,
1985 'required' => FALSE,
1986 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
1987 );
1988 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field');
1989 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
1990 $props['data-select-prompt'] = $props['placeholder'];
1991 $props['data-name'] = $elementName;
1992
1993 $this->_chainSelectFields[$settings['control_field']] = $elementName;
1994
1995 // Passing NULL instead of an array of options
1996 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
1997 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
1998 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
1999 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2000 }
2001
2002 /**
2003 * Set options and attributes for chain select fields based on the controlling field's value
2004 */
2005 private function preProcessChainSelectFields() {
2006 foreach ($this->_chainSelectFields as $control => $target) {
2007 // The 'target' might get missing if extensions do removeElement() in a form hook.
2008 if ($this->elementExists($target)) {
2009 $targetField = $this->getElement($target);
2010 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2011 $options = array();
2012 // If the control field is on the form, setup chain-select and dynamically populate options
2013 if ($this->elementExists($control)) {
2014 $controlField = $this->getElement($control);
2015 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2016
2017 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2018
2019 $css = (string) $controlField->getAttribute('class');
2020 $controlField->updateAttributes(array(
2021 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2022 'data-target' => $target,
2023 ));
2024 $controlValue = $controlField->getValue();
2025 if ($controlValue) {
2026 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2027 if (!$options) {
2028 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2029 }
2030 }
2031 else {
2032 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2033 $targetField->setAttribute('disabled', 'disabled');
2034 }
2035 }
2036 // Control field not present - fall back to loading default options
2037 else {
2038 $options = CRM_Core_PseudoConstant::$targetType();
2039 }
2040 if (!$targetField->getAttribute('multiple')) {
2041 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2042 $targetField->removeAttribute('placeholder');
2043 }
2044 $targetField->_options = array();
2045 $targetField->loadArray($options);
2046 }
2047 }
2048 }
2049
2050 /**
2051 * Validate country / state / county match and suppress unwanted "required" errors
2052 */
2053 private function validateChainSelectFields() {
2054 foreach ($this->_chainSelectFields as $control => $target) {
2055 if ($this->elementExists($control) && $this->elementExists($target)) {
2056 $controlValue = (array) $this->getElementValue($control);
2057 $targetField = $this->getElement($target);
2058 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2059 $targetValue = array_filter((array) $targetField->getValue());
2060 if ($targetValue || $this->getElementError($target)) {
2061 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2062 if ($targetValue) {
2063 if (!array_intersect($targetValue, array_keys($options))) {
2064 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2065 }
2066 } // Suppress "required" error for field if it has no options
2067 elseif (!$options) {
2068 $this->setElementError($target, NULL);
2069 }
2070 }
2071 }
2072 }
2073 }
2074
2075 }