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