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