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