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