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