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