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