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