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