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