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