    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.ui;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.wiki.api.core.Context;
    024import org.apache.wiki.i18n.InternationalizationManager;
    025import org.apache.wiki.modules.ModuleManager;
    026import org.apache.wiki.preferences.Preferences;
    027import org.apache.wiki.util.ClassUtil;
    028
    029import javax.servlet.jsp.PageContext;
    030import javax.servlet.jsp.jstl.fmt.LocaleSupport;
    031import java.util.Enumeration;
    032import java.util.HashMap;
    033import java.util.LinkedHashMap;
    034import java.util.List;
    035import java.util.Locale;
    036import java.util.Map;
    037import java.util.ResourceBundle;
    038import java.util.Set;
    039import java.util.TimeZone;
    040import java.util.Vector;
    041
    042
    043/**
    044 *  This class takes care of managing JSPWiki templates.  This class also provides the ResourceRequest mechanism.
    045 *
    046 *  @since 2.1.62
    047 */
    048public interface TemplateManager extends ModuleManager {
    049
    050    String SKIN_DIRECTORY = "skins";
    051
    052    /** Requests a JavaScript function to be called during window.onload. Value is {@value}. */
    053    String RESOURCE_JSFUNCTION = "jsfunction";
    054
    055    /** Requests a JavaScript associative array with all localized strings. */
    056    String RESOURCE_JSLOCALIZEDSTRINGS = "jslocalizedstrings";
    057
    058    /** Requests a stylesheet to be inserted. Value is {@value}. */
    059    String RESOURCE_STYLESHEET = "stylesheet";
    060
    061    /** Requests a script to be loaded. Value is {@value}. */
    062    String RESOURCE_SCRIPT = "script";
    063
    064    /** Requests inlined CSS. Value is {@value}. */
    065    String RESOURCE_INLINECSS = "inlinecss";
    066
    067    /** The default directory for the properties. Value is {@value}. */
    068    String DIRECTORY = "templates";
    069
    070    /** The name of the default template. Value is {@value}. */
    071    String DEFAULT_TEMPLATE = "default";
    072
    073    /** Name of the file that contains the properties. */
    074    String PROPERTYFILE = "template.properties";
    075
    076    /** Location of I18N Resource bundles, and path prefix and suffixes */
    077    String I18NRESOURCE_PREFIX = "templates/default_";
    078
    079    String I18NRESOURCE_SUFFIX = ".properties";
    080
    081    /** The default (en) RESOURCE name and id. */
    082    String I18NRESOURCE_EN = "templates/default.properties";
    083    String I18NRESOURCE_EN_ID = "en";
    084
    085    /** I18N string to mark the default locale */
    086    String I18NDEFAULT_LOCALE = "prefs.user.language.default";
    087
    088    /** I18N string to mark the server timezone */
    089    String I18NSERVER_TIMEZONE = "prefs.user.timezone.server";
    090
    091    /** Prefix of the default timeformat properties. */
    092    String TIMEFORMATPROPERTIES = "jspwiki.defaultprefs.timeformat.";
    093
    094    /** The name under which the resource includes map is stored in the  WikiContext. */
    095    String RESOURCE_INCLUDES = "jspwiki.resourceincludes";
    096
    097    /** Requests a HTTP header. Value is {@value}. */
    098    String RESOURCE_HTTPHEADER = "httpheader";
    099
    100    /**
    101     *  Check the existence of a template.
    102     */
    103    boolean templateExists( String templateName );
    104
    105    /**
    106     *  A utility method for finding a JSP page.  It searches only under either current context or by the absolute name.
    107     *
    108     *  @param pageContext the JSP PageContext
    109     *  @param name The name of the JSP page to look for (e.g "Wiki.jsp")
    110     *  @return The context path to the resource
    111     */
    112    String findJSP( PageContext pageContext, String name );
    113
    114    /**
    115     *  Attempts to locate a resource under the given template.  If that template does not exist, or the page does not exist under that
    116     *  template, will attempt to locate a similarly named file under the default template.
    117     *  <p>
    118     *  Even though the name suggests only JSP files can be located, but in fact this method can find also other resources than JSP files.
    119     *
    120     *  @param pageContext The JSP PageContext
    121     *  @param template From which template we should seek initially?
    122     *  @param name Which resource are we looking for (e.g. "ViewTemplate.jsp")
    123     *  @return path to the JSP page; null, if it was not found.
    124     */
    125    String findJSP( PageContext pageContext, String template, String name );
    126
    127    /**
    128     *  Attempts to locate a resource under the given template.  This matches the functionality findJSP(), but uses the WikiContext as
    129     *  the argument.  If there is no servlet context (i.e. this is embedded), will just simply return a best-guess.
    130     *  <p>
    131     *  This method is typically used to locate any resource, including JSP pages, images, scripts, etc.
    132     *
    133     *  @since 2.6
    134     *  @param ctx the wiki context
    135     *  @param template the name of the template to use
    136     *  @param name the name of the resource to fine
    137     *  @return the path to the resource
    138     */
    139    String findResource( Context ctx, String template, String name );
    140
    141    /**
    142     *   Lists the skins available under this template.  Returns an empty Set, if there are no extra skins available.  Note that
    143     *   this method does not check whether there is anything actually in the directories, it just lists them.  This may change
    144     *   in the future.
    145     *
    146     *   @param pageContext the JSP PageContext
    147     *   @param template The template to search
    148     *   @return Set of Strings with the skin names.
    149     *   @since 2.3.26
    150     */
    151    Set< String > listSkins( PageContext pageContext, String template );
    152
    153    /**
    154     * List all installed i18n language properties by classpath searching for files like :
    155     *    templates/default_*.properties
    156     *    templates/default.properties
    157     *
    158     * @param pageContext page context
    159     * @return map of installed Languages
    160     * @since 2.7.x
    161     */
    162    default Map< String, String > listLanguages( final PageContext pageContext ) {
    163        final Map< String, String > resultMap = new LinkedHashMap<>();
    164        final String clientLanguage = pageContext.getRequest().getLocale().toString();
    165        final List< String > entries = ClassUtil.classpathEntriesUnder( DIRECTORY );
    166        for( String name : entries ) {
    167            if ( name.equals( I18NRESOURCE_EN ) || (name.startsWith( I18NRESOURCE_PREFIX ) && name.endsWith( I18NRESOURCE_SUFFIX ) ) ) {
    168                if( name.equals( I18NRESOURCE_EN ) ) {
    169                    name = I18NRESOURCE_EN_ID;
    170                } else {
    171                    name = name.substring( I18NRESOURCE_PREFIX.length(), name.lastIndexOf( I18NRESOURCE_SUFFIX ) );
    172                }
    173                final Locale locale = new Locale( name.substring( 0, 2 ), !name.contains( "_" ) ? "" : name.substring( 3, 5 ) );
    174                String defaultLanguage = "";
    175                if( clientLanguage.startsWith( name ) ) {
    176                    defaultLanguage = LocaleSupport.getLocalizedMessage( pageContext, I18NDEFAULT_LOCALE );
    177                }
    178                resultMap.put( name, locale.getDisplayName( locale ) + " " + defaultLanguage );
    179            }
    180        }
    181
    182        return resultMap;
    183    }
    184
    185
    186    /**
    187     * List all available timeformats, read from the jspwiki.properties
    188     *
    189     * @param pageContext page context
    190     * @return map of TimeFormats
    191     * @since 2.7.x
    192     */
    193    Map< String, String > listTimeFormats( final PageContext pageContext );
    194
    195    /**
    196     * List all timezones, with special marker for server timezone
    197     *
    198     * @param pageContext page context
    199     * @return map of TimeZones
    200     * @since 2.7.x
    201     */
    202    default Map< String, String > listTimeZones( final PageContext pageContext ) {
    203        final Map< String, String > resultMap = new LinkedHashMap<>();
    204        final String[][] tzs = {
    205                          { "GMT-12", "Enitwetok, Kwajalien" },
    206                          { "GMT-11", "Nome, Midway Island, Samoa" },
    207                          { "GMT-10", "Hawaii" },
    208                          { "GMT-9", "Alaska" },
    209                          { "GMT-8", "Pacific Time" },
    210                          { "GMT-7", "Mountain Time" },
    211                          { "GMT-6", "Central Time, Mexico City" },
    212                          { "GMT-5", "Eastern Time, Bogota, Lima, Quito" },
    213                          { "GMT-4", "Atlantic Time, Caracas, La Paz" },
    214                          { "GMT-3:30", "Newfoundland" },
    215                          { "GMT-3", "Brazil, Buenos Aires, Georgetown, Falkland Is." },
    216                          { "GMT-2", "Mid-Atlantic, Ascention Is., St Helena" },
    217                          { "GMT-1", "Azores, Cape Verde Islands" },
    218                          { "GMT", "Casablanca, Dublin, Edinburgh, London, Lisbon, Monrovia" },
    219                          { "GMT+1", "Berlin, Brussels, Copenhagen, Madrid, Paris, Rome" },
    220                          { "GMT+2", "Helsinki, Athens, Kaliningrad, South Africa, Warsaw" },
    221                          { "GMT+3", "Baghdad, Riyadh, Moscow, Nairobi" },
    222                          { "GMT+3:30", "Tehran" },
    223                          { "GMT+4", "Adu Dhabi, Baku, Muscat, Tbilisi" },
    224                          { "GMT+4:30", "Kabul" },
    225                          { "GMT+5", "Islamabad, Karachi, Tashkent" },
    226                          { "GMT+5:30", "Bombay, Calcutta, Madras, New Delhi" },
    227                          { "GMT+6", "Almaty, Colomba, Dhakra" },
    228                          { "GMT+7", "Bangkok, Hanoi, Jakarta" },
    229                          { "GMT+8", "Beijing, Hong Kong, Perth, Singapore, Taipei" },
    230                          { "GMT+9", "Osaka, Sapporo, Seoul, Tokyo, Yakutsk" },
    231                          { "GMT+9:30", "Adelaide, Darwin" },
    232                          { "GMT+10", "Melbourne, Papua New Guinea, Sydney, Vladivostok" },
    233                          { "GMT+11", "Magadan, New Caledonia, Solomon Islands" },
    234                          { "GMT+12", "Auckland, Wellington, Fiji, Marshall Island" } };
    235
    236        final TimeZone servertz = TimeZone.getDefault();
    237        for( final String[] strings : tzs ) {
    238            String tzID = strings[ 0 ];
    239            final TimeZone tz = TimeZone.getTimeZone( tzID );
    240            String serverTimeZone = "";
    241            if( servertz.getRawOffset() == tz.getRawOffset() ) {
    242                serverTimeZone = LocaleSupport.getLocalizedMessage( pageContext, I18NSERVER_TIMEZONE );
    243                tzID = servertz.getID();
    244            }
    245
    246            resultMap.put( tzID, "(" + strings[ 0 ] + ") " + strings[ 1 ] + " " + serverTimeZone );
    247        }
    248
    249        return resultMap;
    250    }
    251
    252    /**
    253     *  Returns the include resources marker for a given type.  This is in an
    254     *  HTML or Javascript comment format.
    255     *
    256     *  @param context the wiki context
    257     *  @param type the marker
    258     *  @return the generated marker comment
    259     */
    260    static String getMarker( final Context context, final String type ) {
    261        if( type.equals( RESOURCE_JSLOCALIZEDSTRINGS ) ) {
    262            return getJSLocalizedStrings( context );
    263        } else if( type.equals( RESOURCE_JSFUNCTION ) ) {
    264            return "/* INCLUDERESOURCES ("+type+") */";
    265        }
    266        return "<!-- INCLUDERESOURCES ("+type+") -->";
    267    }
    268
    269    /**
    270     *  Extract all i18n strings in the javascript domain. (javascript.*) Returns a javascript snippet which defines the LocalizedStings array.
    271     *
    272     *  @param context the {@link Context}
    273     *  @return Javascript snippet which defines the LocalizedStrings array
    274     *  @since 2.5.108
    275     */
    276    static String getJSLocalizedStrings( final Context context ) {
    277        final StringBuilder sb = new StringBuilder();
    278        sb.append( "var LocalizedStrings = {\n");
    279        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.DEF_TEMPLATE );
    280        boolean first = true;
    281
    282        for( final Enumeration< String > en = rb.getKeys(); en.hasMoreElements(); ) {
    283            final String key = en.nextElement();
    284            if( key.startsWith("javascript") ) {
    285                if( first ) {
    286                    first = false;
    287                } else {
    288                    sb.append( ",\n" );
    289                }
    290                sb.append( "\"" ).append( key ).append( "\":\"" ).append( rb.getString( key ) ).append( "\"" );
    291            }
    292        }
    293        sb.append("\n};\n");
    294
    295        return( sb.toString() );
    296    }
    297
    298    /**
    299     *  Adds a resource request to the current request context. The content will be added at the resource-type marker
    300     *  (see IncludeResourcesTag) in WikiJSPFilter.
    301     *  <p>
    302     *  The resources can be of different types.  For RESOURCE_SCRIPT and RESOURCE_STYLESHEET this is a URI path to the resource
    303     *  (a script file or an external stylesheet) that needs to be included.  For RESOURCE_INLINECSS the resource should be something
    304     *  that can be added between &lt;style>&lt;/style> in the header file (commonheader.jsp).  For RESOURCE_JSFUNCTION it is the name
    305     *  of the Javascript function that should be run at page load.
    306     *  <p>
    307     *  The IncludeResourceTag inserts code in the template files, which is then filled by the WikiFilter after the request has been
    308     *  rendered but not yet sent to the recipient.
    309     *  <p>
    310     *  Note that ALL resource requests get rendered, so this method does not check if the request already exists in the resources.
    311     *  Therefore, if you have a plugin which makes a new resource request every time, you'll end up with multiple resource requests
    312     *  rendered.  It's thus a good idea to make this request only once during the page life cycle.
    313     *
    314     *  @param ctx The current wiki context
    315     *  @param type What kind of resource should be added?
    316     *  @param resource The resource to add.
    317     */
    318    static void addResourceRequest( final Context ctx, final String type, final String resource ) {
    319        HashMap< String, Vector< String > > resourcemap = ctx.getVariable( RESOURCE_INCLUDES );
    320        if( resourcemap == null ) {
    321            resourcemap = new HashMap<>();
    322        }
    323
    324        Vector< String > resources = resourcemap.get( type );
    325        if( resources == null ) {
    326            resources = new Vector<>();
    327        }
    328        String resolvedResource = resource;
    329        if( StringUtils.startsWith( resource, "engine://" ) ) {
    330            final String val = ctx.getEngine().getWikiProperties().getProperty( resource.substring( 9 ) ); // "engine//:".length() == 9
    331            if( StringUtils.isNotBlank( val ) ) {
    332                resolvedResource = val;
    333            }
    334        }
    335
    336        String resourceString = null;
    337        switch( type ) {
    338        case RESOURCE_SCRIPT:
    339            resourceString = "<script type='text/javascript' src='" + resolvedResource + "'></script>";
    340            break;
    341        case RESOURCE_STYLESHEET:
    342            resourceString = "<link rel='stylesheet' type='text/css' href='" + resolvedResource + "' />";
    343            break;
    344        case RESOURCE_INLINECSS:
    345            resourceString = "<style type='text/css'>\n" + resolvedResource + "\n</style>\n";
    346            break;
    347        case RESOURCE_JSFUNCTION:
    348        case RESOURCE_HTTPHEADER:
    349            resourceString = resolvedResource;
    350            break;
    351        }
    352
    353        if( resourceString != null ) {
    354            resources.add( resourceString );
    355        }
    356
    357        LogManager.getLogger( TemplateManager.class ).debug( "Request to add a resource: {}", resourceString );
    358
    359        resourcemap.put( type, resources );
    360        ctx.setVariable( RESOURCE_INCLUDES, resourcemap );
    361    }
    362
    363    /**
    364     *  Returns resource requests for a particular type.  If there are no resources, returns an empty array.
    365     *
    366     *  @param ctx WikiContext
    367     *  @param type The resource request type
    368     *  @return a String array for the resource requests
    369     */
    370    static String[] getResourceRequests( final Context ctx, final String type ) {
    371        final HashMap< String, Vector< String > > hm = ctx.getVariable( RESOURCE_INCLUDES );
    372        if( hm == null ) {
    373            return new String[0];
    374        }
    375
    376        final Vector<String> resources = hm.get( type );
    377        if( resources == null ){
    378            return new String[0];
    379        }
    380
    381        final String[] res = new String[resources.size()];
    382        return resources.toArray( res );
    383    }
    384
    385    /**
    386     *  Returns all those types that have been requested so far.
    387     *
    388     * @param ctx the wiki context
    389     * @return the array of types requested
    390     */
    391    static String[] getResourceTypes( final Context ctx ) {
    392        String[] res = new String[0];
    393        if( ctx != null ) {
    394            final HashMap< String, String > hm = ctx.getVariable( RESOURCE_INCLUDES );
    395            if( hm != null ) {
    396                final Set< String > keys = hm.keySet();
    397                res = keys.toArray( res );
    398            }
    399        }
    400
    401        return res;
    402    }
    403
    404}