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