[NFC] towards CRM-19840 & others, load date metadata for custom & core profile fields.
[civicrm-core.git] / CRM / Core / Form.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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-2017
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 * Generate ID for some reason & purpose that is unknown & undocumented.
276 */
277 public static function generateID() {
278 }
279
280 /**
281 * Add one or more css classes to the form.
282 *
283 * @param string $className
284 */
285 public function addClass($className) {
286 $classes = $this->getAttribute('class');
287 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
288 }
289
290 /**
291 * Register all the standard rules that most forms potentially use.
292 */
293 public function registerRules() {
294 static $rules = array(
295 'title',
296 'longTitle',
297 'variable',
298 'qfVariable',
299 'phone',
300 'integer',
301 'query',
302 'url',
303 'wikiURL',
304 'domain',
305 'numberOfDigit',
306 'date',
307 'currentDate',
308 'asciiFile',
309 'htmlFile',
310 'utf8File',
311 'objectExists',
312 'optionExists',
313 'postalCode',
314 'money',
315 'positiveInteger',
316 'xssString',
317 'fileExists',
318 'settingPath',
319 'autocomplete',
320 'validContact',
321 );
322
323 foreach ($rules as $rule) {
324 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
325 }
326 }
327
328 /**
329 * Simple easy to use wrapper around addElement.
330 *
331 * Deal with simple validation rules.
332 *
333 * @param string $type
334 * @param string $name
335 * @param string $label
336 * @param string|array $attributes (options for select elements)
337 * @param bool $required
338 * @param array $extra
339 * (attributes for select elements).
340 * For datepicker elements this is consistent with the data
341 * from CRM_Utils_Date::getDatePickerExtra
342 *
343 * @return HTML_QuickForm_Element
344 * Could be an error object
345 */
346 public function &add(
347 $type, $name, $label = '',
348 $attributes = '', $required = FALSE, $extra = NULL
349 ) {
350 // Fudge some extra types that quickform doesn't support
351 $inputType = $type;
352 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
353 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
354 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
355 if ($type == 'wysiwyg' && isset($attributes['preset'])) {
356 $attributes['data-preset'] = $attributes['preset'];
357 unset($attributes['preset']);
358 }
359 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
360 }
361 // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list.
362 if ($inputType == 'select2') {
363 $type = 'text';
364 $options = $attributes;
365 $attributes = $attributes = ($extra ? $extra : array()) + array('class' => '');
366 $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2");
367 $attributes['data-select-params'] = json_encode(array('data' => $options, 'multiple' => !empty($attributes['multiple'])));
368 unset($attributes['multiple']);
369 $extra = NULL;
370 }
371 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
372 if ($type == 'datepicker') {
373 $attributes = ($attributes ? $attributes : array());
374 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
375 $type = "text";
376 }
377 if ($type == 'select' && is_array($extra)) {
378 // Normalize this property
379 if (!empty($extra['multiple'])) {
380 $extra['multiple'] = 'multiple';
381 }
382 else {
383 unset($extra['multiple']);
384 }
385 unset($extra['size'], $extra['maxlength']);
386 // Add placeholder option for select
387 if (isset($extra['placeholder'])) {
388 if ($extra['placeholder'] === TRUE) {
389 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
390 }
391 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
392 $attributes = array('' => $extra['placeholder']) + $attributes;
393 }
394 }
395 }
396 $element = $this->addElement($type, $name, $label, $attributes, $extra);
397 if (HTML_QuickForm::isError($element)) {
398 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
399 }
400
401 if ($inputType == 'color') {
402 $this->addRule($name, ts('%1 must contain a color value e.g. #ffffff.', array(1 => $label)), 'regex', '/#[0-9a-fA-F]{6}/');
403 }
404
405 if ($required) {
406 if ($type == 'file') {
407 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
408 }
409 else {
410 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
411 }
412 if (HTML_QuickForm::isError($error)) {
413 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
414 }
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 $is_pay_later_enabled
734 *
735 * @throws \CRM_Core_Exception
736 */
737 protected function assignPaymentProcessor($is_pay_later_enabled) {
738 $this->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors(
739 array(ucfirst($this->_mode) . 'Mode'),
740 $this->_paymentProcessorIDs
741 );
742
743 if (!empty($this->_paymentProcessors)) {
744 if ($is_pay_later_enabled) {
745 $this->_paymentProcessors[0] = CRM_Financial_BAO_PaymentProcessor::getPayment(0);
746 }
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 if ($required) {
1101 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
1102 }
1103 if ($allowClear) {
1104 $group->setAttribute('allowClear', TRUE);
1105 }
1106 return $group;
1107 }
1108
1109 /**
1110 * @param int $id
1111 * @param $title
1112 * @param bool $allowClear
1113 * @param null $required
1114 * @param array $attributes
1115 */
1116 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
1117 $attributes += array('id_suffix' => $id);
1118 $choice = array();
1119 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1120 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
1121
1122 $group = $this->addGroup($choice, $id, $title);
1123 if ($allowClear) {
1124 $group->setAttribute('allowClear', TRUE);
1125 }
1126 if ($required) {
1127 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
1128 }
1129 }
1130
1131 /**
1132 * @param int $id
1133 * @param $title
1134 * @param $values
1135 * @param null $other
1136 * @param null $attributes
1137 * @param null $required
1138 * @param null $javascriptMethod
1139 * @param string $separator
1140 * @param bool $flipValues
1141 */
1142 public function addCheckBox(
1143 $id, $title, $values, $other = NULL,
1144 $attributes = NULL, $required = NULL,
1145 $javascriptMethod = NULL,
1146 $separator = '<br />', $flipValues = FALSE
1147 ) {
1148 $options = array();
1149
1150 if ($javascriptMethod) {
1151 foreach ($values as $key => $var) {
1152 if (!$flipValues) {
1153 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
1154 }
1155 else {
1156 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
1157 }
1158 }
1159 }
1160 else {
1161 foreach ($values as $key => $var) {
1162 if (!$flipValues) {
1163 $options[] = $this->createElement('checkbox', $var, NULL, $key);
1164 }
1165 else {
1166 $options[] = $this->createElement('checkbox', $key, NULL, $var);
1167 }
1168 }
1169 }
1170
1171 $this->addGroup($options, $id, $title, $separator);
1172
1173 if ($other) {
1174 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1175 }
1176
1177 if ($required) {
1178 $this->addRule($id,
1179 ts('%1 is a required field.', array(1 => $title)),
1180 'required'
1181 );
1182 }
1183 }
1184
1185 public function resetValues() {
1186 $data = $this->controller->container();
1187 $data['values'][$this->_name] = array();
1188 }
1189
1190 /**
1191 * Simple shell that derived classes can call to add buttons to
1192 * the form with a customized title for the main Submit
1193 *
1194 * @param string $title
1195 * Title of the main button.
1196 * @param string $nextType
1197 * Button type for the form after processing.
1198 * @param string $backType
1199 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
1200 */
1201 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
1202 $buttons = array();
1203 if ($backType != NULL) {
1204 $buttons[] = array(
1205 'type' => $backType,
1206 'name' => ts('Previous'),
1207 );
1208 }
1209 if ($nextType != NULL) {
1210 $nextButton = array(
1211 'type' => $nextType,
1212 'name' => $title,
1213 'isDefault' => TRUE,
1214 );
1215 if ($submitOnce) {
1216 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
1217 }
1218 $buttons[] = $nextButton;
1219 }
1220 $this->addButtons($buttons);
1221 }
1222
1223 /**
1224 * @param string $name
1225 * @param string $from
1226 * @param string $to
1227 * @param string $label
1228 * @param string $dateFormat
1229 * @param bool $required
1230 * @param bool $displayTime
1231 */
1232 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1233 if ($displayTime) {
1234 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1235 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1236 }
1237 else {
1238 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1239 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1240 }
1241 }
1242
1243 /**
1244 * Based on form action, return a string representing the api action.
1245 * Used by addField method.
1246 *
1247 * Return string
1248 */
1249 private function getApiAction() {
1250 $action = $this->getAction();
1251 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1252 return 'create';
1253 }
1254 if ($action & (CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
1255 return 'get';
1256 }
1257 // If you get this exception try adding more cases above.
1258 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
1259 }
1260
1261 /**
1262 * Classes extending CRM_Core_Form should implement this method.
1263 * @throws Exception
1264 */
1265 public function getDefaultEntity() {
1266 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
1267 }
1268
1269 /**
1270 * Classes extending CRM_Core_Form should implement this method.
1271 *
1272 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1273 * @throws Exception
1274 */
1275 public function getDefaultContext() {
1276 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1277 }
1278
1279 /**
1280 * Adds a select based on field metadata.
1281 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1282 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1283 * @param $name
1284 * Field name to go on the form.
1285 * @param array $props
1286 * Mix of html attributes and special properties, namely.
1287 * - entity (api entity name, can usually be inferred automatically from the form class)
1288 * - field (field name - only needed if different from name used on the form)
1289 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1290 * - placeholder - set to NULL to disable
1291 * - multiple - bool
1292 * - context - @see CRM_Core_DAO::buildOptionsContext
1293 * @param bool $required
1294 * @throws CRM_Core_Exception
1295 * @return HTML_QuickForm_Element
1296 */
1297 public function addSelect($name, $props = array(), $required = FALSE) {
1298 if (!isset($props['entity'])) {
1299 $props['entity'] = $this->getDefaultEntity();
1300 }
1301 if (!isset($props['field'])) {
1302 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1303 }
1304 if (!isset($props['context'])) {
1305 try {
1306 $props['context'] = $this->getDefaultContext();
1307 }
1308 // This is not a required param, so we'll ignore if this doesn't exist.
1309 catch (Exception $e) {}
1310 }
1311 // Fetch options from the api unless passed explicitly
1312 if (isset($props['options'])) {
1313 $options = $props['options'];
1314 }
1315 else {
1316 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1317 $options = $info['values'];
1318 }
1319 if (!array_key_exists('placeholder', $props)) {
1320 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1321 }
1322 // Handle custom field
1323 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1324 list(, $id) = explode('_', $name);
1325 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1326 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1327 if (CRM_Utils_Array::value('context', $props) != 'search') {
1328 $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);
1329 }
1330 }
1331 // Core field
1332 else {
1333 $info = civicrm_api3($props['entity'], 'getfields');
1334 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1335 if (
1336 $uniqueName === $props['field'] ||
1337 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1338 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
1339 ) {
1340 break;
1341 }
1342 }
1343 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1344 if (CRM_Utils_Array::value('context', $props) != 'search') {
1345 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1346 }
1347 }
1348 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1349 $props['data-api-entity'] = $props['entity'];
1350 $props['data-api-field'] = $props['field'];
1351 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1352 return $this->add('select', $name, $label, $options, $required, $props);
1353 }
1354
1355 /**
1356 * Adds a field based on metadata.
1357 *
1358 * @param $name
1359 * Field name to go on the form.
1360 * @param array $props
1361 * Mix of html attributes and special properties, namely.
1362 * - entity (api entity name, can usually be inferred automatically from the form class)
1363 * - name (field name - only needed if different from name used on the form)
1364 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1365 * - placeholder - set to NULL to disable
1366 * - multiple - bool
1367 * - context - @see CRM_Core_DAO::buildOptionsContext
1368 * @param bool $required
1369 * @throws \CiviCRM_API3_Exception
1370 * @throws \Exception
1371 * @return HTML_QuickForm_Element
1372 */
1373 public function addField($name, $props = array(), $required = FALSE) {
1374 // Resolve context.
1375 if (empty($props['context'])) {
1376 $props['context'] = $this->getDefaultContext();
1377 }
1378 $context = $props['context'];
1379 // Resolve entity.
1380 if (empty($props['entity'])) {
1381 $props['entity'] = $this->getDefaultEntity();
1382 }
1383 // Resolve field.
1384 if (empty($props['name'])) {
1385 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1386 }
1387 // Resolve action.
1388 if (empty($props['action'])) {
1389 $props['action'] = $this->getApiAction();
1390 }
1391
1392 // Handle custom fields
1393 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1394 $fieldId = (int) substr($name, 7);
1395 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
1396 }
1397
1398 // Core field - get metadata.
1399 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1400 $fieldSpec = $fieldSpec['values'];
1401 $label = CRM_Utils_Array::value('label', $props, isset($fieldSpec['title']) ? $fieldSpec['title'] : NULL);
1402
1403 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1404 if ($widget == 'TextArea' && $context == 'search') {
1405 $widget = 'Text';
1406 }
1407
1408 $isSelect = (in_array($widget, array(
1409 'Select',
1410 'CheckBoxGroup',
1411 'RadioGroup',
1412 'Radio',
1413 )));
1414
1415 if ($isSelect) {
1416 // Fetch options from the api unless passed explicitly.
1417 if (isset($props['options'])) {
1418 $options = $props['options'];
1419 }
1420 else {
1421 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1422 }
1423 if ($context == 'search') {
1424 $widget = 'Select';
1425 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
1426 }
1427
1428 // Add data for popup link.
1429 if ((!empty($props['option_url']) || !array_key_exists('option_url', $props)) && ($context != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) {
1430 $props['data-option-edit-path'] = !empty($props['option_url']) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1431 $props['data-api-entity'] = $props['entity'];
1432 $props['data-api-field'] = $props['name'];
1433 }
1434 }
1435 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
1436 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
1437
1438 // TODO: refactor switch statement, to separate methods.
1439 switch ($widget) {
1440 case 'Text':
1441 case 'Url':
1442 case 'Number':
1443 case 'Email':
1444 //TODO: Autodetect ranges
1445 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1446 return $this->add(strtolower($widget), $name, $label, $props, $required);
1447
1448 case 'hidden':
1449 return $this->add('hidden', $name, NULL, $props, $required);
1450
1451 case 'TextArea':
1452 //Set default columns and rows for textarea.
1453 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1454 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1455 return $this->add('textarea', $name, $label, $props, $required);
1456
1457 case 'Select Date':
1458 //TODO: add range support
1459 //TODO: Add date formats
1460 //TODO: Add javascript template for dates.
1461 return $this->addDate($name, $label, $required, $props);
1462
1463 case 'Radio':
1464 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1465 unset($props['separator']);
1466 if (!isset($props['allowClear'])) {
1467 $props['allowClear'] = !$required;
1468 }
1469 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1470
1471 case 'ChainSelect':
1472 $props += array(
1473 'required' => $required,
1474 'label' => $label,
1475 'multiple' => $context == 'search',
1476 );
1477 return $this->addChainSelect($name, $props);
1478
1479 case 'Select':
1480 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
1481 if (!array_key_exists('placeholder', $props)) {
1482 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
1483 }
1484 // TODO: Add and/or option for fields that store multiple values
1485 return $this->add('select', $name, $label, $options, $required, $props);
1486
1487 case 'CheckBoxGroup':
1488 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1489
1490 case 'RadioGroup':
1491 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1492
1493 case 'CheckBox':
1494 $text = isset($props['text']) ? $props['text'] : NULL;
1495 unset($props['text']);
1496 return $this->addElement('checkbox', $name, $label, $text, $props);
1497
1498 //add support for 'Advcheckbox' field
1499 case 'advcheckbox':
1500 $text = isset($props['text']) ? $props['text'] : NULL;
1501 unset($props['text']);
1502 return $this->addElement('advcheckbox', $name, $label, $text, $props);
1503
1504 case 'File':
1505 // We should not build upload file in search mode.
1506 if ($context == 'search') {
1507 return;
1508 }
1509 $file = $this->add('file', $name, $label, $props, $required);
1510 $this->addUploadElement($name);
1511 return $file;
1512
1513 case 'RichTextEditor':
1514 return $this->add('wysiwyg', $name, $label, $props, $required);
1515
1516 case 'EntityRef':
1517 return $this->addEntityRef($name, $label, $props, $required);
1518
1519 // Check datatypes of fields
1520 // case 'Int':
1521 //case 'Float':
1522 //case 'Money':
1523 //case read only fields
1524 default:
1525 throw new Exception("Unsupported html-element " . $widget);
1526 }
1527 }
1528
1529 /**
1530 * Add a widget for selecting/editing/creating/copying a profile form
1531 *
1532 * @param string $name
1533 * HTML form-element name.
1534 * @param string $label
1535 * Printable label.
1536 * @param string $allowCoreTypes
1537 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1538 * @param string $allowSubTypes
1539 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1540 * @param array $entities
1541 * @param bool $default
1542 * //CRM-15427.
1543 * @param string $usedFor
1544 */
1545 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
1546 // Output widget
1547 // FIXME: Instead of adhoc serialization, use a single json_encode()
1548 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1549 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1550 $this->add('text', $name, $label, array(
1551 'class' => 'crm-profile-selector',
1552 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1553 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1554 'data-entities' => json_encode($entities),
1555 //CRM-15427
1556 'data-default' => $default,
1557 'data-usedfor' => json_encode($usedFor),
1558 ));
1559 }
1560
1561 /**
1562 * @return null
1563 */
1564 public function getRootTitle() {
1565 return NULL;
1566 }
1567
1568 /**
1569 * @return string
1570 */
1571 public function getCompleteTitle() {
1572 return $this->getRootTitle() . $this->getTitle();
1573 }
1574
1575 /**
1576 * @return CRM_Core_Smarty
1577 */
1578 public static function &getTemplate() {
1579 return self::$_template;
1580 }
1581
1582 /**
1583 * @param $elementName
1584 */
1585 public function addUploadElement($elementName) {
1586 $uploadNames = $this->get('uploadNames');
1587 if (!$uploadNames) {
1588 $uploadNames = array();
1589 }
1590 if (is_array($elementName)) {
1591 foreach ($elementName as $name) {
1592 if (!in_array($name, $uploadNames)) {
1593 $uploadNames[] = $name;
1594 }
1595 }
1596 }
1597 else {
1598 if (!in_array($elementName, $uploadNames)) {
1599 $uploadNames[] = $elementName;
1600 }
1601 }
1602 $this->set('uploadNames', $uploadNames);
1603
1604 $config = CRM_Core_Config::singleton();
1605 if (!empty($uploadNames)) {
1606 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1607 }
1608 }
1609
1610 /**
1611 * @param $name
1612 *
1613 * @return null
1614 */
1615 public function getVar($name) {
1616 return isset($this->$name) ? $this->$name : NULL;
1617 }
1618
1619 /**
1620 * @param $name
1621 * @param $value
1622 */
1623 public function setVar($name, $value) {
1624 $this->$name = $value;
1625 }
1626
1627 /**
1628 * Add date.
1629 *
1630 * @deprecated
1631 * Use $this->add('datepicker', ...) instead.
1632 *
1633 * @param string $name
1634 * Name of the element.
1635 * @param string $label
1636 * Label of the element.
1637 * @param bool $required
1638 * True if required.
1639 * @param array $attributes
1640 * Key / value pair.
1641 */
1642 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1643 if (!empty($attributes['formatType'])) {
1644 // get actual format
1645 $params = array('name' => $attributes['formatType']);
1646 $values = array();
1647
1648 // cache date information
1649 static $dateFormat;
1650 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1651 if (empty($dateFormat[$key])) {
1652 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1653 $dateFormat[$key] = $values;
1654 }
1655 else {
1656 $values = $dateFormat[$key];
1657 }
1658
1659 if ($values['date_format']) {
1660 $attributes['format'] = $values['date_format'];
1661 }
1662
1663 if (!empty($values['time_format'])) {
1664 $attributes['timeFormat'] = $values['time_format'];
1665 }
1666 $attributes['startOffset'] = $values['start'];
1667 $attributes['endOffset'] = $values['end'];
1668 }
1669
1670 $config = CRM_Core_Config::singleton();
1671 if (empty($attributes['format'])) {
1672 $attributes['format'] = $config->dateInputFormat;
1673 }
1674
1675 if (!isset($attributes['startOffset'])) {
1676 $attributes['startOffset'] = 10;
1677 }
1678
1679 if (!isset($attributes['endOffset'])) {
1680 $attributes['endOffset'] = 10;
1681 }
1682
1683 $this->add('text', $name, $label, $attributes);
1684
1685 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1686
1687 if (!isset($attributes['timeFormat'])) {
1688 $timeFormat = $config->timeInputFormat;
1689 }
1690 else {
1691 $timeFormat = $attributes['timeFormat'];
1692 }
1693
1694 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1695 if ($timeFormat) {
1696 $show24Hours = TRUE;
1697 if ($timeFormat == 1) {
1698 $show24Hours = FALSE;
1699 }
1700
1701 //CRM-6664 -we are having time element name
1702 //in either flat string or an array format.
1703 $elementName = $name . '_time';
1704 if (substr($name, -1) == ']') {
1705 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1706 }
1707
1708 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1709 }
1710 }
1711
1712 if ($required) {
1713 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1714 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1715 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1716 }
1717 }
1718 }
1719
1720 /**
1721 * Function that will add date and time.
1722 *
1723 * @deprecated
1724 * Use $this->add('datepicker', ...) instead.
1725 *
1726 * @param string $name
1727 * @param string $label
1728 * @param bool $required
1729 * @param null $attributes
1730 */
1731 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1732 $addTime = array('addTime' => TRUE);
1733 if (is_array($attributes)) {
1734 $attributes = array_merge($attributes, $addTime);
1735 }
1736 else {
1737 $attributes = $addTime;
1738 }
1739
1740 $this->addDate($name, $label, $required, $attributes);
1741 }
1742
1743 /**
1744 * Add a currency and money element to the form.
1745 *
1746 * @param string $name
1747 * @param string $label
1748 * @param bool $required
1749 * @param null $attributes
1750 * @param bool $addCurrency
1751 * @param string $currencyName
1752 * @param null $defaultCurrency
1753 * @param bool $freezeCurrency
1754 *
1755 * @return \HTML_QuickForm_Element
1756 */
1757 public function addMoney(
1758 $name,
1759 $label,
1760 $required = FALSE,
1761 $attributes = NULL,
1762 $addCurrency = TRUE,
1763 $currencyName = 'currency',
1764 $defaultCurrency = NULL,
1765 $freezeCurrency = FALSE
1766 ) {
1767 $element = $this->add('text', $name, $label, $attributes, $required);
1768 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1769
1770 if ($addCurrency) {
1771 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1772 }
1773
1774 return $element;
1775 }
1776
1777 /**
1778 * Add currency element to the form.
1779 *
1780 * @param string $name
1781 * @param null $label
1782 * @param bool $required
1783 * @param string $defaultCurrency
1784 * @param bool $freezeCurrency
1785 */
1786 public function addCurrency(
1787 $name = 'currency',
1788 $label = NULL,
1789 $required = TRUE,
1790 $defaultCurrency = NULL,
1791 $freezeCurrency = FALSE
1792 ) {
1793 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1794 $options = array('class' => 'crm-select2 eight');
1795 if (!$required) {
1796 $currencies = array('' => '') + $currencies;
1797 $options['placeholder'] = ts('- none -');
1798 }
1799 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1800 if ($freezeCurrency) {
1801 $ele->freeze();
1802 }
1803 if (!$defaultCurrency) {
1804 $config = CRM_Core_Config::singleton();
1805 $defaultCurrency = $config->defaultCurrency;
1806 }
1807 $this->setDefaults(array($name => $defaultCurrency));
1808 }
1809
1810 /**
1811 * Create a single or multiple entity ref field.
1812 * @param string $name
1813 * @param string $label
1814 * @param array $props
1815 * Mix of html and widget properties, including:.
1816 * - select - params to give to select2 widget
1817 * - entity - defaults to contact
1818 * - create - can the user create a new entity on-the-fly?
1819 * Set to TRUE if entity is contact and you want the default profiles,
1820 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1821 * note that permissions are checked automatically
1822 * - api - array of settings for the getlist api wrapper
1823 * note that it accepts a 'params' setting which will be passed to the underlying api
1824 * - placeholder - string
1825 * - multiple - bool
1826 * - class, etc. - other html properties
1827 * @param bool $required
1828 *
1829 * @return HTML_QuickForm_Element
1830 */
1831 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1832 require_once "api/api.php";
1833 $config = CRM_Core_Config::singleton();
1834 // Default properties
1835 $props['api'] = CRM_Utils_Array::value('api', $props, array());
1836 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1837 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1838
1839 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission::check('edit all contacts') || CRM_Core_Permission::check('add contacts'))) {
1840 unset($props['create']);
1841 }
1842
1843 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1844
1845 $defaults = array();
1846 if (!empty($props['multiple'])) {
1847 $defaults['multiple'] = TRUE;
1848 }
1849 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
1850
1851 $this->formatReferenceFieldAttributes($props);
1852 return $this->add('text', $name, $label, $props, $required);
1853 }
1854
1855 /**
1856 * @param $props
1857 */
1858 private function formatReferenceFieldAttributes(&$props) {
1859 $props['data-select-params'] = json_encode($props['select']);
1860 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1861 $props['data-api-entity'] = $props['entity'];
1862 if (!empty($props['create'])) {
1863 $props['data-create-links'] = json_encode($props['create']);
1864 }
1865 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1866 }
1867
1868 /**
1869 * Convert all date fields within the params to mysql date ready for the
1870 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1871 * and if time is defined it is incorporated
1872 *
1873 * @param array $params
1874 * Input params from the form.
1875 *
1876 * @todo it would probably be better to work on $this->_params than a passed array
1877 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1878 * handling from BAO
1879 */
1880 public function convertDateFieldsToMySQL(&$params) {
1881 foreach ($this->_dateFields as $fieldName => $specs) {
1882 if (!empty($params[$fieldName])) {
1883 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1884 CRM_Utils_Date::processDate(
1885 $params[$fieldName],
1886 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1887 );
1888 }
1889 else {
1890 if (isset($specs['default'])) {
1891 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1892 }
1893 }
1894 }
1895 }
1896
1897 /**
1898 * @param $elementName
1899 */
1900 public function removeFileRequiredRules($elementName) {
1901 $this->_required = array_diff($this->_required, array($elementName));
1902 if (isset($this->_rules[$elementName])) {
1903 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1904 if ($ruleInfo['type'] == 'uploadedfile') {
1905 unset($this->_rules[$elementName][$index]);
1906 }
1907 }
1908 if (empty($this->_rules[$elementName])) {
1909 unset($this->_rules[$elementName]);
1910 }
1911 }
1912 }
1913
1914 /**
1915 * Function that can be defined in Form to override or.
1916 * perform specific action on cancel action
1917 */
1918 public function cancelAction() {
1919 }
1920
1921 /**
1922 * Helper function to verify that required fields have been filled.
1923 *
1924 * Typically called within the scope of a FormRule function
1925 *
1926 * @param array $fields
1927 * @param array $values
1928 * @param array $errors
1929 */
1930 public static function validateMandatoryFields($fields, $values, &$errors) {
1931 foreach ($fields as $name => $fld) {
1932 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1933 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1934 }
1935 }
1936 }
1937
1938 /**
1939 * Get contact if for a form object. Prioritise
1940 * - cid in URL if 0 (on behalf on someoneelse)
1941 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1942 * - logged in user id if it matches the one in the cid in the URL
1943 * - contact id validated from a checksum from a checksum
1944 * - cid from the url if the caller has ACL permission to view
1945 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1946 *
1947 * @return NULL|int
1948 */
1949 protected function setContactID() {
1950 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1951 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
1952 $tempID = $this->_params['select_contact_id'];
1953 }
1954 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1955 // event form stores as an indexed array, contribution form not so much...
1956 $tempID = $this->_params[0]['select_contact_id'];
1957 }
1958
1959 // force to ignore the authenticated user
1960 if ($tempID === '0' || $tempID === 0) {
1961 // we set the cid on the form so that this will be retained for the Confirm page
1962 // in the multi-page form & prevent us returning the $userID when this is called
1963 // from that page
1964 // we don't really need to set it when $tempID is set because the params have that stored
1965 $this->set('cid', 0);
1966 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1967 return (int) $tempID;
1968 }
1969
1970 $userID = $this->getLoggedInUserContactID();
1971
1972 if (!is_null($tempID) && $tempID === $userID) {
1973 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1974 return (int) $userID;
1975 }
1976
1977 //check if this is a checksum authentication
1978 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1979 if ($userChecksum) {
1980 //check for anonymous user.
1981 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1982 if ($validUser) {
1983 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1984 CRM_Core_Resources::singleton()->addVars('coreForm', array('checksum' => (int) $tempID));
1985 return $tempID;
1986 }
1987 }
1988 // check if user has permission, CRM-12062
1989 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1990 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1991 return $tempID;
1992 }
1993 if (is_numeric($userID)) {
1994 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $userID));
1995 }
1996 return is_numeric($userID) ? $userID : NULL;
1997 }
1998
1999 /**
2000 * Get the contact id that the form is being submitted for.
2001 *
2002 * @return int|NULL
2003 */
2004 public function getContactID() {
2005 return $this->setContactID();
2006 }
2007
2008 /**
2009 * Get the contact id of the logged in user.
2010 */
2011 public function getLoggedInUserContactID() {
2012 // check if the user is logged in and has a contact ID
2013 $session = CRM_Core_Session::singleton();
2014 return $session->get('userID');
2015 }
2016
2017 /**
2018 * Add autoselector field -if user has permission to view contacts
2019 * If adding this to a form you also need to add to the tpl e.g
2020 *
2021 * {if !empty($selectable)}
2022 * <div class="crm-summary-row">
2023 * <div class="crm-label">{$form.select_contact.label}</div>
2024 * <div class="crm-content">
2025 * {$form.select_contact.html}
2026 * </div>
2027 * </div>
2028 * {/if}
2029 *
2030 * @param array $profiles
2031 * Ids of profiles that are on the form (to be autofilled).
2032 * @param array $autoCompleteField
2033 *
2034 * - name_field
2035 * - id_field
2036 * - url (for ajax lookup)
2037 *
2038 * @todo add data attributes so we can deal with multiple instances on a form
2039 */
2040 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
2041 $autoCompleteField = array_merge(array(
2042 'id_field' => 'select_contact_id',
2043 'placeholder' => ts('Select someone else ...'),
2044 'show_hide' => TRUE,
2045 'api' => array('params' => array('contact_type' => 'Individual')),
2046 ), $autoCompleteField);
2047
2048 if ($this->canUseAjaxContactLookups()) {
2049 $this->assign('selectable', $autoCompleteField['id_field']);
2050 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
2051 'placeholder' => $autoCompleteField['placeholder'],
2052 'api' => $autoCompleteField['api'],
2053 ));
2054
2055 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
2056 ->addSetting(array(
2057 'form' => array('autocompletes' => $autoCompleteField),
2058 'ids' => array('profile' => $profiles),
2059 ));
2060 }
2061 }
2062
2063 /**
2064 */
2065 public function canUseAjaxContactLookups() {
2066 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
2067 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
2068 ) {
2069 return TRUE;
2070 }
2071 }
2072
2073 /**
2074 * Add the options appropriate to cid = zero - ie. autocomplete
2075 *
2076 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2077 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2078 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2079 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
2080 *
2081 * @param $onlinePaymentProcessorEnabled
2082 */
2083 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
2084 $this->assign('nocid', TRUE);
2085 $profiles = array();
2086 if ($this->_values['custom_pre_id']) {
2087 $profiles[] = $this->_values['custom_pre_id'];
2088 }
2089 if ($this->_values['custom_post_id']) {
2090 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
2091 }
2092 if ($onlinePaymentProcessorEnabled) {
2093 $profiles[] = 'billing';
2094 }
2095 if (!empty($this->_values)) {
2096 $this->addAutoSelector($profiles);
2097 }
2098 }
2099
2100 /**
2101 * Set default values on form for given contact (or no contact defaults)
2102 *
2103 * @param mixed $profile_id
2104 * (can be id, or profile name).
2105 * @param int $contactID
2106 *
2107 * @return array
2108 */
2109 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2110 try {
2111 $defaults = civicrm_api3('profile', 'getsingle', array(
2112 'profile_id' => (array) $profile_id,
2113 'contact_id' => $contactID,
2114 ));
2115 return $defaults;
2116 }
2117 catch (Exception $e) {
2118 // the try catch block gives us silent failure -not 100% sure this is a good idea
2119 // as silent failures are often worse than noisy ones
2120 return array();
2121 }
2122 }
2123
2124 /**
2125 * Sets form attribute.
2126 * @see CRM.loadForm
2127 */
2128 public function preventAjaxSubmit() {
2129 $this->setAttribute('data-no-ajax-submit', 'true');
2130 }
2131
2132 /**
2133 * Sets form attribute.
2134 * @see CRM.loadForm
2135 */
2136 public function allowAjaxSubmit() {
2137 $this->removeAttribute('data-no-ajax-submit');
2138 }
2139
2140 /**
2141 * Sets page title based on entity and action.
2142 * @param string $entityLabel
2143 */
2144 public function setPageTitle($entityLabel) {
2145 switch ($this->_action) {
2146 case CRM_Core_Action::ADD:
2147 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2148 break;
2149
2150 case CRM_Core_Action::UPDATE:
2151 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2152 break;
2153
2154 case CRM_Core_Action::VIEW:
2155 case CRM_Core_Action::PREVIEW:
2156 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
2157 break;
2158
2159 case CRM_Core_Action::DELETE:
2160 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2161 break;
2162 }
2163 }
2164
2165 /**
2166 * Create a chain-select target field. All settings are optional; the defaults usually work.
2167 *
2168 * @param string $elementName
2169 * @param array $settings
2170 *
2171 * @return HTML_QuickForm_Element
2172 */
2173 public function addChainSelect($elementName, $settings = array()) {
2174 $props = $settings += array(
2175 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2176 'country',
2177 'Country',
2178 'state_province',
2179 'StateProvince',
2180 ), $elementName),
2181 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2182 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2183 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2184 'data-none-prompt' => ts('- N/A -'),
2185 'multiple' => FALSE,
2186 'required' => FALSE,
2187 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2188 );
2189 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
2190 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2191 $props['data-select-prompt'] = $props['placeholder'];
2192 $props['data-name'] = $elementName;
2193
2194 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2195
2196 // Passing NULL instead of an array of options
2197 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2198 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2199 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2200 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2201 }
2202
2203 /**
2204 * Add actions menu to results form.
2205 *
2206 * @param array $tasks
2207 */
2208 public function addTaskMenu($tasks) {
2209 if (is_array($tasks) && !empty($tasks)) {
2210 // Set constants means this will always load with an empty value, not reloading any submitted value.
2211 // This is appropriate as it is a pseudofield.
2212 $this->setConstants(array('task' => ''));
2213 $this->assign('taskMetaData', $tasks);
2214 $select = $this->add('select', 'task', NULL, array('' => ts('Actions')), FALSE, array(
2215 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions')
2216 );
2217 foreach ($tasks as $key => $task) {
2218 $attributes = array();
2219 if (isset($task['data'])) {
2220 foreach ($task['data'] as $dataKey => $dataValue) {
2221 $attributes['data-' . $dataKey] = $dataValue;
2222 }
2223 }
2224 $select->addOption($task['title'], $key, $attributes);
2225 }
2226 if (empty($this->_actionButtonName)) {
2227 $this->_actionButtonName = $this->getButtonName('next', 'action');
2228 }
2229 $this->assign('actionButtonName', $this->_actionButtonName);
2230 $this->add('submit', $this->_actionButtonName, ts('Go'), array('class' => 'hiddenElement crm-search-go-button'));
2231
2232 // Radio to choose "All items" or "Selected items only"
2233 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', array('checked' => 'checked'));
2234 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2235 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2236 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2237
2238 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2239 }
2240 }
2241
2242 /**
2243 * Set options and attributes for chain select fields based on the controlling field's value
2244 */
2245 private function preProcessChainSelectFields() {
2246 foreach ($this->_chainSelectFields as $control => $target) {
2247 // The 'target' might get missing if extensions do removeElement() in a form hook.
2248 if ($this->elementExists($target)) {
2249 $targetField = $this->getElement($target);
2250 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2251 $options = array();
2252 // If the control field is on the form, setup chain-select and dynamically populate options
2253 if ($this->elementExists($control)) {
2254 $controlField = $this->getElement($control);
2255 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2256
2257 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2258
2259 $css = (string) $controlField->getAttribute('class');
2260 $controlField->updateAttributes(array(
2261 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2262 'data-target' => $target,
2263 ));
2264 $controlValue = $controlField->getValue();
2265 if ($controlValue) {
2266 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2267 if (!$options) {
2268 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2269 }
2270 }
2271 else {
2272 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2273 $targetField->setAttribute('disabled', 'disabled');
2274 }
2275 }
2276 // Control field not present - fall back to loading default options
2277 else {
2278 $options = CRM_Core_PseudoConstant::$targetType();
2279 }
2280 if (!$targetField->getAttribute('multiple')) {
2281 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2282 $targetField->removeAttribute('placeholder');
2283 }
2284 $targetField->_options = array();
2285 $targetField->loadArray($options);
2286 }
2287 }
2288 }
2289
2290 /**
2291 * Validate country / state / county match and suppress unwanted "required" errors
2292 */
2293 private function validateChainSelectFields() {
2294 foreach ($this->_chainSelectFields as $control => $target) {
2295 if ($this->elementExists($control) && $this->elementExists($target)) {
2296 $controlValue = (array) $this->getElementValue($control);
2297 $targetField = $this->getElement($target);
2298 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2299 $targetValue = array_filter((array) $targetField->getValue());
2300 if ($targetValue || $this->getElementError($target)) {
2301 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2302 if ($targetValue) {
2303 if (!array_intersect($targetValue, array_keys($options))) {
2304 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2305 }
2306 } // Suppress "required" error for field if it has no options
2307 elseif (!$options) {
2308 $this->setElementError($target, NULL);
2309 }
2310 }
2311 }
2312 }
2313 }
2314
2315 /**
2316 * Assign billing name to the template.
2317 *
2318 * @param array $params
2319 * Form input params, default to $this->_params.
2320 *
2321 * @return string
2322 */
2323 public function assignBillingName($params = array()) {
2324 $name = '';
2325 if (empty($params)) {
2326 $params = $this->_params;
2327 }
2328 if (!empty($params['billing_first_name'])) {
2329 $name = $params['billing_first_name'];
2330 }
2331
2332 if (!empty($params['billing_middle_name'])) {
2333 $name .= " {$params['billing_middle_name']}";
2334 }
2335
2336 if (!empty($params['billing_last_name'])) {
2337 $name .= " {$params['billing_last_name']}";
2338 }
2339 $name = trim($name);
2340 $this->assign('billingName', $name);
2341 return $name;
2342 }
2343
2344 }