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