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