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