Merge pull request #22663 from braders/manage-event-listing-default-values
[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 *
a1a2a83d 614 * @return array|NULL
a6c01b45 615 * reference to the array of default values
6a488035 616 */
f9f40af3 617 public function setDefaultValues() {
a1a2a83d 618 return NULL;
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);
722
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
6a488035 1116 /**
fe482240 1117 * Render form and return contents.
6a488035
TO
1118 *
1119 * @return string
6a488035 1120 */
00be9182 1121 public function toSmarty() {
1d07e7ab 1122 $this->preProcessChainSelectFields();
6a488035
TO
1123 $renderer = $this->getRenderer();
1124 $this->accept($renderer);
1125 $content = $renderer->toArray();
1126 $content['formName'] = $this->getName();
b50fdacc
CW
1127 // CRM-15153
1128 $content['formClass'] = CRM_Utils_System::getClassName($this);
a1a6b259 1129 foreach (array_merge($this->getOptionalQuickFormElements(), $this->expectedSmartyVariables) as $string) {
0a6789e4
EM
1130 if (!array_key_exists($string, $content)) {
1131 $content[$string] = NULL;
1132 }
1133 }
6a488035
TO
1134 return $content;
1135 }
1136
1137 /**
3bdf1f3a 1138 * Getter function for renderer.
1139 *
1140 * If renderer is not set create one and initialize it.
6a488035
TO
1141 *
1142 * @return object
6a488035 1143 */
00be9182 1144 public function &getRenderer() {
6a488035
TO
1145 if (!isset($this->_renderer)) {
1146 $this->_renderer = CRM_Core_Form_Renderer::singleton();
1147 }
1148 return $this->_renderer;
1149 }
1150
1151 /**
fe482240 1152 * Use the form name to create the tpl file name.
6a488035
TO
1153 *
1154 * @return string
6a488035 1155 */
00be9182 1156 public function getTemplateFileName() {
6a488035
TO
1157 $ext = CRM_Extension_System::singleton()->getMapper();
1158 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
1159 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
1160 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
1161 }
1162 else {
9b591d79
TO
1163 $tplname = strtr(
1164 CRM_Utils_System::getClassName($this),
be2fb01f 1165 [
9b591d79
TO
1166 '_' => DIRECTORY_SEPARATOR,
1167 '\\' => DIRECTORY_SEPARATOR,
be2fb01f 1168 ]
9b591d79 1169 ) . '.tpl';
6a488035
TO
1170 }
1171 return $tplname;
1172 }
1173
8aac22c8 1174 /**
3bdf1f3a 1175 * A wrapper for getTemplateFileName.
1176 *
1177 * This includes calling the hook to prevent us from having to copy & paste the logic of calling the hook.
8aac22c8 1178 */
00be9182 1179 public function getHookedTemplateFileName() {
8aac22c8 1180 $pageTemplateFile = $this->getTemplateFileName();
1181 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
1182 return $pageTemplateFile;
1183 }
1184
6a488035 1185 /**
3bdf1f3a 1186 * Default extra tpl file basically just replaces .tpl with .extra.tpl.
1187 *
1188 * i.e. we do not override.
6a488035
TO
1189 *
1190 * @return string
6a488035 1191 */
00be9182 1192 public function overrideExtraTemplateFileName() {
6a488035
TO
1193 return NULL;
1194 }
1195
1196 /**
fe482240 1197 * Error reporting mechanism.
6a488035 1198 *
6a0b768e
TO
1199 * @param string $message
1200 * Error Message.
1201 * @param int $code
1202 * Error Code.
1203 * @param CRM_Core_DAO $dao
1204 * A data access object on which we perform a rollback if non - empty.
6a488035 1205 */
00be9182 1206 public function error($message, $code = NULL, $dao = NULL) {
6a488035
TO
1207 if ($dao) {
1208 $dao->query('ROLLBACK');
1209 }
1210
1211 $error = CRM_Core_Error::singleton();
1212
1213 $error->push($code, $message);
1214 }
1215
1216 /**
fe482240 1217 * Store the variable with the value in the form scope.
6a488035 1218 *
6c552737
TO
1219 * @param string $name
1220 * Name of the variable.
1221 * @param mixed $value
1222 * Value of the variable.
6a488035 1223 */
00be9182 1224 public function set($name, $value) {
6a488035
TO
1225 $this->controller->set($name, $value);
1226 }
1227
1228 /**
fe482240 1229 * Get the variable from the form scope.
6a488035 1230 *
6c552737
TO
1231 * @param string $name
1232 * Name of the variable
6a488035
TO
1233 *
1234 * @return mixed
6a488035 1235 */
00be9182 1236 public function get($name) {
6a488035
TO
1237 return $this->controller->get($name);
1238 }
1239
1240 /**
fe482240 1241 * Getter for action.
6a488035
TO
1242 *
1243 * @return int
6a488035 1244 */
00be9182 1245 public function getAction() {
6a488035
TO
1246 return $this->_action;
1247 }
1248
1249 /**
fe482240 1250 * Setter for action.
6a488035 1251 *
6a0b768e
TO
1252 * @param int $action
1253 * The mode we want to set the form.
6a488035 1254 */
00be9182 1255 public function setAction($action) {
6a488035
TO
1256 $this->_action = $action;
1257 }
1258
1259 /**
fe482240 1260 * Assign value to name in template.
6a488035 1261 *
6a0b768e
TO
1262 * @param string $var
1263 * Name of variable.
1264 * @param mixed $value
1265 * Value of variable.
6a488035 1266 */
00be9182 1267 public function assign($var, $value = NULL) {
6a488035
TO
1268 self::$_template->assign($var, $value);
1269 }
1270
1271 /**
fe482240 1272 * Assign value to name in template by reference.
6a488035 1273 *
6a0b768e
TO
1274 * @param string $var
1275 * Name of variable.
1276 * @param mixed $value
8eedd10a 1277 * Value of variable.
6a488035 1278 */
00be9182 1279 public function assign_by_ref($var, &$value) {
6a488035
TO
1280 self::$_template->assign_by_ref($var, $value);
1281 }
1282
4a9538ac 1283 /**
fe482240 1284 * Appends values to template variables.
4a9538ac
CW
1285 *
1286 * @param array|string $tpl_var the template variable name(s)
6a0b768e
TO
1287 * @param mixed $value
1288 * The value to append.
4a9538ac
CW
1289 * @param bool $merge
1290 */
f9f40af3 1291 public function append($tpl_var, $value = NULL, $merge = FALSE) {
4a9538ac
CW
1292 self::$_template->append($tpl_var, $value, $merge);
1293 }
1294
1295 /**
fe482240 1296 * Returns an array containing template variables.
4a9538ac
CW
1297 *
1298 * @param string $name
2a6da8d7 1299 *
4a9538ac
CW
1300 * @return array
1301 */
f9f40af3 1302 public function get_template_vars($name = NULL) {
4a9538ac
CW
1303 return self::$_template->get_template_vars($name);
1304 }
1305
a0ee3941 1306 /**
100fef9d 1307 * @param string $name
3fd42bb5
BT
1308 * @param string $title
1309 * @param array $values
a0ee3941 1310 * @param array $attributes
3fd42bb5 1311 * @param string $separator
a0ee3941 1312 * @param bool $required
15a05fc2 1313 * @param array $optionAttributes - Option specific attributes
a0ee3941
EM
1314 *
1315 * @return HTML_QuickForm_group
1316 */
15a05fc2 1317 public function &addRadio($name, $title, $values, $attributes = [], $separator = NULL, $required = FALSE, $optionAttributes = []) {
be2fb01f
CW
1318 $options = [];
1319 $attributes = $attributes ? $attributes : [];
b847e6e7
CW
1320 $allowClear = !empty($attributes['allowClear']);
1321 unset($attributes['allowClear']);
385f11fd 1322 $attributes['id_suffix'] = $name;
6a488035 1323 foreach ($values as $key => $var) {
15a05fc2
SL
1324 $optAttributes = $attributes;
1325 if (!empty($optionAttributes[$key])) {
1326 foreach ($optionAttributes[$key] as $optAttr => $optVal) {
9ca69805 1327 $optAttributes[$optAttr] = ltrim(($optAttributes[$optAttr] ?? '') . ' ' . $optVal);
15a05fc2
SL
1328 }
1329 }
996b358b
SL
1330 // We use a class here to avoid html5 issues with collapsed cutsomfield sets.
1331 $optAttributes['class'] = $optAttributes['class'] ?? '';
dd283518 1332 if ($required) {
996b358b 1333 $optAttributes['class'] .= ' required';
dd283518 1334 }
996b358b 1335 $element = $this->createElement('radio', NULL, NULL, $var, $key, $optAttributes);
dd283518 1336 $options[] = $element;
6a488035
TO
1337 }
1338 $group = $this->addGroup($options, $name, $title, $separator);
3ef93345
MD
1339
1340 $optionEditKey = 'data-option-edit-path';
1341 if (!empty($attributes[$optionEditKey])) {
1342 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1343 }
1344
6a488035 1345 if ($required) {
be2fb01f 1346 $this->addRule($name, ts('%1 is a required field.', [1 => $title]), 'required');
6a488035 1347 }
b847e6e7
CW
1348 if ($allowClear) {
1349 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1350 }
6a488035
TO
1351 return $group;
1352 }
1353
a0ee3941 1354 /**
3fd42bb5
BT
1355 * @param string $id
1356 * @param string $title
a0ee3941 1357 * @param bool $allowClear
3fd42bb5 1358 * @param bool $required
a0ee3941
EM
1359 * @param array $attributes
1360 */
be2fb01f
CW
1361 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = []) {
1362 $attributes += ['id_suffix' => $id];
1363 $choice = [];
8a4f27dc
CW
1364 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1365 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
6a488035 1366
8a4f27dc 1367 $group = $this->addGroup($choice, $id, $title);
b847e6e7
CW
1368 if ($allowClear) {
1369 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1370 }
6a488035 1371 if ($required) {
be2fb01f 1372 $this->addRule($id, ts('%1 is a required field.', [1 => $title]), 'required');
6a488035
TO
1373 }
1374 }
1375
a0ee3941 1376 /**
100fef9d 1377 * @param int $id
3fd42bb5
BT
1378 * @param string $title
1379 * @param array $values
a0ee3941
EM
1380 * @param null $other
1381 * @param null $attributes
1382 * @param null $required
1383 * @param null $javascriptMethod
1384 * @param string $separator
1385 * @param bool $flipValues
1386 */
2da40d21 1387 public function addCheckBox(
f9f40af3
TO
1388 $id, $title, $values, $other = NULL,
1389 $attributes = NULL, $required = NULL,
6a488035 1390 $javascriptMethod = NULL,
f9f40af3 1391 $separator = '<br />', $flipValues = FALSE
6a488035 1392 ) {
be2fb01f 1393 $options = [];
6a488035
TO
1394
1395 if ($javascriptMethod) {
1396 foreach ($values as $key => $var) {
1397 if (!$flipValues) {
3ef93345 1398 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod, $attributes);
6a488035
TO
1399 }
1400 else {
3ef93345 1401 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod, $attributes);
6a488035
TO
1402 }
1403 }
1404 }
1405 else {
1406 foreach ($values as $key => $var) {
1407 if (!$flipValues) {
3ef93345 1408 $options[] = $this->createElement('checkbox', $var, NULL, $key, $attributes);
6a488035
TO
1409 }
1410 else {
3ef93345 1411 $options[] = $this->createElement('checkbox', $key, NULL, $var, $attributes);
6a488035
TO
1412 }
1413 }
1414 }
1415
3ef93345
MD
1416 $group = $this->addGroup($options, $id, $title, $separator);
1417 $optionEditKey = 'data-option-edit-path';
1418 if (!empty($attributes[$optionEditKey])) {
1419 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1420 }
6a488035
TO
1421
1422 if ($other) {
1423 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1424 }
1425
1426 if ($required) {
1427 $this->addRule($id,
be2fb01f 1428 ts('%1 is a required field.', [1 => $title]),
6a488035
TO
1429 'required'
1430 );
1431 }
1432 }
1433
00be9182 1434 public function resetValues() {
6a488035 1435 $data = $this->controller->container();
be2fb01f 1436 $data['values'][$this->_name] = [];
6a488035
TO
1437 }
1438
1439 /**
100fef9d 1440 * Simple shell that derived classes can call to add buttons to
6a488035
TO
1441 * the form with a customized title for the main Submit
1442 *
6a0b768e
TO
1443 * @param string $title
1444 * Title of the main button.
1445 * @param string $nextType
1446 * Button type for the form after processing.
fd31fa4c 1447 * @param string $backType
423616fa 1448 * @param bool|string $submitOnce
6a488035 1449 */
00be9182 1450 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
be2fb01f 1451 $buttons = [];
6a488035 1452 if ($backType != NULL) {
be2fb01f 1453 $buttons[] = [
6a488035
TO
1454 'type' => $backType,
1455 'name' => ts('Previous'),
be2fb01f 1456 ];
6a488035
TO
1457 }
1458 if ($nextType != NULL) {
be2fb01f 1459 $nextButton = [
6a488035
TO
1460 'type' => $nextType,
1461 'name' => $title,
1462 'isDefault' => TRUE,
be2fb01f 1463 ];
6a488035 1464 if ($submitOnce) {
423616fa 1465 $this->submitOnce = TRUE;
6a488035
TO
1466 }
1467 $buttons[] = $nextButton;
1468 }
1469 $this->addButtons($buttons);
1470 }
1471
a0ee3941 1472 /**
100fef9d 1473 * @param string $name
a0ee3941
EM
1474 * @param string $from
1475 * @param string $to
1476 * @param string $label
1477 * @param string $dateFormat
1478 * @param bool $required
1479 * @param bool $displayTime
1480 */
00be9182 1481 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
eb81078f 1482 CRM_Core_Error::deprecatedFunctionWarning('Use CRM_Core_Form::addDatePickerRange insted');
6a488035 1483 if ($displayTime) {
be2fb01f
CW
1484 $this->addDateTime($name . $from, $label, $required, ['formatType' => $dateFormat]);
1485 $this->addDateTime($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
0db6c3e1
TO
1486 }
1487 else {
be2fb01f
CW
1488 $this->addDate($name . $from, $label, $required, ['formatType' => $dateFormat]);
1489 $this->addDate($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
6a488035
TO
1490 }
1491 }
d5965a37 1492
e2123607 1493 /**
1494 * Add a search for a range using date picker fields.
1495 *
1496 * @param string $fieldName
1497 * @param string $label
27cedb98 1498 * @param bool $isDateTime
1499 * Is this a date-time field (not just date).
e2123607 1500 * @param bool $required
1501 * @param string $fromLabel
1502 * @param string $toLabel
6d6630cf
SL
1503 * @param array $additionalOptions
1504 * @param string $to string to append to the to field.
1505 * @param string $from string to append to the from field.
e2123607 1506 */
6d6630cf
SL
1507 public function addDatePickerRange($fieldName, $label, $isDateTime = FALSE, $required = FALSE, $fromLabel = 'From', $toLabel = 'To', $additionalOptions = [],
1508 $to = '_high', $from = '_low') {
e2123607 1509
be2fb01f 1510 $options = [
e2123607 1511 '' => ts('- any -'),
1512 0 => ts('Choose Date Range'),
be2fb01f 1513 ] + CRM_Core_OptionGroup::values('relative_date_filters');
e2123607 1514
6d6630cf
SL
1515 if ($additionalOptions) {
1516 foreach ($additionalOptions as $key => $optionLabel) {
1517 $options[$key] = $optionLabel;
1518 }
1519 }
1520
e2123607 1521 $this->add('select',
1522 "{$fieldName}_relative",
1523 $label,
1524 $options,
1525 $required,
736fb4c2 1526 ['class' => 'crm-select2']
e2123607 1527 );
db91a581 1528 $attributes = ['formatType' => 'searchDate'];
27cedb98 1529 $extra = ['time' => $isDateTime];
6d6630cf
SL
1530 $this->add('datepicker', $fieldName . $from, ts($fromLabel), $attributes, $required, $extra);
1531 $this->add('datepicker', $fieldName . $to, ts($toLabel), $attributes, $required, $extra);
e2123607 1532 }
1533
03225ad6
CW
1534 /**
1535 * Based on form action, return a string representing the api action.
1536 * Used by addField method.
1537 *
1538 * Return string
1539 */
d5e4784e 1540 protected function getApiAction() {
03225ad6
CW
1541 $action = $this->getAction();
1542 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1543 return 'create';
1544 }
889dbed8 1545 if ($action & (CRM_Core_Action::VIEW + CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
03225ad6
CW
1546 return 'get';
1547 }
079c7954
CW
1548 if ($action & (CRM_Core_Action::DELETE)) {
1549 return 'delete';
1550 }
03225ad6 1551 // If you get this exception try adding more cases above.
0e02cb01 1552 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
03225ad6
CW
1553 }
1554
6e62b28c 1555 /**
d5965a37 1556 * Classes extending CRM_Core_Form should implement this method.
6e62b28c
TM
1557 * @throws Exception
1558 */
1559 public function getDefaultEntity() {
0e02cb01 1560 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
6e62b28c 1561 }
6a488035 1562
1ae720b3
TM
1563 /**
1564 * Classes extending CRM_Core_Form should implement this method.
1565 *
1566 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1567 * @throws Exception
1568 */
1569 public function getDefaultContext() {
0e02cb01 1570 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1ae720b3
TM
1571 }
1572
5fafc9b0 1573 /**
fe482240 1574 * Adds a select based on field metadata.
5fafc9b0 1575 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
475e9f44 1576 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
fa3fdebc 1577 * @param string $name
6a0b768e
TO
1578 * Field name to go on the form.
1579 * @param array $props
1580 * Mix of html attributes and special properties, namely.
920600e1
CW
1581 * - entity (api entity name, can usually be inferred automatically from the form class)
1582 * - field (field name - only needed if different from name used on the form)
1583 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1584 * - placeholder - set to NULL to disable
d0def949 1585 * - multiple - bool
76773c5a 1586 * - context - @see CRM_Core_DAO::buildOptionsContext
5fafc9b0
CW
1587 * @param bool $required
1588 * @throws CRM_Core_Exception
1589 * @return HTML_QuickForm_Element
1590 */
be2fb01f 1591 public function addSelect($name, $props = [], $required = FALSE) {
920600e1 1592 if (!isset($props['entity'])) {
6e62b28c 1593 $props['entity'] = $this->getDefaultEntity();
6a488035 1594 }
920600e1
CW
1595 if (!isset($props['field'])) {
1596 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
e869b07d 1597 }
65e8615b
CW
1598 if (!isset($props['context'])) {
1599 try {
1600 $props['context'] = $this->getDefaultContext();
1601 }
1602 // This is not a required param, so we'll ignore if this doesn't exist.
518fa0ee
SL
1603 catch (Exception $e) {
1604 }
65e8615b 1605 }
f76b27fe
CW
1606 // Fetch options from the api unless passed explicitly
1607 if (isset($props['options'])) {
1608 $options = $props['options'];
1609 }
1610 else {
76352fbc 1611 $info = civicrm_api3($props['entity'], 'getoptions', $props);
f76b27fe
CW
1612 $options = $info['values'];
1613 }
a2eb6152
AH
1614 if (!array_key_exists('placeholder', $props) && $placeholder = self::selectOrAnyPlaceholder($props, $required)) {
1615 $props['placeholder'] = $placeholder;
5fafc9b0 1616 }
5fafc9b0
CW
1617 // Handle custom field
1618 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
0a6789e4 1619 [, $id] = explode('_', $name);
2e1f50d6 1620 $label = $props['label'] ?? CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
475e9f44 1621 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
76773c5a
CW
1622 if (CRM_Utils_Array::value('context', $props) != 'search') {
1623 $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);
1624 }
5fafc9b0
CW
1625 }
1626 // Core field
6a488035 1627 else {
f76b27fe 1628 $info = civicrm_api3($props['entity'], 'getfields');
22e263ad 1629 foreach ($info['values'] as $uniqueName => $fieldSpec) {
e869b07d 1630 if (
920600e1
CW
1631 $uniqueName === $props['field'] ||
1632 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
be2fb01f 1633 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, []))
e869b07d
CW
1634 ) {
1635 break;
1636 }
6a488035 1637 }
2e1f50d6 1638 $label = $props['label'] ?? $fieldSpec['title'];
76773c5a 1639 if (CRM_Utils_Array::value('context', $props) != 'search') {
599ae208 1640 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
76773c5a 1641 }
6a488035 1642 }
920600e1
CW
1643 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1644 $props['data-api-entity'] = $props['entity'];
1645 $props['data-api-field'] = $props['field'];
76773c5a 1646 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
5fafc9b0 1647 return $this->add('select', $name, $label, $options, $required, $props);
6a488035
TO
1648 }
1649
a2eb6152
AH
1650 /**
1651 * Handles a repeated bit supplying a placeholder for entity selection
1652 *
1653 * @param string $props
1654 * The field properties, including the entity and context.
1655 * @param bool $required
1656 * If the field is required.
abe8ec96
AH
1657 * @param string $title
1658 * A field title, if applicable.
a2eb6152
AH
1659 * @return string
1660 * The placeholder text.
1661 */
abe8ec96 1662 private static function selectOrAnyPlaceholder($props, $required, $title = NULL) {
a2eb6152
AH
1663 if (empty($props['entity'])) {
1664 return NULL;
1665 }
abe8ec96
AH
1666 if (!$title) {
1667 $daoToClass = CRM_Core_DAO_AllCoreTables::daoToClass();
1668 if (array_key_exists($props['entity'], $daoToClass)) {
1669 $daoClass = $daoToClass[$props['entity']];
1670 $title = $daoClass::getEntityTitle();
1671 }
1672 else {
1673 $title = ts('option');
1674 }
a2eb6152
AH
1675 }
1676 if (($props['context'] ?? '') == 'search' && !$required) {
abe8ec96 1677 return ts('- any %1 -', [1 => $title]);
a2eb6152 1678 }
abe8ec96 1679 return ts('- select %1 -', [1 => $title]);
a2eb6152
AH
1680 }
1681
7ec4548b
TM
1682 /**
1683 * Adds a field based on metadata.
1684 *
1685 * @param $name
1686 * Field name to go on the form.
1687 * @param array $props
1688 * Mix of html attributes and special properties, namely.
1689 * - entity (api entity name, can usually be inferred automatically from the form class)
03225ad6 1690 * - name (field name - only needed if different from name used on the form)
7ec4548b
TM
1691 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1692 * - placeholder - set to NULL to disable
1693 * - multiple - bool
1694 * - context - @see CRM_Core_DAO::buildOptionsContext
1695 * @param bool $required
ed0ca248 1696 * @param bool $legacyDate
1697 * Temporary param to facilitate the conversion of fields to use the datepicker in
1698 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1699 * tpl as well. That file is intended to be EOL.
1700 *
03225ad6
CW
1701 * @throws \CiviCRM_API3_Exception
1702 * @throws \Exception
518fa0ee
SL
1703 * @return mixed
1704 * HTML_QuickForm_Element
1705 * void
7ec4548b 1706 */
be2fb01f 1707 public function addField($name, $props = [], $required = FALSE, $legacyDate = TRUE) {
1ae720b3 1708 // Resolve context.
916b6181 1709 if (empty($props['context'])) {
1ae720b3
TM
1710 $props['context'] = $this->getDefaultContext();
1711 }
916b6181 1712 $context = $props['context'];
7ec4548b 1713 // Resolve entity.
916b6181 1714 if (empty($props['entity'])) {
7ec4548b
TM
1715 $props['entity'] = $this->getDefaultEntity();
1716 }
1717 // Resolve field.
916b6181 1718 if (empty($props['name'])) {
03225ad6 1719 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
7ec4548b 1720 }
03225ad6 1721 // Resolve action.
916b6181 1722 if (empty($props['action'])) {
03225ad6 1723 $props['action'] = $this->getApiAction();
7ec4548b 1724 }
2b31bc15
CW
1725
1726 // Handle custom fields
1727 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1728 $fieldId = (int) substr($name, 7);
916b6181 1729 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
2b31bc15
CW
1730 }
1731
1732 // Core field - get metadata.
d60a6fba 1733 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
03225ad6 1734 $fieldSpec = $fieldSpec['values'];
2e1f50d6 1735 $fieldSpecLabel = $fieldSpec['html']['label'] ?? CRM_Utils_Array::value('title', $fieldSpec);
80a96508 1736 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
7ec4548b 1737
2e1f50d6 1738 $widget = $props['type'] ?? $fieldSpec['html']['type'];
916b6181 1739 if ($widget == 'TextArea' && $context == 'search') {
7ec4548b
TM
1740 $widget = 'Text';
1741 }
1742
be2fb01f 1743 $isSelect = (in_array($widget, [
518fa0ee 1744 'Select',
18680e7a 1745 'Select2',
518fa0ee
SL
1746 'CheckBoxGroup',
1747 'RadioGroup',
1748 'Radio',
be2fb01f 1749 ]));
7ec4548b
TM
1750
1751 if ($isSelect) {
2f32ed10 1752 // Fetch options from the api unless passed explicitly.
7ec4548b
TM
1753 if (isset($props['options'])) {
1754 $options = $props['options'];
1755 }
1756 else {
2e1f50d6 1757 $options = $fieldSpec['options'] ?? NULL;
7ec4548b 1758 }
916b6181 1759 if ($context == 'search') {
18680e7a 1760 $widget = $widget == 'Select2' ? $widget : 'Select';
65e8615b 1761 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
7ec4548b 1762 }
7ec4548b
TM
1763
1764 // Add data for popup link.
3ef93345
MD
1765 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1766 $hasOptionUrl = !empty($props['option_url']);
1767 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1768 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1769
1770 // Only add if key is not set, or if non-empty option url is provided
1771 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1772 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1773 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1774 $props['data-option-edit-path'] = $optionUrl;
7ec4548b 1775 $props['data-api-entity'] = $props['entity'];
03225ad6 1776 $props['data-api-field'] = $props['name'];
7ec4548b
TM
1777 }
1778 }
be2fb01f 1779 $props += CRM_Utils_Array::value('html', $fieldSpec, []);
abe8ec96
AH
1780 if (in_array($widget, ['Select', 'Select2'])
1781 && !array_key_exists('placeholder', $props)
1782 && $placeholder = self::selectOrAnyPlaceholder($props, $required, $label)) {
1783 $props['placeholder'] = $placeholder;
1784 }
65e8615b 1785 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
599ae208 1786
b44e3f84 1787 // TODO: refactor switch statement, to separate methods.
7ec4548b
TM
1788 switch ($widget) {
1789 case 'Text':
d8f1758d
CW
1790 case 'Url':
1791 case 'Number':
1792 case 'Email':
7ec4548b 1793 //TODO: Autodetect ranges
2e1f50d6 1794 $props['size'] = $props['size'] ?? 60;
d8f1758d 1795 return $this->add(strtolower($widget), $name, $label, $props, $required);
7ec4548b 1796
b4b53245 1797 case 'hidden':
2a300b65 1798 return $this->add('hidden', $name, NULL, $props, $required);
b4b53245 1799
0efbca68
TM
1800 case 'TextArea':
1801 //Set default columns and rows for textarea.
2e1f50d6
CW
1802 $props['rows'] = $props['rows'] ?? 4;
1803 $props['cols'] = $props['cols'] ?? 60;
079f52de 1804 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
ed71bbca 1805 $props['maxlength'] = $fieldSpec['length'];
1806 }
599ae208 1807 return $this->add('textarea', $name, $label, $props, $required);
0efbca68 1808
db3ec100 1809 case 'Select Date':
ed0ca248 1810 // This is a white list for fields that have been tested with
1811 // date picker. We should be able to remove the other
1812 if ($legacyDate) {
1813 //TODO: add range support
1814 //TODO: Add date formats
1815 //TODO: Add javascript template for dates.
1816 return $this->addDate($name, $label, $required, $props);
1817 }
1818 else {
1819 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
db91a581 1820 $attributes = ['format' => $fieldSpec['date_format']];
ed0ca248 1821 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1822 }
db3ec100 1823
a4969aee 1824 case 'Radio':
2e1f50d6 1825 $separator = $props['separator'] ?? NULL;
125d54e1 1826 unset($props['separator']);
ef3a048a 1827 if (!isset($props['allowClear'])) {
125d54e1 1828 $props['allowClear'] = !$required;
ef3a048a 1829 }
2a300b65 1830 return $this->addRadio($name, $label, $options, $props, $separator, $required);
a4969aee 1831
b248d52b 1832 case 'ChainSelect':
be2fb01f 1833 $props += [
b248d52b
CW
1834 'required' => $required,
1835 'label' => $label,
916b6181 1836 'multiple' => $context == 'search',
be2fb01f 1837 ];
b248d52b
CW
1838 return $this->addChainSelect($name, $props);
1839
7ec4548b 1840 case 'Select':
18680e7a 1841 case 'Select2':
b248d52b 1842 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
7ec4548b 1843 // TODO: Add and/or option for fields that store multiple values
18680e7a 1844 return $this->add(strtolower($widget), $name, $label, $options, $required, $props);
7ec4548b 1845
dd4706ef 1846 case 'CheckBoxGroup':
2a300b65 1847 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
dd4706ef
TM
1848
1849 case 'RadioGroup':
2a300b65 1850 return $this->addRadio($name, $label, $options, $props, NULL, $required);
dd4706ef 1851
a4969aee 1852 case 'CheckBox':
95c2e666 1853 if ($context === 'search') {
1854 $this->addYesNo($name, $label, TRUE, FALSE, $props);
1855 return;
1856 }
2e1f50d6 1857 $text = $props['text'] ?? NULL;
999ab5e1 1858 unset($props['text']);
2a300b65 1859 return $this->addElement('checkbox', $name, $label, $text, $props);
a4969aee 1860
50471995 1861 //add support for 'Advcheckbox' field
1862 case 'advcheckbox':
2e1f50d6 1863 $text = $props['text'] ?? NULL;
b0964781 1864 unset($props['text']);
1865 return $this->addElement('advcheckbox', $name, $label, $text, $props);
50471995 1866
33fa033c
TM
1867 case 'File':
1868 // We should not build upload file in search mode.
916b6181 1869 if ($context == 'search') {
33fa033c
TM
1870 return;
1871 }
2a300b65 1872 $file = $this->add('file', $name, $label, $props, $required);
33fa033c 1873 $this->addUploadElement($name);
2a300b65 1874 return $file;
33fa033c 1875
b66c1d2c
CW
1876 case 'RichTextEditor':
1877 return $this->add('wysiwyg', $name, $label, $props, $required);
1878
b58770ea 1879 case 'EntityRef':
2a300b65 1880 return $this->addEntityRef($name, $label, $props, $required);
b58770ea 1881
e9bc5dcc 1882 case 'Password':
2e1f50d6 1883 $props['size'] = $props['size'] ?? 60;
e9bc5dcc
SL
1884 return $this->add('password', $name, $label, $props, $required);
1885
7ec4548b
TM
1886 // Check datatypes of fields
1887 // case 'Int':
1888 //case 'Float':
1889 //case 'Money':
7ec4548b
TM
1890 //case read only fields
1891 default:
1892 throw new Exception("Unsupported html-element " . $widget);
1893 }
1894 }
1895
6a488035
TO
1896 /**
1897 * Add a widget for selecting/editing/creating/copying a profile form
1898 *
6a0b768e
TO
1899 * @param string $name
1900 * HTML form-element name.
1901 * @param string $label
1902 * Printable label.
1903 * @param string $allowCoreTypes
1904 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1905 * @param string $allowSubTypes
1906 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
6a488035 1907 * @param array $entities
6a0b768e
TO
1908 * @param bool $default
1909 * //CRM-15427.
54957108 1910 * @param string $usedFor
6a488035 1911 */
37375016 1912 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
6a488035
TO
1913 // Output widget
1914 // FIXME: Instead of adhoc serialization, use a single json_encode()
1915 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1916 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
be2fb01f 1917 $this->add('text', $name, $label, [
6a488035
TO
1918 'class' => 'crm-profile-selector',
1919 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1920 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1921 'data-entities' => json_encode($entities),
99e239bc 1922 //CRM-15427
1923 'data-default' => $default,
37375016 1924 'data-usedfor' => json_encode($usedFor),
be2fb01f 1925 ]);
6a488035
TO
1926 }
1927
a0ee3941
EM
1928 /**
1929 * @return null
1930 */
6a488035
TO
1931 public function getRootTitle() {
1932 return NULL;
1933 }
1934
a0ee3941
EM
1935 /**
1936 * @return string
1937 */
6a488035
TO
1938 public function getCompleteTitle() {
1939 return $this->getRootTitle() . $this->getTitle();
1940 }
1941
a0ee3941
EM
1942 /**
1943 * @return CRM_Core_Smarty
1944 */
00be9182 1945 public static function &getTemplate() {
6a488035
TO
1946 return self::$_template;
1947 }
1948
a0ee3941 1949 /**
3fd42bb5 1950 * @param string[]|string $elementName
a0ee3941 1951 */
00be9182 1952 public function addUploadElement($elementName) {
6a488035
TO
1953 $uploadNames = $this->get('uploadNames');
1954 if (!$uploadNames) {
be2fb01f 1955 $uploadNames = [];
6a488035
TO
1956 }
1957 if (is_array($elementName)) {
1958 foreach ($elementName as $name) {
1959 if (!in_array($name, $uploadNames)) {
1960 $uploadNames[] = $name;
1961 }
1962 }
1963 }
1964 else {
1965 if (!in_array($elementName, $uploadNames)) {
1966 $uploadNames[] = $elementName;
1967 }
1968 }
1969 $this->set('uploadNames', $uploadNames);
1970
1971 $config = CRM_Core_Config::singleton();
1972 if (!empty($uploadNames)) {
1973 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1974 }
1975 }
1976
a0ee3941 1977 /**
3fd42bb5 1978 * @param string $name
a0ee3941 1979 *
3fd42bb5 1980 * @return mixed
a0ee3941 1981 */
00be9182 1982 public function getVar($name) {
2e1f50d6 1983 return $this->$name ?? NULL;
6a488035
TO
1984 }
1985
a0ee3941 1986 /**
3fd42bb5
BT
1987 * @param string $name
1988 * @param mixed $value
a0ee3941 1989 */
00be9182 1990 public function setVar($name, $value) {
6a488035
TO
1991 $this->$name = $value;
1992 }
1993
1994 /**
fe482240 1995 * Add date.
6a488035 1996 *
013ac5df
CW
1997 * @deprecated
1998 * Use $this->add('datepicker', ...) instead.
a1a2a83d
TO
1999 *
2000 * @param string $name
2001 * Name of the element.
2002 * @param string $label
2003 * Label of the element.
6a0b768e
TO
2004 * @param bool $required
2005 * True if required.
a1a2a83d
TO
2006 * @param array $attributes
2007 * Key / value pair.
6a488035 2008 */
00be9182 2009 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
a7488080 2010 if (!empty($attributes['formatType'])) {
6a488035 2011 // get actual format
be2fb01f
CW
2012 $params = ['name' => $attributes['formatType']];
2013 $values = [];
6a488035
TO
2014
2015 // cache date information
2016 static $dateFormat;
2017 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
a7488080 2018 if (empty($dateFormat[$key])) {
6a488035
TO
2019 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
2020 $dateFormat[$key] = $values;
2021 }
2022 else {
2023 $values = $dateFormat[$key];
2024 }
2025
2026 if ($values['date_format']) {
2027 $attributes['format'] = $values['date_format'];
2028 }
2029
a7488080 2030 if (!empty($values['time_format'])) {
6a488035
TO
2031 $attributes['timeFormat'] = $values['time_format'];
2032 }
2033 $attributes['startOffset'] = $values['start'];
2034 $attributes['endOffset'] = $values['end'];
2035 }
2036
2037 $config = CRM_Core_Config::singleton();
a7488080 2038 if (empty($attributes['format'])) {
6a488035
TO
2039 $attributes['format'] = $config->dateInputFormat;
2040 }
2041
2042 if (!isset($attributes['startOffset'])) {
2043 $attributes['startOffset'] = 10;
2044 }
2045
2046 if (!isset($attributes['endOffset'])) {
2047 $attributes['endOffset'] = 10;
2048 }
2049
2050 $this->add('text', $name, $label, $attributes);
2051
8cc574cf 2052 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
6a488035
TO
2053
2054 if (!isset($attributes['timeFormat'])) {
2055 $timeFormat = $config->timeInputFormat;
2056 }
2057 else {
2058 $timeFormat = $attributes['timeFormat'];
2059 }
2060
2061 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
2062 if ($timeFormat) {
2063 $show24Hours = TRUE;
2064 if ($timeFormat == 1) {
2065 $show24Hours = FALSE;
2066 }
2067
2068 //CRM-6664 -we are having time element name
2069 //in either flat string or an array format.
2070 $elementName = $name . '_time';
2071 if (substr($name, -1) == ']') {
2072 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
2073 }
2074
be2fb01f 2075 $this->add('text', $elementName, ts('Time'), ['timeFormat' => $show24Hours]);
6a488035
TO
2076 }
2077 }
2078
2079 if ($required) {
be2fb01f 2080 $this->addRule($name, ts('Please select %1', [1 => $label]), 'required');
8cc574cf 2081 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
6a488035
TO
2082 $this->addRule($elementName, ts('Please enter a time.'), 'required');
2083 }
2084 }
2085 }
2086
2087 /**
013ac5df
CW
2088 * Function that will add date and time.
2089 *
2090 * @deprecated
2091 * Use $this->add('datepicker', ...) instead.
54957108 2092 *
2093 * @param string $name
2094 * @param string $label
2095 * @param bool $required
3fd42bb5 2096 * @param array $attributes
6a488035 2097 */
00be9182 2098 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
be2fb01f 2099 $addTime = ['addTime' => TRUE];
6a488035
TO
2100 if (is_array($attributes)) {
2101 $attributes = array_merge($attributes, $addTime);
2102 }
2103 else {
2104 $attributes = $addTime;
2105 }
2106
2107 $this->addDate($name, $label, $required, $attributes);
2108 }
2109
2110 /**
fe482240 2111 * Add a currency and money element to the form.
3bdf1f3a 2112 *
2113 * @param string $name
2114 * @param string $label
2115 * @param bool $required
3fd42bb5 2116 * @param array $attributes
3bdf1f3a 2117 * @param bool $addCurrency
2118 * @param string $currencyName
3fd42bb5 2119 * @param string $defaultCurrency
3bdf1f3a 2120 * @param bool $freezeCurrency
2121 *
2122 * @return \HTML_QuickForm_Element
6a488035 2123 */
2da40d21 2124 public function addMoney(
f9f40af3 2125 $name,
6a488035 2126 $label,
f9f40af3
TO
2127 $required = FALSE,
2128 $attributes = NULL,
2129 $addCurrency = TRUE,
2130 $currencyName = 'currency',
6a488035 2131 $defaultCurrency = NULL,
f9f40af3 2132 $freezeCurrency = FALSE
6a488035
TO
2133 ) {
2134 $element = $this->add('text', $name, $label, $attributes, $required);
2135 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
2136
2137 if ($addCurrency) {
2138 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
2139 }
2140
2141 return $element;
2142 }
2143
2144 /**
fe482240 2145 * Add currency element to the form.
54957108 2146 *
2147 * @param string $name
3fd42bb5 2148 * @param string $label
54957108 2149 * @param bool $required
2150 * @param string $defaultCurrency
2151 * @param bool $freezeCurrency
483a53a8 2152 * @param bool $setDefaultCurrency
6a488035 2153 */
2da40d21 2154 public function addCurrency(
f9f40af3
TO
2155 $name = 'currency',
2156 $label = NULL,
2157 $required = TRUE,
6a488035 2158 $defaultCurrency = NULL,
483a53a8 2159 $freezeCurrency = FALSE,
2160 $setDefaultCurrency = TRUE
6a488035
TO
2161 ) {
2162 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
91a33228 2163 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
b740ee4b
MW
2164 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
2165 $currencies[$defaultCurrency] = $defaultCurrency;
2166 }
be2fb01f 2167 $options = ['class' => 'crm-select2 eight'];
6a488035 2168 if (!$required) {
be2fb01f 2169 $currencies = ['' => ''] + $currencies;
e1462487 2170 $options['placeholder'] = ts('- none -');
6a488035 2171 }
e1462487 2172 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
6a488035
TO
2173 if ($freezeCurrency) {
2174 $ele->freeze();
2175 }
2176 if (!$defaultCurrency) {
2177 $config = CRM_Core_Config::singleton();
2178 $defaultCurrency = $config->defaultCurrency;
2179 }
483a53a8 2180 // In some case, setting currency field by default might override the default value
2181 // as encountered in CRM-20527 for batch data entry
2182 if ($setDefaultCurrency) {
be2fb01f 2183 $this->setDefaults([$name => $defaultCurrency]);
483a53a8 2184 }
6a488035
TO
2185 }
2186
47f21f3a 2187 /**
fe482240 2188 * Create a single or multiple entity ref field.
47f21f3a
CW
2189 * @param string $name
2190 * @param string $label
6a0b768e
TO
2191 * @param array $props
2192 * Mix of html and widget properties, including:.
16b10e64 2193 * - select - params to give to select2 widget
2229cf4f 2194 * - entity - defaults to Contact
16b10e64 2195 * - create - can the user create a new entity on-the-fly?
79ae07d9 2196 * Set to TRUE if entity is contact and you want the default profiles,
2229cf4f 2197 * or pass in your own set of links. @see CRM_Campaign_BAO_Campaign::getEntityRefCreateLinks for format
353ea873 2198 * note that permissions are checked automatically
16b10e64 2199 * - api - array of settings for the getlist api wrapper
353ea873 2200 * note that it accepts a 'params' setting which will be passed to the underlying api
16b10e64
CW
2201 * - placeholder - string
2202 * - multiple - bool
2203 * - class, etc. - other html properties
fd36866a 2204 * @param bool $required
79ae07d9 2205 *
47f21f3a
CW
2206 * @return HTML_QuickForm_Element
2207 */
be2fb01f 2208 public function addEntityRef($name, $label = '', $props = [], $required = FALSE) {
76ec9ca7 2209 // Default properties
be2fb01f 2210 $props['api'] = CRM_Utils_Array::value('api', $props, []);
a2eb6152 2211 $props['entity'] = CRM_Core_DAO_AllCoreTables::convertEntityNameToCamel($props['entity'] ?? 'Contact');
9ca69805 2212 $props['class'] = ltrim(($props['class'] ?? '') . ' crm-form-entityref');
47f21f3a 2213
8dbd6052 2214 if (array_key_exists('create', $props) && empty($props['create'])) {
79ae07d9
CW
2215 unset($props['create']);
2216 }
79ae07d9 2217
a2eb6152 2218 $props['placeholder'] = $props['placeholder'] ?? self::selectOrAnyPlaceholder($props, $required);
a88cf11a 2219
be2fb01f 2220 $defaults = [];
a88cf11a
CW
2221 if (!empty($props['multiple'])) {
2222 $defaults['multiple'] = TRUE;
79ae07d9 2223 }
be2fb01f 2224 $props['select'] = CRM_Utils_Array::value('select', $props, []) + $defaults;
47f21f3a 2225
f9585de5 2226 $this->formatReferenceFieldAttributes($props, get_class($this));
47f21f3a
CW
2227 return $this->add('text', $name, $label, $props, $required);
2228 }
2229
2230 /**
f9585de5 2231 * @param array $props
2232 * @param string $formName
47f21f3a 2233 */
f9585de5 2234 private function formatReferenceFieldAttributes(&$props, $formName) {
2235 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
47f21f3a 2236 $props['data-select-params'] = json_encode($props['select']);
76ec9ca7
CW
2237 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
2238 $props['data-api-entity'] = $props['entity'];
79ae07d9
CW
2239 if (!empty($props['create'])) {
2240 $props['data-create-links'] = json_encode($props['create']);
47f21f3a 2241 }
a88cf11a 2242 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
47f21f3a
CW
2243 }
2244
a0ee3941
EM
2245 /**
2246 * @param $elementName
2247 */
00be9182 2248 public function removeFileRequiredRules($elementName) {
be2fb01f 2249 $this->_required = array_diff($this->_required, [$elementName]);
6a488035
TO
2250 if (isset($this->_rules[$elementName])) {
2251 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2252 if ($ruleInfo['type'] == 'uploadedfile') {
2253 unset($this->_rules[$elementName][$index]);
2254 }
2255 }
2256 if (empty($this->_rules[$elementName])) {
2257 unset($this->_rules[$elementName]);
2258 }
2259 }
2260 }
2261
2262 /**
fe482240 2263 * Function that can be defined in Form to override or.
6a488035 2264 * perform specific action on cancel action
6a488035 2265 */
f9f40af3
TO
2266 public function cancelAction() {
2267 }
7cb3d4f0
CW
2268
2269 /**
fe482240 2270 * Helper function to verify that required fields have been filled.
3bdf1f3a 2271 *
7cb3d4f0 2272 * Typically called within the scope of a FormRule function
3bdf1f3a 2273 *
2274 * @param array $fields
2275 * @param array $values
2276 * @param array $errors
7cb3d4f0 2277 */
00be9182 2278 public static function validateMandatoryFields($fields, $values, &$errors) {
7cb3d4f0
CW
2279 foreach ($fields as $name => $fld) {
2280 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
be2fb01f 2281 $errors[$name] = ts('%1 is a required field.', [1 => $fld['title']]);
7cb3d4f0
CW
2282 }
2283 }
2284 }
da8d9879 2285
aa1b1481
EM
2286 /**
2287 * Get contact if for a form object. Prioritise
16b10e64 2288 * - cid in URL if 0 (on behalf on someoneelse)
aa1b1481 2289 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
16b10e64
CW
2290 * - logged in user id if it matches the one in the cid in the URL
2291 * - contact id validated from a checksum from a checksum
2292 * - cid from the url if the caller has ACL permission to view
2293 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
aa1b1481 2294 *
5c766a0b 2295 * @return NULL|int
aa1b1481 2296 */
8d388047 2297 protected function setContactID() {
da8d9879 2298 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
7b4d7ab8 2299 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
596bff78 2300 $tempID = $this->_params['select_contact_id'];
2301 }
22e263ad 2302 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
e1ce628e 2303 // event form stores as an indexed array, contribution form not so much...
2304 $tempID = $this->_params[0]['select_contact_id'];
2305 }
c156d4d6 2306
da8d9879 2307 // force to ignore the authenticated user
c156d4d6
E
2308 if ($tempID === '0' || $tempID === 0) {
2309 // we set the cid on the form so that this will be retained for the Confirm page
2310 // in the multi-page form & prevent us returning the $userID when this is called
2311 // from that page
2312 // we don't really need to set it when $tempID is set because the params have that stored
2313 $this->set('cid', 0);
be2fb01f 2314 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2315 return (int) $tempID;
da8d9879
DG
2316 }
2317
52129c88 2318 $userID = CRM_Core_Session::getLoggedInContactID();
da8d9879 2319
18406494 2320 if (!is_null($tempID) && $tempID === $userID) {
be2fb01f 2321 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2322 return (int) $userID;
da8d9879
DG
2323 }
2324
2325 //check if this is a checksum authentication
2326 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2327 if ($userChecksum) {
2328 //check for anonymous user.
2329 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2330 if ($validUser) {
be2fb01f
CW
2331 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2332 CRM_Core_Resources::singleton()->addVars('coreForm', ['checksum' => $userChecksum]);
da8d9879
DG
2333 return $tempID;
2334 }
2335 }
2336 // check if user has permission, CRM-12062
4c9b6178 2337 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
be2fb01f 2338 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
da8d9879
DG
2339 return $tempID;
2340 }
064af727 2341 if (is_numeric($userID)) {
be2fb01f 2342 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $userID]);
064af727 2343 }
f03d4901 2344 return is_numeric($userID) ? $userID : NULL;
da8d9879 2345 }
596bff78 2346
3bdf1f3a 2347 /**
2348 * Get the contact id that the form is being submitted for.
2349 *
e97c66ff 2350 * @return int|null
3bdf1f3a 2351 */
8d388047 2352 public function getContactID() {
2353 return $this->setContactID();
2354 }
2355
f9f40af3 2356 /**
fe482240 2357 * Get the contact id of the logged in user.
52129c88 2358 * @deprecated
3f4728dd 2359 *
2360 * @return int|false
f9f40af3 2361 */
00be9182 2362 public function getLoggedInUserContactID() {
52129c88 2363 CRM_Core_Error::deprecatedFunctionWarning('CRM_Core_Session::getLoggedInContactID()');
596bff78 2364 // check if the user is logged in and has a contact ID
2365 $session = CRM_Core_Session::singleton();
3f4728dd 2366 return $session->get('userID') ? (int) $session->get('userID') : FALSE;
596bff78 2367 }
2368
2369 /**
100fef9d 2370 * Add autoselector field -if user has permission to view contacts
596bff78 2371 * If adding this to a form you also need to add to the tpl e.g
2372 *
2373 * {if !empty($selectable)}
2374 * <div class="crm-summary-row">
2375 * <div class="crm-label">{$form.select_contact.label}</div>
2376 * <div class="crm-content">
2377 * {$form.select_contact.html}
2378 * </div>
2379 * </div>
2380 * {/if}
77b97be7 2381 *
6a0b768e
TO
2382 * @param array $profiles
2383 * Ids of profiles that are on the form (to be autofilled).
77b97be7
EM
2384 * @param array $autoCompleteField
2385 *
16b10e64
CW
2386 * - name_field
2387 * - id_field
2388 * - url (for ajax lookup)
596bff78 2389 *
3f4728dd 2390 * @throws \CRM_Core_Exception
77b97be7 2391 * @todo add data attributes so we can deal with multiple instances on a form
596bff78 2392 */
be2fb01f
CW
2393 public function addAutoSelector($profiles = [], $autoCompleteField = []) {
2394 $autoCompleteField = array_merge([
353ffa53
TO
2395 'id_field' => 'select_contact_id',
2396 'placeholder' => ts('Select someone else ...'),
2397 'show_hide' => TRUE,
be2fb01f
CW
2398 'api' => ['params' => ['contact_type' => 'Individual']],
2399 ], $autoCompleteField);
596bff78 2400
22e263ad 2401 if ($this->canUseAjaxContactLookups()) {
25977d86 2402 $this->assign('selectable', $autoCompleteField['id_field']);
be2fb01f 2403 $this->addEntityRef($autoCompleteField['id_field'], NULL, [
518fa0ee
SL
2404 'placeholder' => $autoCompleteField['placeholder'],
2405 'api' => $autoCompleteField['api'],
2406 ]);
596bff78 2407
96ed17aa 2408 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
be2fb01f
CW
2409 ->addSetting([
2410 'form' => ['autocompletes' => $autoCompleteField],
2411 'ids' => ['profile' => $profiles],
2412 ]);
596bff78 2413 }
2414 }
2415
dc677c00 2416 /**
3fd42bb5 2417 * @return bool
dc677c00 2418 */
00be9182 2419 public function canUseAjaxContactLookups() {
be2fb01f
CW
2420 if (0 < (civicrm_api3('contact', 'getcount', ['check_permissions' => 1])) &&
2421 CRM_Core_Permission::check([['access AJAX API', 'access CiviCRM']])
353ffa53 2422 ) {
f9f40af3
TO
2423 return TRUE;
2424 }
3fd42bb5 2425 return FALSE;
dc677c00
EM
2426 }
2427
596bff78 2428 /**
2429 * Add the options appropriate to cid = zero - ie. autocomplete
2430 *
2431 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2432 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2433 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2434 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
2435 */
f956cd24 2436 public function addCIDZeroOptions() {
596bff78 2437 $this->assign('nocid', TRUE);
be2fb01f 2438 $profiles = [];
22e263ad 2439 if ($this->_values['custom_pre_id']) {
596bff78 2440 $profiles[] = $this->_values['custom_pre_id'];
2441 }
22e263ad 2442 if ($this->_values['custom_post_id']) {
cc57909a 2443 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
596bff78 2444 }
f956cd24 2445 $profiles[] = 'billing';
22e263ad 2446 if (!empty($this->_values)) {
596bff78 2447 $this->addAutoSelector($profiles);
2448 }
2449 }
9d665938 2450
2451 /**
2452 * Set default values on form for given contact (or no contact defaults)
77b97be7 2453 *
6a0b768e
TO
2454 * @param mixed $profile_id
2455 * (can be id, or profile name).
2456 * @param int $contactID
77b97be7
EM
2457 *
2458 * @return array
9d665938 2459 */
00be9182 2460 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
92e4c2a5 2461 try {
be2fb01f 2462 $defaults = civicrm_api3('profile', 'getsingle', [
9d665938 2463 'profile_id' => (array) $profile_id,
2464 'contact_id' => $contactID,
be2fb01f 2465 ]);
9d665938 2466 return $defaults;
2467 }
2468 catch (Exception $e) {
9d665938 2469 // the try catch block gives us silent failure -not 100% sure this is a good idea
2470 // as silent failures are often worse than noisy ones
be2fb01f 2471 return [];
9d665938 2472 }
2473 }
cae80d9f
CW
2474
2475 /**
fe482240 2476 * Sets form attribute.
cae80d9f
CW
2477 * @see CRM.loadForm
2478 */
00be9182 2479 public function preventAjaxSubmit() {
cae80d9f
CW
2480 $this->setAttribute('data-no-ajax-submit', 'true');
2481 }
2482
2483 /**
fe482240 2484 * Sets form attribute.
cae80d9f
CW
2485 * @see CRM.loadForm
2486 */
00be9182 2487 public function allowAjaxSubmit() {
cae80d9f
CW
2488 $this->removeAttribute('data-no-ajax-submit');
2489 }
e2046b33
CW
2490
2491 /**
fe482240 2492 * Sets page title based on entity and action.
e2046b33
CW
2493 * @param string $entityLabel
2494 */
00be9182 2495 public function setPageTitle($entityLabel) {
e2046b33
CW
2496 switch ($this->_action) {
2497 case CRM_Core_Action::ADD:
94fd9d17 2498 $this->setTitle(ts('New %1', [1 => $entityLabel]));
e2046b33 2499 break;
f9f40af3 2500
e2046b33 2501 case CRM_Core_Action::UPDATE:
94fd9d17 2502 $this->setTitle(ts('Edit %1', [1 => $entityLabel]));
e2046b33 2503 break;
f9f40af3 2504
e2046b33
CW
2505 case CRM_Core_Action::VIEW:
2506 case CRM_Core_Action::PREVIEW:
94fd9d17 2507 $this->setTitle(ts('View %1', [1 => $entityLabel]));
e2046b33 2508 break;
f9f40af3 2509
e2046b33 2510 case CRM_Core_Action::DELETE:
94fd9d17 2511 $this->setTitle(ts('Delete %1', [1 => $entityLabel]));
e2046b33
CW
2512 break;
2513 }
2514 }
1d07e7ab
CW
2515
2516 /**
2517 * Create a chain-select target field. All settings are optional; the defaults usually work.
2518 *
2519 * @param string $elementName
2520 * @param array $settings
2521 *
2522 * @return HTML_QuickForm_Element
2523 */
be2fb01f 2524 public function addChainSelect($elementName, $settings = []) {
a9442e6e 2525 $required = $settings['required'] ?? FALSE;
a2eb6152 2526 $label = strpos($elementName, 'rovince') ? CRM_Core_DAO_StateProvince::getEntityTitle() : CRM_Core_DAO_County::getEntityTitle();
be2fb01f
CW
2527 $props = $settings += [
2528 'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
518fa0ee
SL
2529 'country',
2530 'Country',
2531 'state_province',
2532 'StateProvince',
2533 ], $elementName),
1d07e7ab 2534 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
a2eb6152 2535 'label' => $label,
1d07e7ab
CW
2536 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2537 'data-none-prompt' => ts('- N/A -'),
2538 'multiple' => FALSE,
a9442e6e 2539 'required' => $required,
a2eb6152 2540 'placeholder' => ts('- select %1 -', [1 => $label]),
be2fb01f 2541 ];
b248d52b 2542 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
a9442e6e 2543 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2' . ($required ? ' required crm-field-required' : '');
1d07e7ab
CW
2544 $props['data-select-prompt'] = $props['placeholder'];
2545 $props['data-name'] = $elementName;
2546
2547 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2548
6a6ab43a
CW
2549 // Passing NULL instead of an array of options
2550 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2551 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2552 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
a9442e6e 2553 return $this->add('select', $elementName, $settings['label'], NULL, $required, $props);
1d07e7ab
CW
2554 }
2555
87ecd5b7 2556 /**
2557 * Add actions menu to results form.
2558 *
c794f667 2559 * @param array $tasks
87ecd5b7 2560 */
2561 public function addTaskMenu($tasks) {
2562 if (is_array($tasks) && !empty($tasks)) {
1a7356e7 2563 // Set constants means this will always load with an empty value, not reloading any submitted value.
2564 // This is appropriate as it is a pseudofield.
be2fb01f 2565 $this->setConstants(['task' => '']);
44543184 2566 $this->assign('taskMetaData', $tasks);
be2fb01f 2567 $select = $this->add('select', 'task', NULL, ['' => ts('Actions')], FALSE, [
518fa0ee
SL
2568 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions',
2569 ]
44543184 2570 );
2571 foreach ($tasks as $key => $task) {
be2fb01f 2572 $attributes = [];
1a7356e7 2573 if (isset($task['data'])) {
2574 foreach ($task['data'] as $dataKey => $dataValue) {
2575 $attributes['data-' . $dataKey] = $dataValue;
2576 }
44543184 2577 }
2578 $select->addOption($task['title'], $key, $attributes);
2579 }
87ecd5b7 2580 if (empty($this->_actionButtonName)) {
2581 $this->_actionButtonName = $this->getButtonName('next', 'action');
2582 }
2583 $this->assign('actionButtonName', $this->_actionButtonName);
cbd83dde
AH
2584 $this->add('xbutton', $this->_actionButtonName, ts('Go'), [
2585 'type' => 'submit',
2586 'class' => 'hiddenElement crm-search-go-button',
2587 ]);
87ecd5b7 2588
2589 // Radio to choose "All items" or "Selected items only"
be2fb01f 2590 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', ['checked' => 'checked']);
87ecd5b7 2591 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2592 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2593 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2594
2595 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2596 }
2597 }
2598
1d07e7ab
CW
2599 /**
2600 * Set options and attributes for chain select fields based on the controlling field's value
2601 */
2602 private function preProcessChainSelectFields() {
2603 foreach ($this->_chainSelectFields as $control => $target) {
a3984622
OB
2604 // The 'target' might get missing if extensions do removeElement() in a form hook.
2605 if ($this->elementExists($target)) {
2606 $targetField = $this->getElement($target);
2607 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
be2fb01f 2608 $options = [];
a3984622
OB
2609 // If the control field is on the form, setup chain-select and dynamically populate options
2610 if ($this->elementExists($control)) {
2611 $controlField = $this->getElement($control);
2612 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2613
2614 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2615
2616 $css = (string) $controlField->getAttribute('class');
be2fb01f 2617 $controlField->updateAttributes([
a3984622
OB
2618 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2619 'data-target' => $target,
be2fb01f 2620 ]);
a3984622
OB
2621 $controlValue = $controlField->getValue();
2622 if ($controlValue) {
2623 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2624 if (!$options) {
2625 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2626 }
4a44fd8a 2627 }
b71cb966 2628 else {
a3984622
OB
2629 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2630 $targetField->setAttribute('disabled', 'disabled');
8f9c3cbe 2631 }
0db6c3e1 2632 }
a3984622 2633 // Control field not present - fall back to loading default options
0db6c3e1 2634 else {
a3984622 2635 $options = CRM_Core_PseudoConstant::$targetType();
1d07e7ab 2636 }
a3984622 2637 if (!$targetField->getAttribute('multiple')) {
be2fb01f 2638 $options = ['' => $targetField->getAttribute('placeholder')] + $options;
a3984622
OB
2639 $targetField->removeAttribute('placeholder');
2640 }
be2fb01f 2641 $targetField->_options = [];
a3984622 2642 $targetField->loadArray($options);
1d07e7ab 2643 }
1d07e7ab
CW
2644 }
2645 }
bc999cd1
CW
2646
2647 /**
2648 * Validate country / state / county match and suppress unwanted "required" errors
2649 */
2650 private function validateChainSelectFields() {
2651 foreach ($this->_chainSelectFields as $control => $target) {
a3984622 2652 if ($this->elementExists($control) && $this->elementExists($target)) {
f9f40af3 2653 $controlValue = (array) $this->getElementValue($control);
14b2ff15
CW
2654 $targetField = $this->getElement($target);
2655 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
f9f40af3 2656 $targetValue = array_filter((array) $targetField->getValue());
14b2ff15
CW
2657 if ($targetValue || $this->getElementError($target)) {
2658 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2659 if ($targetValue) {
2660 if (!array_intersect($targetValue, array_keys($options))) {
2661 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2662 }
518fa0ee
SL
2663 }
2664 // Suppress "required" error for field if it has no options
14b2ff15
CW
2665 elseif (!$options) {
2666 $this->setElementError($target, NULL);
bc999cd1
CW
2667 }
2668 }
bc999cd1
CW
2669 }
2670 }
2671 }
96025800 2672
0b50eca0 2673 /**
2674 * Assign billing name to the template.
2675 *
2676 * @param array $params
2677 * Form input params, default to $this->_params.
f3f00653 2678 *
2679 * @return string
0b50eca0 2680 */
be2fb01f 2681 public function assignBillingName($params = []) {
0b50eca0 2682 $name = '';
2683 if (empty($params)) {
2684 $params = $this->_params;
2685 }
2686 if (!empty($params['billing_first_name'])) {
2687 $name = $params['billing_first_name'];
2688 }
2689
2690 if (!empty($params['billing_middle_name'])) {
2691 $name .= " {$params['billing_middle_name']}";
2692 }
2693
2694 if (!empty($params['billing_last_name'])) {
2695 $name .= " {$params['billing_last_name']}";
2696 }
2697 $name = trim($name);
2698 $this->assign('billingName', $name);
2699 return $name;
2700 }
2701
fd0770bc 2702 /**
2703 * Get the currency for the form.
2704 *
2705 * @todo this should be overriden on the forms rather than having this
2706 * historic, possible handling in here. As we clean that up we should
2707 * add deprecation notices into here.
e9bb043a 2708 *
2709 * @param array $submittedValues
2710 * Array allowed so forms inheriting this class do not break.
2711 * Ideally we would make a clear standard around how submitted values
2712 * are stored (is $this->_values consistently doing that?).
2713 *
2714 * @return string
fd0770bc 2715 */
be2fb01f 2716 public function getCurrency($submittedValues = []) {
9c1bc317 2717 $currency = $this->_values['currency'] ?? NULL;
fd0770bc 2718 // For event forms, currency is in a different spot
2719 if (empty($currency)) {
2720 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2721 }
2722 if (empty($currency)) {
2723 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2724 }
2725 // @todo If empty there is a problem - we should probably put in a deprecation notice
2726 // to warn if that seems to be happening.
2727 return $currency;
2728 }
2729
240b0e65 2730 /**
2731 * Is the form in view or edit mode.
2732 *
2733 * The 'addField' function relies on the form action being one of a set list
2734 * of actions. Checking for these allows for an early return.
2735 *
2736 * @return bool
2737 */
189701bb
ML
2738 protected function isFormInViewOrEditMode() {
2739 return $this->isFormInViewMode() || $this->isFormInEditMode();
2740 }
2741
2742 /**
2743 * Is the form in edit mode.
2744 *
2745 * Helper function, notably for extensions implementing the buildForm hook,
2746 * so that they can return early.
2747 *
2748 * @return bool
2749 */
2750 public function isFormInEditMode() {
240b0e65 2751 return in_array($this->_action, [
2752 CRM_Core_Action::UPDATE,
2753 CRM_Core_Action::ADD,
240b0e65 2754 CRM_Core_Action::BROWSE,
2755 CRM_Core_Action::BASIC,
2756 CRM_Core_Action::ADVANCED,
2757 CRM_Core_Action::PREVIEW,
2758 ]);
2759 }
2760
189701bb
ML
2761 /**
2762 * Is the form in view mode.
2763 *
2764 * Helper function, notably for extensions implementing the buildForm hook,
2765 * so that they can return early.
2766 *
2767 * @return bool
2768 */
2769 public function isFormInViewMode() {
2770 return $this->_action == CRM_Core_Action::VIEW;
2771 }
2772
7885e669
MW
2773 /**
2774 * Set the active tab
2775 *
2776 * @param string $default
2777 *
2778 * @throws \CRM_Core_Exception
2779 */
2780 public function setSelectedChild($default = NULL) {
2781 $selectedChild = CRM_Utils_Request::retrieve('selectedChild', 'Alphanumeric', $this, FALSE, $default);
2782 if (!empty($selectedChild)) {
2783 $this->set('selectedChild', $selectedChild);
2784 $this->assign('selectedChild', $selectedChild);
6a6458b6 2785 Civi::resources()->addSetting(['tabSettings' => ['active' => $selectedChild]]);
7885e669
MW
2786 }
2787 }
2788
3f4728dd 2789 /**
2790 * Get the contact if from the url, using the checksum or the cid if it is the logged in user.
2791 *
2792 * This function returns the user being validated. It is not intended to get another user
2793 * they have permission to (setContactID does do that) and can be used to check if the user is
2794 * accessing their own record.
2795 *
2796 * @return int|false
2797 * @throws \CRM_Core_Exception
2798 */
2799 protected function getContactIDIfAccessingOwnRecord() {
2800 $contactID = (int) CRM_Utils_Request::retrieve('cid', 'Positive', $this);
2801 if (!$contactID) {
2802 return FALSE;
2803 }
52129c88 2804 if ($contactID === CRM_Core_Session::getLoggedInContactID()) {
3f4728dd 2805 return $contactID;
2806 }
2807 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2808 return CRM_Contact_BAO_Contact_Utils::validChecksum($contactID, $userChecksum) ? $contactID : FALSE;
2809 }
2810
6aef1389 2811 /**
2812 * Get values submitted by the user.
2813 *
2814 * These values have been validated against the fields added to the form.
2815 * https://pear.php.net/manual/en/package.html.html-quickform.html-quickform.exportvalues.php
2816 *
2817 * @param string $fieldName
2818 *
2819 * @return mixed|null
2820 */
61d0bf1f 2821 public function getSubmittedValue(string $fieldName) {
6aef1389 2822 if (empty($this->exportedValues)) {
2823 $this->exportedValues = $this->controller->exportValues($this->_name);
2824 }
34a9221c
EM
2825 $value = $this->exportedValues[$fieldName] ?? NULL;
2826 if (in_array($fieldName, $this->submittableMoneyFields, TRUE)) {
2827 return CRM_Utils_Rule::cleanMoney($value);
2828 }
2829 return $value;
6aef1389 2830 }
2831
1ebb7282
MW
2832 /**
2833 * Get the active UFGroups (profiles) on this form
2834 * Many forms load one or more UFGroups (profiles).
2835 * This provides a standard function to retrieve the IDs of those profiles from the form
2836 * so that you can implement things such as "is is_captcha field set on any of the active profiles on this form?"
2837 *
2838 * NOT SUPPORTED FOR USE OUTSIDE CORE EXTENSIONS - Added for reCAPTCHA core extension.
2839 *
2840 * @return array
2841 */
2842 public function getUFGroupIDs() {
2843 return [];
2844 }
2845
6a488035 2846}