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