colorTest
[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 */
166 protected $unsavedChangesWarn;
167
168 /**
169 * What to return to the client if in ajax mode (snippet=json)
170 *
171 * @var array
172 */
173 public $ajaxResponse = [];
174
175 /**
176 * Url path used to reach this page
177 *
178 * @var array
179 */
180 public $urlPath = [];
181
182 /**
183 * Context of the form being loaded.
184 *
185 * 'event' or null
186 *
187 * @var string
188 */
189 protected $context;
190
191 /**
192 * @return string
193 */
194 public function getContext() {
195 return $this->context;
196 }
197
198 /**
199 * Set context variable.
200 */
201 public function setContext() {
202 $this->context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this);
203 }
204
205 /**
206 * @var CRM_Core_Controller
207 */
208 public $controller;
209
210 /**
211 * Constants for attributes for various form elements
212 * attempt to standardize on the number of variations that we
213 * use of the below form elements
214 *
215 * @var const string
216 */
217 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
218
219 /**
220 * All checkboxes are defined with a common prefix. This allows us to
221 * have the same javascript to check / clear all the checkboxes etc
222 * If u have multiple groups of checkboxes, you will need to give them different
223 * ids to avoid potential name collision
224 *
225 * @var string|int
226 */
227 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
228
229 /**
230 * @internal to keep track of chain-select fields
231 * @var array
232 */
233 private $_chainSelectFields = [];
234
235 /**
236 * Extra input types we support via the "add" method
237 * @var array
238 */
239 public static $html5Types = [
240 'number',
241 'url',
242 'email',
243 'color',
244 ];
245
246 /**
247 * Constructor for the basic form page.
248 *
249 * We should not use QuickForm directly. This class provides a lot
250 * of default convenient functions, rules and buttons
251 *
252 * @param object $state
253 * State associated with this form.
254 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
255 * @param string $method
256 * The type of http method used (GET/POST).
257 * @param string $name
258 * The name of the form if different from class name.
259 *
260 * @return \CRM_Core_Form
261 */
262 public function __construct(
263 $state = NULL,
264 $action = CRM_Core_Action::NONE,
265 $method = 'post',
266 $name = NULL
267 ) {
268
269 if ($name) {
270 $this->_name = $name;
271 }
272 else {
273 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
274 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
275 }
276
277 parent::__construct($this->_name, $method);
278
279 $this->_state =& $state;
280 if ($this->_state) {
281 $this->_state->setName($this->_name);
282 }
283 $this->_action = (int) $action;
284
285 $this->registerRules();
286
287 // let the constructor initialize this, should happen only once
288 if (!isset(self::$_template)) {
289 self::$_template = CRM_Core_Smarty::singleton();
290 }
291 // Workaround for CRM-15153 - give each form a reasonably unique css class
292 $this->addClass(CRM_Utils_System::getClassName($this));
293
294 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
295 $this->setTranslatedFields();
296 }
297
298 /**
299 * Set translated fields.
300 *
301 * This function is called from the class constructor, allowing us to set
302 * fields on the class that can't be set as properties due to need for
303 * translation or other non-input specific handling.
304 */
305 protected function setTranslatedFields() {}
306
307 /**
308 * Add one or more css classes to the form.
309 *
310 * @param string $className
311 */
312 public function addClass($className) {
313 $classes = $this->getAttribute('class');
314 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
315 }
316
317 /**
318 * Register all the standard rules that most forms potentially use.
319 */
320 public function registerRules() {
321 static $rules = [
322 'title',
323 'longTitle',
324 'variable',
325 'qfVariable',
326 'phone',
327 'integer',
328 'query',
329 'url',
330 'wikiURL',
331 'domain',
332 'numberOfDigit',
333 'date',
334 'currentDate',
335 'asciiFile',
336 'htmlFile',
337 'utf8File',
338 'objectExists',
339 'optionExists',
340 'postalCode',
341 'money',
342 'positiveInteger',
343 'xssString',
344 'fileExists',
345 'settingPath',
346 'autocomplete',
347 'validContact',
348 ];
349
350 foreach ($rules as $rule) {
351 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
352 }
353 }
354
355 /**
356 * Simple easy to use wrapper around addElement.
357 *
358 * Deal with simple validation rules.
359 *
360 * @param string $type
361 * @param string $name
362 * @param string $label
363 * @param string|array $attributes (options for select elements)
364 * @param bool $required
365 * @param array $extra
366 * (attributes for select elements).
367 * For datepicker elements this is consistent with the data
368 * from CRM_Utils_Date::getDatePickerExtra
369 *
370 * @return HTML_QuickForm_Element
371 * Could be an error object
372 */
373 public function &add(
374 $type, $name, $label = '',
375 $attributes = '', $required = FALSE, $extra = NULL
376 ) {
377 // Fudge some extra types that quickform doesn't support
378 $inputType = $type;
379 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
380 $attributes = ($attributes ? $attributes : []) + ['class' => ''];
381 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
382 if ($type == 'wysiwyg' && isset($attributes['preset'])) {
383 $attributes['data-preset'] = $attributes['preset'];
384 unset($attributes['preset']);
385 }
386 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
387 }
388 // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list.
389 if ($inputType == 'select2') {
390 $type = 'text';
391 $options = $attributes;
392 $attributes = $attributes = ($extra ? $extra : []) + ['class' => ''];
393 $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2");
394 $attributes['data-select-params'] = json_encode(['data' => $options, 'multiple' => !empty($attributes['multiple'])]);
395 unset($attributes['multiple']);
396 $extra = NULL;
397 }
398 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
399 if ($type == 'datepicker') {
400 $attributes = ($attributes ? $attributes : []);
401 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
402 if (!empty($attributes['aria-label']) || $label) {
403 $attributes['aria-label'] = CRM_Utils_Array::value('aria-label', $attributes, $label);
404 }
405 $type = "text";
406 }
407 if ($type == 'select' && is_array($extra)) {
408 // Normalize this property
409 if (!empty($extra['multiple'])) {
410 $extra['multiple'] = 'multiple';
411 }
412 else {
413 unset($extra['multiple']);
414 }
415 unset($extra['size'], $extra['maxlength']);
416 // Add placeholder option for select
417 if (isset($extra['placeholder'])) {
418 if ($extra['placeholder'] === TRUE) {
419 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
420 }
421 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
422 $attributes = ['' => $extra['placeholder']] + $attributes;
423 }
424 }
425 }
426 $element = $this->addElement($type, $name, $label, $attributes, $extra);
427 if (HTML_QuickForm::isError($element)) {
428 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
429 }
430
431 if ($inputType == 'color') {
432 $this->addRule($name, ts('%1 must contain a color value e.g. #ffffff.', [1 => $label]), 'regex', '/#[0-9a-fA-F]{6}/');
433 }
434
435 if ($required) {
436 if ($type == 'file') {
437 $error = $this->addRule($name, ts('%1 is a required field.', [1 => $label]), 'uploadedfile');
438 }
439 else {
440 $error = $this->addRule($name, ts('%1 is a required field.', [1 => $label]), 'required');
441 }
442 if (HTML_QuickForm::isError($error)) {
443 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
444 }
445 }
446
447 // Add context for the editing of option groups
448 if (isset($extra['option_context'])) {
449 $context = json_encode($extra['option_context']);
450 $element->setAttribute('data-option-edit-context', $context);
451 }
452
453 return $element;
454 }
455
456 /**
457 * Preprocess form.
458 *
459 * This is called before buildForm. Any pre-processing that
460 * needs to be done for buildForm should be done here.
461 *
462 * This is a virtual function and should be redefined if needed.
463 */
464 public function preProcess() {
465 }
466
467 /**
468 * Called after the form is validated.
469 *
470 * Any processing of form state etc should be done in this function.
471 * Typically all processing associated with a form should be done
472 * here and relevant state should be stored in the session
473 *
474 * This is a virtual function and should be redefined if needed
475 */
476 public function postProcess() {
477 }
478
479 /**
480 * Main process wrapper.
481 *
482 * Implemented so that we can call all the hook functions.
483 *
484 * @param bool $allowAjax
485 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
486 * and bury it deeper down in the controller
487 */
488 public function mainProcess($allowAjax = TRUE) {
489 $this->postProcess();
490 $this->postProcessHook();
491
492 // Respond with JSON if in AJAX context (also support legacy value '6')
493 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], [
494 CRM_Core_Smarty::PRINT_JSON,
495 6,
496 ])
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 // Fetch options from the api unless passed explicitly
1397 if (isset($props['options'])) {
1398 $options = $props['options'];
1399 }
1400 else {
1401 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1402 $options = $info['values'];
1403 }
1404 if (!array_key_exists('placeholder', $props)) {
1405 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1406 }
1407 // Handle custom field
1408 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1409 list(, $id) = explode('_', $name);
1410 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1411 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1412 if (CRM_Utils_Array::value('context', $props) != 'search') {
1413 $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);
1414 }
1415 }
1416 // Core field
1417 else {
1418 $info = civicrm_api3($props['entity'], 'getfields');
1419 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1420 if (
1421 $uniqueName === $props['field'] ||
1422 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1423 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, []))
1424 ) {
1425 break;
1426 }
1427 }
1428 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1429 if (CRM_Utils_Array::value('context', $props) != 'search') {
1430 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1431 }
1432 }
1433 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1434 $props['data-api-entity'] = $props['entity'];
1435 $props['data-api-field'] = $props['field'];
1436 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1437 return $this->add('select', $name, $label, $options, $required, $props);
1438 }
1439
1440 /**
1441 * Adds a field based on metadata.
1442 *
1443 * @param $name
1444 * Field name to go on the form.
1445 * @param array $props
1446 * Mix of html attributes and special properties, namely.
1447 * - entity (api entity name, can usually be inferred automatically from the form class)
1448 * - name (field name - only needed if different from name used on the form)
1449 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1450 * - placeholder - set to NULL to disable
1451 * - multiple - bool
1452 * - context - @see CRM_Core_DAO::buildOptionsContext
1453 * @param bool $required
1454 * @param bool $legacyDate
1455 * Temporary param to facilitate the conversion of fields to use the datepicker in
1456 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1457 * tpl as well. That file is intended to be EOL.
1458 *
1459 * @throws \CiviCRM_API3_Exception
1460 * @throws \Exception
1461 * @return HTML_QuickForm_Element
1462 */
1463 public function addField($name, $props = [], $required = FALSE, $legacyDate = TRUE) {
1464 // Resolve context.
1465 if (empty($props['context'])) {
1466 $props['context'] = $this->getDefaultContext();
1467 }
1468 $context = $props['context'];
1469 // Resolve entity.
1470 if (empty($props['entity'])) {
1471 $props['entity'] = $this->getDefaultEntity();
1472 }
1473 // Resolve field.
1474 if (empty($props['name'])) {
1475 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1476 }
1477 // Resolve action.
1478 if (empty($props['action'])) {
1479 $props['action'] = $this->getApiAction();
1480 }
1481
1482 // Handle custom fields
1483 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1484 $fieldId = (int) substr($name, 7);
1485 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
1486 }
1487
1488 // Core field - get metadata.
1489 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1490 $fieldSpec = $fieldSpec['values'];
1491 $fieldSpecLabel = isset($fieldSpec['html']['label']) ? $fieldSpec['html']['label'] : CRM_Utils_Array::value('title', $fieldSpec);
1492 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
1493
1494 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1495 if ($widget == 'TextArea' && $context == 'search') {
1496 $widget = 'Text';
1497 }
1498
1499 $isSelect = (in_array($widget, [
1500 'Select',
1501 'CheckBoxGroup',
1502 'RadioGroup',
1503 'Radio',
1504 ]));
1505
1506 if ($isSelect) {
1507 // Fetch options from the api unless passed explicitly.
1508 if (isset($props['options'])) {
1509 $options = $props['options'];
1510 }
1511 else {
1512 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1513 }
1514 if ($context == 'search') {
1515 $widget = 'Select';
1516 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
1517 }
1518
1519 // Add data for popup link.
1520 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1521 $hasOptionUrl = !empty($props['option_url']);
1522 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1523 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1524
1525 // Only add if key is not set, or if non-empty option url is provided
1526 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1527 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1528 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1529 $props['data-option-edit-path'] = $optionUrl;
1530 $props['data-api-entity'] = $props['entity'];
1531 $props['data-api-field'] = $props['name'];
1532 }
1533 }
1534 $props += CRM_Utils_Array::value('html', $fieldSpec, []);
1535 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
1536
1537 // TODO: refactor switch statement, to separate methods.
1538 switch ($widget) {
1539 case 'Text':
1540 case 'Url':
1541 case 'Number':
1542 case 'Email':
1543 //TODO: Autodetect ranges
1544 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1545 return $this->add(strtolower($widget), $name, $label, $props, $required);
1546
1547 case 'hidden':
1548 return $this->add('hidden', $name, NULL, $props, $required);
1549
1550 case 'TextArea':
1551 //Set default columns and rows for textarea.
1552 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1553 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1554 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
1555 $props['maxlength'] = $fieldSpec['length'];
1556 }
1557 return $this->add('textarea', $name, $label, $props, $required);
1558
1559 case 'Select Date':
1560 // This is a white list for fields that have been tested with
1561 // date picker. We should be able to remove the other
1562 if ($legacyDate) {
1563 //TODO: add range support
1564 //TODO: Add date formats
1565 //TODO: Add javascript template for dates.
1566 return $this->addDate($name, $label, $required, $props);
1567 }
1568 else {
1569 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
1570 $attributes = ['format' => $fieldSpec['date_format']];
1571 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1572 }
1573
1574 case 'Radio':
1575 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1576 unset($props['separator']);
1577 if (!isset($props['allowClear'])) {
1578 $props['allowClear'] = !$required;
1579 }
1580 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1581
1582 case 'ChainSelect':
1583 $props += [
1584 'required' => $required,
1585 'label' => $label,
1586 'multiple' => $context == 'search',
1587 ];
1588 return $this->addChainSelect($name, $props);
1589
1590 case 'Select':
1591 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
1592 if (!array_key_exists('placeholder', $props)) {
1593 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
1594 }
1595 // TODO: Add and/or option for fields that store multiple values
1596 return $this->add('select', $name, $label, $options, $required, $props);
1597
1598 case 'CheckBoxGroup':
1599 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1600
1601 case 'RadioGroup':
1602 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1603
1604 case 'CheckBox':
1605 $text = isset($props['text']) ? $props['text'] : NULL;
1606 unset($props['text']);
1607 return $this->addElement('checkbox', $name, $label, $text, $props);
1608
1609 //add support for 'Advcheckbox' field
1610 case 'advcheckbox':
1611 $text = isset($props['text']) ? $props['text'] : NULL;
1612 unset($props['text']);
1613 return $this->addElement('advcheckbox', $name, $label, $text, $props);
1614
1615 case 'File':
1616 // We should not build upload file in search mode.
1617 if ($context == 'search') {
1618 return;
1619 }
1620 $file = $this->add('file', $name, $label, $props, $required);
1621 $this->addUploadElement($name);
1622 return $file;
1623
1624 case 'RichTextEditor':
1625 return $this->add('wysiwyg', $name, $label, $props, $required);
1626
1627 case 'EntityRef':
1628 return $this->addEntityRef($name, $label, $props, $required);
1629
1630 case 'Password':
1631 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1632 return $this->add('password', $name, $label, $props, $required);
1633
1634 // Check datatypes of fields
1635 // case 'Int':
1636 //case 'Float':
1637 //case 'Money':
1638 //case read only fields
1639 default:
1640 throw new Exception("Unsupported html-element " . $widget);
1641 }
1642 }
1643
1644 /**
1645 * Add a widget for selecting/editing/creating/copying a profile form
1646 *
1647 * @param string $name
1648 * HTML form-element name.
1649 * @param string $label
1650 * Printable label.
1651 * @param string $allowCoreTypes
1652 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1653 * @param string $allowSubTypes
1654 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1655 * @param array $entities
1656 * @param bool $default
1657 * //CRM-15427.
1658 * @param string $usedFor
1659 */
1660 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
1661 // Output widget
1662 // FIXME: Instead of adhoc serialization, use a single json_encode()
1663 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1664 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1665 $this->add('text', $name, $label, [
1666 'class' => 'crm-profile-selector',
1667 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1668 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1669 'data-entities' => json_encode($entities),
1670 //CRM-15427
1671 'data-default' => $default,
1672 'data-usedfor' => json_encode($usedFor),
1673 ]);
1674 }
1675
1676 /**
1677 * @return null
1678 */
1679 public function getRootTitle() {
1680 return NULL;
1681 }
1682
1683 /**
1684 * @return string
1685 */
1686 public function getCompleteTitle() {
1687 return $this->getRootTitle() . $this->getTitle();
1688 }
1689
1690 /**
1691 * @return CRM_Core_Smarty
1692 */
1693 public static function &getTemplate() {
1694 return self::$_template;
1695 }
1696
1697 /**
1698 * @param $elementName
1699 */
1700 public function addUploadElement($elementName) {
1701 $uploadNames = $this->get('uploadNames');
1702 if (!$uploadNames) {
1703 $uploadNames = [];
1704 }
1705 if (is_array($elementName)) {
1706 foreach ($elementName as $name) {
1707 if (!in_array($name, $uploadNames)) {
1708 $uploadNames[] = $name;
1709 }
1710 }
1711 }
1712 else {
1713 if (!in_array($elementName, $uploadNames)) {
1714 $uploadNames[] = $elementName;
1715 }
1716 }
1717 $this->set('uploadNames', $uploadNames);
1718
1719 $config = CRM_Core_Config::singleton();
1720 if (!empty($uploadNames)) {
1721 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1722 }
1723 }
1724
1725 /**
1726 * @param $name
1727 *
1728 * @return null
1729 */
1730 public function getVar($name) {
1731 return isset($this->$name) ? $this->$name : NULL;
1732 }
1733
1734 /**
1735 * @param $name
1736 * @param $value
1737 */
1738 public function setVar($name, $value) {
1739 $this->$name = $value;
1740 }
1741
1742 /**
1743 * Add date.
1744 *
1745 * @deprecated
1746 * Use $this->add('datepicker', ...) instead.
1747 *
1748 * @param string $name
1749 * Name of the element.
1750 * @param string $label
1751 * Label of the element.
1752 * @param bool $required
1753 * True if required.
1754 * @param array $attributes
1755 * Key / value pair.
1756 */
1757 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1758 if (!empty($attributes['formatType'])) {
1759 // get actual format
1760 $params = ['name' => $attributes['formatType']];
1761 $values = [];
1762
1763 // cache date information
1764 static $dateFormat;
1765 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1766 if (empty($dateFormat[$key])) {
1767 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1768 $dateFormat[$key] = $values;
1769 }
1770 else {
1771 $values = $dateFormat[$key];
1772 }
1773
1774 if ($values['date_format']) {
1775 $attributes['format'] = $values['date_format'];
1776 }
1777
1778 if (!empty($values['time_format'])) {
1779 $attributes['timeFormat'] = $values['time_format'];
1780 }
1781 $attributes['startOffset'] = $values['start'];
1782 $attributes['endOffset'] = $values['end'];
1783 }
1784
1785 $config = CRM_Core_Config::singleton();
1786 if (empty($attributes['format'])) {
1787 $attributes['format'] = $config->dateInputFormat;
1788 }
1789
1790 if (!isset($attributes['startOffset'])) {
1791 $attributes['startOffset'] = 10;
1792 }
1793
1794 if (!isset($attributes['endOffset'])) {
1795 $attributes['endOffset'] = 10;
1796 }
1797
1798 $this->add('text', $name, $label, $attributes);
1799
1800 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1801
1802 if (!isset($attributes['timeFormat'])) {
1803 $timeFormat = $config->timeInputFormat;
1804 }
1805 else {
1806 $timeFormat = $attributes['timeFormat'];
1807 }
1808
1809 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1810 if ($timeFormat) {
1811 $show24Hours = TRUE;
1812 if ($timeFormat == 1) {
1813 $show24Hours = FALSE;
1814 }
1815
1816 //CRM-6664 -we are having time element name
1817 //in either flat string or an array format.
1818 $elementName = $name . '_time';
1819 if (substr($name, -1) == ']') {
1820 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1821 }
1822
1823 $this->add('text', $elementName, ts('Time'), ['timeFormat' => $show24Hours]);
1824 }
1825 }
1826
1827 if ($required) {
1828 $this->addRule($name, ts('Please select %1', [1 => $label]), 'required');
1829 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1830 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1831 }
1832 }
1833 }
1834
1835 /**
1836 * Function that will add date and time.
1837 *
1838 * @deprecated
1839 * Use $this->add('datepicker', ...) instead.
1840 *
1841 * @param string $name
1842 * @param string $label
1843 * @param bool $required
1844 * @param null $attributes
1845 */
1846 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1847 $addTime = ['addTime' => TRUE];
1848 if (is_array($attributes)) {
1849 $attributes = array_merge($attributes, $addTime);
1850 }
1851 else {
1852 $attributes = $addTime;
1853 }
1854
1855 $this->addDate($name, $label, $required, $attributes);
1856 }
1857
1858 /**
1859 * Add a currency and money element to the form.
1860 *
1861 * @param string $name
1862 * @param string $label
1863 * @param bool $required
1864 * @param null $attributes
1865 * @param bool $addCurrency
1866 * @param string $currencyName
1867 * @param null $defaultCurrency
1868 * @param bool $freezeCurrency
1869 *
1870 * @return \HTML_QuickForm_Element
1871 */
1872 public function addMoney(
1873 $name,
1874 $label,
1875 $required = FALSE,
1876 $attributes = NULL,
1877 $addCurrency = TRUE,
1878 $currencyName = 'currency',
1879 $defaultCurrency = NULL,
1880 $freezeCurrency = FALSE
1881 ) {
1882 $element = $this->add('text', $name, $label, $attributes, $required);
1883 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1884
1885 if ($addCurrency) {
1886 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1887 }
1888
1889 return $element;
1890 }
1891
1892 /**
1893 * Add currency element to the form.
1894 *
1895 * @param string $name
1896 * @param null $label
1897 * @param bool $required
1898 * @param string $defaultCurrency
1899 * @param bool $freezeCurrency
1900 * @param bool $setDefaultCurrency
1901 */
1902 public function addCurrency(
1903 $name = 'currency',
1904 $label = NULL,
1905 $required = TRUE,
1906 $defaultCurrency = NULL,
1907 $freezeCurrency = FALSE,
1908 $setDefaultCurrency = TRUE
1909 ) {
1910 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1911 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
1912 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1913 $currencies[$defaultCurrency] = $defaultCurrency;
1914 }
1915 $options = ['class' => 'crm-select2 eight'];
1916 if (!$required) {
1917 $currencies = ['' => ''] + $currencies;
1918 $options['placeholder'] = ts('- none -');
1919 }
1920 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1921 if ($freezeCurrency) {
1922 $ele->freeze();
1923 }
1924 if (!$defaultCurrency) {
1925 $config = CRM_Core_Config::singleton();
1926 $defaultCurrency = $config->defaultCurrency;
1927 }
1928 // In some case, setting currency field by default might override the default value
1929 // as encountered in CRM-20527 for batch data entry
1930 if ($setDefaultCurrency) {
1931 $this->setDefaults([$name => $defaultCurrency]);
1932 }
1933 }
1934
1935 /**
1936 * Create a single or multiple entity ref field.
1937 * @param string $name
1938 * @param string $label
1939 * @param array $props
1940 * Mix of html and widget properties, including:.
1941 * - select - params to give to select2 widget
1942 * - entity - defaults to Contact
1943 * - create - can the user create a new entity on-the-fly?
1944 * Set to TRUE if entity is contact and you want the default profiles,
1945 * or pass in your own set of links. @see CRM_Campaign_BAO_Campaign::getEntityRefCreateLinks for format
1946 * note that permissions are checked automatically
1947 * - api - array of settings for the getlist api wrapper
1948 * note that it accepts a 'params' setting which will be passed to the underlying api
1949 * - placeholder - string
1950 * - multiple - bool
1951 * - class, etc. - other html properties
1952 * @param bool $required
1953 *
1954 * @return HTML_QuickForm_Element
1955 */
1956 public function addEntityRef($name, $label = '', $props = [], $required = FALSE) {
1957 // Default properties
1958 $props['api'] = CRM_Utils_Array::value('api', $props, []);
1959 $props['entity'] = CRM_Utils_String::convertStringToCamel(CRM_Utils_Array::value('entity', $props, 'Contact'));
1960 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1961
1962 if (array_key_exists('create', $props) && empty($props['create'])) {
1963 unset($props['create']);
1964 }
1965
1966 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', [1 => ts(str_replace('_', ' ', $props['entity']))]) : ts('- none -'));
1967
1968 $defaults = [];
1969 if (!empty($props['multiple'])) {
1970 $defaults['multiple'] = TRUE;
1971 }
1972 $props['select'] = CRM_Utils_Array::value('select', $props, []) + $defaults;
1973
1974 $this->formatReferenceFieldAttributes($props, get_class($this));
1975 return $this->add('text', $name, $label, $props, $required);
1976 }
1977
1978 /**
1979 * @param array $props
1980 * @param string $formName
1981 */
1982 private function formatReferenceFieldAttributes(&$props, $formName) {
1983 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
1984 $props['data-select-params'] = json_encode($props['select']);
1985 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1986 $props['data-api-entity'] = $props['entity'];
1987 if (!empty($props['create'])) {
1988 $props['data-create-links'] = json_encode($props['create']);
1989 }
1990 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1991 }
1992
1993 /**
1994 * Convert all date fields within the params to mysql date ready for the
1995 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1996 * and if time is defined it is incorporated
1997 *
1998 * @param array $params
1999 * Input params from the form.
2000 *
2001 * @todo it would probably be better to work on $this->_params than a passed array
2002 * @todo standardise the format which dates are passed to the BAO layer in & remove date
2003 * handling from BAO
2004 */
2005 public function convertDateFieldsToMySQL(&$params) {
2006 foreach ($this->_dateFields as $fieldName => $specs) {
2007 if (!empty($params[$fieldName])) {
2008 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
2009 CRM_Utils_Date::processDate(
2010 $params[$fieldName],
2011 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
2012 );
2013 }
2014 else {
2015 if (isset($specs['default'])) {
2016 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
2017 }
2018 }
2019 }
2020 }
2021
2022 /**
2023 * @param $elementName
2024 */
2025 public function removeFileRequiredRules($elementName) {
2026 $this->_required = array_diff($this->_required, [$elementName]);
2027 if (isset($this->_rules[$elementName])) {
2028 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2029 if ($ruleInfo['type'] == 'uploadedfile') {
2030 unset($this->_rules[$elementName][$index]);
2031 }
2032 }
2033 if (empty($this->_rules[$elementName])) {
2034 unset($this->_rules[$elementName]);
2035 }
2036 }
2037 }
2038
2039 /**
2040 * Function that can be defined in Form to override or.
2041 * perform specific action on cancel action
2042 */
2043 public function cancelAction() {
2044 }
2045
2046 /**
2047 * Helper function to verify that required fields have been filled.
2048 *
2049 * Typically called within the scope of a FormRule function
2050 *
2051 * @param array $fields
2052 * @param array $values
2053 * @param array $errors
2054 */
2055 public static function validateMandatoryFields($fields, $values, &$errors) {
2056 foreach ($fields as $name => $fld) {
2057 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
2058 $errors[$name] = ts('%1 is a required field.', [1 => $fld['title']]);
2059 }
2060 }
2061 }
2062
2063 /**
2064 * Get contact if for a form object. Prioritise
2065 * - cid in URL if 0 (on behalf on someoneelse)
2066 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
2067 * - logged in user id if it matches the one in the cid in the URL
2068 * - contact id validated from a checksum from a checksum
2069 * - cid from the url if the caller has ACL permission to view
2070 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
2071 *
2072 * @return NULL|int
2073 */
2074 protected function setContactID() {
2075 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
2076 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
2077 $tempID = $this->_params['select_contact_id'];
2078 }
2079 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
2080 // event form stores as an indexed array, contribution form not so much...
2081 $tempID = $this->_params[0]['select_contact_id'];
2082 }
2083
2084 // force to ignore the authenticated user
2085 if ($tempID === '0' || $tempID === 0) {
2086 // we set the cid on the form so that this will be retained for the Confirm page
2087 // in the multi-page form & prevent us returning the $userID when this is called
2088 // from that page
2089 // we don't really need to set it when $tempID is set because the params have that stored
2090 $this->set('cid', 0);
2091 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2092 return (int) $tempID;
2093 }
2094
2095 $userID = $this->getLoggedInUserContactID();
2096
2097 if (!is_null($tempID) && $tempID === $userID) {
2098 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2099 return (int) $userID;
2100 }
2101
2102 //check if this is a checksum authentication
2103 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2104 if ($userChecksum) {
2105 //check for anonymous user.
2106 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2107 if ($validUser) {
2108 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2109 CRM_Core_Resources::singleton()->addVars('coreForm', ['checksum' => $userChecksum]);
2110 return $tempID;
2111 }
2112 }
2113 // check if user has permission, CRM-12062
2114 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
2115 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $tempID]);
2116 return $tempID;
2117 }
2118 if (is_numeric($userID)) {
2119 CRM_Core_Resources::singleton()->addVars('coreForm', ['contact_id' => (int) $userID]);
2120 }
2121 return is_numeric($userID) ? $userID : NULL;
2122 }
2123
2124 /**
2125 * Get the contact id that the form is being submitted for.
2126 *
2127 * @return int|NULL
2128 */
2129 public function getContactID() {
2130 return $this->setContactID();
2131 }
2132
2133 /**
2134 * Get the contact id of the logged in user.
2135 */
2136 public function getLoggedInUserContactID() {
2137 // check if the user is logged in and has a contact ID
2138 $session = CRM_Core_Session::singleton();
2139 return $session->get('userID');
2140 }
2141
2142 /**
2143 * Add autoselector field -if user has permission to view contacts
2144 * If adding this to a form you also need to add to the tpl e.g
2145 *
2146 * {if !empty($selectable)}
2147 * <div class="crm-summary-row">
2148 * <div class="crm-label">{$form.select_contact.label}</div>
2149 * <div class="crm-content">
2150 * {$form.select_contact.html}
2151 * </div>
2152 * </div>
2153 * {/if}
2154 *
2155 * @param array $profiles
2156 * Ids of profiles that are on the form (to be autofilled).
2157 * @param array $autoCompleteField
2158 *
2159 * - name_field
2160 * - id_field
2161 * - url (for ajax lookup)
2162 *
2163 * @todo add data attributes so we can deal with multiple instances on a form
2164 */
2165 public function addAutoSelector($profiles = [], $autoCompleteField = []) {
2166 $autoCompleteField = array_merge([
2167 'id_field' => 'select_contact_id',
2168 'placeholder' => ts('Select someone else ...'),
2169 'show_hide' => TRUE,
2170 'api' => ['params' => ['contact_type' => 'Individual']],
2171 ], $autoCompleteField);
2172
2173 if ($this->canUseAjaxContactLookups()) {
2174 $this->assign('selectable', $autoCompleteField['id_field']);
2175 $this->addEntityRef($autoCompleteField['id_field'], NULL, [
2176 'placeholder' => $autoCompleteField['placeholder'],
2177 'api' => $autoCompleteField['api'],
2178 ]);
2179
2180 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
2181 ->addSetting([
2182 'form' => ['autocompletes' => $autoCompleteField],
2183 'ids' => ['profile' => $profiles],
2184 ]);
2185 }
2186 }
2187
2188 /**
2189 */
2190 public function canUseAjaxContactLookups() {
2191 if (0 < (civicrm_api3('contact', 'getcount', ['check_permissions' => 1])) &&
2192 CRM_Core_Permission::check([['access AJAX API', 'access CiviCRM']])
2193 ) {
2194 return TRUE;
2195 }
2196 }
2197
2198 /**
2199 * Add the options appropriate to cid = zero - ie. autocomplete
2200 *
2201 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2202 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2203 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2204 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
2205 *
2206 * @param $onlinePaymentProcessorEnabled
2207 */
2208 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
2209 $this->assign('nocid', TRUE);
2210 $profiles = [];
2211 if ($this->_values['custom_pre_id']) {
2212 $profiles[] = $this->_values['custom_pre_id'];
2213 }
2214 if ($this->_values['custom_post_id']) {
2215 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
2216 }
2217 if ($onlinePaymentProcessorEnabled) {
2218 $profiles[] = 'billing';
2219 }
2220 if (!empty($this->_values)) {
2221 $this->addAutoSelector($profiles);
2222 }
2223 }
2224
2225 /**
2226 * Set default values on form for given contact (or no contact defaults)
2227 *
2228 * @param mixed $profile_id
2229 * (can be id, or profile name).
2230 * @param int $contactID
2231 *
2232 * @return array
2233 */
2234 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2235 try {
2236 $defaults = civicrm_api3('profile', 'getsingle', [
2237 'profile_id' => (array) $profile_id,
2238 'contact_id' => $contactID,
2239 ]);
2240 return $defaults;
2241 }
2242 catch (Exception $e) {
2243 // the try catch block gives us silent failure -not 100% sure this is a good idea
2244 // as silent failures are often worse than noisy ones
2245 return [];
2246 }
2247 }
2248
2249 /**
2250 * Sets form attribute.
2251 * @see CRM.loadForm
2252 */
2253 public function preventAjaxSubmit() {
2254 $this->setAttribute('data-no-ajax-submit', 'true');
2255 }
2256
2257 /**
2258 * Sets form attribute.
2259 * @see CRM.loadForm
2260 */
2261 public function allowAjaxSubmit() {
2262 $this->removeAttribute('data-no-ajax-submit');
2263 }
2264
2265 /**
2266 * Sets page title based on entity and action.
2267 * @param string $entityLabel
2268 */
2269 public function setPageTitle($entityLabel) {
2270 switch ($this->_action) {
2271 case CRM_Core_Action::ADD:
2272 CRM_Utils_System::setTitle(ts('New %1', [1 => $entityLabel]));
2273 break;
2274
2275 case CRM_Core_Action::UPDATE:
2276 CRM_Utils_System::setTitle(ts('Edit %1', [1 => $entityLabel]));
2277 break;
2278
2279 case CRM_Core_Action::VIEW:
2280 case CRM_Core_Action::PREVIEW:
2281 CRM_Utils_System::setTitle(ts('View %1', [1 => $entityLabel]));
2282 break;
2283
2284 case CRM_Core_Action::DELETE:
2285 CRM_Utils_System::setTitle(ts('Delete %1', [1 => $entityLabel]));
2286 break;
2287 }
2288 }
2289
2290 /**
2291 * Create a chain-select target field. All settings are optional; the defaults usually work.
2292 *
2293 * @param string $elementName
2294 * @param array $settings
2295 *
2296 * @return HTML_QuickForm_Element
2297 */
2298 public function addChainSelect($elementName, $settings = []) {
2299 $props = $settings += [
2300 'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
2301 'country',
2302 'Country',
2303 'state_province',
2304 'StateProvince',
2305 ], $elementName),
2306 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2307 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2308 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2309 'data-none-prompt' => ts('- N/A -'),
2310 'multiple' => FALSE,
2311 'required' => FALSE,
2312 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2313 ];
2314 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
2315 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2316 $props['data-select-prompt'] = $props['placeholder'];
2317 $props['data-name'] = $elementName;
2318
2319 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2320
2321 // Passing NULL instead of an array of options
2322 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2323 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2324 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2325 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2326 }
2327
2328 /**
2329 * Add actions menu to results form.
2330 *
2331 * @param array $tasks
2332 */
2333 public function addTaskMenu($tasks) {
2334 if (is_array($tasks) && !empty($tasks)) {
2335 // Set constants means this will always load with an empty value, not reloading any submitted value.
2336 // This is appropriate as it is a pseudofield.
2337 $this->setConstants(['task' => '']);
2338 $this->assign('taskMetaData', $tasks);
2339 $select = $this->add('select', 'task', NULL, ['' => ts('Actions')], FALSE, [
2340 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions']
2341 );
2342 foreach ($tasks as $key => $task) {
2343 $attributes = [];
2344 if (isset($task['data'])) {
2345 foreach ($task['data'] as $dataKey => $dataValue) {
2346 $attributes['data-' . $dataKey] = $dataValue;
2347 }
2348 }
2349 $select->addOption($task['title'], $key, $attributes);
2350 }
2351 if (empty($this->_actionButtonName)) {
2352 $this->_actionButtonName = $this->getButtonName('next', 'action');
2353 }
2354 $this->assign('actionButtonName', $this->_actionButtonName);
2355 $this->add('submit', $this->_actionButtonName, ts('Go'), ['class' => 'hiddenElement crm-search-go-button']);
2356
2357 // Radio to choose "All items" or "Selected items only"
2358 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', ['checked' => 'checked']);
2359 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2360 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2361 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2362
2363 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2364 }
2365 }
2366
2367 /**
2368 * Set options and attributes for chain select fields based on the controlling field's value
2369 */
2370 private function preProcessChainSelectFields() {
2371 foreach ($this->_chainSelectFields as $control => $target) {
2372 // The 'target' might get missing if extensions do removeElement() in a form hook.
2373 if ($this->elementExists($target)) {
2374 $targetField = $this->getElement($target);
2375 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2376 $options = [];
2377 // If the control field is on the form, setup chain-select and dynamically populate options
2378 if ($this->elementExists($control)) {
2379 $controlField = $this->getElement($control);
2380 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2381
2382 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2383
2384 $css = (string) $controlField->getAttribute('class');
2385 $controlField->updateAttributes([
2386 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2387 'data-target' => $target,
2388 ]);
2389 $controlValue = $controlField->getValue();
2390 if ($controlValue) {
2391 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2392 if (!$options) {
2393 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2394 }
2395 }
2396 else {
2397 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2398 $targetField->setAttribute('disabled', 'disabled');
2399 }
2400 }
2401 // Control field not present - fall back to loading default options
2402 else {
2403 $options = CRM_Core_PseudoConstant::$targetType();
2404 }
2405 if (!$targetField->getAttribute('multiple')) {
2406 $options = ['' => $targetField->getAttribute('placeholder')] + $options;
2407 $targetField->removeAttribute('placeholder');
2408 }
2409 $targetField->_options = [];
2410 $targetField->loadArray($options);
2411 }
2412 }
2413 }
2414
2415 /**
2416 * Validate country / state / county match and suppress unwanted "required" errors
2417 */
2418 private function validateChainSelectFields() {
2419 foreach ($this->_chainSelectFields as $control => $target) {
2420 if ($this->elementExists($control) && $this->elementExists($target)) {
2421 $controlValue = (array) $this->getElementValue($control);
2422 $targetField = $this->getElement($target);
2423 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2424 $targetValue = array_filter((array) $targetField->getValue());
2425 if ($targetValue || $this->getElementError($target)) {
2426 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2427 if ($targetValue) {
2428 if (!array_intersect($targetValue, array_keys($options))) {
2429 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2430 }
2431 } // Suppress "required" error for field if it has no options
2432 elseif (!$options) {
2433 $this->setElementError($target, NULL);
2434 }
2435 }
2436 }
2437 }
2438 }
2439
2440 /**
2441 * Assign billing name to the template.
2442 *
2443 * @param array $params
2444 * Form input params, default to $this->_params.
2445 *
2446 * @return string
2447 */
2448 public function assignBillingName($params = []) {
2449 $name = '';
2450 if (empty($params)) {
2451 $params = $this->_params;
2452 }
2453 if (!empty($params['billing_first_name'])) {
2454 $name = $params['billing_first_name'];
2455 }
2456
2457 if (!empty($params['billing_middle_name'])) {
2458 $name .= " {$params['billing_middle_name']}";
2459 }
2460
2461 if (!empty($params['billing_last_name'])) {
2462 $name .= " {$params['billing_last_name']}";
2463 }
2464 $name = trim($name);
2465 $this->assign('billingName', $name);
2466 return $name;
2467 }
2468
2469 /**
2470 * Get the currency for the form.
2471 *
2472 * @todo this should be overriden on the forms rather than having this
2473 * historic, possible handling in here. As we clean that up we should
2474 * add deprecation notices into here.
2475 *
2476 * @param array $submittedValues
2477 * Array allowed so forms inheriting this class do not break.
2478 * Ideally we would make a clear standard around how submitted values
2479 * are stored (is $this->_values consistently doing that?).
2480 *
2481 * @return string
2482 */
2483 public function getCurrency($submittedValues = []) {
2484 $currency = CRM_Utils_Array::value('currency', $this->_values);
2485 // For event forms, currency is in a different spot
2486 if (empty($currency)) {
2487 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2488 }
2489 if (empty($currency)) {
2490 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2491 }
2492 // @todo If empty there is a problem - we should probably put in a deprecation notice
2493 // to warn if that seems to be happening.
2494 return $currency;
2495 }
2496
2497 /**
2498 * Is the form in view or edit mode.
2499 *
2500 * The 'addField' function relies on the form action being one of a set list
2501 * of actions. Checking for these allows for an early return.
2502 *
2503 * @return bool
2504 */
2505 protected function isFormInViewOrEditMode() {
2506 return in_array($this->_action, [
2507 CRM_Core_Action::UPDATE,
2508 CRM_Core_Action::ADD,
2509 CRM_Core_Action::VIEW,
2510 CRM_Core_Action::BROWSE,
2511 CRM_Core_Action::BASIC,
2512 CRM_Core_Action::ADVANCED,
2513 CRM_Core_Action::PREVIEW,
2514 ]);
2515 }
2516
2517 }