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