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