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