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