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