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