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