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