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