001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.preferences; 020 021import com.google.gson.Gson; 022import org.apache.commons.lang3.LocaleUtils; 023import org.apache.commons.lang3.StringUtils; 024import org.apache.logging.log4j.LogManager; 025import org.apache.logging.log4j.Logger; 026import org.apache.wiki.InternalWikiException; 027import org.apache.wiki.api.core.Context; 028import org.apache.wiki.i18n.InternationalizationManager; 029import org.apache.wiki.util.HttpUtil; 030import org.apache.wiki.util.PropertyReader; 031import org.apache.wiki.util.TextUtil; 032 033import jakarta.servlet.http.HttpServletRequest; 034import jakarta.servlet.jsp.PageContext; 035import java.text.DateFormat; 036import java.text.SimpleDateFormat; 037import java.util.Date; 038import java.util.HashMap; 039import java.util.Locale; 040import java.util.Map; 041import java.util.MissingResourceException; 042import java.util.Properties; 043import java.util.ResourceBundle; 044import java.util.TimeZone; 045 046 047/** 048 * Represents an object which is used to store user preferences. 049 */ 050public class Preferences extends HashMap< String,String > { 051 052 private static final long serialVersionUID = 1L; 053 054 /** 055 * The name under which a Preferences object is stored in the HttpSession. Its value is {@value}. 056 */ 057 public static final String SESSIONPREFS = "prefs"; 058 059 public static final String COOKIE_USER_PREFS_NAME = "JSPWikiUserPrefs"; 060 061 private static final Logger LOG = LogManager.getLogger( Preferences.class ); 062 063 /** 064 * This is an utility method which is called to make sure that the 065 * JSP pages do have proper access to any user preferences. It should be 066 * called from the commonheader.jsp. 067 * <p> 068 * This method reads user cookie preferences and mixes them up with any 069 * default preferences (and in the future, any user-specific preferences) 070 * and puts them all in the session, so that they do not have to be rewritten 071 * again. 072 * <p> 073 * This method will remember if the user has already changed his prefs. 074 * 075 * @param pageContext The JSP PageContext. 076 */ 077 public static void setupPreferences( final PageContext pageContext ) { 078 //HttpSession session = pageContext.getSession(); 079 //if( session.getAttribute( SESSIONPREFS ) == null ) 080 //{ 081 reloadPreferences( pageContext ); 082 //} 083 } 084 085 /** 086 * Reloads the preferences from the PageContext into the WikiContext. 087 * 088 * @param pageContext The page context. 089 */ 090 // FIXME: The way that date preferences are chosen is currently a bit wacky: it all gets saved to the cookie based on the browser state 091 // with which the user happened to first arrive to the site with. This, unfortunately, means that even if the user changes e.g. 092 // language preferences (like in a web cafe), the old preferences still remain in a site cookie. 093 public static void reloadPreferences( final PageContext pageContext ) { 094 final Preferences prefs = new Preferences(); 095 final Properties props = PropertyReader.loadWebAppProps( pageContext.getServletContext() ); 096 final Context ctx = Context.findContext( pageContext ); 097 final String dateFormat = ctx.getEngine().getManager( InternationalizationManager.class ) 098 .get( InternationalizationManager.CORE_BUNDLE, getLocale( ctx ), "common.datetimeformat" ); 099 100 prefs.put("SkinName", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.skinname", "PlainVanilla" ) ); 101 prefs.put("DateFormat", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.dateformat", dateFormat ) ); 102 prefs.put("TimeZone", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.timezone", TimeZone.getDefault().getID() ) ); 103 prefs.put("Orientation", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.orientation", "fav-left" ) ); 104 prefs.put("Sidebar", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.sidebar", "active" ) ); 105 prefs.put("Layout", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.layout", "fluid" ) ); 106 prefs.put("Language", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.language", getLocale( ctx ).toString() ) ); 107 prefs.put("SectionEditing", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.sectionediting", "true" ) ); 108 //prefs.put("Appearance", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.appearance", "true" ) ); 109 110 //editor cookies 111 prefs.put("autosuggest", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.autosuggest", "true" ) ); 112 prefs.put("tabcompletion", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.tabcompletion", "true" ) ); 113 prefs.put("smartpairs", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.smartpairs", "false" ) ); 114 prefs.put("livepreview", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.livepreview", "true" ) ); 115 prefs.put("previewcolumn", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.previewcolumn", "true" ) ); 116 117 118 // FIXME: editormanager reads jspwiki.editor -- which of both properties should continue 119 prefs.put("editor", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.editor", "plain" ) ); 120 parseJSONPreferences( (HttpServletRequest) pageContext.getRequest(), prefs ); 121 pageContext.getSession().setAttribute( SESSIONPREFS, prefs ); 122 } 123 124 125 /** 126 * Parses new-style preferences stored as JSON objects and stores them in the session. Everything in the cookie is stored. 127 * 128 * @param request 129 * @param prefs The default hashmap of preferences 130 */ 131 private static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { 132 final String prefVal = TextUtil.urlDecodeUTF8( HttpUtil.retrieveCookieValue( request, COOKIE_USER_PREFS_NAME ) ); 133 if( prefVal != null ) { 134 // Convert prefVal JSON to a generic hashmap 135 @SuppressWarnings( "unchecked" ) final Map< String, String > map = new Gson().fromJson( prefVal, Map.class ); 136 for( String key : map.keySet() ) { 137 key = TextUtil.replaceEntities( key ); 138 // Sometimes this is not a String as it comes from the Cookie set by Javascript 139 final Object value = map.get( key ); 140 if( value != null ) { 141 prefs.put( key, value.toString() ); 142 } 143 } 144 } 145 } 146 147 /** 148 * Returns a preference value programmatically. 149 * FIXME 150 * 151 * @param wikiContext 152 * @param name 153 * @return the preference value 154 */ 155 public static String getPreference( final Context wikiContext, final String name ) { 156 final HttpServletRequest request = wikiContext.getHttpRequest(); 157 if ( request == null ) { 158 return null; 159 } 160 161 final Preferences prefs = (Preferences)request.getSession().getAttribute( SESSIONPREFS ); 162 if( prefs != null ) { 163 return prefs.get( name ); 164 } 165 166 return null; 167 } 168 169 /** 170 * Returns a preference value programmatically. 171 * 172 * @param pageContext 173 * @param name 174 * @return the preference value 175 */ 176 public static String getPreference( final PageContext pageContext, final String name ) { 177 final Preferences prefs = ( Preferences )pageContext.getSession().getAttribute( SESSIONPREFS ); 178 if( prefs != null ) { 179 return prefs.get( name ); 180 } 181 182 return null; 183 } 184 185 /** 186 * Get Locale according to user-preference settings or the user browser locale 187 * 188 * @param context The context to examine. 189 * @return a Locale object. 190 * @since 2.8 191 */ 192 public static Locale getLocale( final Context context ) { 193 Locale loc = null; 194 195 final String langSetting = getPreference( context, "Language" ); 196 197 // parse language and construct valid Locale object 198 if( langSetting != null ) { 199 String language = ""; 200 String country = ""; 201 String variant = ""; 202 203 final String[] res = StringUtils.split( langSetting, "-_" ); 204 final int resLength = res.length; 205 if( resLength > 2 ) { 206 variant = res[ 2 ]; 207 } 208 if( resLength > 1 ) { 209 country = res[ 1 ]; 210 } 211 if( resLength > 0 ) { 212 language = res[ 0 ]; 213 loc = new Locale( language, country, variant ); 214 } 215 } 216 217 // see if default locale is set server side 218 if( loc == null ) { 219 final String locale = context.getEngine().getWikiProperties().getProperty( "jspwiki.preferences.default-locale" ); 220 if (locale != null) { 221 //this can be null under unit test/mock contexts but normally is 222 //not null under normal operating circumstances. 223 try { 224 loc = LocaleUtils.toLocale( locale ); 225 } catch( final IllegalArgumentException iae ) { 226 LOG.error( iae.getMessage() ); 227 } 228 } 229 } 230 231 // otherwise try to find out the browser's preferred language setting, or use the JVM's default 232 if( loc == null ) { 233 final HttpServletRequest request = context.getHttpRequest(); 234 loc = ( request != null ) ? request.getLocale() : Locale.getDefault(); 235 } 236 if ( loc == null) { 237 loc = Locale.getDefault(); 238 } 239 240 LOG.debug( "using locale " + loc.toString() ); 241 return loc; 242 } 243 244 /** 245 * Locates the i18n ResourceBundle given. This method interprets the request locale, and uses that to figure out which language the 246 * user wants. 247 * 248 * @param context {@link Context} holding the user's locale 249 * @param bundle The name of the bundle you are looking for. 250 * @return A localized string (or from the default language, if not found) 251 * @throws MissingResourceException If the bundle cannot be found 252 * @see org.apache.wiki.i18n.InternationalizationManager 253 */ 254 public static ResourceBundle getBundle( final Context context, final String bundle ) throws MissingResourceException { 255 final Locale loc = getLocale( context ); 256 final InternationalizationManager i18n = context.getEngine().getManager( InternationalizationManager.class ); 257 return i18n.getBundle( bundle, loc ); 258 } 259 260 /** 261 * Get SimpleTimeFormat according to user browser locale and preferred time formats. If not found, it will revert to whichever format 262 * is set for the default. 263 * 264 * @param context WikiContext to use for rendering. 265 * @param tf Which version of the dateformat you are looking for? 266 * @return A SimpleTimeFormat object which you can use to render 267 * @since 2.8 268 */ 269 public static SimpleDateFormat getDateFormat( final Context context, final TimeFormat tf ) { 270 final InternationalizationManager imgr = context.getEngine().getManager( InternationalizationManager.class ); 271 final Locale clientLocale = getLocale( context ); 272 final String prefTimeZone = getPreference( context, "TimeZone" ); 273 String prefDateFormat; 274 275 LOG.debug("Checking for preferences..."); 276 switch( tf ) { 277 case DATETIME: 278 prefDateFormat = getPreference( context, "DateFormat" ); 279 LOG.debug("Preferences fmt = "+prefDateFormat); 280 if( prefDateFormat == null ) { 281 prefDateFormat = imgr.get( InternationalizationManager.CORE_BUNDLE, clientLocale,"common.datetimeformat" ); 282 LOG.debug("Using locale-format = "+prefDateFormat); 283 } 284 break; 285 286 case TIME: 287 prefDateFormat = imgr.get( "common.timeformat" ); 288 break; 289 290 case DATE: 291 prefDateFormat = imgr.get( "common.dateformat" ); 292 break; 293 294 default: 295 throw new InternalWikiException( "Got a TimeFormat for which we have no value!" ); 296 } 297 298 try { 299 final SimpleDateFormat fmt = new SimpleDateFormat( prefDateFormat, clientLocale ); 300 if( prefTimeZone != null ) { 301 final TimeZone tz = TimeZone.getTimeZone( prefTimeZone ); 302 // TimeZone tz = TimeZone.getDefault(); 303 // tz.setRawOffset(Integer.parseInt(prefTimeZone)); 304 fmt.setTimeZone( tz ); 305 } 306 307 return fmt; 308 } catch( final Exception e ) { 309 LOG.debug(e.getMessage(), e); 310 return null; 311 } 312 } 313 314 /** 315 * A simple helper function to render a date based on the user preferences. This is useful for example for all plugins. 316 * 317 * @param context The context which is used to get the preferences 318 * @param date The date to render. 319 * @param tf In which format the date should be rendered. 320 * @return A ready-rendered date. 321 * @since 2.8 322 */ 323 public static String renderDate( final Context context, final Date date, final TimeFormat tf ) { 324 final DateFormat df = getDateFormat( context, tf ); 325 return df.format( date ); 326 } 327 328 /** 329 * Is used to choose between the different date formats that JSPWiki supports. 330 * <ul> 331 * <li>TIME: A time format, without date</li> 332 * <li>DATE: A date format, without a time</li> 333 * <li>DATETIME: A date format, with a time</li> 334 * </ul> 335 * 336 * @since 2.8 337 */ 338 public enum TimeFormat { 339 /** A time format, no date. */ 340 TIME, 341 342 /** A date format, no time. */ 343 DATE, 344 345 /** A date+time format. */ 346 DATETIME 347 } 348 349}