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