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