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