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