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