Add select2 to input field of datepicker
[civicrm-core.git] / CRM / Core / Form.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
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
6b83d5bd 34 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
35 */
36
37require_once 'HTML/QuickForm/Page.php';
28518c90
EM
38
39/**
40 * Class CRM_Core_Form
41 */
6a488035
TO
42class CRM_Core_Form extends HTML_QuickForm_Page {
43
44 /**
45 * The state object that this form belongs to
46 * @var object
47 */
48 protected $_state;
49
50 /**
51 * The name of this form
52 * @var string
53 */
54 protected $_name;
55
56 /**
57 * The title of this form
58 * @var string
59 */
60 protected $_title = NULL;
61
73afe1e6 62 /**
63 * The default values for the form.
64 *
65 * @var array
66 */
be2fb01f 67 public $_defaults = [];
73afe1e6 68
6a488035 69 /**
2eee184e
TO
70 * (QUASI-PROTECTED) The options passed into this form
71 *
72 * This field should marked `protected` and is not generally
73 * intended for external callers, but some edge-cases do use it.
74 *
6a488035
TO
75 * @var mixed
76 */
2eee184e 77 public $_options = NULL;
6a488035
TO
78
79 /**
2eee184e
TO
80 * (QUASI-PROTECTED) The mode of operation for this form
81 *
82 * This field should marked `protected` and is not generally
83 * intended for external callers, but some edge-cases do use it.
84 *
6a488035
TO
85 * @var int
86 */
2eee184e 87 public $_action;
6a488035 88
1b9f9ca3
EM
89 /**
90 * Available payment processors.
91 *
92 * As part of trying to consolidate various payment pages we store processors here & have functions
93 * at this level to manage them.
94 *
95 * @var array
96 * An array of payment processor details with objects loaded in the 'object' field.
97 */
42e3a033 98 protected $_paymentProcessors;
1b9f9ca3
EM
99
100 /**
101 * Available payment processors (IDS).
102 *
103 * As part of trying to consolidate various payment pages we store processors here & have functions
cbcb5b49 104 * at this level to manage them. An alternative would be to have a separate Form that is inherited
105 * by all forms that allow payment processing.
1b9f9ca3
EM
106 *
107 * @var array
108 * An array of the IDS available on this form.
109 */
110 public $_paymentProcessorIDs;
111
cbcb5b49 112 /**
113 * Default or selected processor id.
114 *
115 * As part of trying to consolidate various payment pages we store processors here & have functions
116 * at this level to manage them. An alternative would be to have a separate Form that is inherited
117 * by all forms that allow payment processing.
118 *
119 * @var int
120 */
121 protected $_paymentProcessorID;
122
123 /**
124 * Is pay later enabled for the form.
125 *
126 * As part of trying to consolidate various payment pages we store processors here & have functions
127 * at this level to manage them. An alternative would be to have a separate Form that is inherited
128 * by all forms that allow payment processing.
129 *
130 * @var int
131 */
132 protected $_is_pay_later_enabled;
133
6a488035 134 /**
100fef9d 135 * The renderer used for this form
6a488035
TO
136 *
137 * @var object
138 */
139 protected $_renderer;
140
5d86176b 141 /**
142 * An array to hold a list of datefields on the form
143 * so that they can be converted to ISO in a consistent manner
144 *
145 * @var array
146 *
147 * e.g on a form declare $_dateFields = array(
148 * 'receive_date' => array('default' => 'now'),
149 * );
150 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
151 * to have the time field re-incorporated into the field & 'now' set if
152 * no value has been passed in
153 */
be2fb01f 154 protected $_dateFields = [];
5d86176b 155
6a488035 156 /**
100fef9d 157 * Cache the smarty template for efficiency reasons
6a488035
TO
158 *
159 * @var CRM_Core_Smarty
160 */
161 static protected $_template;
162
461fa5fb 163 /**
164 * Indicate if this form should warn users of unsaved changes
518fa0ee 165 * @var bool
461fa5fb 166 */
167 protected $unsavedChangesWarn;
168
03a7ec8f 169 /**
fc05b8da 170 * What to return to the client if in ajax mode (snippet=json)
03a7ec8f
CW
171 *
172 * @var array
173 */
be2fb01f 174 public $ajaxResponse = [];
03a7ec8f 175
118e964e
CW
176 /**
177 * Url path used to reach this page
178 *
179 * @var array
180 */
be2fb01f 181 public $urlPath = [];
118e964e 182
2d69ef96 183 /**
184 * Context of the form being loaded.
185 *
186 * 'event' or null
187 *
188 * @var string
189 */
190 protected $context;
191
192 /**
193 * @return string
194 */
195 public function getContext() {
196 return $this->context;
197 }
198
199 /**
200 * Set context variable.
201 */
202 public function setContext() {
203 $this->context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this);
204 }
205
d77a0a58
EM
206 /**
207 * @var CRM_Core_Controller
208 */
209 public $controller;
4a44fd8a 210
6a488035 211 /**
100fef9d 212 * Constants for attributes for various form elements
6a488035
TO
213 * attempt to standardize on the number of variations that we
214 * use of the below form elements
215 *
216 * @var const string
217 */
7da04cde 218 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
6a488035
TO
219
220 /**
221 * All checkboxes are defined with a common prefix. This allows us to
222 * have the same javascript to check / clear all the checkboxes etc
223 * If u have multiple groups of checkboxes, you will need to give them different
224 * ids to avoid potential name collision
225 *
fffe9ee1 226 * @var string|int
6a488035 227 */
7da04cde 228 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
6a488035 229
1d07e7ab 230 /**
1d07e7ab 231 * @var array
518fa0ee 232 * @internal to keep track of chain-select fields
1d07e7ab 233 */
be2fb01f 234 private $_chainSelectFields = [];
1d07e7ab 235
d8f1758d
CW
236 /**
237 * Extra input types we support via the "add" method
238 * @var array
239 */
be2fb01f 240 public static $html5Types = [
d8f1758d
CW
241 'number',
242 'url',
243 'email',
1192bd09 244 'color',
be2fb01f 245 ];
d8f1758d 246
6a488035 247 /**
4b62bc4f 248 * Constructor for the basic form page.
6a488035
TO
249 *
250 * We should not use QuickForm directly. This class provides a lot
251 * of default convenient functions, rules and buttons
252 *
6a0b768e
TO
253 * @param object $state
254 * State associated with this form.
fffe9ee1 255 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
6a0b768e
TO
256 * @param string $method
257 * The type of http method used (GET/POST).
258 * @param string $name
259 * The name of the form if different from class name.
6a488035 260 *
dd244018 261 * @return \CRM_Core_Form
6a488035 262 */
2da40d21 263 public function __construct(
6a488035
TO
264 $state = NULL,
265 $action = CRM_Core_Action::NONE,
266 $method = 'post',
f9f40af3 267 $name = NULL
6a488035
TO
268 ) {
269
270 if ($name) {
271 $this->_name = $name;
272 }
273 else {
b50fdacc 274 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
6a488035
TO
275 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
276 }
277
6ef04c72 278 parent::__construct($this->_name, $method);
6a488035
TO
279
280 $this->_state =& $state;
281 if ($this->_state) {
282 $this->_state->setName($this->_name);
283 }
284 $this->_action = (int) $action;
285
286 $this->registerRules();
287
288 // let the constructor initialize this, should happen only once
289 if (!isset(self::$_template)) {
290 self::$_template = CRM_Core_Smarty::singleton();
291 }
b50fdacc
CW
292 // Workaround for CRM-15153 - give each form a reasonably unique css class
293 $this->addClass(CRM_Utils_System::getClassName($this));
03a7ec8f 294
819d4cbb 295 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
d84ae5f6 296 $this->setTranslatedFields();
6a488035
TO
297 }
298
d84ae5f6 299 /**
300 * Set translated fields.
301 *
302 * This function is called from the class constructor, allowing us to set
303 * fields on the class that can't be set as properties due to need for
304 * translation or other non-input specific handling.
305 */
306 protected function setTranslatedFields() {}
307
023e90c3 308 /**
e51d62d3 309 * Add one or more css classes to the form.
310 *
100fef9d 311 * @param string $className
023e90c3
CW
312 */
313 public function addClass($className) {
314 $classes = $this->getAttribute('class');
315 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
316 }
317
6a488035 318 /**
fe482240 319 * Register all the standard rules that most forms potentially use.
6a488035 320 */
00be9182 321 public function registerRules() {
be2fb01f 322 static $rules = [
353ffa53
TO
323 'title',
324 'longTitle',
325 'variable',
326 'qfVariable',
327 'phone',
328 'integer',
329 'query',
330 'url',
331 'wikiURL',
332 'domain',
333 'numberOfDigit',
334 'date',
335 'currentDate',
336 'asciiFile',
337 'htmlFile',
338 'utf8File',
339 'objectExists',
340 'optionExists',
341 'postalCode',
342 'money',
343 'positiveInteger',
344 'xssString',
345 'fileExists',
d9d7e7dd 346 'settingPath',
353ffa53
TO
347 'autocomplete',
348 'validContact',
be2fb01f 349 ];
6a488035
TO
350
351 foreach ($rules as $rule) {
352 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
353 }
354 }
355
356 /**
e51d62d3 357 * Simple easy to use wrapper around addElement.
358 *
359 * Deal with simple validation rules.
6a488035 360 *
bae056c6
CW
361 * @param string $type
362 * @param string $name
77b97be7 363 * @param string $label
bae056c6 364 * @param string|array $attributes (options for select elements)
77b97be7 365 * @param bool $required
6a0b768e
TO
366 * @param array $extra
367 * (attributes for select elements).
d86e674f 368 * For datepicker elements this is consistent with the data
369 * from CRM_Utils_Date::getDatePickerExtra
6a488035 370 *
e51d62d3 371 * @return HTML_QuickForm_Element
372 * Could be an error object
6a488035 373 */
2da40d21 374 public function &add(
f9f40af3 375 $type, $name, $label = '',
908fe4e6 376 $attributes = '', $required = FALSE, $extra = NULL
6a488035 377 ) {
092cb9c5 378 // Fudge some extra types that quickform doesn't support
1192bd09 379 $inputType = $type;
d8f1758d 380 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
be2fb01f 381 $attributes = ($attributes ? $attributes : []) + ['class' => ''];
092cb9c5 382 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
7ad5ae6a
CW
383 if ($type == 'wysiwyg' && isset($attributes['preset'])) {
384 $attributes['data-preset'] = $attributes['preset'];
385 unset($attributes['preset']);
386 }
092cb9c5 387 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
b608cfb1 388 }
b733747a
CW
389 // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list.
390 if ($inputType == 'select2') {
391 $type = 'text';
392 $options = $attributes;
11b9f280 393 $attributes = ($extra ? $extra : []) + ['class' => ''];
b733747a 394 $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2");
be2fb01f 395 $attributes['data-select-params'] = json_encode(['data' => $options, 'multiple' => !empty($attributes['multiple'])]);
b733747a
CW
396 unset($attributes['multiple']);
397 $extra = NULL;
398 }
a2973721 399 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
238fee7f 400 if ($type == 'datepicker') {
be2fb01f 401 $attributes = ($attributes ? $attributes : []);
238fee7f 402 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
1f1410dc 403 if (!empty($attributes['aria-label']) || $label) {
404 $attributes['aria-label'] = CRM_Utils_Array::value('aria-label', $attributes, $label);
405 }
238fee7f
CW
406 $type = "text";
407 }
1d07e7ab 408 if ($type == 'select' && is_array($extra)) {
bae056c6 409 // Normalize this property
1d07e7ab
CW
410 if (!empty($extra['multiple'])) {
411 $extra['multiple'] = 'multiple';
412 }
413 else {
414 unset($extra['multiple']);
415 }
65e8615b 416 unset($extra['size'], $extra['maxlength']);
bae056c6
CW
417 // Add placeholder option for select
418 if (isset($extra['placeholder'])) {
419 if ($extra['placeholder'] === TRUE) {
420 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
421 }
422 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
be2fb01f 423 $attributes = ['' => $extra['placeholder']] + $attributes;
bae056c6
CW
424 }
425 }
908fe4e6
CW
426 }
427 $element = $this->addElement($type, $name, $label, $attributes, $extra);
6a488035
TO
428 if (HTML_QuickForm::isError($element)) {
429 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
430 }
431
1192bd09 432 if ($inputType == 'color') {
be2fb01f 433 $this->addRule($name, ts('%1 must contain a color value e.g. #ffffff.', [1 => $label]), 'regex', '/#[0-9a-fA-F]{6}/');
1192bd09
CW
434 }
435
6a488035
TO
436 if ($required) {
437 if ($type == 'file') {
be2fb01f 438 $error = $this->addRule($name, ts('%1 is a required field.', [1 => $label]), 'uploadedfile');
6a488035
TO
439 }
440 else {
be2fb01f 441 $error = $this->addRule($name, ts('%1 is a required field.', [1 => $label]), 'required');
6a488035
TO
442 }
443 if (HTML_QuickForm::isError($error)) {
444 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
445 }
446 }
447
b3ee84c9
MD
448 // Add context for the editing of option groups
449 if (isset($extra['option_context'])) {
450 $context = json_encode($extra['option_context']);
451 $element->setAttribute('data-option-edit-context', $context);
452 }
453
6a488035
TO
454 return $element;
455 }
456
457 /**
e51d62d3 458 * Preprocess form.
459 *
460 * This is called before buildForm. Any pre-processing that
461 * needs to be done for buildForm should be done here.
6a488035 462 *
8eedd10a 463 * This is a virtual function and should be redefined if needed.
6a488035 464 */
f9f40af3
TO
465 public function preProcess() {
466 }
6a488035
TO
467
468 /**
8eedd10a 469 * Called after the form is validated.
470 *
471 * Any processing of form state etc should be done in this function.
6a488035
TO
472 * Typically all processing associated with a form should be done
473 * here and relevant state should be stored in the session
474 *
475 * This is a virtual function and should be redefined if needed
6a488035 476 */
f9f40af3
TO
477 public function postProcess() {
478 }
6a488035
TO
479
480 /**
e51d62d3 481 * Main process wrapper.
482 *
483 * Implemented so that we can call all the hook functions.
484 *
6a0b768e
TO
485 * @param bool $allowAjax
486 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
7e9fdecf 487 * and bury it deeper down in the controller
6a488035 488 */
00be9182 489 public function mainProcess($allowAjax = TRUE) {
6a488035 490 $this->postProcess();
6a488035 491 $this->postProcessHook();
03a7ec8f 492
fc05b8da 493 // Respond with JSON if in AJAX context (also support legacy value '6')
be2fb01f 494 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], [
518fa0ee
SL
495 CRM_Core_Smarty::PRINT_JSON,
496 6,
497 ])) {
03a7ec8f
CW
498 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
499 $this->ajaxResponse['action'] = $this->_action;
18ddc127
CW
500 if (isset($this->_id) || isset($this->id)) {
501 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
502 }
03a7ec8f
CW
503 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
504 }
6a488035
TO
505 }
506
507 /**
4b62bc4f 508 * The postProcess hook is typically called by the framework.
e51d62d3 509 *
6a488035
TO
510 * However in a few cases, the form exits or redirects early in which
511 * case it needs to call this function so other modules can do the needful
512 * Calling this function directly should be avoided if possible. In general a
513 * better way is to do setUserContext so the framework does the redirect
6a488035 514 */
00be9182 515 public function postProcessHook() {
6a488035
TO
516 CRM_Utils_Hook::postProcess(get_class($this), $this);
517 }
518
519 /**
8eedd10a 520 * This virtual function is used to build the form.
6a488035 521 *
8eedd10a 522 * It replaces the buildForm associated with QuickForm_Page. This allows us to put
523 * preProcess in front of the actual form building routine
6a488035 524 */
f9f40af3
TO
525 public function buildQuickForm() {
526 }
6a488035
TO
527
528 /**
8eedd10a 529 * This virtual function is used to set the default values of various form elements.
6a488035 530 *
a1a2a83d 531 * @return array|NULL
a6c01b45 532 * reference to the array of default values
6a488035 533 */
f9f40af3 534 public function setDefaultValues() {
a1a2a83d 535 return NULL;
f9f40af3 536 }
6a488035
TO
537
538 /**
8eedd10a 539 * This is a virtual function that adds group and global rules to the form.
6a488035 540 *
8eedd10a 541 * Keeping it distinct from the form to keep code small
542 * and localized in the form building code
6a488035 543 */
f9f40af3
TO
544 public function addRules() {
545 }
6a488035 546
b5c2afd0 547 /**
fe482240 548 * Performs the server side validation.
b5c2afd0 549 * @since 1.0
5c766a0b 550 * @return bool
a6c01b45 551 * true if no error found
b5c2afd0
EM
552 * @throws HTML_QuickForm_Error
553 */
00be9182 554 public function validate() {
6a488035
TO
555 $error = parent::validate();
556
bc999cd1
CW
557 $this->validateChainSelectFields();
558
be2fb01f 559 $hookErrors = [];
6a488035
TO
560
561 CRM_Utils_Hook::validateForm(
562 get_class($this),
563 $this->_submitValues,
564 $this->_submitFiles,
565 $this,
566 $hookErrors
567 );
568
569 if (!empty($hookErrors)) {
570 $this->_errors += $hookErrors;
571 }
572
573 return (0 == count($this->_errors));
574 }
575
576 /**
3bdf1f3a 577 * Core function that builds the form.
578 *
579 * We redefine this function here and expect all CRM forms to build their form in the function
6a488035 580 * buildQuickForm.
6a488035 581 */
00be9182 582 public function buildForm() {
6a488035
TO
583 $this->_formBuilt = TRUE;
584
585 $this->preProcess();
586
21d2903d
AN
587 CRM_Utils_Hook::preProcess(get_class($this), $this);
588
6a488035
TO
589 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
590
591 if (
592 $this->controller->_key &&
593 $this->controller->_generateQFKey
594 ) {
595 $this->addElement('hidden', 'qfKey', $this->controller->_key);
596 $this->assign('qfKey', $this->controller->_key);
ab435bd4 597
6a488035
TO
598 }
599
ab435bd4
DL
600 // _generateQFKey suppresses the qfKey generation on form snippets that
601 // are part of other forms, hence we use that to avoid adding entryURL
602 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
3ab88a8c
DL
603 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
604 }
6a488035
TO
605
606 $this->buildQuickForm();
607
608 $defaults = $this->setDefaultValues();
609 unset($defaults['qfKey']);
610
611 if (!empty($defaults)) {
612 $this->setDefaults($defaults);
613 }
614
615 // call the form hook
b44e3f84 616 // also call the hook function so any modules can set their own custom defaults
6a488035
TO
617 // the user can do both the form and set default values with this hook
618 CRM_Utils_Hook::buildForm(get_class($this), $this);
619
620 $this->addRules();
3e201321 621
622 //Set html data-attribute to enable warning user of unsaved changes
f9f40af3 623 if ($this->unsavedChangesWarn === TRUE
353ffa53
TO
624 || (!isset($this->unsavedChangesWarn)
625 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
626 )
627 ) {
f9f40af3 628 $this->setAttribute('data-warn-changes', 'true');
3e201321 629 }
6a488035
TO
630 }
631
632 /**
3bdf1f3a 633 * Add default Next / Back buttons.
6a488035 634 *
6c552737
TO
635 * @param array $params
636 * Array of associative arrays in the order in which the buttons should be
637 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
638 * The base form class will define a bunch of static arrays for commonly used
639 * formats.
6a488035 640 */
00be9182 641 public function addButtons($params) {
be2fb01f 642 $prevnext = $spacing = [];
6a488035 643 foreach ($params as $button) {
d7fccec7
MWMC
644 if (!empty($button['submitOnce'])) {
645 $button['js']['onclick'] = "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');";
646 }
647
be2fb01f 648 $attrs = ['class' => 'crm-form-submit'] + (array) CRM_Utils_Array::value('js', $button);
6a488035 649
b01812e5
CW
650 if (!empty($button['class'])) {
651 $attrs['class'] .= ' ' . $button['class'];
652 }
653
fdb0ca2c
CW
654 if (!empty($button['isDefault'])) {
655 $attrs['class'] .= ' default';
6a488035
TO
656 }
657
be2fb01f 658 if (in_array($button['type'], ['upload', 'next', 'submit', 'done', 'process', 'refresh'])) {
fdb0ca2c 659 $attrs['class'] .= ' validate';
f62db3ac 660 $defaultIcon = 'fa-check';
fdb0ca2c
CW
661 }
662 else {
b61fd8cf 663 $attrs['class'] .= ' cancel';
f62db3ac 664 $defaultIcon = $button['type'] == 'back' ? 'fa-chevron-left' : 'fa-times';
b61fd8cf
CW
665 }
666
6a488035
TO
667 if ($button['type'] === 'reset') {
668 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
669 }
670 else {
a7488080 671 if (!empty($button['subName'])) {
deae896d 672 if ($button['subName'] == 'new') {
f62db3ac 673 $defaultIcon = 'fa-plus-circle';
deae896d 674 }
fdb0ca2c 675 if ($button['subName'] == 'done') {
f62db3ac 676 $defaultIcon = 'fa-check-circle';
fdb0ca2c
CW
677 }
678 if ($button['subName'] == 'next') {
f62db3ac 679 $defaultIcon = 'fa-chevron-right';
fdb0ca2c 680 }
6a488035
TO
681 }
682
be2fb01f 683 if (in_array($button['type'], ['next', 'upload', 'done']) && $button['name'] === ts('Save')) {
fdb0ca2c 684 $attrs['accesskey'] = 'S';
6a488035 685 }
deae896d
CW
686 $icon = CRM_Utils_Array::value('icon', $button, $defaultIcon);
687 if ($icon) {
688 $attrs['crm-icon'] = $icon;
689 }
fdb0ca2c 690 $buttonName = $this->getButtonName($button['type'], CRM_Utils_Array::value('subName', $button));
6a488035
TO
691 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
692 }
a7488080 693 if (!empty($button['isDefault'])) {
6a488035
TO
694 $this->setDefaultAction($button['type']);
695 }
696
697 // if button type is upload, set the enctype
698 if ($button['type'] == 'upload') {
be2fb01f 699 $this->updateAttributes(['enctype' => 'multipart/form-data']);
6a488035
TO
700 $this->setMaxFileSize();
701 }
702
703 // hack - addGroup uses an array to express variable spacing, read from the last element
704 $spacing[] = CRM_Utils_Array::value('spacing', $button, self::ATTR_SPACING);
705 }
706 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
707 }
708
709 /**
fe482240 710 * Getter function for Name.
6a488035
TO
711 *
712 * @return string
6a488035 713 */
00be9182 714 public function getName() {
6a488035
TO
715 return $this->_name;
716 }
717
718 /**
fe482240 719 * Getter function for State.
6a488035
TO
720 *
721 * @return object
6a488035 722 */
00be9182 723 public function &getState() {
6a488035
TO
724 return $this->_state;
725 }
726
727 /**
fe482240 728 * Getter function for StateType.
6a488035
TO
729 *
730 * @return int
6a488035 731 */
00be9182 732 public function getStateType() {
6a488035
TO
733 return $this->_state->getType();
734 }
735
736 /**
3bdf1f3a 737 * Getter function for title.
738 *
739 * Should be over-ridden by derived class.
6a488035
TO
740 *
741 * @return string
6a488035 742 */
00be9182 743 public function getTitle() {
6a488035
TO
744 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
745 }
746
747 /**
100fef9d 748 * Setter function for title.
6a488035 749 *
6a0b768e
TO
750 * @param string $title
751 * The title of the form.
6a488035 752 */
00be9182 753 public function setTitle($title) {
6a488035 754 $this->_title = $title;
d7188a5d 755 CRM_Utils_System::setTitle($title);
6a488035
TO
756 }
757
8345c9d3
EM
758 /**
759 * Assign billing type id to bltID.
760 *
761 * @throws CRM_Core_Exception
762 */
763 public function assignBillingType() {
b576d770 764 $this->_bltID = CRM_Core_BAO_LocationType::getBilling();
8345c9d3
EM
765 $this->set('bltID', $this->_bltID);
766 $this->assign('bltID', $this->_bltID);
767 }
768
1b9f9ca3
EM
769 /**
770 * This if a front end form function for setting the payment processor.
771 *
772 * It would be good to sync it with the back-end function on abstractEditPayment & use one everywhere.
773 *
682c12c0 774 * @param bool $isPayLaterEnabled
cbcb5b49 775 *
1b9f9ca3
EM
776 * @throws \CRM_Core_Exception
777 */
682c12c0 778 protected function assignPaymentProcessor($isPayLaterEnabled) {
1b9f9ca3 779 $this->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors(
be2fb01f 780 [ucfirst($this->_mode) . 'Mode'],
1b9f9ca3
EM
781 $this->_paymentProcessorIDs
782 );
682c12c0
JP
783 if ($isPayLaterEnabled) {
784 $this->_paymentProcessors[0] = CRM_Financial_BAO_PaymentProcessor::getPayment(0);
785 }
1b9f9ca3
EM
786
787 if (!empty($this->_paymentProcessors)) {
788 foreach ($this->_paymentProcessors as $paymentProcessorID => $paymentProcessorDetail) {
789 if (empty($this->_paymentProcessor) && $paymentProcessorDetail['is_default'] == 1 || (count($this->_paymentProcessors) == 1)
790 ) {
791 $this->_paymentProcessor = $paymentProcessorDetail;
792 $this->assign('paymentProcessor', $this->_paymentProcessor);
793 // Setting this is a bit of a legacy overhang.
794 $this->_paymentObject = $paymentProcessorDetail['object'];
795 }
796 }
797 // It's not clear why we set this on the form.
798 $this->set('paymentProcessors', $this->_paymentProcessors);
799 }
800 else {
801 throw new CRM_Core_Exception(ts('A payment processor configured for this page might be disabled (contact the site administrator for assistance).'));
802 }
f48e6cf7 803
1b9f9ca3
EM
804 }
805
bddc8a28 806 /**
807 * Format the fields for the payment processor.
808 *
809 * In order to pass fields to the payment processor in a consistent way we add some renamed
810 * parameters.
811 *
812 * @param array $fields
813 *
814 * @return array
815 */
816 protected function formatParamsForPaymentProcessor($fields) {
817 // also add location name to the array
818 $this->_params["address_name-{$this->_bltID}"] = CRM_Utils_Array::value('billing_first_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_middle_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_last_name', $this->_params);
819 $this->_params["address_name-{$this->_bltID}"] = trim($this->_params["address_name-{$this->_bltID}"]);
820 // Add additional parameters that the payment processors are used to receiving.
821 if (!empty($this->_params["billing_state_province_id-{$this->_bltID}"])) {
822 $this->_params['state_province'] = $this->_params["state_province-{$this->_bltID}"] = $this->_params["billing_state_province-{$this->_bltID}"] = CRM_Core_PseudoConstant::stateProvinceAbbreviation($this->_params["billing_state_province_id-{$this->_bltID}"]);
823 }
824 if (!empty($this->_params["billing_country_id-{$this->_bltID}"])) {
825 $this->_params['country'] = $this->_params["country-{$this->_bltID}"] = $this->_params["billing_country-{$this->_bltID}"] = CRM_Core_PseudoConstant::countryIsoCode($this->_params["billing_country_id-{$this->_bltID}"]);
826 }
827
828 list($hasAddressField, $addressParams) = CRM_Contribute_BAO_Contribution::getPaymentProcessorReadyAddressParams($this->_params, $this->_bltID);
829 if ($hasAddressField) {
830 $this->_params = array_merge($this->_params, $addressParams);
831 }
832
be2fb01f 833 $nameFields = ['first_name', 'middle_name', 'last_name'];
bddc8a28 834 foreach ($nameFields as $name) {
835 $fields[$name] = 1;
836 if (array_key_exists("billing_$name", $this->_params)) {
837 $this->_params[$name] = $this->_params["billing_{$name}"];
838 $this->_params['preserveDBName'] = TRUE;
839 }
840 }
841 return $fields;
842 }
843
42e3a033
EM
844 /**
845 * Handle Payment Processor switching for contribution and event registration forms.
846 *
847 * This function is shared between contribution & event forms & this is their common class.
848 *
849 * However, this should be seen as an in-progress refactor, the end goal being to also align the
850 * backoffice forms that action payments.
851 *
852 * This function overlaps assignPaymentProcessor, in a bad way.
853 */
854 protected function preProcessPaymentOptions() {
855 $this->_paymentProcessorID = NULL;
856 if ($this->_paymentProcessors) {
857 if (!empty($this->_submitValues)) {
858 $this->_paymentProcessorID = CRM_Utils_Array::value('payment_processor_id', $this->_submitValues);
859 $this->_paymentProcessor = CRM_Utils_Array::value($this->_paymentProcessorID, $this->_paymentProcessors);
860 $this->set('type', $this->_paymentProcessorID);
861 $this->set('mode', $this->_mode);
862 $this->set('paymentProcessor', $this->_paymentProcessor);
863 }
864 // Set default payment processor
865 else {
866 foreach ($this->_paymentProcessors as $values) {
867 if (!empty($values['is_default']) || count($this->_paymentProcessors) == 1) {
868 $this->_paymentProcessorID = $values['id'];
869 break;
870 }
871 }
872 }
1d1fee72 873 if ($this->_paymentProcessorID
874 || (isset($this->_submitValues['payment_processor_id']) && $this->_submitValues['payment_processor_id'] == 0)
875 ) {
42e3a033
EM
876 CRM_Core_Payment_ProcessorForm::preProcess($this);
877 }
878 else {
be2fb01f 879 $this->_paymentProcessor = [];
42e3a033 880 }
cb5962bd 881 CRM_Financial_Form_Payment::addCreditCardJs($this->_paymentProcessorID);
42e3a033
EM
882 }
883 $this->assign('paymentProcessorID', $this->_paymentProcessorID);
f48e6cf7 884 // We save the fact that the profile 'billing' is required on the payment form.
885 // Currently pay-later is the only 'processor' that takes notice of this - but ideally
886 // 1) it would be possible to select the minimum_billing_profile_id for the contribution form
887 // 2) that profile_id would be set on the payment processor
888 // 3) the payment processor would return a billing form that combines these user-configured
889 // minimums with the payment processor minimums. This would lead to fields like 'postal_code'
890 // only being on the form if either the admin has configured it as wanted or the processor
891 // requires it.
892 $this->assign('billing_profile_id', (CRM_Utils_Array::value('is_billing_required', $this->_values) ? 'billing' : ''));
42e3a033 893 }
1b9f9ca3 894
ec022878 895 /**
896 * Handle pre approval for processors.
897 *
898 * This fits with the flow where a pre-approval is done and then confirmed in the next stage when confirm is hit.
899 *
900 * This function is shared between contribution & event forms & this is their common class.
901 *
902 * However, this should be seen as an in-progress refactor, the end goal being to also align the
903 * backoffice forms that action payments.
904 *
905 * @param array $params
906 */
907 protected function handlePreApproval(&$params) {
908 try {
909 $payment = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
910 $params['component'] = 'contribute';
911 $result = $payment->doPreApproval($params);
912 if (empty($result)) {
913 // This could happen, for example, when paypal looks at the button value & decides it is not paypal express.
914 return;
915 }
916 }
917 catch (\Civi\Payment\Exception\PaymentProcessorException $e) {
abfb35ee 918 CRM_Core_Error::statusBounce(ts('Payment approval failed with message :') . $e->getMessage(), $payment->getCancelUrl($params['qfKey'], CRM_Utils_Array::value('participant_id', $params)));
ec022878 919 }
920
921 $this->set('pre_approval_parameters', $result['pre_approval_parameters']);
922 if (!empty($result['redirect_url'])) {
923 CRM_Utils_System::redirect($result['redirect_url']);
924 }
925 }
926
6a488035 927 /**
fe482240 928 * Setter function for options.
6a488035 929 *
6c552737 930 * @param mixed $options
6a488035 931 */
00be9182 932 public function setOptions($options) {
6a488035
TO
933 $this->_options = $options;
934 }
935
6a488035 936 /**
fe482240 937 * Render form and return contents.
6a488035
TO
938 *
939 * @return string
6a488035 940 */
00be9182 941 public function toSmarty() {
1d07e7ab 942 $this->preProcessChainSelectFields();
6a488035
TO
943 $renderer = $this->getRenderer();
944 $this->accept($renderer);
945 $content = $renderer->toArray();
946 $content['formName'] = $this->getName();
b50fdacc
CW
947 // CRM-15153
948 $content['formClass'] = CRM_Utils_System::getClassName($this);
6a488035
TO
949 return $content;
950 }
951
952 /**
3bdf1f3a 953 * Getter function for renderer.
954 *
955 * If renderer is not set create one and initialize it.
6a488035
TO
956 *
957 * @return object
6a488035 958 */
00be9182 959 public function &getRenderer() {
6a488035
TO
960 if (!isset($this->_renderer)) {
961 $this->_renderer = CRM_Core_Form_Renderer::singleton();
962 }
963 return $this->_renderer;
964 }
965
966 /**
fe482240 967 * Use the form name to create the tpl file name.
6a488035
TO
968 *
969 * @return string
6a488035 970 */
00be9182 971 public function getTemplateFileName() {
6a488035
TO
972 $ext = CRM_Extension_System::singleton()->getMapper();
973 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
974 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
975 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
976 }
977 else {
9b591d79
TO
978 $tplname = strtr(
979 CRM_Utils_System::getClassName($this),
be2fb01f 980 [
9b591d79
TO
981 '_' => DIRECTORY_SEPARATOR,
982 '\\' => DIRECTORY_SEPARATOR,
be2fb01f 983 ]
9b591d79 984 ) . '.tpl';
6a488035
TO
985 }
986 return $tplname;
987 }
988
8aac22c8 989 /**
3bdf1f3a 990 * A wrapper for getTemplateFileName.
991 *
992 * This includes calling the hook to prevent us from having to copy & paste the logic of calling the hook.
8aac22c8 993 */
00be9182 994 public function getHookedTemplateFileName() {
8aac22c8 995 $pageTemplateFile = $this->getTemplateFileName();
996 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
997 return $pageTemplateFile;
998 }
999
6a488035 1000 /**
3bdf1f3a 1001 * Default extra tpl file basically just replaces .tpl with .extra.tpl.
1002 *
1003 * i.e. we do not override.
6a488035
TO
1004 *
1005 * @return string
6a488035 1006 */
00be9182 1007 public function overrideExtraTemplateFileName() {
6a488035
TO
1008 return NULL;
1009 }
1010
1011 /**
fe482240 1012 * Error reporting mechanism.
6a488035 1013 *
6a0b768e
TO
1014 * @param string $message
1015 * Error Message.
1016 * @param int $code
1017 * Error Code.
1018 * @param CRM_Core_DAO $dao
1019 * A data access object on which we perform a rollback if non - empty.
6a488035 1020 */
00be9182 1021 public function error($message, $code = NULL, $dao = NULL) {
6a488035
TO
1022 if ($dao) {
1023 $dao->query('ROLLBACK');
1024 }
1025
1026 $error = CRM_Core_Error::singleton();
1027
1028 $error->push($code, $message);
1029 }
1030
1031 /**
fe482240 1032 * Store the variable with the value in the form scope.
6a488035 1033 *
6c552737
TO
1034 * @param string $name
1035 * Name of the variable.
1036 * @param mixed $value
1037 * Value of the variable.
6a488035 1038 */
00be9182 1039 public function set($name, $value) {
6a488035
TO
1040 $this->controller->set($name, $value);
1041 }
1042
1043 /**
fe482240 1044 * Get the variable from the form scope.
6a488035 1045 *
6c552737
TO
1046 * @param string $name
1047 * Name of the variable
6a488035
TO
1048 *
1049 * @return mixed
6a488035 1050 */
00be9182 1051 public function get($name) {
6a488035
TO
1052 return $this->controller->get($name);
1053 }
1054
1055 /**
fe482240 1056 * Getter for action.
6a488035
TO
1057 *
1058 * @return int
6a488035 1059 */
00be9182 1060 public function getAction() {
6a488035
TO
1061 return $this->_action;
1062 }
1063
1064 /**
fe482240 1065 * Setter for action.
6a488035 1066 *
6a0b768e
TO
1067 * @param int $action
1068 * The mode we want to set the form.
6a488035 1069 */
00be9182 1070 public function setAction($action) {
6a488035
TO
1071 $this->_action = $action;
1072 }
1073
1074 /**
fe482240 1075 * Assign value to name in template.
6a488035 1076 *
6a0b768e
TO
1077 * @param string $var
1078 * Name of variable.
1079 * @param mixed $value
1080 * Value of variable.
6a488035 1081 */
00be9182 1082 public function assign($var, $value = NULL) {
6a488035
TO
1083 self::$_template->assign($var, $value);
1084 }
1085
1086 /**
fe482240 1087 * Assign value to name in template by reference.
6a488035 1088 *
6a0b768e
TO
1089 * @param string $var
1090 * Name of variable.
1091 * @param mixed $value
8eedd10a 1092 * Value of variable.
6a488035 1093 */
00be9182 1094 public function assign_by_ref($var, &$value) {
6a488035
TO
1095 self::$_template->assign_by_ref($var, $value);
1096 }
1097
4a9538ac 1098 /**
fe482240 1099 * Appends values to template variables.
4a9538ac
CW
1100 *
1101 * @param array|string $tpl_var the template variable name(s)
6a0b768e
TO
1102 * @param mixed $value
1103 * The value to append.
4a9538ac
CW
1104 * @param bool $merge
1105 */
f9f40af3 1106 public function append($tpl_var, $value = NULL, $merge = FALSE) {
4a9538ac
CW
1107 self::$_template->append($tpl_var, $value, $merge);
1108 }
1109
1110 /**
fe482240 1111 * Returns an array containing template variables.
4a9538ac
CW
1112 *
1113 * @param string $name
2a6da8d7 1114 *
4a9538ac
CW
1115 * @return array
1116 */
f9f40af3 1117 public function get_template_vars($name = NULL) {
4a9538ac
CW
1118 return self::$_template->get_template_vars($name);
1119 }
1120
a0ee3941 1121 /**
100fef9d 1122 * @param string $name
a0ee3941
EM
1123 * @param $title
1124 * @param $values
1125 * @param array $attributes
1126 * @param null $separator
1127 * @param bool $required
1128 *
1129 * @return HTML_QuickForm_group
1130 */
be2fb01f
CW
1131 public function &addRadio($name, $title, $values, $attributes = [], $separator = NULL, $required = FALSE) {
1132 $options = [];
1133 $attributes = $attributes ? $attributes : [];
b847e6e7
CW
1134 $allowClear = !empty($attributes['allowClear']);
1135 unset($attributes['allowClear']);
385f11fd 1136 $attributes['id_suffix'] = $name;
6a488035
TO
1137 foreach ($values as $key => $var) {
1138 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
1139 }
1140 $group = $this->addGroup($options, $name, $title, $separator);
3ef93345
MD
1141
1142 $optionEditKey = 'data-option-edit-path';
1143 if (!empty($attributes[$optionEditKey])) {
1144 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1145 }
1146
6a488035 1147 if ($required) {
be2fb01f 1148 $this->addRule($name, ts('%1 is a required field.', [1 => $title]), 'required');
6a488035 1149 }
b847e6e7
CW
1150 if ($allowClear) {
1151 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1152 }
6a488035
TO
1153 return $group;
1154 }
1155
a0ee3941 1156 /**
100fef9d 1157 * @param int $id
a0ee3941
EM
1158 * @param $title
1159 * @param bool $allowClear
1160 * @param null $required
1161 * @param array $attributes
1162 */
be2fb01f
CW
1163 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = []) {
1164 $attributes += ['id_suffix' => $id];
1165 $choice = [];
8a4f27dc
CW
1166 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1167 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
6a488035 1168
8a4f27dc 1169 $group = $this->addGroup($choice, $id, $title);
b847e6e7
CW
1170 if ($allowClear) {
1171 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1172 }
6a488035 1173 if ($required) {
be2fb01f 1174 $this->addRule($id, ts('%1 is a required field.', [1 => $title]), 'required');
6a488035
TO
1175 }
1176 }
1177
a0ee3941 1178 /**
100fef9d 1179 * @param int $id
a0ee3941
EM
1180 * @param $title
1181 * @param $values
1182 * @param null $other
1183 * @param null $attributes
1184 * @param null $required
1185 * @param null $javascriptMethod
1186 * @param string $separator
1187 * @param bool $flipValues
1188 */
2da40d21 1189 public function addCheckBox(
f9f40af3
TO
1190 $id, $title, $values, $other = NULL,
1191 $attributes = NULL, $required = NULL,
6a488035 1192 $javascriptMethod = NULL,
f9f40af3 1193 $separator = '<br />', $flipValues = FALSE
6a488035 1194 ) {
be2fb01f 1195 $options = [];
6a488035
TO
1196
1197 if ($javascriptMethod) {
1198 foreach ($values as $key => $var) {
1199 if (!$flipValues) {
3ef93345 1200 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod, $attributes);
6a488035
TO
1201 }
1202 else {
3ef93345 1203 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod, $attributes);
6a488035
TO
1204 }
1205 }
1206 }
1207 else {
1208 foreach ($values as $key => $var) {
1209 if (!$flipValues) {
3ef93345 1210 $options[] = $this->createElement('checkbox', $var, NULL, $key, $attributes);
6a488035
TO
1211 }
1212 else {
3ef93345 1213 $options[] = $this->createElement('checkbox', $key, NULL, $var, $attributes);
6a488035
TO
1214 }
1215 }
1216 }
1217
3ef93345
MD
1218 $group = $this->addGroup($options, $id, $title, $separator);
1219 $optionEditKey = 'data-option-edit-path';
1220 if (!empty($attributes[$optionEditKey])) {
1221 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1222 }
6a488035
TO
1223
1224 if ($other) {
1225 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1226 }
1227
1228 if ($required) {
1229 $this->addRule($id,
be2fb01f 1230 ts('%1 is a required field.', [1 => $title]),
6a488035
TO
1231 'required'
1232 );
1233 }
1234 }
1235
00be9182 1236 public function resetValues() {
6a488035 1237 $data = $this->controller->container();
be2fb01f 1238 $data['values'][$this->_name] = [];
6a488035
TO
1239 }
1240
1241 /**
100fef9d 1242 * Simple shell that derived classes can call to add buttons to
6a488035
TO
1243 * the form with a customized title for the main Submit
1244 *
6a0b768e
TO
1245 * @param string $title
1246 * Title of the main button.
1247 * @param string $nextType
1248 * Button type for the form after processing.
fd31fa4c
EM
1249 * @param string $backType
1250 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
6a488035 1251 */
00be9182 1252 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
be2fb01f 1253 $buttons = [];
6a488035 1254 if ($backType != NULL) {
be2fb01f 1255 $buttons[] = [
6a488035
TO
1256 'type' => $backType,
1257 'name' => ts('Previous'),
be2fb01f 1258 ];
6a488035
TO
1259 }
1260 if ($nextType != NULL) {
be2fb01f 1261 $nextButton = [
6a488035
TO
1262 'type' => $nextType,
1263 'name' => $title,
1264 'isDefault' => TRUE,
be2fb01f 1265 ];
6a488035 1266 if ($submitOnce) {
be2fb01f 1267 $nextButton['js'] = ['onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');"];
6a488035
TO
1268 }
1269 $buttons[] = $nextButton;
1270 }
1271 $this->addButtons($buttons);
1272 }
1273
a0ee3941 1274 /**
100fef9d 1275 * @param string $name
a0ee3941
EM
1276 * @param string $from
1277 * @param string $to
1278 * @param string $label
1279 * @param string $dateFormat
1280 * @param bool $required
1281 * @param bool $displayTime
1282 */
00be9182 1283 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
6a488035 1284 if ($displayTime) {
be2fb01f
CW
1285 $this->addDateTime($name . $from, $label, $required, ['formatType' => $dateFormat]);
1286 $this->addDateTime($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
0db6c3e1
TO
1287 }
1288 else {
be2fb01f
CW
1289 $this->addDate($name . $from, $label, $required, ['formatType' => $dateFormat]);
1290 $this->addDate($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
6a488035
TO
1291 }
1292 }
d5965a37 1293
e2123607 1294 /**
1295 * Add a search for a range using date picker fields.
1296 *
1297 * @param string $fieldName
1298 * @param string $label
27cedb98 1299 * @param bool $isDateTime
1300 * Is this a date-time field (not just date).
e2123607 1301 * @param bool $required
1302 * @param string $fromLabel
1303 * @param string $toLabel
1304 */
27cedb98 1305 public function addDatePickerRange($fieldName, $label, $isDateTime = FALSE, $required = FALSE, $fromLabel = 'From', $toLabel = 'To') {
e2123607 1306
be2fb01f 1307 $options = [
e2123607 1308 '' => ts('- any -'),
1309 0 => ts('Choose Date Range'),
be2fb01f 1310 ] + CRM_Core_OptionGroup::values('relative_date_filters');
e2123607 1311
1312 $this->add('select',
1313 "{$fieldName}_relative",
1314 $label,
1315 $options,
1316 $required,
736fb4c2 1317 ['class' => 'crm-select2']
e2123607 1318 );
1319 $attributes = ['format' => 'searchDate'];
27cedb98 1320 $extra = ['time' => $isDateTime];
e2123607 1321 $this->add('datepicker', $fieldName . '_low', ts($fromLabel), $attributes, $required, $extra);
1322 $this->add('datepicker', $fieldName . '_high', ts($toLabel), $attributes, $required, $extra);
1323 }
1324
03225ad6
CW
1325 /**
1326 * Based on form action, return a string representing the api action.
1327 * Used by addField method.
1328 *
1329 * Return string
1330 */
d5e4784e 1331 protected function getApiAction() {
03225ad6
CW
1332 $action = $this->getAction();
1333 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1334 return 'create';
1335 }
889dbed8 1336 if ($action & (CRM_Core_Action::VIEW + CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
03225ad6
CW
1337 return 'get';
1338 }
079c7954
CW
1339 if ($action & (CRM_Core_Action::DELETE)) {
1340 return 'delete';
1341 }
03225ad6 1342 // If you get this exception try adding more cases above.
0e02cb01 1343 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
03225ad6
CW
1344 }
1345
6e62b28c 1346 /**
d5965a37 1347 * Classes extending CRM_Core_Form should implement this method.
6e62b28c
TM
1348 * @throws Exception
1349 */
1350 public function getDefaultEntity() {
0e02cb01 1351 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
6e62b28c 1352 }
6a488035 1353
1ae720b3
TM
1354 /**
1355 * Classes extending CRM_Core_Form should implement this method.
1356 *
1357 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1358 * @throws Exception
1359 */
1360 public function getDefaultContext() {
0e02cb01 1361 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1ae720b3
TM
1362 }
1363
5fafc9b0 1364 /**
fe482240 1365 * Adds a select based on field metadata.
5fafc9b0 1366 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
475e9f44 1367 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
6a0b768e
TO
1368 * @param $name
1369 * Field name to go on the form.
1370 * @param array $props
1371 * Mix of html attributes and special properties, namely.
920600e1
CW
1372 * - entity (api entity name, can usually be inferred automatically from the form class)
1373 * - field (field name - only needed if different from name used on the form)
1374 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1375 * - placeholder - set to NULL to disable
d0def949 1376 * - multiple - bool
76773c5a 1377 * - context - @see CRM_Core_DAO::buildOptionsContext
5fafc9b0
CW
1378 * @param bool $required
1379 * @throws CRM_Core_Exception
1380 * @return HTML_QuickForm_Element
1381 */
be2fb01f 1382 public function addSelect($name, $props = [], $required = FALSE) {
920600e1 1383 if (!isset($props['entity'])) {
6e62b28c 1384 $props['entity'] = $this->getDefaultEntity();
6a488035 1385 }
920600e1
CW
1386 if (!isset($props['field'])) {
1387 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
e869b07d 1388 }
65e8615b
CW
1389 if (!isset($props['context'])) {
1390 try {
1391 $props['context'] = $this->getDefaultContext();
1392 }
1393 // This is not a required param, so we'll ignore if this doesn't exist.
518fa0ee
SL
1394 catch (Exception $e) {
1395 }
65e8615b 1396 }
f76b27fe
CW
1397 // Fetch options from the api unless passed explicitly
1398 if (isset($props['options'])) {
1399 $options = $props['options'];
1400 }
1401 else {
76352fbc 1402 $info = civicrm_api3($props['entity'], 'getoptions', $props);
f76b27fe
CW
1403 $options = $info['values'];
1404 }
5fafc9b0 1405 if (!array_key_exists('placeholder', $props)) {
76773c5a 1406 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
5fafc9b0 1407 }
5fafc9b0
CW
1408 // Handle custom field
1409 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1410 list(, $id) = explode('_', $name);
1411 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
475e9f44 1412 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
76773c5a
CW
1413 if (CRM_Utils_Array::value('context', $props) != 'search') {
1414 $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);
1415 }
5fafc9b0
CW
1416 }
1417 // Core field
6a488035 1418 else {
f76b27fe 1419 $info = civicrm_api3($props['entity'], 'getfields');
22e263ad 1420 foreach ($info['values'] as $uniqueName => $fieldSpec) {
e869b07d 1421 if (
920600e1
CW
1422 $uniqueName === $props['field'] ||
1423 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
be2fb01f 1424 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, []))
e869b07d
CW
1425 ) {
1426 break;
1427 }
6a488035 1428 }
e869b07d 1429 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
76773c5a 1430 if (CRM_Utils_Array::value('context', $props) != 'search') {
599ae208 1431 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
76773c5a 1432 }
6a488035 1433 }
920600e1
CW
1434 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1435 $props['data-api-entity'] = $props['entity'];
1436 $props['data-api-field'] = $props['field'];
76773c5a 1437 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
5fafc9b0 1438 return $this->add('select', $name, $label, $options, $required, $props);
6a488035
TO
1439 }
1440
7ec4548b
TM
1441 /**
1442 * Adds a field based on metadata.
1443 *
1444 * @param $name
1445 * Field name to go on the form.
1446 * @param array $props
1447 * Mix of html attributes and special properties, namely.
1448 * - entity (api entity name, can usually be inferred automatically from the form class)
03225ad6 1449 * - name (field name - only needed if different from name used on the form)
7ec4548b
TM
1450 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1451 * - placeholder - set to NULL to disable
1452 * - multiple - bool
1453 * - context - @see CRM_Core_DAO::buildOptionsContext
1454 * @param bool $required
ed0ca248 1455 * @param bool $legacyDate
1456 * Temporary param to facilitate the conversion of fields to use the datepicker in
1457 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1458 * tpl as well. That file is intended to be EOL.
1459 *
03225ad6
CW
1460 * @throws \CiviCRM_API3_Exception
1461 * @throws \Exception
518fa0ee
SL
1462 * @return mixed
1463 * HTML_QuickForm_Element
1464 * void
7ec4548b 1465 */
be2fb01f 1466 public function addField($name, $props = [], $required = FALSE, $legacyDate = TRUE) {
1ae720b3 1467 // Resolve context.
916b6181 1468 if (empty($props['context'])) {
1ae720b3
TM
1469 $props['context'] = $this->getDefaultContext();
1470 }
916b6181 1471 $context = $props['context'];
7ec4548b 1472 // Resolve entity.
916b6181 1473 if (empty($props['entity'])) {
7ec4548b
TM
1474 $props['entity'] = $this->getDefaultEntity();
1475 }
1476 // Resolve field.
916b6181 1477 if (empty($props['name'])) {
03225ad6 1478 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
7ec4548b 1479 }
03225ad6 1480 // Resolve action.
916b6181 1481 if (empty($props['action'])) {
03225ad6 1482 $props['action'] = $this->getApiAction();
7ec4548b 1483 }
2b31bc15
CW
1484
1485 // Handle custom fields
1486 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1487 $fieldId = (int) substr($name, 7);
916b6181 1488 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
2b31bc15
CW
1489 }
1490
1491 // Core field - get metadata.
d60a6fba 1492 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
03225ad6 1493 $fieldSpec = $fieldSpec['values'];
80a96508 1494 $fieldSpecLabel = isset($fieldSpec['html']['label']) ? $fieldSpec['html']['label'] : CRM_Utils_Array::value('title', $fieldSpec);
1495 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
7ec4548b 1496
7ec4548b 1497 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
916b6181 1498 if ($widget == 'TextArea' && $context == 'search') {
7ec4548b
TM
1499 $widget = 'Text';
1500 }
1501
be2fb01f 1502 $isSelect = (in_array($widget, [
518fa0ee
SL
1503 'Select',
1504 'CheckBoxGroup',
1505 'RadioGroup',
1506 'Radio',
be2fb01f 1507 ]));
7ec4548b
TM
1508
1509 if ($isSelect) {
2f32ed10 1510 // Fetch options from the api unless passed explicitly.
7ec4548b
TM
1511 if (isset($props['options'])) {
1512 $options = $props['options'];
1513 }
1514 else {
a4969aee 1515 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
7ec4548b 1516 }
916b6181 1517 if ($context == 'search') {
7ec4548b 1518 $widget = 'Select';
65e8615b 1519 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
7ec4548b 1520 }
7ec4548b
TM
1521
1522 // Add data for popup link.
3ef93345
MD
1523 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1524 $hasOptionUrl = !empty($props['option_url']);
1525 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1526 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1527
1528 // Only add if key is not set, or if non-empty option url is provided
1529 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1530 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1531 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1532 $props['data-option-edit-path'] = $optionUrl;
7ec4548b 1533 $props['data-api-entity'] = $props['entity'];
03225ad6 1534 $props['data-api-field'] = $props['name'];
7ec4548b
TM
1535 }
1536 }
be2fb01f 1537 $props += CRM_Utils_Array::value('html', $fieldSpec, []);
65e8615b 1538 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
599ae208 1539
b44e3f84 1540 // TODO: refactor switch statement, to separate methods.
7ec4548b
TM
1541 switch ($widget) {
1542 case 'Text':
d8f1758d
CW
1543 case 'Url':
1544 case 'Number':
1545 case 'Email':
7ec4548b 1546 //TODO: Autodetect ranges
5b8080ad 1547 $props['size'] = isset($props['size']) ? $props['size'] : 60;
d8f1758d 1548 return $this->add(strtolower($widget), $name, $label, $props, $required);
7ec4548b 1549
b4b53245 1550 case 'hidden':
2a300b65 1551 return $this->add('hidden', $name, NULL, $props, $required);
b4b53245 1552
0efbca68
TM
1553 case 'TextArea':
1554 //Set default columns and rows for textarea.
1555 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1556 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
079f52de 1557 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
ed71bbca 1558 $props['maxlength'] = $fieldSpec['length'];
1559 }
599ae208 1560 return $this->add('textarea', $name, $label, $props, $required);
0efbca68 1561
db3ec100 1562 case 'Select Date':
ed0ca248 1563 // This is a white list for fields that have been tested with
1564 // date picker. We should be able to remove the other
1565 if ($legacyDate) {
1566 //TODO: add range support
1567 //TODO: Add date formats
1568 //TODO: Add javascript template for dates.
1569 return $this->addDate($name, $label, $required, $props);
1570 }
1571 else {
1572 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
be2fb01f 1573 $attributes = ['format' => $fieldSpec['date_format']];
ed0ca248 1574 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1575 }
db3ec100 1576
a4969aee
TM
1577 case 'Radio':
1578 $separator = isset($props['separator']) ? $props['separator'] : NULL;
125d54e1 1579 unset($props['separator']);
ef3a048a 1580 if (!isset($props['allowClear'])) {
125d54e1 1581 $props['allowClear'] = !$required;
ef3a048a 1582 }
2a300b65 1583 return $this->addRadio($name, $label, $options, $props, $separator, $required);
a4969aee 1584
b248d52b 1585 case 'ChainSelect':
be2fb01f 1586 $props += [
b248d52b
CW
1587 'required' => $required,
1588 'label' => $label,
916b6181 1589 'multiple' => $context == 'search',
be2fb01f 1590 ];
b248d52b
CW
1591 return $this->addChainSelect($name, $props);
1592
7ec4548b 1593 case 'Select':
b248d52b 1594 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
65e8615b 1595 if (!array_key_exists('placeholder', $props)) {
78e1efac 1596 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
7ec4548b 1597 }
7ec4548b 1598 // TODO: Add and/or option for fields that store multiple values
b2da03d6 1599 return $this->add('select', $name, $label, $options, $required, $props);
7ec4548b 1600
dd4706ef 1601 case 'CheckBoxGroup':
2a300b65 1602 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
dd4706ef
TM
1603
1604 case 'RadioGroup':
2a300b65 1605 return $this->addRadio($name, $label, $options, $props, NULL, $required);
dd4706ef 1606
a4969aee 1607 case 'CheckBox':
999ab5e1
TM
1608 $text = isset($props['text']) ? $props['text'] : NULL;
1609 unset($props['text']);
2a300b65 1610 return $this->addElement('checkbox', $name, $label, $text, $props);
a4969aee 1611
50471995 1612 //add support for 'Advcheckbox' field
1613 case 'advcheckbox':
b0964781 1614 $text = isset($props['text']) ? $props['text'] : NULL;
1615 unset($props['text']);
1616 return $this->addElement('advcheckbox', $name, $label, $text, $props);
50471995 1617
33fa033c
TM
1618 case 'File':
1619 // We should not build upload file in search mode.
916b6181 1620 if ($context == 'search') {
33fa033c
TM
1621 return;
1622 }
2a300b65 1623 $file = $this->add('file', $name, $label, $props, $required);
33fa033c 1624 $this->addUploadElement($name);
2a300b65 1625 return $file;
33fa033c 1626
b66c1d2c
CW
1627 case 'RichTextEditor':
1628 return $this->add('wysiwyg', $name, $label, $props, $required);
1629
b58770ea 1630 case 'EntityRef':
2a300b65 1631 return $this->addEntityRef($name, $label, $props, $required);
b58770ea 1632
e9bc5dcc 1633 case 'Password':
a7e59a48 1634 $props['size'] = isset($props['size']) ? $props['size'] : 60;
e9bc5dcc
SL
1635 return $this->add('password', $name, $label, $props, $required);
1636
7ec4548b
TM
1637 // Check datatypes of fields
1638 // case 'Int':
1639 //case 'Float':
1640 //case 'Money':
7ec4548b
TM
1641 //case read only fields
1642 default:
1643 throw new Exception("Unsupported html-element " . $widget);
1644 }
1645 }
1646
6a488035
TO
1647 /**
1648 * Add a widget for selecting/editing/creating/copying a profile form
1649 *
6a0b768e
TO
1650 * @param string $name
1651 * HTML form-element name.
1652 * @param string $label
1653 * Printable label.
1654 * @param string $allowCoreTypes
1655 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1656 * @param string $allowSubTypes
1657 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
6a488035 1658 * @param array $entities
6a0b768e
TO
1659 * @param bool $default
1660 * //CRM-15427.
54957108 1661 * @param string $usedFor
6a488035 1662 */
37375016 1663 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
6a488035
TO
1664 // Output widget
1665 // FIXME: Instead of adhoc serialization, use a single json_encode()
1666 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1667 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
be2fb01f 1668 $this->add('text', $name, $label, [
6a488035
TO
1669 'class' => 'crm-profile-selector',
1670 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1671 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1672 'data-entities' => json_encode($entities),
99e239bc 1673 //CRM-15427
1674 'data-default' => $default,
37375016 1675 'data-usedfor' => json_encode($usedFor),
be2fb01f 1676 ]);
6a488035
TO
1677 }
1678
a0ee3941
EM
1679 /**
1680 * @return null
1681 */
6a488035
TO
1682 public function getRootTitle() {
1683 return NULL;
1684 }
1685
a0ee3941
EM
1686 /**
1687 * @return string
1688 */
6a488035
TO
1689 public function getCompleteTitle() {
1690 return $this->getRootTitle() . $this->getTitle();
1691 }
1692
a0ee3941
EM
1693 /**
1694 * @return CRM_Core_Smarty
1695 */
00be9182 1696 public static function &getTemplate() {
6a488035
TO
1697 return self::$_template;
1698 }
1699
a0ee3941
EM
1700 /**
1701 * @param $elementName
1702 */
00be9182 1703 public function addUploadElement($elementName) {
6a488035
TO
1704 $uploadNames = $this->get('uploadNames');
1705 if (!$uploadNames) {
be2fb01f 1706 $uploadNames = [];
6a488035
TO
1707 }
1708 if (is_array($elementName)) {
1709 foreach ($elementName as $name) {
1710 if (!in_array($name, $uploadNames)) {
1711 $uploadNames[] = $name;
1712 }
1713 }
1714 }
1715 else {
1716 if (!in_array($elementName, $uploadNames)) {
1717 $uploadNames[] = $elementName;
1718 }
1719 }
1720 $this->set('uploadNames', $uploadNames);
1721
1722 $config = CRM_Core_Config::singleton();
1723 if (!empty($uploadNames)) {
1724 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1725 }
1726 }
1727
a0ee3941
EM
1728 /**
1729 * @param $name
1730 *
1731 * @return null
1732 */
00be9182 1733 public function getVar($name) {
6a488035
TO
1734 return isset($this->$name) ? $this->$name : NULL;
1735 }
1736
a0ee3941
EM
1737 /**
1738 * @param $name
1739 * @param $value
1740 */
00be9182 1741 public function setVar($name, $value) {
6a488035
TO
1742 $this->$name = $value;
1743 }
1744
1745 /**
fe482240 1746 * Add date.
6a488035 1747 *
013ac5df
CW
1748 * @deprecated
1749 * Use $this->add('datepicker', ...) instead.
a1a2a83d
TO
1750 *
1751 * @param string $name
1752 * Name of the element.
1753 * @param string $label
1754 * Label of the element.
6a0b768e
TO
1755 * @param bool $required
1756 * True if required.
a1a2a83d
TO
1757 * @param array $attributes
1758 * Key / value pair.
6a488035 1759 */
00be9182 1760 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
a7488080 1761 if (!empty($attributes['formatType'])) {
6a488035 1762 // get actual format
be2fb01f
CW
1763 $params = ['name' => $attributes['formatType']];
1764 $values = [];
6a488035
TO
1765
1766 // cache date information
1767 static $dateFormat;
1768 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
a7488080 1769 if (empty($dateFormat[$key])) {
6a488035
TO
1770 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1771 $dateFormat[$key] = $values;
1772 }
1773 else {
1774 $values = $dateFormat[$key];
1775 }
1776
1777 if ($values['date_format']) {
1778 $attributes['format'] = $values['date_format'];
1779 }
1780
a7488080 1781 if (!empty($values['time_format'])) {
6a488035
TO
1782 $attributes['timeFormat'] = $values['time_format'];
1783 }
1784 $attributes['startOffset'] = $values['start'];
1785 $attributes['endOffset'] = $values['end'];
1786 }
1787
1788 $config = CRM_Core_Config::singleton();
a7488080 1789 if (empty($attributes['format'])) {
6a488035
TO
1790 $attributes['format'] = $config->dateInputFormat;
1791 }
1792
1793 if (!isset($attributes['startOffset'])) {
1794 $attributes['startOffset'] = 10;
1795 }
1796
1797 if (!isset($attributes['endOffset'])) {
1798 $attributes['endOffset'] = 10;
1799 }
1800
1801 $this->add('text', $name, $label, $attributes);
1802
8cc574cf 1803 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
6a488035
TO
1804
1805 if (!isset($attributes['timeFormat'])) {
1806 $timeFormat = $config->timeInputFormat;
1807 }
1808 else {
1809 $timeFormat = $attributes['timeFormat'];
1810 }
1811
1812 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1813 if ($timeFormat) {
1814 $show24Hours = TRUE;
1815 if ($timeFormat == 1) {
1816 $show24Hours = FALSE;
1817 }
1818
1819 //CRM-6664 -we are having time element name
1820 //in either flat string or an array format.
1821 $elementName = $name . '_time';
1822 if (substr($name, -1) == ']') {
1823 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1824 }
1825
be2fb01f 1826 $this->add('text', $elementName, ts('Time'), ['timeFormat' => $show24Hours]);
6a488035
TO
1827 }
1828 }
1829
1830 if ($required) {
be2fb01f 1831 $this->addRule($name, ts('Please select %1', [1 => $label]), 'required');
8cc574cf 1832 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
6a488035
TO
1833 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1834 }
1835 }
1836 }
1837
1838 /**
013ac5df
CW
1839 * Function that will add date and time.
1840 *
1841 * @deprecated
1842 * Use $this->add('datepicker', ...) instead.
54957108 1843 *
1844 * @param string $name
1845 * @param string $label
1846 * @param bool $required
1847 * @param null $attributes
6a488035 1848 */
00be9182 1849 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
be2fb01f 1850 $addTime = ['addTime' => TRUE];
6a488035
TO
1851 if (is_array($attributes)) {
1852 $attributes = array_merge($attributes, $addTime);
1853 }
1854 else {
1855 $attributes = $addTime;
1856 }
1857
1858 $this->addDate($name, $label, $required, $attributes);
1859 }
1860
1861 /**
fe482240 1862 * Add a currency and money element to the form.
3bdf1f3a 1863 *
1864 * @param string $name
1865 * @param string $label
1866 * @param bool $required
1867 * @param null $attributes
1868 * @param bool $addCurrency
1869 * @param string $currencyName
1870 * @param null $defaultCurrency
1871 * @param bool $freezeCurrency
1872 *
1873 * @return \HTML_QuickForm_Element
6a488035 1874 */
2da40d21 1875 public function addMoney(
f9f40af3 1876 $name,
6a488035 1877 $label,
f9f40af3
TO
1878 $required = FALSE,
1879 $attributes = NULL,
1880 $addCurrency = TRUE,
1881 $currencyName = 'currency',
6a488035 1882 $defaultCurrency = NULL,
f9f40af3 1883 $freezeCurrency = FALSE
6a488035
TO
1884 ) {
1885 $element = $this->add('text', $name, $label, $attributes, $required);
1886 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1887
1888 if ($addCurrency) {
1889 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1890 }
1891
1892 return $element;
1893 }
1894
1895 /**
fe482240 1896 * Add currency element to the form.
54957108 1897 *
1898 * @param string $name
1899 * @param null $label
1900 * @param bool $required
1901 * @param string $defaultCurrency
1902 * @param bool $freezeCurrency
483a53a8 1903 * @param bool $setDefaultCurrency
6a488035 1904 */
2da40d21 1905 public function addCurrency(
f9f40af3
TO
1906 $name = 'currency',
1907 $label = NULL,
1908 $required = TRUE,
6a488035 1909 $defaultCurrency = NULL,
483a53a8 1910 $freezeCurrency = FALSE,
1911 $setDefaultCurrency = TRUE
6a488035
TO
1912 ) {
1913 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
91a33228 1914 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
b740ee4b
MW
1915 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1916 $currencies[$defaultCurrency] = $defaultCurrency;
1917 }
be2fb01f 1918 $options = ['class' => 'crm-select2 eight'];
6a488035 1919 if (!$required) {
be2fb01f 1920 $currencies = ['' => ''] + $currencies;
e1462487 1921 $options['placeholder'] = ts('- none -');
6a488035 1922 }
e1462487 1923 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
6a488035
TO
1924 if ($freezeCurrency) {
1925 $ele->freeze();
1926 }
1927 if (!$defaultCurrency) {
1928 $config = CRM_Core_Config::singleton();
1929 $defaultCurrency = $config->defaultCurrency;
1930 }
483a53a8 1931 // In some case, setting currency field by default might override the default value
1932 // as encountered in CRM-20527 for batch data entry
1933 if ($setDefaultCurrency) {
be2fb01f 1934 $this->setDefaults([$name => $defaultCurrency]);
483a53a8 1935 }
6a488035
TO
1936 }
1937
47f21f3a 1938 /**
fe482240 1939 * Create a single or multiple entity ref field.
47f21f3a
CW
1940 * @param string $name
1941 * @param string $label
6a0b768e
TO
1942 * @param array $props
1943 * Mix of html and widget properties, including:.
16b10e64 1944 * - select - params to give to select2 widget
2229cf4f 1945 * - entity - defaults to Contact
16b10e64 1946 * - create - can the user create a new entity on-the-fly?
79ae07d9 1947 * Set to TRUE if entity is contact and you want the default profiles,
2229cf4f 1948 * or pass in your own set of links. @see CRM_Campaign_BAO_Campaign::getEntityRefCreateLinks for format
353ea873 1949 * note that permissions are checked automatically
16b10e64 1950 * - api - array of settings for the getlist api wrapper
353ea873 1951 * note that it accepts a 'params' setting which will be passed to the underlying api
16b10e64
CW
1952 * - placeholder - string
1953 * - multiple - bool
1954 * - class, etc. - other html properties
fd36866a 1955 * @param bool $required
79ae07d9 1956 *
47f21f3a
CW
1957 * @return HTML_QuickForm_Element
1958 */
be2fb01f 1959 public function addEntityRef($name, $label = '', $props = [], $required = FALSE) {
76ec9ca7 1960 // Default properties
be2fb01f 1961 $props['api'] = CRM_Utils_Array::value('api', $props, []);
2229cf4f 1962 $props['entity'] = CRM_Utils_String::convertStringToCamel(CRM_Utils_Array::value('entity', $props, 'Contact'));
a88cf11a 1963 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
47f21f3a 1964
8dbd6052 1965 if (array_key_exists('create', $props) && empty($props['create'])) {
79ae07d9
CW
1966 unset($props['create']);
1967 }
79ae07d9 1968
be2fb01f 1969 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', [1 => ts(str_replace('_', ' ', $props['entity']))]) : ts('- none -'));
a88cf11a 1970
be2fb01f 1971 $defaults = [];
a88cf11a
CW
1972 if (!empty($props['multiple'])) {
1973 $defaults['multiple'] = TRUE;
79ae07d9 1974 }
be2fb01f 1975 $props['select'] = CRM_Utils_Array::value('select', $props, []) + $defaults;
47f21f3a 1976
f9585de5 1977 $this->formatReferenceFieldAttributes($props, get_class($this));
47f21f3a
CW
1978 return $this->add('text', $name, $label, $props, $required);
1979 }
1980
1981 /**
f9585de5 1982 * @param array $props
1983 * @param string $formName
47f21f3a 1984 */
f9585de5 1985 private function formatReferenceFieldAttributes(&$props, $formName) {
1986 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
47f21f3a 1987 $props['data-select-params'] = json_encode($props['select']);
76ec9ca7
CW
1988 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1989 $props['data-api-entity'] = $props['entity'];
79ae07d9
CW
1990 if (!empty($props['create'])) {
1991 $props['data-create-links'] = json_encode($props['create']);
47f21f3a 1992 }
a88cf11a 1993 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
47f21f3a
CW
1994 }
1995
5d86176b 1996 /**
1997 * Convert all date fields within the params to mysql date ready for the
1998 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1999 * and if time is defined it is incorporated
2000 *
6a0b768e
TO
2001 * @param array $params
2002 * Input params from the form.
5d86176b 2003 *
2004 * @todo it would probably be better to work on $this->_params than a passed array
2005 * @todo standardise the format which dates are passed to the BAO layer in & remove date
2006 * handling from BAO
2007 */
9b873358
TO
2008 public function convertDateFieldsToMySQL(&$params) {
2009 foreach ($this->_dateFields as $fieldName => $specs) {
2010 if (!empty($params[$fieldName])) {
5d86176b 2011 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
2012 CRM_Utils_Date::processDate(
353ffa53
TO
2013 $params[$fieldName],
2014 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
5d86176b 2015 );
2016 }
92e4c2a5 2017 else {
9b873358 2018 if (isset($specs['default'])) {
5d86176b 2019 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
2020 }
2021 }
2022 }
2023 }
2024
a0ee3941
EM
2025 /**
2026 * @param $elementName
2027 */
00be9182 2028 public function removeFileRequiredRules($elementName) {
be2fb01f 2029 $this->_required = array_diff($this->_required, [$elementName]);
6a488035
TO
2030 if (isset($this->_rules[$elementName])) {
2031 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2032 if ($ruleInfo['type'] == 'uploadedfile') {
2033 unset($this->_rules[$elementName][$index]);
2034 }
2035 }
2036 if (empty($this->_rules[$elementName])) {
2037 unset($this->_rules[$elementName]);
2038 }
2039 }
2040 }
2041
2042 /**
fe482240 2043 * Function that can be defined in Form to override or.
6a488035 2044 * perform specific action on cancel action
6a488035 2045 */
f9f40af3
TO
2046 public function cancelAction() {
2047 }
7cb3d4f0
CW
2048
2049 /**
fe482240 2050 * Helper function to verify that required fields have been filled.
3bdf1f3a 2051 *
7cb3d4f0 2052 * Typically called within the scope of a FormRule function
3bdf1f3a 2053 *
2054 * @param array $fields
2055 * @param array $values
2056 * @param array $errors
7cb3d4f0 2057 */
00be9182 2058 public static function validateMandatoryFields($fields, $values, &$errors) {
7cb3d4f0
CW
2059 foreach ($fields as $name => $fld) {
2060 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
be2fb01f 2061 $errors[$name] = ts('%1 is a required field.', [1 => $fld['title']]);
7cb3d4f0
CW
2062 }
2063 }
2064 }
da8d9879 2065
aa1b1481
EM
2066 /**
2067 * Get contact if for a form object. Prioritise
16b10e64 2068 * - cid in URL if 0 (on behalf on someoneelse)
aa1b1481 2069 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
16b10e64
CW
2070 * - logged in user id if it matches the one in the cid in the URL
2071 * - contact id validated from a checksum from a checksum
2072 * - cid from the url if the caller has ACL permission to view
2073 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
aa1b1481 2074 *
5c766a0b 2075 * @return NULL|int
aa1b1481 2076 */
8d388047 2077 protected function setContactID() {
da8d9879 2078 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
7b4d7ab8 2079 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
596bff78 2080 $tempID = $this->_params['select_contact_id'];
2081 }
22e263ad 2082 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
e1ce628e 2083 // event form stores as an indexed array, contribution form not so much...
2084 $tempID = $this->_params[0]['select_contact_id'];
2085 }
c156d4d6 2086
da8d9879 2087 // force to ignore the authenticated user
c156d4d6
E
2088 if ($tempID === '0' || $tempID === 0) {
2089 // we set the cid on the form so that this will be retained for the Confirm page
2090 // in the multi-page form & prevent us returning the $userID when this is called
2091 // from that page
2092 // we don't really need to set it when $tempID is set because the params have that stored
2093 $this->set('cid', 0);
be2fb01f 2094 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2095 return (int) $tempID;
da8d9879
DG
2096 }
2097
596bff78 2098 $userID = $this->getLoggedInUserContactID();
da8d9879 2099
18406494 2100 if (!is_null($tempID) && $tempID === $userID) {
be2fb01f 2101 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2102 return (int) $userID;
da8d9879
DG
2103 }
2104
2105 //check if this is a checksum authentication
2106 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2107 if ($userChecksum) {
2108 //check for anonymous user.
2109 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2110 if ($validUser) {
be2fb01f
CW
2111 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2112 CRM_Core_Resources::singleton()->addVars('coreForm', ['checksum' => $userChecksum]);
da8d9879
DG
2113 return $tempID;
2114 }
2115 }
2116 // check if user has permission, CRM-12062
4c9b6178 2117 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
be2fb01f 2118 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
da8d9879
DG
2119 return $tempID;
2120 }
064af727 2121 if (is_numeric($userID)) {
be2fb01f 2122 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $userID]);
064af727 2123 }
f03d4901 2124 return is_numeric($userID) ? $userID : NULL;
da8d9879 2125 }
596bff78 2126
3bdf1f3a 2127 /**
2128 * Get the contact id that the form is being submitted for.
2129 *
2130 * @return int|NULL
2131 */
8d388047 2132 public function getContactID() {
2133 return $this->setContactID();
2134 }
2135
f9f40af3 2136 /**
fe482240 2137 * Get the contact id of the logged in user.
f9f40af3 2138 */
00be9182 2139 public function getLoggedInUserContactID() {
596bff78 2140 // check if the user is logged in and has a contact ID
2141 $session = CRM_Core_Session::singleton();
2142 return $session->get('userID');
2143 }
2144
2145 /**
100fef9d 2146 * Add autoselector field -if user has permission to view contacts
596bff78 2147 * If adding this to a form you also need to add to the tpl e.g
2148 *
2149 * {if !empty($selectable)}
2150 * <div class="crm-summary-row">
2151 * <div class="crm-label">{$form.select_contact.label}</div>
2152 * <div class="crm-content">
2153 * {$form.select_contact.html}
2154 * </div>
2155 * </div>
2156 * {/if}
77b97be7 2157 *
6a0b768e
TO
2158 * @param array $profiles
2159 * Ids of profiles that are on the form (to be autofilled).
77b97be7
EM
2160 * @param array $autoCompleteField
2161 *
16b10e64
CW
2162 * - name_field
2163 * - id_field
2164 * - url (for ajax lookup)
596bff78 2165 *
77b97be7 2166 * @todo add data attributes so we can deal with multiple instances on a form
596bff78 2167 */
be2fb01f
CW
2168 public function addAutoSelector($profiles = [], $autoCompleteField = []) {
2169 $autoCompleteField = array_merge([
353ffa53
TO
2170 'id_field' => 'select_contact_id',
2171 'placeholder' => ts('Select someone else ...'),
2172 'show_hide' => TRUE,
be2fb01f
CW
2173 'api' => ['params' => ['contact_type' => 'Individual']],
2174 ], $autoCompleteField);
596bff78 2175
22e263ad 2176 if ($this->canUseAjaxContactLookups()) {
25977d86 2177 $this->assign('selectable', $autoCompleteField['id_field']);
be2fb01f 2178 $this->addEntityRef($autoCompleteField['id_field'], NULL, [
518fa0ee
SL
2179 'placeholder' => $autoCompleteField['placeholder'],
2180 'api' => $autoCompleteField['api'],
2181 ]);
596bff78 2182
96ed17aa 2183 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
be2fb01f
CW
2184 ->addSetting([
2185 'form' => ['autocompletes' => $autoCompleteField],
2186 'ids' => ['profile' => $profiles],
2187 ]);
596bff78 2188 }
2189 }
2190
dc677c00 2191 /**
dc677c00 2192 */
00be9182 2193 public function canUseAjaxContactLookups() {
be2fb01f
CW
2194 if (0 < (civicrm_api3('contact', 'getcount', ['check_permissions' => 1])) &&
2195 CRM_Core_Permission::check([['access AJAX API', 'access CiviCRM']])
353ffa53 2196 ) {
f9f40af3
TO
2197 return TRUE;
2198 }
dc677c00
EM
2199 }
2200
596bff78 2201 /**
2202 * Add the options appropriate to cid = zero - ie. autocomplete
2203 *
2204 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2205 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2206 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2207 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
7a9ab499
EM
2208 *
2209 * @param $onlinePaymentProcessorEnabled
596bff78 2210 */
00be9182 2211 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
596bff78 2212 $this->assign('nocid', TRUE);
be2fb01f 2213 $profiles = [];
22e263ad 2214 if ($this->_values['custom_pre_id']) {
596bff78 2215 $profiles[] = $this->_values['custom_pre_id'];
2216 }
22e263ad 2217 if ($this->_values['custom_post_id']) {
cc57909a 2218 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
596bff78 2219 }
22e263ad 2220 if ($onlinePaymentProcessorEnabled) {
596bff78 2221 $profiles[] = 'billing';
2222 }
22e263ad 2223 if (!empty($this->_values)) {
596bff78 2224 $this->addAutoSelector($profiles);
2225 }
2226 }
9d665938 2227
2228 /**
2229 * Set default values on form for given contact (or no contact defaults)
77b97be7 2230 *
6a0b768e
TO
2231 * @param mixed $profile_id
2232 * (can be id, or profile name).
2233 * @param int $contactID
77b97be7
EM
2234 *
2235 * @return array
9d665938 2236 */
00be9182 2237 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
92e4c2a5 2238 try {
be2fb01f 2239 $defaults = civicrm_api3('profile', 'getsingle', [
9d665938 2240 'profile_id' => (array) $profile_id,
2241 'contact_id' => $contactID,
be2fb01f 2242 ]);
9d665938 2243 return $defaults;
2244 }
2245 catch (Exception $e) {
9d665938 2246 // the try catch block gives us silent failure -not 100% sure this is a good idea
2247 // as silent failures are often worse than noisy ones
be2fb01f 2248 return [];
9d665938 2249 }
2250 }
cae80d9f
CW
2251
2252 /**
fe482240 2253 * Sets form attribute.
cae80d9f
CW
2254 * @see CRM.loadForm
2255 */
00be9182 2256 public function preventAjaxSubmit() {
cae80d9f
CW
2257 $this->setAttribute('data-no-ajax-submit', 'true');
2258 }
2259
2260 /**
fe482240 2261 * Sets form attribute.
cae80d9f
CW
2262 * @see CRM.loadForm
2263 */
00be9182 2264 public function allowAjaxSubmit() {
cae80d9f
CW
2265 $this->removeAttribute('data-no-ajax-submit');
2266 }
e2046b33
CW
2267
2268 /**
fe482240 2269 * Sets page title based on entity and action.
e2046b33
CW
2270 * @param string $entityLabel
2271 */
00be9182 2272 public function setPageTitle($entityLabel) {
e2046b33
CW
2273 switch ($this->_action) {
2274 case CRM_Core_Action::ADD:
be2fb01f 2275 CRM_Utils_System::setTitle(ts('New %1', [1 => $entityLabel]));
e2046b33 2276 break;
f9f40af3 2277
e2046b33 2278 case CRM_Core_Action::UPDATE:
be2fb01f 2279 CRM_Utils_System::setTitle(ts('Edit %1', [1 => $entityLabel]));
e2046b33 2280 break;
f9f40af3 2281
e2046b33
CW
2282 case CRM_Core_Action::VIEW:
2283 case CRM_Core_Action::PREVIEW:
be2fb01f 2284 CRM_Utils_System::setTitle(ts('View %1', [1 => $entityLabel]));
e2046b33 2285 break;
f9f40af3 2286
e2046b33 2287 case CRM_Core_Action::DELETE:
be2fb01f 2288 CRM_Utils_System::setTitle(ts('Delete %1', [1 => $entityLabel]));
e2046b33
CW
2289 break;
2290 }
2291 }
1d07e7ab
CW
2292
2293 /**
2294 * Create a chain-select target field. All settings are optional; the defaults usually work.
2295 *
2296 * @param string $elementName
2297 * @param array $settings
2298 *
2299 * @return HTML_QuickForm_Element
2300 */
be2fb01f
CW
2301 public function addChainSelect($elementName, $settings = []) {
2302 $props = $settings += [
2303 'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
518fa0ee
SL
2304 'country',
2305 'Country',
2306 'state_province',
2307 'StateProvince',
2308 ], $elementName),
1d07e7ab 2309 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
757069de 2310 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
1d07e7ab
CW
2311 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2312 'data-none-prompt' => ts('- N/A -'),
2313 'multiple' => FALSE,
2314 'required' => FALSE,
2315 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
be2fb01f 2316 ];
b248d52b 2317 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
8f9c3cbe 2318 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
1d07e7ab
CW
2319 $props['data-select-prompt'] = $props['placeholder'];
2320 $props['data-name'] = $elementName;
2321
2322 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2323
6a6ab43a
CW
2324 // Passing NULL instead of an array of options
2325 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2326 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2327 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
c46f87cf 2328 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
1d07e7ab
CW
2329 }
2330
87ecd5b7 2331 /**
2332 * Add actions menu to results form.
2333 *
c794f667 2334 * @param array $tasks
87ecd5b7 2335 */
2336 public function addTaskMenu($tasks) {
2337 if (is_array($tasks) && !empty($tasks)) {
1a7356e7 2338 // Set constants means this will always load with an empty value, not reloading any submitted value.
2339 // This is appropriate as it is a pseudofield.
be2fb01f 2340 $this->setConstants(['task' => '']);
44543184 2341 $this->assign('taskMetaData', $tasks);
be2fb01f 2342 $select = $this->add('select', 'task', NULL, ['' => ts('Actions')], FALSE, [
518fa0ee
SL
2343 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions',
2344 ]
44543184 2345 );
2346 foreach ($tasks as $key => $task) {
be2fb01f 2347 $attributes = [];
1a7356e7 2348 if (isset($task['data'])) {
2349 foreach ($task['data'] as $dataKey => $dataValue) {
2350 $attributes['data-' . $dataKey] = $dataValue;
2351 }
44543184 2352 }
2353 $select->addOption($task['title'], $key, $attributes);
2354 }
87ecd5b7 2355 if (empty($this->_actionButtonName)) {
2356 $this->_actionButtonName = $this->getButtonName('next', 'action');
2357 }
2358 $this->assign('actionButtonName', $this->_actionButtonName);
be2fb01f 2359 $this->add('submit', $this->_actionButtonName, ts('Go'), ['class' => 'hiddenElement crm-search-go-button']);
87ecd5b7 2360
2361 // Radio to choose "All items" or "Selected items only"
be2fb01f 2362 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', ['checked' => 'checked']);
87ecd5b7 2363 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2364 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2365 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2366
2367 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2368 }
2369 }
2370
1d07e7ab
CW
2371 /**
2372 * Set options and attributes for chain select fields based on the controlling field's value
2373 */
2374 private function preProcessChainSelectFields() {
2375 foreach ($this->_chainSelectFields as $control => $target) {
a3984622
OB
2376 // The 'target' might get missing if extensions do removeElement() in a form hook.
2377 if ($this->elementExists($target)) {
2378 $targetField = $this->getElement($target);
2379 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
be2fb01f 2380 $options = [];
a3984622
OB
2381 // If the control field is on the form, setup chain-select and dynamically populate options
2382 if ($this->elementExists($control)) {
2383 $controlField = $this->getElement($control);
2384 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2385
2386 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2387
2388 $css = (string) $controlField->getAttribute('class');
be2fb01f 2389 $controlField->updateAttributes([
a3984622
OB
2390 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2391 'data-target' => $target,
be2fb01f 2392 ]);
a3984622
OB
2393 $controlValue = $controlField->getValue();
2394 if ($controlValue) {
2395 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2396 if (!$options) {
2397 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2398 }
4a44fd8a 2399 }
b71cb966 2400 else {
a3984622
OB
2401 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2402 $targetField->setAttribute('disabled', 'disabled');
8f9c3cbe 2403 }
0db6c3e1 2404 }
a3984622 2405 // Control field not present - fall back to loading default options
0db6c3e1 2406 else {
a3984622 2407 $options = CRM_Core_PseudoConstant::$targetType();
1d07e7ab 2408 }
a3984622 2409 if (!$targetField->getAttribute('multiple')) {
be2fb01f 2410 $options = ['' => $targetField->getAttribute('placeholder')] + $options;
a3984622
OB
2411 $targetField->removeAttribute('placeholder');
2412 }
be2fb01f 2413 $targetField->_options = [];
a3984622 2414 $targetField->loadArray($options);
1d07e7ab 2415 }
1d07e7ab
CW
2416 }
2417 }
bc999cd1
CW
2418
2419 /**
2420 * Validate country / state / county match and suppress unwanted "required" errors
2421 */
2422 private function validateChainSelectFields() {
2423 foreach ($this->_chainSelectFields as $control => $target) {
a3984622 2424 if ($this->elementExists($control) && $this->elementExists($target)) {
f9f40af3 2425 $controlValue = (array) $this->getElementValue($control);
14b2ff15
CW
2426 $targetField = $this->getElement($target);
2427 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
f9f40af3 2428 $targetValue = array_filter((array) $targetField->getValue());
14b2ff15
CW
2429 if ($targetValue || $this->getElementError($target)) {
2430 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2431 if ($targetValue) {
2432 if (!array_intersect($targetValue, array_keys($options))) {
2433 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2434 }
518fa0ee
SL
2435 }
2436 // Suppress "required" error for field if it has no options
14b2ff15
CW
2437 elseif (!$options) {
2438 $this->setElementError($target, NULL);
bc999cd1
CW
2439 }
2440 }
bc999cd1
CW
2441 }
2442 }
2443 }
96025800 2444
0b50eca0 2445 /**
2446 * Assign billing name to the template.
2447 *
2448 * @param array $params
2449 * Form input params, default to $this->_params.
f3f00653 2450 *
2451 * @return string
0b50eca0 2452 */
be2fb01f 2453 public function assignBillingName($params = []) {
0b50eca0 2454 $name = '';
2455 if (empty($params)) {
2456 $params = $this->_params;
2457 }
2458 if (!empty($params['billing_first_name'])) {
2459 $name = $params['billing_first_name'];
2460 }
2461
2462 if (!empty($params['billing_middle_name'])) {
2463 $name .= " {$params['billing_middle_name']}";
2464 }
2465
2466 if (!empty($params['billing_last_name'])) {
2467 $name .= " {$params['billing_last_name']}";
2468 }
2469 $name = trim($name);
2470 $this->assign('billingName', $name);
2471 return $name;
2472 }
2473
fd0770bc 2474 /**
2475 * Get the currency for the form.
2476 *
2477 * @todo this should be overriden on the forms rather than having this
2478 * historic, possible handling in here. As we clean that up we should
2479 * add deprecation notices into here.
e9bb043a 2480 *
2481 * @param array $submittedValues
2482 * Array allowed so forms inheriting this class do not break.
2483 * Ideally we would make a clear standard around how submitted values
2484 * are stored (is $this->_values consistently doing that?).
2485 *
2486 * @return string
fd0770bc 2487 */
be2fb01f 2488 public function getCurrency($submittedValues = []) {
fd0770bc 2489 $currency = CRM_Utils_Array::value('currency', $this->_values);
2490 // For event forms, currency is in a different spot
2491 if (empty($currency)) {
2492 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2493 }
2494 if (empty($currency)) {
2495 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2496 }
2497 // @todo If empty there is a problem - we should probably put in a deprecation notice
2498 // to warn if that seems to be happening.
2499 return $currency;
2500 }
2501
240b0e65 2502 /**
2503 * Is the form in view or edit mode.
2504 *
2505 * The 'addField' function relies on the form action being one of a set list
2506 * of actions. Checking for these allows for an early return.
2507 *
2508 * @return bool
2509 */
2510 protected function isFormInViewOrEditMode() {
2511 return in_array($this->_action, [
2512 CRM_Core_Action::UPDATE,
2513 CRM_Core_Action::ADD,
2514 CRM_Core_Action::VIEW,
2515 CRM_Core_Action::BROWSE,
2516 CRM_Core_Action::BASIC,
2517 CRM_Core_Action::ADVANCED,
2518 CRM_Core_Action::PREVIEW,
2519 ]);
2520 }
2521
6a488035 2522}