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