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