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