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