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