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