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