    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}