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