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