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