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