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