Merge pull request #14872 from eileenmcnaughton/import_mapper
[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 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 // also add location name to the array
834 $this->_params["address_name-{$this->_bltID}"] = CRM_Utils_Array::value('billing_first_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_middle_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_last_name', $this->_params);
835 $this->_params["address_name-{$this->_bltID}"] = trim($this->_params["address_name-{$this->_bltID}"]);
836 // Add additional parameters that the payment processors are used to receiving.
837 if (!empty($this->_params["billing_state_province_id-{$this->_bltID}"])) {
838 $this->_params['state_province'] = $this->_params["state_province-{$this->_bltID}"] = $this->_params["billing_state_province-{$this->_bltID}"] = CRM_Core_PseudoConstant::stateProvinceAbbreviation($this->_params["billing_state_province_id-{$this->_bltID}"]);
839 }
840 if (!empty($this->_params["billing_country_id-{$this->_bltID}"])) {
841 $this->_params['country'] = $this->_params["country-{$this->_bltID}"] = $this->_params["billing_country-{$this->_bltID}"] = CRM_Core_PseudoConstant::countryIsoCode($this->_params["billing_country_id-{$this->_bltID}"]);
842 }
843
844 list($hasAddressField, $addressParams) = CRM_Contribute_BAO_Contribution::getPaymentProcessorReadyAddressParams($this->_params, $this->_bltID);
845 if ($hasAddressField) {
846 $this->_params = array_merge($this->_params, $addressParams);
847 }
848
849 // @fixme it would be really nice to have a comment here so I had a clue why we are setting $fields[$name] = 1
850 // Also how does relate to similar code in CRM_Contact_BAO_Contact::addBillingNameFieldsIfOtherwiseNotSet()
851 $nameFields = ['first_name', 'middle_name', 'last_name'];
852 foreach ($nameFields as $name) {
853 $fields[$name] = 1;
854 if (array_key_exists("billing_$name", $this->_params)) {
855 $this->_params[$name] = $this->_params["billing_{$name}"];
856 $this->_params['preserveDBName'] = TRUE;
857 }
858 }
859 return $fields;
860 }
861
862 /**
863 * Handle Payment Processor switching for contribution and event registration forms.
864 *
865 * This function is shared between contribution & event forms & this is their common class.
866 *
867 * However, this should be seen as an in-progress refactor, the end goal being to also align the
868 * backoffice forms that action payments.
869 *
870 * This function overlaps assignPaymentProcessor, in a bad way.
871 */
872 protected function preProcessPaymentOptions() {
873 $this->_paymentProcessorID = NULL;
874 if ($this->_paymentProcessors) {
875 if (!empty($this->_submitValues)) {
876 $this->_paymentProcessorID = CRM_Utils_Array::value('payment_processor_id', $this->_submitValues);
877 $this->_paymentProcessor = CRM_Utils_Array::value($this->_paymentProcessorID, $this->_paymentProcessors);
878 $this->set('type', $this->_paymentProcessorID);
879 $this->set('mode', $this->_mode);
880 $this->set('paymentProcessor', $this->_paymentProcessor);
881 }
882 // Set default payment processor
883 else {
884 foreach ($this->_paymentProcessors as $values) {
885 if (!empty($values['is_default']) || count($this->_paymentProcessors) == 1) {
886 $this->_paymentProcessorID = $values['id'];
887 break;
888 }
889 }
890 }
891 if ($this->_paymentProcessorID
892 || (isset($this->_submitValues['payment_processor_id']) && $this->_submitValues['payment_processor_id'] == 0)
893 ) {
894 CRM_Core_Payment_ProcessorForm::preProcess($this);
895 }
896 else {
897 $this->_paymentProcessor = [];
898 }
899 }
900
901 // We save the fact that the profile 'billing' is required on the payment form.
902 // Currently pay-later is the only 'processor' that takes notice of this - but ideally
903 // 1) it would be possible to select the minimum_billing_profile_id for the contribution form
904 // 2) that profile_id would be set on the payment processor
905 // 3) the payment processor would return a billing form that combines these user-configured
906 // minimums with the payment processor minimums. This would lead to fields like 'postal_code'
907 // only being on the form if either the admin has configured it as wanted or the processor
908 // requires it.
909 $this->assign('billing_profile_id', (CRM_Utils_Array::value('is_billing_required', $this->_values) ? 'billing' : ''));
910 }
911
912 /**
913 * Handle pre approval for processors.
914 *
915 * This fits with the flow where a pre-approval is done and then confirmed in the next stage when confirm is hit.
916 *
917 * This function is shared between contribution & event forms & this is their common class.
918 *
919 * However, this should be seen as an in-progress refactor, the end goal being to also align the
920 * backoffice forms that action payments.
921 *
922 * @param array $params
923 */
924 protected function handlePreApproval(&$params) {
925 try {
926 $payment = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
927 $params['component'] = 'contribute';
928 $result = $payment->doPreApproval($params);
929 if (empty($result)) {
930 // This could happen, for example, when paypal looks at the button value & decides it is not paypal express.
931 return;
932 }
933 }
934 catch (\Civi\Payment\Exception\PaymentProcessorException $e) {
935 CRM_Core_Error::statusBounce(ts('Payment approval failed with message :') . $e->getMessage(), $payment->getCancelUrl($params['qfKey'], CRM_Utils_Array::value('participant_id', $params)));
936 }
937
938 $this->set('pre_approval_parameters', $result['pre_approval_parameters']);
939 if (!empty($result['redirect_url'])) {
940 CRM_Utils_System::redirect($result['redirect_url']);
941 }
942 }
943
944 /**
945 * Setter function for options.
946 *
947 * @param mixed $options
948 */
949 public function setOptions($options) {
950 $this->_options = $options;
951 }
952
953 /**
954 * Render form and return contents.
955 *
956 * @return string
957 */
958 public function toSmarty() {
959 $this->preProcessChainSelectFields();
960 $renderer = $this->getRenderer();
961 $this->accept($renderer);
962 $content = $renderer->toArray();
963 $content['formName'] = $this->getName();
964 // CRM-15153
965 $content['formClass'] = CRM_Utils_System::getClassName($this);
966 return $content;
967 }
968
969 /**
970 * Getter function for renderer.
971 *
972 * If renderer is not set create one and initialize it.
973 *
974 * @return object
975 */
976 public function &getRenderer() {
977 if (!isset($this->_renderer)) {
978 $this->_renderer = CRM_Core_Form_Renderer::singleton();
979 }
980 return $this->_renderer;
981 }
982
983 /**
984 * Use the form name to create the tpl file name.
985 *
986 * @return string
987 */
988 public function getTemplateFileName() {
989 $ext = CRM_Extension_System::singleton()->getMapper();
990 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
991 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
992 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
993 }
994 else {
995 $tplname = strtr(
996 CRM_Utils_System::getClassName($this),
997 [
998 '_' => DIRECTORY_SEPARATOR,
999 '\\' => DIRECTORY_SEPARATOR,
1000 ]
1001 ) . '.tpl';
1002 }
1003 return $tplname;
1004 }
1005
1006 /**
1007 * A wrapper for getTemplateFileName.
1008 *
1009 * This includes calling the hook to prevent us from having to copy & paste the logic of calling the hook.
1010 */
1011 public function getHookedTemplateFileName() {
1012 $pageTemplateFile = $this->getTemplateFileName();
1013 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
1014 return $pageTemplateFile;
1015 }
1016
1017 /**
1018 * Default extra tpl file basically just replaces .tpl with .extra.tpl.
1019 *
1020 * i.e. we do not override.
1021 *
1022 * @return string
1023 */
1024 public function overrideExtraTemplateFileName() {
1025 return NULL;
1026 }
1027
1028 /**
1029 * Error reporting mechanism.
1030 *
1031 * @param string $message
1032 * Error Message.
1033 * @param int $code
1034 * Error Code.
1035 * @param CRM_Core_DAO $dao
1036 * A data access object on which we perform a rollback if non - empty.
1037 */
1038 public function error($message, $code = NULL, $dao = NULL) {
1039 if ($dao) {
1040 $dao->query('ROLLBACK');
1041 }
1042
1043 $error = CRM_Core_Error::singleton();
1044
1045 $error->push($code, $message);
1046 }
1047
1048 /**
1049 * Store the variable with the value in the form scope.
1050 *
1051 * @param string $name
1052 * Name of the variable.
1053 * @param mixed $value
1054 * Value of the variable.
1055 */
1056 public function set($name, $value) {
1057 $this->controller->set($name, $value);
1058 }
1059
1060 /**
1061 * Get the variable from the form scope.
1062 *
1063 * @param string $name
1064 * Name of the variable
1065 *
1066 * @return mixed
1067 */
1068 public function get($name) {
1069 return $this->controller->get($name);
1070 }
1071
1072 /**
1073 * Getter for action.
1074 *
1075 * @return int
1076 */
1077 public function getAction() {
1078 return $this->_action;
1079 }
1080
1081 /**
1082 * Setter for action.
1083 *
1084 * @param int $action
1085 * The mode we want to set the form.
1086 */
1087 public function setAction($action) {
1088 $this->_action = $action;
1089 }
1090
1091 /**
1092 * Assign value to name in template.
1093 *
1094 * @param string $var
1095 * Name of variable.
1096 * @param mixed $value
1097 * Value of variable.
1098 */
1099 public function assign($var, $value = NULL) {
1100 self::$_template->assign($var, $value);
1101 }
1102
1103 /**
1104 * Assign value to name in template by reference.
1105 *
1106 * @param string $var
1107 * Name of variable.
1108 * @param mixed $value
1109 * Value of variable.
1110 */
1111 public function assign_by_ref($var, &$value) {
1112 self::$_template->assign_by_ref($var, $value);
1113 }
1114
1115 /**
1116 * Appends values to template variables.
1117 *
1118 * @param array|string $tpl_var the template variable name(s)
1119 * @param mixed $value
1120 * The value to append.
1121 * @param bool $merge
1122 */
1123 public function append($tpl_var, $value = NULL, $merge = FALSE) {
1124 self::$_template->append($tpl_var, $value, $merge);
1125 }
1126
1127 /**
1128 * Returns an array containing template variables.
1129 *
1130 * @param string $name
1131 *
1132 * @return array
1133 */
1134 public function get_template_vars($name = NULL) {
1135 return self::$_template->get_template_vars($name);
1136 }
1137
1138 /**
1139 * @param string $name
1140 * @param $title
1141 * @param $values
1142 * @param array $attributes
1143 * @param null $separator
1144 * @param bool $required
1145 *
1146 * @return HTML_QuickForm_group
1147 */
1148 public function &addRadio($name, $title, $values, $attributes = [], $separator = NULL, $required = FALSE) {
1149 $options = [];
1150 $attributes = $attributes ? $attributes : [];
1151 $allowClear = !empty($attributes['allowClear']);
1152 unset($attributes['allowClear']);
1153 $attributes['id_suffix'] = $name;
1154 foreach ($values as $key => $var) {
1155 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
1156 }
1157 $group = $this->addGroup($options, $name, $title, $separator);
1158
1159 $optionEditKey = 'data-option-edit-path';
1160 if (!empty($attributes[$optionEditKey])) {
1161 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1162 }
1163
1164 if ($required) {
1165 $this->addRule($name, ts('%1 is a required field.', [1 => $title]), 'required');
1166 }
1167 if ($allowClear) {
1168 $group->setAttribute('allowClear', TRUE);
1169 }
1170 return $group;
1171 }
1172
1173 /**
1174 * @param int $id
1175 * @param $title
1176 * @param bool $allowClear
1177 * @param null $required
1178 * @param array $attributes
1179 */
1180 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = []) {
1181 $attributes += ['id_suffix' => $id];
1182 $choice = [];
1183 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1184 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
1185
1186 $group = $this->addGroup($choice, $id, $title);
1187 if ($allowClear) {
1188 $group->setAttribute('allowClear', TRUE);
1189 }
1190 if ($required) {
1191 $this->addRule($id, ts('%1 is a required field.', [1 => $title]), 'required');
1192 }
1193 }
1194
1195 /**
1196 * @param int $id
1197 * @param $title
1198 * @param $values
1199 * @param null $other
1200 * @param null $attributes
1201 * @param null $required
1202 * @param null $javascriptMethod
1203 * @param string $separator
1204 * @param bool $flipValues
1205 */
1206 public function addCheckBox(
1207 $id, $title, $values, $other = NULL,
1208 $attributes = NULL, $required = NULL,
1209 $javascriptMethod = NULL,
1210 $separator = '<br />', $flipValues = FALSE
1211 ) {
1212 $options = [];
1213
1214 if ($javascriptMethod) {
1215 foreach ($values as $key => $var) {
1216 if (!$flipValues) {
1217 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod, $attributes);
1218 }
1219 else {
1220 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod, $attributes);
1221 }
1222 }
1223 }
1224 else {
1225 foreach ($values as $key => $var) {
1226 if (!$flipValues) {
1227 $options[] = $this->createElement('checkbox', $var, NULL, $key, $attributes);
1228 }
1229 else {
1230 $options[] = $this->createElement('checkbox', $key, NULL, $var, $attributes);
1231 }
1232 }
1233 }
1234
1235 $group = $this->addGroup($options, $id, $title, $separator);
1236 $optionEditKey = 'data-option-edit-path';
1237 if (!empty($attributes[$optionEditKey])) {
1238 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1239 }
1240
1241 if ($other) {
1242 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1243 }
1244
1245 if ($required) {
1246 $this->addRule($id,
1247 ts('%1 is a required field.', [1 => $title]),
1248 'required'
1249 );
1250 }
1251 }
1252
1253 public function resetValues() {
1254 $data = $this->controller->container();
1255 $data['values'][$this->_name] = [];
1256 }
1257
1258 /**
1259 * Simple shell that derived classes can call to add buttons to
1260 * the form with a customized title for the main Submit
1261 *
1262 * @param string $title
1263 * Title of the main button.
1264 * @param string $nextType
1265 * Button type for the form after processing.
1266 * @param string $backType
1267 * @param bool|string $submitOnce
1268 */
1269 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
1270 $buttons = [];
1271 if ($backType != NULL) {
1272 $buttons[] = [
1273 'type' => $backType,
1274 'name' => ts('Previous'),
1275 ];
1276 }
1277 if ($nextType != NULL) {
1278 $nextButton = [
1279 'type' => $nextType,
1280 'name' => $title,
1281 'isDefault' => TRUE,
1282 ];
1283 if ($submitOnce) {
1284 $this->submitOnce = TRUE;
1285 }
1286 $buttons[] = $nextButton;
1287 }
1288 $this->addButtons($buttons);
1289 }
1290
1291 /**
1292 * @param string $name
1293 * @param string $from
1294 * @param string $to
1295 * @param string $label
1296 * @param string $dateFormat
1297 * @param bool $required
1298 * @param bool $displayTime
1299 */
1300 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1301 if ($displayTime) {
1302 $this->addDateTime($name . $from, $label, $required, ['formatType' => $dateFormat]);
1303 $this->addDateTime($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
1304 }
1305 else {
1306 $this->addDate($name . $from, $label, $required, ['formatType' => $dateFormat]);
1307 $this->addDate($name . $to, ts('To:'), $required, ['formatType' => $dateFormat]);
1308 }
1309 }
1310
1311 /**
1312 * Add a search for a range using date picker fields.
1313 *
1314 * @param string $fieldName
1315 * @param string $label
1316 * @param bool $isDateTime
1317 * Is this a date-time field (not just date).
1318 * @param bool $required
1319 * @param string $fromLabel
1320 * @param string $toLabel
1321 */
1322 public function addDatePickerRange($fieldName, $label, $isDateTime = FALSE, $required = FALSE, $fromLabel = 'From', $toLabel = 'To') {
1323
1324 $options = [
1325 '' => ts('- any -'),
1326 0 => ts('Choose Date Range'),
1327 ] + CRM_Core_OptionGroup::values('relative_date_filters');
1328
1329 $this->add('select',
1330 "{$fieldName}_relative",
1331 $label,
1332 $options,
1333 $required,
1334 ['class' => 'crm-select2']
1335 );
1336 $attributes = ['format' => 'searchDate'];
1337 $extra = ['time' => $isDateTime];
1338 $this->add('datepicker', $fieldName . '_low', ts($fromLabel), $attributes, $required, $extra);
1339 $this->add('datepicker', $fieldName . '_high', ts($toLabel), $attributes, $required, $extra);
1340 }
1341
1342 /**
1343 * Based on form action, return a string representing the api action.
1344 * Used by addField method.
1345 *
1346 * Return string
1347 */
1348 protected function getApiAction() {
1349 $action = $this->getAction();
1350 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1351 return 'create';
1352 }
1353 if ($action & (CRM_Core_Action::VIEW + CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
1354 return 'get';
1355 }
1356 if ($action & (CRM_Core_Action::DELETE)) {
1357 return 'delete';
1358 }
1359 // If you get this exception try adding more cases above.
1360 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
1361 }
1362
1363 /**
1364 * Classes extending CRM_Core_Form should implement this method.
1365 * @throws Exception
1366 */
1367 public function getDefaultEntity() {
1368 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
1369 }
1370
1371 /**
1372 * Classes extending CRM_Core_Form should implement this method.
1373 *
1374 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1375 * @throws Exception
1376 */
1377 public function getDefaultContext() {
1378 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1379 }
1380
1381 /**
1382 * Adds a select based on field metadata.
1383 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1384 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1385 * @param $name
1386 * Field name to go on the form.
1387 * @param array $props
1388 * Mix of html attributes and special properties, namely.
1389 * - entity (api entity name, can usually be inferred automatically from the form class)
1390 * - field (field name - only needed if different from name used on the form)
1391 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1392 * - placeholder - set to NULL to disable
1393 * - multiple - bool
1394 * - context - @see CRM_Core_DAO::buildOptionsContext
1395 * @param bool $required
1396 * @throws CRM_Core_Exception
1397 * @return HTML_QuickForm_Element
1398 */
1399 public function addSelect($name, $props = [], $required = FALSE) {
1400 if (!isset($props['entity'])) {
1401 $props['entity'] = $this->getDefaultEntity();
1402 }
1403 if (!isset($props['field'])) {
1404 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1405 }
1406 if (!isset($props['context'])) {
1407 try {
1408 $props['context'] = $this->getDefaultContext();
1409 }
1410 // This is not a required param, so we'll ignore if this doesn't exist.
1411 catch (Exception $e) {
1412 }
1413 }
1414 // Fetch options from the api unless passed explicitly
1415 if (isset($props['options'])) {
1416 $options = $props['options'];
1417 }
1418 else {
1419 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1420 $options = $info['values'];
1421 }
1422 if (!array_key_exists('placeholder', $props)) {
1423 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1424 }
1425 // Handle custom field
1426 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1427 list(, $id) = explode('_', $name);
1428 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1429 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1430 if (CRM_Utils_Array::value('context', $props) != 'search') {
1431 $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);
1432 }
1433 }
1434 // Core field
1435 else {
1436 $info = civicrm_api3($props['entity'], 'getfields');
1437 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1438 if (
1439 $uniqueName === $props['field'] ||
1440 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1441 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, []))
1442 ) {
1443 break;
1444 }
1445 }
1446 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1447 if (CRM_Utils_Array::value('context', $props) != 'search') {
1448 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1449 }
1450 }
1451 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1452 $props['data-api-entity'] = $props['entity'];
1453 $props['data-api-field'] = $props['field'];
1454 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1455 return $this->add('select', $name, $label, $options, $required, $props);
1456 }
1457
1458 /**
1459 * Adds a field based on metadata.
1460 *
1461 * @param $name
1462 * Field name to go on the form.
1463 * @param array $props
1464 * Mix of html attributes and special properties, namely.
1465 * - entity (api entity name, can usually be inferred automatically from the form class)
1466 * - name (field name - only needed if different from name used on the form)
1467 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1468 * - placeholder - set to NULL to disable
1469 * - multiple - bool
1470 * - context - @see CRM_Core_DAO::buildOptionsContext
1471 * @param bool $required
1472 * @param bool $legacyDate
1473 * Temporary param to facilitate the conversion of fields to use the datepicker in
1474 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1475 * tpl as well. That file is intended to be EOL.
1476 *
1477 * @throws \CiviCRM_API3_Exception
1478 * @throws \Exception
1479 * @return mixed
1480 * HTML_QuickForm_Element
1481 * void
1482 */
1483 public function addField($name, $props = [], $required = FALSE, $legacyDate = TRUE) {
1484 // Resolve context.
1485 if (empty($props['context'])) {
1486 $props['context'] = $this->getDefaultContext();
1487 }
1488 $context = $props['context'];
1489 // Resolve entity.
1490 if (empty($props['entity'])) {
1491 $props['entity'] = $this->getDefaultEntity();
1492 }
1493 // Resolve field.
1494 if (empty($props['name'])) {
1495 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1496 }
1497 // Resolve action.
1498 if (empty($props['action'])) {
1499 $props['action'] = $this->getApiAction();
1500 }
1501
1502 // Handle custom fields
1503 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1504 $fieldId = (int) substr($name, 7);
1505 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
1506 }
1507
1508 // Core field - get metadata.
1509 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1510 $fieldSpec = $fieldSpec['values'];
1511 $fieldSpecLabel = isset($fieldSpec['html']['label']) ? $fieldSpec['html']['label'] : CRM_Utils_Array::value('title', $fieldSpec);
1512 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
1513
1514 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1515 if ($widget == 'TextArea' && $context == 'search') {
1516 $widget = 'Text';
1517 }
1518
1519 $isSelect = (in_array($widget, [
1520 'Select',
1521 'Select2',
1522 'CheckBoxGroup',
1523 'RadioGroup',
1524 'Radio',
1525 ]));
1526
1527 if ($isSelect) {
1528 // Fetch options from the api unless passed explicitly.
1529 if (isset($props['options'])) {
1530 $options = $props['options'];
1531 }
1532 else {
1533 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1534 }
1535 if ($context == 'search') {
1536 $widget = $widget == 'Select2' ? $widget : 'Select';
1537 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
1538 }
1539
1540 // Add data for popup link.
1541 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1542 $hasOptionUrl = !empty($props['option_url']);
1543 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1544 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1545
1546 // Only add if key is not set, or if non-empty option url is provided
1547 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1548 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1549 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1550 $props['data-option-edit-path'] = $optionUrl;
1551 $props['data-api-entity'] = $props['entity'];
1552 $props['data-api-field'] = $props['name'];
1553 }
1554 }
1555 $props += CRM_Utils_Array::value('html', $fieldSpec, []);
1556 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
1557
1558 // TODO: refactor switch statement, to separate methods.
1559 switch ($widget) {
1560 case 'Text':
1561 case 'Url':
1562 case 'Number':
1563 case 'Email':
1564 //TODO: Autodetect ranges
1565 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1566 return $this->add(strtolower($widget), $name, $label, $props, $required);
1567
1568 case 'hidden':
1569 return $this->add('hidden', $name, NULL, $props, $required);
1570
1571 case 'TextArea':
1572 //Set default columns and rows for textarea.
1573 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1574 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1575 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
1576 $props['maxlength'] = $fieldSpec['length'];
1577 }
1578 return $this->add('textarea', $name, $label, $props, $required);
1579
1580 case 'Select Date':
1581 // This is a white list for fields that have been tested with
1582 // date picker. We should be able to remove the other
1583 if ($legacyDate) {
1584 //TODO: add range support
1585 //TODO: Add date formats
1586 //TODO: Add javascript template for dates.
1587 return $this->addDate($name, $label, $required, $props);
1588 }
1589 else {
1590 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
1591 $attributes = ['format' => $fieldSpec['date_format']];
1592 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1593 }
1594
1595 case 'Radio':
1596 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1597 unset($props['separator']);
1598 if (!isset($props['allowClear'])) {
1599 $props['allowClear'] = !$required;
1600 }
1601 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1602
1603 case 'ChainSelect':
1604 $props += [
1605 'required' => $required,
1606 'label' => $label,
1607 'multiple' => $context == 'search',
1608 ];
1609 return $this->addChainSelect($name, $props);
1610
1611 case 'Select':
1612 case 'Select2':
1613 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
1614 if (!array_key_exists('placeholder', $props)) {
1615 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
1616 }
1617 // TODO: Add and/or option for fields that store multiple values
1618 return $this->add(strtolower($widget), $name, $label, $options, $required, $props);
1619
1620 case 'CheckBoxGroup':
1621 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1622
1623 case 'RadioGroup':
1624 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1625
1626 case 'CheckBox':
1627 $text = isset($props['text']) ? $props['text'] : NULL;
1628 unset($props['text']);
1629 return $this->addElement('checkbox', $name, $label, $text, $props);
1630
1631 //add support for 'Advcheckbox' field
1632 case 'advcheckbox':
1633 $text = isset($props['text']) ? $props['text'] : NULL;
1634 unset($props['text']);
1635 return $this->addElement('advcheckbox', $name, $label, $text, $props);
1636
1637 case 'File':
1638 // We should not build upload file in search mode.
1639 if ($context == 'search') {
1640 return;
1641 }
1642 $file = $this->add('file', $name, $label, $props, $required);
1643 $this->addUploadElement($name);
1644 return $file;
1645
1646 case 'RichTextEditor':
1647 return $this->add('wysiwyg', $name, $label, $props, $required);
1648
1649 case 'EntityRef':
1650 return $this->addEntityRef($name, $label, $props, $required);
1651
1652 case 'Password':
1653 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1654 return $this->add('password', $name, $label, $props, $required);
1655
1656 // Check datatypes of fields
1657 // case 'Int':
1658 //case 'Float':
1659 //case 'Money':
1660 //case read only fields
1661 default:
1662 throw new Exception("Unsupported html-element " . $widget);
1663 }
1664 }
1665
1666 /**
1667 * Add a widget for selecting/editing/creating/copying a profile form
1668 *
1669 * @param string $name
1670 * HTML form-element name.
1671 * @param string $label
1672 * Printable label.
1673 * @param string $allowCoreTypes
1674 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1675 * @param string $allowSubTypes
1676 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1677 * @param array $entities
1678 * @param bool $default
1679 * //CRM-15427.
1680 * @param string $usedFor
1681 */
1682 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
1683 // Output widget
1684 // FIXME: Instead of adhoc serialization, use a single json_encode()
1685 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1686 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1687 $this->add('text', $name, $label, [
1688 'class' => 'crm-profile-selector',
1689 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1690 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1691 'data-entities' => json_encode($entities),
1692 //CRM-15427
1693 'data-default' => $default,
1694 'data-usedfor' => json_encode($usedFor),
1695 ]);
1696 }
1697
1698 /**
1699 * @return null
1700 */
1701 public function getRootTitle() {
1702 return NULL;
1703 }
1704
1705 /**
1706 * @return string
1707 */
1708 public function getCompleteTitle() {
1709 return $this->getRootTitle() . $this->getTitle();
1710 }
1711
1712 /**
1713 * @return CRM_Core_Smarty
1714 */
1715 public static function &getTemplate() {
1716 return self::$_template;
1717 }
1718
1719 /**
1720 * @param $elementName
1721 */
1722 public function addUploadElement($elementName) {
1723 $uploadNames = $this->get('uploadNames');
1724 if (!$uploadNames) {
1725 $uploadNames = [];
1726 }
1727 if (is_array($elementName)) {
1728 foreach ($elementName as $name) {
1729 if (!in_array($name, $uploadNames)) {
1730 $uploadNames[] = $name;
1731 }
1732 }
1733 }
1734 else {
1735 if (!in_array($elementName, $uploadNames)) {
1736 $uploadNames[] = $elementName;
1737 }
1738 }
1739 $this->set('uploadNames', $uploadNames);
1740
1741 $config = CRM_Core_Config::singleton();
1742 if (!empty($uploadNames)) {
1743 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1744 }
1745 }
1746
1747 /**
1748 * @param $name
1749 *
1750 * @return null
1751 */
1752 public function getVar($name) {
1753 return isset($this->$name) ? $this->$name : NULL;
1754 }
1755
1756 /**
1757 * @param $name
1758 * @param $value
1759 */
1760 public function setVar($name, $value) {
1761 $this->$name = $value;
1762 }
1763
1764 /**
1765 * Add date.
1766 *
1767 * @deprecated
1768 * Use $this->add('datepicker', ...) instead.
1769 *
1770 * @param string $name
1771 * Name of the element.
1772 * @param string $label
1773 * Label of the element.
1774 * @param bool $required
1775 * True if required.
1776 * @param array $attributes
1777 * Key / value pair.
1778 */
1779 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1780 if (!empty($attributes['formatType'])) {
1781 // get actual format
1782 $params = ['name' => $attributes['formatType']];
1783 $values = [];
1784
1785 // cache date information
1786 static $dateFormat;
1787 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1788 if (empty($dateFormat[$key])) {
1789 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1790 $dateFormat[$key] = $values;
1791 }
1792 else {
1793 $values = $dateFormat[$key];
1794 }
1795
1796 if ($values['date_format']) {
1797 $attributes['format'] = $values['date_format'];
1798 }
1799
1800 if (!empty($values['time_format'])) {
1801 $attributes['timeFormat'] = $values['time_format'];
1802 }
1803 $attributes['startOffset'] = $values['start'];
1804 $attributes['endOffset'] = $values['end'];
1805 }
1806
1807 $config = CRM_Core_Config::singleton();
1808 if (empty($attributes['format'])) {
1809 $attributes['format'] = $config->dateInputFormat;
1810 }
1811
1812 if (!isset($attributes['startOffset'])) {
1813 $attributes['startOffset'] = 10;
1814 }
1815
1816 if (!isset($attributes['endOffset'])) {
1817 $attributes['endOffset'] = 10;
1818 }
1819
1820 $this->add('text', $name, $label, $attributes);
1821
1822 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1823
1824 if (!isset($attributes['timeFormat'])) {
1825 $timeFormat = $config->timeInputFormat;
1826 }
1827 else {
1828 $timeFormat = $attributes['timeFormat'];
1829 }
1830
1831 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1832 if ($timeFormat) {
1833 $show24Hours = TRUE;
1834 if ($timeFormat == 1) {
1835 $show24Hours = FALSE;
1836 }
1837
1838 //CRM-6664 -we are having time element name
1839 //in either flat string or an array format.
1840 $elementName = $name . '_time';
1841 if (substr($name, -1) == ']') {
1842 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1843 }
1844
1845 $this->add('text', $elementName, ts('Time'), ['timeFormat' => $show24Hours]);
1846 }
1847 }
1848
1849 if ($required) {
1850 $this->addRule($name, ts('Please select %1', [1 => $label]), 'required');
1851 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1852 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1853 }
1854 }
1855 }
1856
1857 /**
1858 * Function that will add date and time.
1859 *
1860 * @deprecated
1861 * Use $this->add('datepicker', ...) instead.
1862 *
1863 * @param string $name
1864 * @param string $label
1865 * @param bool $required
1866 * @param null $attributes
1867 */
1868 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1869 $addTime = ['addTime' => TRUE];
1870 if (is_array($attributes)) {
1871 $attributes = array_merge($attributes, $addTime);
1872 }
1873 else {
1874 $attributes = $addTime;
1875 }
1876
1877 $this->addDate($name, $label, $required, $attributes);
1878 }
1879
1880 /**
1881 * Add a currency and money element to the form.
1882 *
1883 * @param string $name
1884 * @param string $label
1885 * @param bool $required
1886 * @param null $attributes
1887 * @param bool $addCurrency
1888 * @param string $currencyName
1889 * @param null $defaultCurrency
1890 * @param bool $freezeCurrency
1891 *
1892 * @return \HTML_QuickForm_Element
1893 */
1894 public function addMoney(
1895 $name,
1896 $label,
1897 $required = FALSE,
1898 $attributes = NULL,
1899 $addCurrency = TRUE,
1900 $currencyName = 'currency',
1901 $defaultCurrency = NULL,
1902 $freezeCurrency = FALSE
1903 ) {
1904 $element = $this->add('text', $name, $label, $attributes, $required);
1905 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1906
1907 if ($addCurrency) {
1908 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1909 }
1910
1911 return $element;
1912 }
1913
1914 /**
1915 * Add currency element to the form.
1916 *
1917 * @param string $name
1918 * @param null $label
1919 * @param bool $required
1920 * @param string $defaultCurrency
1921 * @param bool $freezeCurrency
1922 * @param bool $setDefaultCurrency
1923 */
1924 public function addCurrency(
1925 $name = 'currency',
1926 $label = NULL,
1927 $required = TRUE,
1928 $defaultCurrency = NULL,
1929 $freezeCurrency = FALSE,
1930 $setDefaultCurrency = TRUE
1931 ) {
1932 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1933 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
1934 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1935 $currencies[$defaultCurrency] = $defaultCurrency;
1936 }
1937 $options = ['class' => 'crm-select2 eight'];
1938 if (!$required) {
1939 $currencies = ['' => ''] + $currencies;
1940 $options['placeholder'] = ts('- none -');
1941 }
1942 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1943 if ($freezeCurrency) {
1944 $ele->freeze();
1945 }
1946 if (!$defaultCurrency) {
1947 $config = CRM_Core_Config::singleton();
1948 $defaultCurrency = $config->defaultCurrency;
1949 }
1950 // In some case, setting currency field by default might override the default value
1951 // as encountered in CRM-20527 for batch data entry
1952 if ($setDefaultCurrency) {
1953 $this->setDefaults([$name => $defaultCurrency]);
1954 }
1955 }
1956
1957 /**
1958 * Create a single or multiple entity ref field.
1959 * @param string $name
1960 * @param string $label
1961 * @param array $props
1962 * Mix of html and widget properties, including:.
1963 * - select - params to give to select2 widget
1964 * - entity - defaults to Contact
1965 * - create - can the user create a new entity on-the-fly?
1966 * Set to TRUE if entity is contact and you want the default profiles,
1967 * or pass in your own set of links. @see CRM_Campaign_BAO_Campaign::getEntityRefCreateLinks for format
1968 * note that permissions are checked automatically
1969 * - api - array of settings for the getlist api wrapper
1970 * note that it accepts a 'params' setting which will be passed to the underlying api
1971 * - placeholder - string
1972 * - multiple - bool
1973 * - class, etc. - other html properties
1974 * @param bool $required
1975 *
1976 * @return HTML_QuickForm_Element
1977 */
1978 public function addEntityRef($name, $label = '', $props = [], $required = FALSE) {
1979 // Default properties
1980 $props['api'] = CRM_Utils_Array::value('api', $props, []);
1981 $props['entity'] = CRM_Utils_String::convertStringToCamel(CRM_Utils_Array::value('entity', $props, 'Contact'));
1982 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1983
1984 if (array_key_exists('create', $props) && empty($props['create'])) {
1985 unset($props['create']);
1986 }
1987
1988 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', [1 => ts(str_replace('_', ' ', $props['entity']))]) : ts('- none -'));
1989
1990 $defaults = [];
1991 if (!empty($props['multiple'])) {
1992 $defaults['multiple'] = TRUE;
1993 }
1994 $props['select'] = CRM_Utils_Array::value('select', $props, []) + $defaults;
1995
1996 $this->formatReferenceFieldAttributes($props, get_class($this));
1997 return $this->add('text', $name, $label, $props, $required);
1998 }
1999
2000 /**
2001 * @param array $props
2002 * @param string $formName
2003 */
2004 private function formatReferenceFieldAttributes(&$props, $formName) {
2005 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
2006 $props['data-select-params'] = json_encode($props['select']);
2007 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
2008 $props['data-api-entity'] = $props['entity'];
2009 if (!empty($props['create'])) {
2010 $props['data-create-links'] = json_encode($props['create']);
2011 }
2012 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
2013 }
2014
2015 /**
2016 * Convert all date fields within the params to mysql date ready for the
2017 * BAO layer. In this case fields are checked against the $_datefields defined for the form
2018 * and if time is defined it is incorporated
2019 *
2020 * @param array $params
2021 * Input params from the form.
2022 *
2023 * @todo it would probably be better to work on $this->_params than a passed array
2024 * @todo standardise the format which dates are passed to the BAO layer in & remove date
2025 * handling from BAO
2026 */
2027 public function convertDateFieldsToMySQL(&$params) {
2028 foreach ($this->_dateFields as $fieldName => $specs) {
2029 if (!empty($params[$fieldName])) {
2030 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
2031 CRM_Utils_Date::processDate(
2032 $params[$fieldName],
2033 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
2034 );
2035 }
2036 else {
2037 if (isset($specs['default'])) {
2038 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
2039 }
2040 }
2041 }
2042 }
2043
2044 /**
2045 * @param $elementName
2046 */
2047 public function removeFileRequiredRules($elementName) {
2048 $this->_required = array_diff($this->_required, [$elementName]);
2049 if (isset($this->_rules[$elementName])) {
2050 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2051 if ($ruleInfo['type'] == 'uploadedfile') {
2052 unset($this->_rules[$elementName][$index]);
2053 }
2054 }
2055 if (empty($this->_rules[$elementName])) {
2056 unset($this->_rules[$elementName]);
2057 }
2058 }
2059 }
2060
2061 /**
2062 * Function that can be defined in Form to override or.
2063 * perform specific action on cancel action
2064 */
2065 public function cancelAction() {
2066 }
2067
2068 /**
2069 * Helper function to verify that required fields have been filled.
2070 *
2071 * Typically called within the scope of a FormRule function
2072 *
2073 * @param array $fields
2074 * @param array $values
2075 * @param array $errors
2076 */
2077 public static function validateMandatoryFields($fields, $values, &$errors) {
2078 foreach ($fields as $name => $fld) {
2079 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
2080 $errors[$name] = ts('%1 is a required field.', [1 => $fld['title']]);
2081 }
2082 }
2083 }
2084
2085 /**
2086 * Get contact if for a form object. Prioritise
2087 * - cid in URL if 0 (on behalf on someoneelse)
2088 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
2089 * - logged in user id if it matches the one in the cid in the URL
2090 * - contact id validated from a checksum from a checksum
2091 * - cid from the url if the caller has ACL permission to view
2092 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
2093 *
2094 * @return NULL|int
2095 */
2096 protected function setContactID() {
2097 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
2098 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
2099 $tempID = $this->_params['select_contact_id'];
2100 }
2101 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
2102 // event form stores as an indexed array, contribution form not so much...
2103 $tempID = $this->_params[0]['select_contact_id'];
2104 }
2105
2106 // force to ignore the authenticated user
2107 if ($tempID === '0' || $tempID === 0) {
2108 // we set the cid on the form so that this will be retained for the Confirm page
2109 // in the multi-page form & prevent us returning the $userID when this is called
2110 // from that page
2111 // we don't really need to set it when $tempID is set because the params have that stored
2112 $this->set('cid', 0);
2113 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2114 return (int) $tempID;
2115 }
2116
2117 $userID = $this->getLoggedInUserContactID();
2118
2119 if (!is_null($tempID) && $tempID === $userID) {
2120 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2121 return (int) $userID;
2122 }
2123
2124 //check if this is a checksum authentication
2125 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2126 if ($userChecksum) {
2127 //check for anonymous user.
2128 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2129 if ($validUser) {
2130 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2131 CRM_Core_Resources::singleton()->addVars('coreForm', ['checksum' => $userChecksum]);
2132 return $tempID;
2133 }
2134 }
2135 // check if user has permission, CRM-12062
2136 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
2137 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2138 return $tempID;
2139 }
2140 if (is_numeric($userID)) {
2141 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $userID]);
2142 }
2143 return is_numeric($userID) ? $userID : NULL;
2144 }
2145
2146 /**
2147 * Get the contact id that the form is being submitted for.
2148 *
2149 * @return int|null
2150 */
2151 public function getContactID() {
2152 return $this->setContactID();
2153 }
2154
2155 /**
2156 * Get the contact id of the logged in user.
2157 */
2158 public function getLoggedInUserContactID() {
2159 // check if the user is logged in and has a contact ID
2160 $session = CRM_Core_Session::singleton();
2161 return $session->get('userID');
2162 }
2163
2164 /**
2165 * Add autoselector field -if user has permission to view contacts
2166 * If adding this to a form you also need to add to the tpl e.g
2167 *
2168 * {if !empty($selectable)}
2169 * <div class="crm-summary-row">
2170 * <div class="crm-label">{$form.select_contact.label}</div>
2171 * <div class="crm-content">
2172 * {$form.select_contact.html}
2173 * </div>
2174 * </div>
2175 * {/if}
2176 *
2177 * @param array $profiles
2178 * Ids of profiles that are on the form (to be autofilled).
2179 * @param array $autoCompleteField
2180 *
2181 * - name_field
2182 * - id_field
2183 * - url (for ajax lookup)
2184 *
2185 * @todo add data attributes so we can deal with multiple instances on a form
2186 */
2187 public function addAutoSelector($profiles = [], $autoCompleteField = []) {
2188 $autoCompleteField = array_merge([
2189 'id_field' => 'select_contact_id',
2190 'placeholder' => ts('Select someone else ...'),
2191 'show_hide' => TRUE,
2192 'api' => ['params' => ['contact_type' => 'Individual']],
2193 ], $autoCompleteField);
2194
2195 if ($this->canUseAjaxContactLookups()) {
2196 $this->assign('selectable', $autoCompleteField['id_field']);
2197 $this->addEntityRef($autoCompleteField['id_field'], NULL, [
2198 'placeholder' => $autoCompleteField['placeholder'],
2199 'api' => $autoCompleteField['api'],
2200 ]);
2201
2202 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
2203 ->addSetting([
2204 'form' => ['autocompletes' => $autoCompleteField],
2205 'ids' => ['profile' => $profiles],
2206 ]);
2207 }
2208 }
2209
2210 /**
2211 */
2212 public function canUseAjaxContactLookups() {
2213 if (0 < (civicrm_api3('contact', 'getcount', ['check_permissions' => 1])) &&
2214 CRM_Core_Permission::check([['access AJAX API', 'access CiviCRM']])
2215 ) {
2216 return TRUE;
2217 }
2218 }
2219
2220 /**
2221 * Add the options appropriate to cid = zero - ie. autocomplete
2222 *
2223 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2224 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2225 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2226 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
2227 *
2228 * @param $onlinePaymentProcessorEnabled
2229 */
2230 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
2231 $this->assign('nocid', TRUE);
2232 $profiles = [];
2233 if ($this->_values['custom_pre_id']) {
2234 $profiles[] = $this->_values['custom_pre_id'];
2235 }
2236 if ($this->_values['custom_post_id']) {
2237 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
2238 }
2239 if ($onlinePaymentProcessorEnabled) {
2240 $profiles[] = 'billing';
2241 }
2242 if (!empty($this->_values)) {
2243 $this->addAutoSelector($profiles);
2244 }
2245 }
2246
2247 /**
2248 * Set default values on form for given contact (or no contact defaults)
2249 *
2250 * @param mixed $profile_id
2251 * (can be id, or profile name).
2252 * @param int $contactID
2253 *
2254 * @return array
2255 */
2256 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2257 try {
2258 $defaults = civicrm_api3('profile', 'getsingle', [
2259 'profile_id' => (array) $profile_id,
2260 'contact_id' => $contactID,
2261 ]);
2262 return $defaults;
2263 }
2264 catch (Exception $e) {
2265 // the try catch block gives us silent failure -not 100% sure this is a good idea
2266 // as silent failures are often worse than noisy ones
2267 return [];
2268 }
2269 }
2270
2271 /**
2272 * Sets form attribute.
2273 * @see CRM.loadForm
2274 */
2275 public function preventAjaxSubmit() {
2276 $this->setAttribute('data-no-ajax-submit', 'true');
2277 }
2278
2279 /**
2280 * Sets form attribute.
2281 * @see CRM.loadForm
2282 */
2283 public function allowAjaxSubmit() {
2284 $this->removeAttribute('data-no-ajax-submit');
2285 }
2286
2287 /**
2288 * Sets page title based on entity and action.
2289 * @param string $entityLabel
2290 */
2291 public function setPageTitle($entityLabel) {
2292 switch ($this->_action) {
2293 case CRM_Core_Action::ADD:
2294 CRM_Utils_System::setTitle(ts('New %1', [1 => $entityLabel]));
2295 break;
2296
2297 case CRM_Core_Action::UPDATE:
2298 CRM_Utils_System::setTitle(ts('Edit %1', [1 => $entityLabel]));
2299 break;
2300
2301 case CRM_Core_Action::VIEW:
2302 case CRM_Core_Action::PREVIEW:
2303 CRM_Utils_System::setTitle(ts('View %1', [1 => $entityLabel]));
2304 break;
2305
2306 case CRM_Core_Action::DELETE:
2307 CRM_Utils_System::setTitle(ts('Delete %1', [1 => $entityLabel]));
2308 break;
2309 }
2310 }
2311
2312 /**
2313 * Create a chain-select target field. All settings are optional; the defaults usually work.
2314 *
2315 * @param string $elementName
2316 * @param array $settings
2317 *
2318 * @return HTML_QuickForm_Element
2319 */
2320 public function addChainSelect($elementName, $settings = []) {
2321 $props = $settings += [
2322 'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
2323 'country',
2324 'Country',
2325 'state_province',
2326 'StateProvince',
2327 ], $elementName),
2328 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2329 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2330 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2331 'data-none-prompt' => ts('- N/A -'),
2332 'multiple' => FALSE,
2333 'required' => FALSE,
2334 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2335 ];
2336 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
2337 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2338 $props['data-select-prompt'] = $props['placeholder'];
2339 $props['data-name'] = $elementName;
2340
2341 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2342
2343 // Passing NULL instead of an array of options
2344 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2345 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2346 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2347 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2348 }
2349
2350 /**
2351 * Add actions menu to results form.
2352 *
2353 * @param array $tasks
2354 */
2355 public function addTaskMenu($tasks) {
2356 if (is_array($tasks) && !empty($tasks)) {
2357 // Set constants means this will always load with an empty value, not reloading any submitted value.
2358 // This is appropriate as it is a pseudofield.
2359 $this->setConstants(['task' => '']);
2360 $this->assign('taskMetaData', $tasks);
2361 $select = $this->add('select', 'task', NULL, ['' => ts('Actions')], FALSE, [
2362 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions',
2363 ]
2364 );
2365 foreach ($tasks as $key => $task) {
2366 $attributes = [];
2367 if (isset($task['data'])) {
2368 foreach ($task['data'] as $dataKey => $dataValue) {
2369 $attributes['data-' . $dataKey] = $dataValue;
2370 }
2371 }
2372 $select->addOption($task['title'], $key, $attributes);
2373 }
2374 if (empty($this->_actionButtonName)) {
2375 $this->_actionButtonName = $this->getButtonName('next', 'action');
2376 }
2377 $this->assign('actionButtonName', $this->_actionButtonName);
2378 $this->add('submit', $this->_actionButtonName, ts('Go'), ['class' => 'hiddenElement crm-search-go-button']);
2379
2380 // Radio to choose "All items" or "Selected items only"
2381 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', ['checked' => 'checked']);
2382 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2383 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2384 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2385
2386 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2387 }
2388 }
2389
2390 /**
2391 * Set options and attributes for chain select fields based on the controlling field's value
2392 */
2393 private function preProcessChainSelectFields() {
2394 foreach ($this->_chainSelectFields as $control => $target) {
2395 // The 'target' might get missing if extensions do removeElement() in a form hook.
2396 if ($this->elementExists($target)) {
2397 $targetField = $this->getElement($target);
2398 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2399 $options = [];
2400 // If the control field is on the form, setup chain-select and dynamically populate options
2401 if ($this->elementExists($control)) {
2402 $controlField = $this->getElement($control);
2403 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2404
2405 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2406
2407 $css = (string) $controlField->getAttribute('class');
2408 $controlField->updateAttributes([
2409 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2410 'data-target' => $target,
2411 ]);
2412 $controlValue = $controlField->getValue();
2413 if ($controlValue) {
2414 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2415 if (!$options) {
2416 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2417 }
2418 }
2419 else {
2420 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2421 $targetField->setAttribute('disabled', 'disabled');
2422 }
2423 }
2424 // Control field not present - fall back to loading default options
2425 else {
2426 $options = CRM_Core_PseudoConstant::$targetType();
2427 }
2428 if (!$targetField->getAttribute('multiple')) {
2429 $options = ['' => $targetField->getAttribute('placeholder')] + $options;
2430 $targetField->removeAttribute('placeholder');
2431 }
2432 $targetField->_options = [];
2433 $targetField->loadArray($options);
2434 }
2435 }
2436 }
2437
2438 /**
2439 * Validate country / state / county match and suppress unwanted "required" errors
2440 */
2441 private function validateChainSelectFields() {
2442 foreach ($this->_chainSelectFields as $control => $target) {
2443 if ($this->elementExists($control) && $this->elementExists($target)) {
2444 $controlValue = (array) $this->getElementValue($control);
2445 $targetField = $this->getElement($target);
2446 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2447 $targetValue = array_filter((array) $targetField->getValue());
2448 if ($targetValue || $this->getElementError($target)) {
2449 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2450 if ($targetValue) {
2451 if (!array_intersect($targetValue, array_keys($options))) {
2452 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2453 }
2454 }
2455 // Suppress "required" error for field if it has no options
2456 elseif (!$options) {
2457 $this->setElementError($target, NULL);
2458 }
2459 }
2460 }
2461 }
2462 }
2463
2464 /**
2465 * Assign billing name to the template.
2466 *
2467 * @param array $params
2468 * Form input params, default to $this->_params.
2469 *
2470 * @return string
2471 */
2472 public function assignBillingName($params = []) {
2473 $name = '';
2474 if (empty($params)) {
2475 $params = $this->_params;
2476 }
2477 if (!empty($params['billing_first_name'])) {
2478 $name = $params['billing_first_name'];
2479 }
2480
2481 if (!empty($params['billing_middle_name'])) {
2482 $name .= " {$params['billing_middle_name']}";
2483 }
2484
2485 if (!empty($params['billing_last_name'])) {
2486 $name .= " {$params['billing_last_name']}";
2487 }
2488 $name = trim($name);
2489 $this->assign('billingName', $name);
2490 return $name;
2491 }
2492
2493 /**
2494 * Get the currency for the form.
2495 *
2496 * @todo this should be overriden on the forms rather than having this
2497 * historic, possible handling in here. As we clean that up we should
2498 * add deprecation notices into here.
2499 *
2500 * @param array $submittedValues
2501 * Array allowed so forms inheriting this class do not break.
2502 * Ideally we would make a clear standard around how submitted values
2503 * are stored (is $this->_values consistently doing that?).
2504 *
2505 * @return string
2506 */
2507 public function getCurrency($submittedValues = []) {
2508 $currency = CRM_Utils_Array::value('currency', $this->_values);
2509 // For event forms, currency is in a different spot
2510 if (empty($currency)) {
2511 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2512 }
2513 if (empty($currency)) {
2514 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2515 }
2516 // @todo If empty there is a problem - we should probably put in a deprecation notice
2517 // to warn if that seems to be happening.
2518 return $currency;
2519 }
2520
2521 /**
2522 * Is the form in view or edit mode.
2523 *
2524 * The 'addField' function relies on the form action being one of a set list
2525 * of actions. Checking for these allows for an early return.
2526 *
2527 * @return bool
2528 */
2529 protected function isFormInViewOrEditMode() {
2530 return in_array($this->_action, [
2531 CRM_Core_Action::UPDATE,
2532 CRM_Core_Action::ADD,
2533 CRM_Core_Action::VIEW,
2534 CRM_Core_Action::BROWSE,
2535 CRM_Core_Action::BASIC,
2536 CRM_Core_Action::ADVANCED,
2537 CRM_Core_Action::PREVIEW,
2538 ]);
2539 }
2540
2541 }