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