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