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