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}