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}