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