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