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