dev/core#1409 Remove net_amount from Addtional Payment form
[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':
95c2e666 1651 if ($context === 'search') {
1652 $this->addYesNo($name, $label, TRUE, FALSE, $props);
1653 return;
1654 }
999ab5e1
TM
1655 $text = isset($props['text']) ? $props['text'] : NULL;
1656 unset($props['text']);
2a300b65 1657 return $this->addElement('checkbox', $name, $label, $text, $props);
a4969aee 1658
50471995 1659 //add support for 'Advcheckbox' field
1660 case 'advcheckbox':
b0964781 1661 $text = isset($props['text']) ? $props['text'] : NULL;
1662 unset($props['text']);
1663 return $this->addElement('advcheckbox', $name, $label, $text, $props);
50471995 1664
33fa033c
TM
1665 case 'File':
1666 // We should not build upload file in search mode.
916b6181 1667 if ($context == 'search') {
33fa033c
TM
1668 return;
1669 }
2a300b65 1670 $file = $this->add('file', $name, $label, $props, $required);
33fa033c 1671 $this->addUploadElement($name);
2a300b65 1672 return $file;
33fa033c 1673
b66c1d2c
CW
1674 case 'RichTextEditor':
1675 return $this->add('wysiwyg', $name, $label, $props, $required);
1676
b58770ea 1677 case 'EntityRef':
2a300b65 1678 return $this->addEntityRef($name, $label, $props, $required);
b58770ea 1679
e9bc5dcc 1680 case 'Password':
a7e59a48 1681 $props['size'] = isset($props['size']) ? $props['size'] : 60;
e9bc5dcc
SL
1682 return $this->add('password', $name, $label, $props, $required);
1683
7ec4548b
TM
1684 // Check datatypes of fields
1685 // case 'Int':
1686 //case 'Float':
1687 //case 'Money':
7ec4548b
TM
1688 //case read only fields
1689 default:
1690 throw new Exception("Unsupported html-element " . $widget);
1691 }
1692 }
1693
6a488035
TO
1694 /**
1695 * Add a widget for selecting/editing/creating/copying a profile form
1696 *
6a0b768e
TO
1697 * @param string $name
1698 * HTML form-element name.
1699 * @param string $label
1700 * Printable label.
1701 * @param string $allowCoreTypes
1702 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1703 * @param string $allowSubTypes
1704 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
6a488035 1705 * @param array $entities
6a0b768e
TO
1706 * @param bool $default
1707 * //CRM-15427.
54957108 1708 * @param string $usedFor
6a488035 1709 */
37375016 1710 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
6a488035
TO
1711 // Output widget
1712 // FIXME: Instead of adhoc serialization, use a single json_encode()
1713 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1714 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
be2fb01f 1715 $this->add('text', $name, $label, [
6a488035
TO
1716 'class' => 'crm-profile-selector',
1717 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1718 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1719 'data-entities' => json_encode($entities),
99e239bc 1720 //CRM-15427
1721 'data-default' => $default,
37375016 1722 'data-usedfor' => json_encode($usedFor),
be2fb01f 1723 ]);
6a488035
TO
1724 }
1725
a0ee3941
EM
1726 /**
1727 * @return null
1728 */
6a488035
TO
1729 public function getRootTitle() {
1730 return NULL;
1731 }
1732
a0ee3941
EM
1733 /**
1734 * @return string
1735 */
6a488035
TO
1736 public function getCompleteTitle() {
1737 return $this->getRootTitle() . $this->getTitle();
1738 }
1739
a0ee3941
EM
1740 /**
1741 * @return CRM_Core_Smarty
1742 */
00be9182 1743 public static function &getTemplate() {
6a488035
TO
1744 return self::$_template;
1745 }
1746
a0ee3941
EM
1747 /**
1748 * @param $elementName
1749 */
00be9182 1750 public function addUploadElement($elementName) {
6a488035
TO
1751 $uploadNames = $this->get('uploadNames');
1752 if (!$uploadNames) {
be2fb01f 1753 $uploadNames = [];
6a488035
TO
1754 }
1755 if (is_array($elementName)) {
1756 foreach ($elementName as $name) {
1757 if (!in_array($name, $uploadNames)) {
1758 $uploadNames[] = $name;
1759 }
1760 }
1761 }
1762 else {
1763 if (!in_array($elementName, $uploadNames)) {
1764 $uploadNames[] = $elementName;
1765 }
1766 }
1767 $this->set('uploadNames', $uploadNames);
1768
1769 $config = CRM_Core_Config::singleton();
1770 if (!empty($uploadNames)) {
1771 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1772 }
1773 }
1774
a0ee3941
EM
1775 /**
1776 * @param $name
1777 *
1778 * @return null
1779 */
00be9182 1780 public function getVar($name) {
6a488035
TO
1781 return isset($this->$name) ? $this->$name : NULL;
1782 }
1783
a0ee3941
EM
1784 /**
1785 * @param $name
1786 * @param $value
1787 */
00be9182 1788 public function setVar($name, $value) {
6a488035
TO
1789 $this->$name = $value;
1790 }
1791
1792 /**
fe482240 1793 * Add date.
6a488035 1794 *
013ac5df
CW
1795 * @deprecated
1796 * Use $this->add('datepicker', ...) instead.
a1a2a83d
TO
1797 *
1798 * @param string $name
1799 * Name of the element.
1800 * @param string $label
1801 * Label of the element.
6a0b768e
TO
1802 * @param bool $required
1803 * True if required.
a1a2a83d
TO
1804 * @param array $attributes
1805 * Key / value pair.
6a488035 1806 */
00be9182 1807 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
a7488080 1808 if (!empty($attributes['formatType'])) {
6a488035 1809 // get actual format
be2fb01f
CW
1810 $params = ['name' => $attributes['formatType']];
1811 $values = [];
6a488035
TO
1812
1813 // cache date information
1814 static $dateFormat;
1815 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
a7488080 1816 if (empty($dateFormat[$key])) {
6a488035
TO
1817 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1818 $dateFormat[$key] = $values;
1819 }
1820 else {
1821 $values = $dateFormat[$key];
1822 }
1823
1824 if ($values['date_format']) {
1825 $attributes['format'] = $values['date_format'];
1826 }
1827
a7488080 1828 if (!empty($values['time_format'])) {
6a488035
TO
1829 $attributes['timeFormat'] = $values['time_format'];
1830 }
1831 $attributes['startOffset'] = $values['start'];
1832 $attributes['endOffset'] = $values['end'];
1833 }
1834
1835 $config = CRM_Core_Config::singleton();
a7488080 1836 if (empty($attributes['format'])) {
6a488035
TO
1837 $attributes['format'] = $config->dateInputFormat;
1838 }
1839
1840 if (!isset($attributes['startOffset'])) {
1841 $attributes['startOffset'] = 10;
1842 }
1843
1844 if (!isset($attributes['endOffset'])) {
1845 $attributes['endOffset'] = 10;
1846 }
1847
1848 $this->add('text', $name, $label, $attributes);
1849
8cc574cf 1850 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
6a488035
TO
1851
1852 if (!isset($attributes['timeFormat'])) {
1853 $timeFormat = $config->timeInputFormat;
1854 }
1855 else {
1856 $timeFormat = $attributes['timeFormat'];
1857 }
1858
1859 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1860 if ($timeFormat) {
1861 $show24Hours = TRUE;
1862 if ($timeFormat == 1) {
1863 $show24Hours = FALSE;
1864 }
1865
1866 //CRM-6664 -we are having time element name
1867 //in either flat string or an array format.
1868 $elementName = $name . '_time';
1869 if (substr($name, -1) == ']') {
1870 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1871 }
1872
be2fb01f 1873 $this->add('text', $elementName, ts('Time'), ['timeFormat' => $show24Hours]);
6a488035
TO
1874 }
1875 }
1876
1877 if ($required) {
be2fb01f 1878 $this->addRule($name, ts('Please select %1', [1 => $label]), 'required');
8cc574cf 1879 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
6a488035
TO
1880 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1881 }
1882 }
1883 }
1884
1885 /**
013ac5df
CW
1886 * Function that will add date and time.
1887 *
1888 * @deprecated
1889 * Use $this->add('datepicker', ...) instead.
54957108 1890 *
1891 * @param string $name
1892 * @param string $label
1893 * @param bool $required
1894 * @param null $attributes
6a488035 1895 */
00be9182 1896 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
be2fb01f 1897 $addTime = ['addTime' => TRUE];
6a488035
TO
1898 if (is_array($attributes)) {
1899 $attributes = array_merge($attributes, $addTime);
1900 }
1901 else {
1902 $attributes = $addTime;
1903 }
1904
1905 $this->addDate($name, $label, $required, $attributes);
1906 }
1907
1908 /**
fe482240 1909 * Add a currency and money element to the form.
3bdf1f3a 1910 *
1911 * @param string $name
1912 * @param string $label
1913 * @param bool $required
1914 * @param null $attributes
1915 * @param bool $addCurrency
1916 * @param string $currencyName
1917 * @param null $defaultCurrency
1918 * @param bool $freezeCurrency
1919 *
1920 * @return \HTML_QuickForm_Element
6a488035 1921 */
2da40d21 1922 public function addMoney(
f9f40af3 1923 $name,
6a488035 1924 $label,
f9f40af3
TO
1925 $required = FALSE,
1926 $attributes = NULL,
1927 $addCurrency = TRUE,
1928 $currencyName = 'currency',
6a488035 1929 $defaultCurrency = NULL,
f9f40af3 1930 $freezeCurrency = FALSE
6a488035
TO
1931 ) {
1932 $element = $this->add('text', $name, $label, $attributes, $required);
1933 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1934
1935 if ($addCurrency) {
1936 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1937 }
1938
1939 return $element;
1940 }
1941
1942 /**
fe482240 1943 * Add currency element to the form.
54957108 1944 *
1945 * @param string $name
1946 * @param null $label
1947 * @param bool $required
1948 * @param string $defaultCurrency
1949 * @param bool $freezeCurrency
483a53a8 1950 * @param bool $setDefaultCurrency
6a488035 1951 */
2da40d21 1952 public function addCurrency(
f9f40af3
TO
1953 $name = 'currency',
1954 $label = NULL,
1955 $required = TRUE,
6a488035 1956 $defaultCurrency = NULL,
483a53a8 1957 $freezeCurrency = FALSE,
1958 $setDefaultCurrency = TRUE
6a488035
TO
1959 ) {
1960 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
91a33228 1961 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
b740ee4b
MW
1962 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1963 $currencies[$defaultCurrency] = $defaultCurrency;
1964 }
be2fb01f 1965 $options = ['class' => 'crm-select2 eight'];
6a488035 1966 if (!$required) {
be2fb01f 1967 $currencies = ['' => ''] + $currencies;
e1462487 1968 $options['placeholder'] = ts('- none -');
6a488035 1969 }
e1462487 1970 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
6a488035
TO
1971 if ($freezeCurrency) {
1972 $ele->freeze();
1973 }
1974 if (!$defaultCurrency) {
1975 $config = CRM_Core_Config::singleton();
1976 $defaultCurrency = $config->defaultCurrency;
1977 }
483a53a8 1978 // In some case, setting currency field by default might override the default value
1979 // as encountered in CRM-20527 for batch data entry
1980 if ($setDefaultCurrency) {
be2fb01f 1981 $this->setDefaults([$name => $defaultCurrency]);
483a53a8 1982 }
6a488035
TO
1983 }
1984
47f21f3a 1985 /**
fe482240 1986 * Create a single or multiple entity ref field.
47f21f3a
CW
1987 * @param string $name
1988 * @param string $label
6a0b768e
TO
1989 * @param array $props
1990 * Mix of html and widget properties, including:.
16b10e64 1991 * - select - params to give to select2 widget
2229cf4f 1992 * - entity - defaults to Contact
16b10e64 1993 * - create - can the user create a new entity on-the-fly?
79ae07d9 1994 * Set to TRUE if entity is contact and you want the default profiles,
2229cf4f 1995 * or pass in your own set of links. @see CRM_Campaign_BAO_Campaign::getEntityRefCreateLinks for format
353ea873 1996 * note that permissions are checked automatically
16b10e64 1997 * - api - array of settings for the getlist api wrapper
353ea873 1998 * note that it accepts a 'params' setting which will be passed to the underlying api
16b10e64
CW
1999 * - placeholder - string
2000 * - multiple - bool
2001 * - class, etc. - other html properties
fd36866a 2002 * @param bool $required
79ae07d9 2003 *
47f21f3a
CW
2004 * @return HTML_QuickForm_Element
2005 */
be2fb01f 2006 public function addEntityRef($name, $label = '', $props = [], $required = FALSE) {
76ec9ca7 2007 // Default properties
be2fb01f 2008 $props['api'] = CRM_Utils_Array::value('api', $props, []);
2229cf4f 2009 $props['entity'] = CRM_Utils_String::convertStringToCamel(CRM_Utils_Array::value('entity', $props, 'Contact'));
a88cf11a 2010 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
47f21f3a 2011
8dbd6052 2012 if (array_key_exists('create', $props) && empty($props['create'])) {
79ae07d9
CW
2013 unset($props['create']);
2014 }
79ae07d9 2015
be2fb01f 2016 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', [1 => ts(str_replace('_', ' ', $props['entity']))]) : ts('- none -'));
a88cf11a 2017
be2fb01f 2018 $defaults = [];
a88cf11a
CW
2019 if (!empty($props['multiple'])) {
2020 $defaults['multiple'] = TRUE;
79ae07d9 2021 }
be2fb01f 2022 $props['select'] = CRM_Utils_Array::value('select', $props, []) + $defaults;
47f21f3a 2023
f9585de5 2024 $this->formatReferenceFieldAttributes($props, get_class($this));
47f21f3a
CW
2025 return $this->add('text', $name, $label, $props, $required);
2026 }
2027
2028 /**
f9585de5 2029 * @param array $props
2030 * @param string $formName
47f21f3a 2031 */
f9585de5 2032 private function formatReferenceFieldAttributes(&$props, $formName) {
2033 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
47f21f3a 2034 $props['data-select-params'] = json_encode($props['select']);
76ec9ca7
CW
2035 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
2036 $props['data-api-entity'] = $props['entity'];
79ae07d9
CW
2037 if (!empty($props['create'])) {
2038 $props['data-create-links'] = json_encode($props['create']);
47f21f3a 2039 }
a88cf11a 2040 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
47f21f3a
CW
2041 }
2042
5d86176b 2043 /**
2044 * Convert all date fields within the params to mysql date ready for the
2045 * BAO layer. In this case fields are checked against the $_datefields defined for the form
2046 * and if time is defined it is incorporated
2047 *
6a0b768e
TO
2048 * @param array $params
2049 * Input params from the form.
5d86176b 2050 *
2051 * @todo it would probably be better to work on $this->_params than a passed array
2052 * @todo standardise the format which dates are passed to the BAO layer in & remove date
2053 * handling from BAO
2054 */
9b873358
TO
2055 public function convertDateFieldsToMySQL(&$params) {
2056 foreach ($this->_dateFields as $fieldName => $specs) {
2057 if (!empty($params[$fieldName])) {
5d86176b 2058 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
2059 CRM_Utils_Date::processDate(
353ffa53
TO
2060 $params[$fieldName],
2061 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
5d86176b 2062 );
2063 }
92e4c2a5 2064 else {
9b873358 2065 if (isset($specs['default'])) {
5d86176b 2066 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
2067 }
2068 }
2069 }
2070 }
2071
a0ee3941
EM
2072 /**
2073 * @param $elementName
2074 */
00be9182 2075 public function removeFileRequiredRules($elementName) {
be2fb01f 2076 $this->_required = array_diff($this->_required, [$elementName]);
6a488035
TO
2077 if (isset($this->_rules[$elementName])) {
2078 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2079 if ($ruleInfo['type'] == 'uploadedfile') {
2080 unset($this->_rules[$elementName][$index]);
2081 }
2082 }
2083 if (empty($this->_rules[$elementName])) {
2084 unset($this->_rules[$elementName]);
2085 }
2086 }
2087 }
2088
2089 /**
fe482240 2090 * Function that can be defined in Form to override or.
6a488035 2091 * perform specific action on cancel action
6a488035 2092 */
f9f40af3
TO
2093 public function cancelAction() {
2094 }
7cb3d4f0
CW
2095
2096 /**
fe482240 2097 * Helper function to verify that required fields have been filled.
3bdf1f3a 2098 *
7cb3d4f0 2099 * Typically called within the scope of a FormRule function
3bdf1f3a 2100 *
2101 * @param array $fields
2102 * @param array $values
2103 * @param array $errors
7cb3d4f0 2104 */
00be9182 2105 public static function validateMandatoryFields($fields, $values, &$errors) {
7cb3d4f0
CW
2106 foreach ($fields as $name => $fld) {
2107 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
be2fb01f 2108 $errors[$name] = ts('%1 is a required field.', [1 => $fld['title']]);
7cb3d4f0
CW
2109 }
2110 }
2111 }
da8d9879 2112
aa1b1481
EM
2113 /**
2114 * Get contact if for a form object. Prioritise
16b10e64 2115 * - cid in URL if 0 (on behalf on someoneelse)
aa1b1481 2116 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
16b10e64
CW
2117 * - logged in user id if it matches the one in the cid in the URL
2118 * - contact id validated from a checksum from a checksum
2119 * - cid from the url if the caller has ACL permission to view
2120 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
aa1b1481 2121 *
5c766a0b 2122 * @return NULL|int
aa1b1481 2123 */
8d388047 2124 protected function setContactID() {
da8d9879 2125 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
7b4d7ab8 2126 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
596bff78 2127 $tempID = $this->_params['select_contact_id'];
2128 }
22e263ad 2129 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
e1ce628e 2130 // event form stores as an indexed array, contribution form not so much...
2131 $tempID = $this->_params[0]['select_contact_id'];
2132 }
c156d4d6 2133
da8d9879 2134 // force to ignore the authenticated user
c156d4d6
E
2135 if ($tempID === '0' || $tempID === 0) {
2136 // we set the cid on the form so that this will be retained for the Confirm page
2137 // in the multi-page form & prevent us returning the $userID when this is called
2138 // from that page
2139 // we don't really need to set it when $tempID is set because the params have that stored
2140 $this->set('cid', 0);
be2fb01f 2141 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2142 return (int) $tempID;
da8d9879
DG
2143 }
2144
596bff78 2145 $userID = $this->getLoggedInUserContactID();
da8d9879 2146
18406494 2147 if (!is_null($tempID) && $tempID === $userID) {
be2fb01f 2148 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
aa288d3f 2149 return (int) $userID;
da8d9879
DG
2150 }
2151
2152 //check if this is a checksum authentication
2153 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2154 if ($userChecksum) {
2155 //check for anonymous user.
2156 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2157 if ($validUser) {
be2fb01f
CW
2158 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2159 CRM_Core_Resources::singleton()->addVars('coreForm', ['checksum' => $userChecksum]);
da8d9879
DG
2160 return $tempID;
2161 }
2162 }
2163 // check if user has permission, CRM-12062
4c9b6178 2164 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
be2fb01f 2165 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
da8d9879
DG
2166 return $tempID;
2167 }
064af727 2168 if (is_numeric($userID)) {
be2fb01f 2169 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $userID]);
064af727 2170 }
f03d4901 2171 return is_numeric($userID) ? $userID : NULL;
da8d9879 2172 }
596bff78 2173
3bdf1f3a 2174 /**
2175 * Get the contact id that the form is being submitted for.
2176 *
e97c66ff 2177 * @return int|null
3bdf1f3a 2178 */
8d388047 2179 public function getContactID() {
2180 return $this->setContactID();
2181 }
2182
f9f40af3 2183 /**
fe482240 2184 * Get the contact id of the logged in user.
f9f40af3 2185 */
00be9182 2186 public function getLoggedInUserContactID() {
596bff78 2187 // check if the user is logged in and has a contact ID
2188 $session = CRM_Core_Session::singleton();
2189 return $session->get('userID');
2190 }
2191
2192 /**
100fef9d 2193 * Add autoselector field -if user has permission to view contacts
596bff78 2194 * If adding this to a form you also need to add to the tpl e.g
2195 *
2196 * {if !empty($selectable)}
2197 * <div class="crm-summary-row">
2198 * <div class="crm-label">{$form.select_contact.label}</div>
2199 * <div class="crm-content">
2200 * {$form.select_contact.html}
2201 * </div>
2202 * </div>
2203 * {/if}
77b97be7 2204 *
6a0b768e
TO
2205 * @param array $profiles
2206 * Ids of profiles that are on the form (to be autofilled).
77b97be7
EM
2207 * @param array $autoCompleteField
2208 *
16b10e64
CW
2209 * - name_field
2210 * - id_field
2211 * - url (for ajax lookup)
596bff78 2212 *
77b97be7 2213 * @todo add data attributes so we can deal with multiple instances on a form
596bff78 2214 */
be2fb01f
CW
2215 public function addAutoSelector($profiles = [], $autoCompleteField = []) {
2216 $autoCompleteField = array_merge([
353ffa53
TO
2217 'id_field' => 'select_contact_id',
2218 'placeholder' => ts('Select someone else ...'),
2219 'show_hide' => TRUE,
be2fb01f
CW
2220 'api' => ['params' => ['contact_type' => 'Individual']],
2221 ], $autoCompleteField);
596bff78 2222
22e263ad 2223 if ($this->canUseAjaxContactLookups()) {
25977d86 2224 $this->assign('selectable', $autoCompleteField['id_field']);
be2fb01f 2225 $this->addEntityRef($autoCompleteField['id_field'], NULL, [
518fa0ee
SL
2226 'placeholder' => $autoCompleteField['placeholder'],
2227 'api' => $autoCompleteField['api'],
2228 ]);
596bff78 2229
96ed17aa 2230 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
be2fb01f
CW
2231 ->addSetting([
2232 'form' => ['autocompletes' => $autoCompleteField],
2233 'ids' => ['profile' => $profiles],
2234 ]);
596bff78 2235 }
2236 }
2237
dc677c00 2238 /**
dc677c00 2239 */
00be9182 2240 public function canUseAjaxContactLookups() {
be2fb01f
CW
2241 if (0 < (civicrm_api3('contact', 'getcount', ['check_permissions' => 1])) &&
2242 CRM_Core_Permission::check([['access AJAX API', 'access CiviCRM']])
353ffa53 2243 ) {
f9f40af3
TO
2244 return TRUE;
2245 }
dc677c00
EM
2246 }
2247
596bff78 2248 /**
2249 * Add the options appropriate to cid = zero - ie. autocomplete
2250 *
2251 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2252 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2253 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2254 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
2255 */
f956cd24 2256 public function addCIDZeroOptions() {
596bff78 2257 $this->assign('nocid', TRUE);
be2fb01f 2258 $profiles = [];
22e263ad 2259 if ($this->_values['custom_pre_id']) {
596bff78 2260 $profiles[] = $this->_values['custom_pre_id'];
2261 }
22e263ad 2262 if ($this->_values['custom_post_id']) {
cc57909a 2263 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
596bff78 2264 }
f956cd24 2265 $profiles[] = 'billing';
22e263ad 2266 if (!empty($this->_values)) {
596bff78 2267 $this->addAutoSelector($profiles);
2268 }
2269 }
9d665938 2270
2271 /**
2272 * Set default values on form for given contact (or no contact defaults)
77b97be7 2273 *
6a0b768e
TO
2274 * @param mixed $profile_id
2275 * (can be id, or profile name).
2276 * @param int $contactID
77b97be7
EM
2277 *
2278 * @return array
9d665938 2279 */
00be9182 2280 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
92e4c2a5 2281 try {
be2fb01f 2282 $defaults = civicrm_api3('profile', 'getsingle', [
9d665938 2283 'profile_id' => (array) $profile_id,
2284 'contact_id' => $contactID,
be2fb01f 2285 ]);
9d665938 2286 return $defaults;
2287 }
2288 catch (Exception $e) {
9d665938 2289 // the try catch block gives us silent failure -not 100% sure this is a good idea
2290 // as silent failures are often worse than noisy ones
be2fb01f 2291 return [];
9d665938 2292 }
2293 }
cae80d9f
CW
2294
2295 /**
fe482240 2296 * Sets form attribute.
cae80d9f
CW
2297 * @see CRM.loadForm
2298 */
00be9182 2299 public function preventAjaxSubmit() {
cae80d9f
CW
2300 $this->setAttribute('data-no-ajax-submit', 'true');
2301 }
2302
2303 /**
fe482240 2304 * Sets form attribute.
cae80d9f
CW
2305 * @see CRM.loadForm
2306 */
00be9182 2307 public function allowAjaxSubmit() {
cae80d9f
CW
2308 $this->removeAttribute('data-no-ajax-submit');
2309 }
e2046b33
CW
2310
2311 /**
fe482240 2312 * Sets page title based on entity and action.
e2046b33
CW
2313 * @param string $entityLabel
2314 */
00be9182 2315 public function setPageTitle($entityLabel) {
e2046b33
CW
2316 switch ($this->_action) {
2317 case CRM_Core_Action::ADD:
be2fb01f 2318 CRM_Utils_System::setTitle(ts('New %1', [1 => $entityLabel]));
e2046b33 2319 break;
f9f40af3 2320
e2046b33 2321 case CRM_Core_Action::UPDATE:
be2fb01f 2322 CRM_Utils_System::setTitle(ts('Edit %1', [1 => $entityLabel]));
e2046b33 2323 break;
f9f40af3 2324
e2046b33
CW
2325 case CRM_Core_Action::VIEW:
2326 case CRM_Core_Action::PREVIEW:
be2fb01f 2327 CRM_Utils_System::setTitle(ts('View %1', [1 => $entityLabel]));
e2046b33 2328 break;
f9f40af3 2329
e2046b33 2330 case CRM_Core_Action::DELETE:
be2fb01f 2331 CRM_Utils_System::setTitle(ts('Delete %1', [1 => $entityLabel]));
e2046b33
CW
2332 break;
2333 }
2334 }
1d07e7ab
CW
2335
2336 /**
2337 * Create a chain-select target field. All settings are optional; the defaults usually work.
2338 *
2339 * @param string $elementName
2340 * @param array $settings
2341 *
2342 * @return HTML_QuickForm_Element
2343 */
be2fb01f
CW
2344 public function addChainSelect($elementName, $settings = []) {
2345 $props = $settings += [
2346 'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
518fa0ee
SL
2347 'country',
2348 'Country',
2349 'state_province',
2350 'StateProvince',
2351 ], $elementName),
1d07e7ab 2352 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
757069de 2353 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
1d07e7ab
CW
2354 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2355 'data-none-prompt' => ts('- N/A -'),
2356 'multiple' => FALSE,
2357 'required' => FALSE,
2358 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
be2fb01f 2359 ];
b248d52b 2360 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
8f9c3cbe 2361 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
1d07e7ab
CW
2362 $props['data-select-prompt'] = $props['placeholder'];
2363 $props['data-name'] = $elementName;
2364
2365 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2366
6a6ab43a
CW
2367 // Passing NULL instead of an array of options
2368 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2369 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2370 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
c46f87cf 2371 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
1d07e7ab
CW
2372 }
2373
87ecd5b7 2374 /**
2375 * Add actions menu to results form.
2376 *
c794f667 2377 * @param array $tasks
87ecd5b7 2378 */
2379 public function addTaskMenu($tasks) {
2380 if (is_array($tasks) && !empty($tasks)) {
1a7356e7 2381 // Set constants means this will always load with an empty value, not reloading any submitted value.
2382 // This is appropriate as it is a pseudofield.
be2fb01f 2383 $this->setConstants(['task' => '']);
44543184 2384 $this->assign('taskMetaData', $tasks);
be2fb01f 2385 $select = $this->add('select', 'task', NULL, ['' => ts('Actions')], FALSE, [
518fa0ee
SL
2386 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions',
2387 ]
44543184 2388 );
2389 foreach ($tasks as $key => $task) {
be2fb01f 2390 $attributes = [];
1a7356e7 2391 if (isset($task['data'])) {
2392 foreach ($task['data'] as $dataKey => $dataValue) {
2393 $attributes['data-' . $dataKey] = $dataValue;
2394 }
44543184 2395 }
2396 $select->addOption($task['title'], $key, $attributes);
2397 }
87ecd5b7 2398 if (empty($this->_actionButtonName)) {
2399 $this->_actionButtonName = $this->getButtonName('next', 'action');
2400 }
2401 $this->assign('actionButtonName', $this->_actionButtonName);
be2fb01f 2402 $this->add('submit', $this->_actionButtonName, ts('Go'), ['class' => 'hiddenElement crm-search-go-button']);
87ecd5b7 2403
2404 // Radio to choose "All items" or "Selected items only"
be2fb01f 2405 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', ['checked' => 'checked']);
87ecd5b7 2406 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2407 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2408 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2409
2410 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2411 }
2412 }
2413
1d07e7ab
CW
2414 /**
2415 * Set options and attributes for chain select fields based on the controlling field's value
2416 */
2417 private function preProcessChainSelectFields() {
2418 foreach ($this->_chainSelectFields as $control => $target) {
a3984622
OB
2419 // The 'target' might get missing if extensions do removeElement() in a form hook.
2420 if ($this->elementExists($target)) {
2421 $targetField = $this->getElement($target);
2422 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
be2fb01f 2423 $options = [];
a3984622
OB
2424 // If the control field is on the form, setup chain-select and dynamically populate options
2425 if ($this->elementExists($control)) {
2426 $controlField = $this->getElement($control);
2427 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2428
2429 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2430
2431 $css = (string) $controlField->getAttribute('class');
be2fb01f 2432 $controlField->updateAttributes([
a3984622
OB
2433 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2434 'data-target' => $target,
be2fb01f 2435 ]);
a3984622
OB
2436 $controlValue = $controlField->getValue();
2437 if ($controlValue) {
2438 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2439 if (!$options) {
2440 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2441 }
4a44fd8a 2442 }
b71cb966 2443 else {
a3984622
OB
2444 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2445 $targetField->setAttribute('disabled', 'disabled');
8f9c3cbe 2446 }
0db6c3e1 2447 }
a3984622 2448 // Control field not present - fall back to loading default options
0db6c3e1 2449 else {
a3984622 2450 $options = CRM_Core_PseudoConstant::$targetType();
1d07e7ab 2451 }
a3984622 2452 if (!$targetField->getAttribute('multiple')) {
be2fb01f 2453 $options = ['' => $targetField->getAttribute('placeholder')] + $options;
a3984622
OB
2454 $targetField->removeAttribute('placeholder');
2455 }
be2fb01f 2456 $targetField->_options = [];
a3984622 2457 $targetField->loadArray($options);
1d07e7ab 2458 }
1d07e7ab
CW
2459 }
2460 }
bc999cd1
CW
2461
2462 /**
2463 * Validate country / state / county match and suppress unwanted "required" errors
2464 */
2465 private function validateChainSelectFields() {
2466 foreach ($this->_chainSelectFields as $control => $target) {
a3984622 2467 if ($this->elementExists($control) && $this->elementExists($target)) {
f9f40af3 2468 $controlValue = (array) $this->getElementValue($control);
14b2ff15
CW
2469 $targetField = $this->getElement($target);
2470 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
f9f40af3 2471 $targetValue = array_filter((array) $targetField->getValue());
14b2ff15
CW
2472 if ($targetValue || $this->getElementError($target)) {
2473 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2474 if ($targetValue) {
2475 if (!array_intersect($targetValue, array_keys($options))) {
2476 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2477 }
518fa0ee
SL
2478 }
2479 // Suppress "required" error for field if it has no options
14b2ff15
CW
2480 elseif (!$options) {
2481 $this->setElementError($target, NULL);
bc999cd1
CW
2482 }
2483 }
bc999cd1
CW
2484 }
2485 }
2486 }
96025800 2487
0b50eca0 2488 /**
2489 * Assign billing name to the template.
2490 *
2491 * @param array $params
2492 * Form input params, default to $this->_params.
f3f00653 2493 *
2494 * @return string
0b50eca0 2495 */
be2fb01f 2496 public function assignBillingName($params = []) {
0b50eca0 2497 $name = '';
2498 if (empty($params)) {
2499 $params = $this->_params;
2500 }
2501 if (!empty($params['billing_first_name'])) {
2502 $name = $params['billing_first_name'];
2503 }
2504
2505 if (!empty($params['billing_middle_name'])) {
2506 $name .= " {$params['billing_middle_name']}";
2507 }
2508
2509 if (!empty($params['billing_last_name'])) {
2510 $name .= " {$params['billing_last_name']}";
2511 }
2512 $name = trim($name);
2513 $this->assign('billingName', $name);
2514 return $name;
2515 }
2516
fd0770bc 2517 /**
2518 * Get the currency for the form.
2519 *
2520 * @todo this should be overriden on the forms rather than having this
2521 * historic, possible handling in here. As we clean that up we should
2522 * add deprecation notices into here.
e9bb043a 2523 *
2524 * @param array $submittedValues
2525 * Array allowed so forms inheriting this class do not break.
2526 * Ideally we would make a clear standard around how submitted values
2527 * are stored (is $this->_values consistently doing that?).
2528 *
2529 * @return string
fd0770bc 2530 */
be2fb01f 2531 public function getCurrency($submittedValues = []) {
fd0770bc 2532 $currency = CRM_Utils_Array::value('currency', $this->_values);
2533 // For event forms, currency is in a different spot
2534 if (empty($currency)) {
2535 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2536 }
2537 if (empty($currency)) {
2538 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2539 }
2540 // @todo If empty there is a problem - we should probably put in a deprecation notice
2541 // to warn if that seems to be happening.
2542 return $currency;
2543 }
2544
240b0e65 2545 /**
2546 * Is the form in view or edit mode.
2547 *
2548 * The 'addField' function relies on the form action being one of a set list
2549 * of actions. Checking for these allows for an early return.
2550 *
2551 * @return bool
2552 */
2553 protected function isFormInViewOrEditMode() {
2554 return in_array($this->_action, [
2555 CRM_Core_Action::UPDATE,
2556 CRM_Core_Action::ADD,
2557 CRM_Core_Action::VIEW,
2558 CRM_Core_Action::BROWSE,
2559 CRM_Core_Action::BASIC,
2560 CRM_Core_Action::ADVANCED,
2561 CRM_Core_Action::PREVIEW,
2562 ]);
2563 }
2564
6a488035 2565}