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