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