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