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