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 <style></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}