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