001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.loader; 029 030import org.opencms.cache.CmsVfsMemoryObjectCache; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsProperty; 034import org.opencms.file.CmsResource; 035import org.opencms.json.JSONException; 036import org.opencms.json.JSONObject; 037import org.opencms.json.JSONTokener; 038import org.opencms.main.CmsLog; 039import org.opencms.main.OpenCms; 040import org.opencms.security.CmsRole; 041import org.opencms.util.CmsStringUtil; 042import org.opencms.util.CmsUUID; 043import org.opencms.xml.containerpage.CmsFunctionFormatterBean; 044import org.opencms.xml.containerpage.I_CmsFormatterBean; 045 046import java.nio.charset.StandardCharsets; 047import java.security.MessageDigest; 048import java.security.NoSuchAlgorithmException; 049import java.util.Collections; 050import java.util.HashMap; 051import java.util.HashSet; 052import java.util.LinkedHashMap; 053import java.util.Locale; 054import java.util.Map; 055import java.util.Set; 056import java.util.regex.Pattern; 057 058import javax.servlet.http.HttpServletRequest; 059 060import org.apache.commons.codec.binary.Hex; 061import org.apache.commons.logging.Log; 062 063import com.google.common.base.Supplier; 064import com.google.common.base.Suppliers; 065 066/** 067 * Template context provider that can be used to migrate from one template to another. 068 * 069 * <p>Note: The template provider by itself does not transform anything, the feature is just named like that. 070 */ 071public class CmsTransformerTemplateProvider implements I_CmsTemplateContextProvider { 072 073 /** 074 * Contains the configuration data for the provider, usually read from a configuration file. 075 */ 076 public class Configuration { 077 078 /** The map of template contexts. */ 079 private Map<String, CmsTemplateContext> m_contextMap = new HashMap<>(); 080 081 /** The path filter regexes for restricting functions in the gallery search results.*/ 082 private Map<String, Pattern> m_functionFilters = new HashMap<>(); 083 084 /** The context menu label. */ 085 private CmsJsonMessageContainer m_menuLabel; 086 087 /** Map from template key to template compatibility string. */ 088 private Map<String, String> m_templateCompatibility = new HashMap<>(); 089 090 /** 091 * Creates a new instance. 092 */ 093 public Configuration() {} 094 095 /** 096 * Creates a new instance. 097 * 098 * @param configJson the configuration JSON object 099 * @throws JSONException if something goes wrong with the JSON 100 */ 101 public Configuration(JSONObject configJson) 102 throws JSONException { 103 104 Map<String, CmsTemplateContext> contextMap = new LinkedHashMap<>(); 105 JSONObject source = configJson.getJSONObject(JsonKeys.sourceTemplate.name()); 106 JSONObject target = configJson.getJSONObject(JsonKeys.targetTemplate.name()); 107 CmsTemplateContext sourceContext = parseTemplateContext(TEMPLATE_KEY_SOURCE, source); 108 CmsTemplateContext targetContext = parseTemplateContext(TEMPLATE_KEY_TARGET, target); 109 contextMap.put(TEMPLATE_KEY_SOURCE, sourceContext); 110 contextMap.put(TEMPLATE_KEY_TARGET, targetContext); 111 m_contextMap = Collections.unmodifiableMap(contextMap); 112 Object menuLabel = configJson.opt(JsonKeys.menuLabel.name()); 113 if (menuLabel != null) { 114 m_menuLabel = new CmsJsonMessageContainer(menuLabel); 115 } 116 117 } 118 119 /** 120 * Gets the map of template contexts, with their internal names as keys. 121 * 122 * @return the map of template contexts 123 */ 124 public Map<String, CmsTemplateContext> getContextMap() { 125 126 return m_contextMap; 127 } 128 129 /** 130 * Gets the function filter pattern used to filter dynamic function paths. 131 * 132 * <p>If this returns null, dynamic functions shouldn't be filtered. 133 * 134 * @param key the template context key 135 * @return the dynamic function filter 136 */ 137 public Pattern getFunctionFilter(String key) { 138 139 return m_functionFilters.get(key); 140 } 141 142 /** 143 * Gets the label for the context menu 144 * 145 * @return the label for the context menu 146 */ 147 public CmsJsonMessageContainer getMenuLabel() { 148 149 return m_menuLabel; 150 } 151 152 /** 153 * Gets the template compatibility for the given template context. 154 * 155 * @param currentContext the current template context 156 * @return the template compatibility for the template context 157 */ 158 public String getTemplateCompatibility(String currentContext) { 159 160 return m_templateCompatibility.get(currentContext); 161 } 162 163 /** 164 * Helper method to read a template context from a JSON value. 165 * 166 * @param key the name of the template context 167 * @param object the JSON value to construct the template context from 168 * @return the constructed template context 169 * 170 * @throws JSONException if something goes wrong with the JSON 171 */ 172 private CmsTemplateContext parseTemplateContext(String key, JSONObject object) throws JSONException { 173 174 Object niceNameValue = object.opt(JsonKeys.niceName.name()); 175 CmsTemplateContext context = new CmsTemplateContext( 176 key, 177 object.getString(JsonKeys.path.name()), 178 niceNameValue != null ? new CmsJsonMessageContainer(niceNameValue) : null, 179 CmsTransformerTemplateProvider.this, 180 Collections.emptyList(), 181 false); 182 String functionFilter = object.optString(JsonKeys.functionFilter.name(), null); 183 if (functionFilter != null) { 184 m_functionFilters.put(key, Pattern.compile(functionFilter)); 185 } 186 String templateCompatibility = object.optString(JsonKeys.compatibility.name(), null); 187 if (templateCompatibility != null) { 188 m_templateCompatibility.put(key, templateCompatibility); 189 } 190 191 return context; 192 } 193 } 194 195 /** Enum representing the keys in the configuration JSON file. */ 196 enum JsonKeys { 197 /** Key for the template compatibility. */ 198 compatibility, 199 200 /** Key for the regex used for filtering dynamic function paths. */ 201 functionFilter, 202 203 /** Key for the context menu label. */ 204 menuLabel, 205 206 /** Key for the nice name of a template. */ 207 niceName, 208 209 /** Key for the template path. */ 210 path, 211 212 /** Key for the source template. */ 213 sourceTemplate, 214 215 /** Key for the target template. */ 216 targetTemplate; 217 } 218 219 /** Version string used for cookie name calculation. */ 220 private static final String VERSION = "2"; 221 222 /** The cookie prefix. */ 223 public static final String COOKIE_PREFIX = "templatetransformer_override_"; 224 225 /** Parameter for the configuration file in the template provider string. */ 226 public static final String PARAM_CONFIG = "config"; 227 228 /** The template context key for the source template. */ 229 public static final String TEMPLATE_KEY_SOURCE = "source"; 230 231 /** The template context key for the target template. */ 232 public static final String TEMPLATE_KEY_TARGET = "target"; 233 234 /** Logger instance for this class. */ 235 private static final Log LOG = CmsLog.getLog(CmsTransformerTemplateProvider.class); 236 237 /** Instantiates the configuration cache when accessed. */ 238 private static Supplier<CmsVfsMemoryObjectCache> m_configCacheProvider = Suppliers.memoize( 239 () -> new CmsVfsMemoryObjectCache()); 240 241 /** The CmsObject this provider was initialized with (in the Online project). */ 242 private CmsObject m_cms; 243 244 /** The path for the configuration file. */ 245 private String m_configPath; 246 247 /** Cookie name for the template context override cookie. */ 248 private String m_cookieName; 249 250 /** 251 * @see org.opencms.loader.I_CmsTemplateContextProvider#getAllContexts() 252 */ 253 public Map<String, CmsTemplateContext> getAllContexts() { 254 255 return getConfiguration().getContextMap(); 256 } 257 258 /** 259 * Gets the configuration data that was read from the config file. 260 * 261 * @return the configuration data from the config file 262 */ 263 public Configuration getConfiguration() { 264 265 Configuration config = (Configuration)(m_configCacheProvider.get().getCachedObject(m_cms, m_configPath)); 266 if (config != null) { 267 return config; 268 } else { 269 try { 270 config = loadConfiguration(); 271 m_configCacheProvider.get().putCachedObject(m_cms, m_configPath, config); 272 return config; 273 } catch (Exception e) { 274 LOG.error(e.getLocalizedMessage(), e); 275 return new Configuration(); 276 } 277 } 278 } 279 280 /** 281 * @see org.opencms.loader.I_CmsTemplateContextProvider#getDefaultLabel(java.util.Locale) 282 */ 283 public String getDefaultLabel(Locale locale) { 284 285 Configuration config = getConfiguration(); 286 return config.getContextMap().get(TEMPLATE_KEY_SOURCE).getLocalizedName(locale); 287 } 288 289 /** 290 * @see org.opencms.loader.I_CmsTemplateContextProvider#getEditorStyleSheet(org.opencms.file.CmsObject, java.lang.String) 291 */ 292 public String getEditorStyleSheet(CmsObject cms, String editedResourcePath) { 293 294 // we assume here that the WYSIWYG editor stylesheet is configured via sitemap configuration. 295 return null; 296 } 297 298 /** 299 * @see org.opencms.loader.I_CmsTemplateContextProvider#getFunctionsForGallery(org.opencms.file.CmsObject, java.lang.String) 300 */ 301 public Set<CmsUUID> getFunctionsForGallery(CmsObject cms, String templateContext) { 302 303 Configuration config = getConfiguration(); 304 Pattern functionFilter = config.getFunctionFilter(templateContext); 305 if (functionFilter == null) { 306 // everything allowed 307 return null; 308 } 309 Set<CmsUUID> result = new HashSet<>(); 310 for (I_CmsFormatterBean formatter : OpenCms.getADEManager().getCachedFormatters( 311 false).getFormatters().values()) { 312 if (!(formatter instanceof CmsFunctionFormatterBean)) { 313 continue; 314 } 315 if (!CmsUUID.isValidUUID(formatter.getId()) || (formatter.getLocation() == null)) { 316 continue; 317 } 318 if (!functionFilter.matcher(formatter.getLocation()).matches()) { 319 continue; 320 } 321 result.add(new CmsUUID(formatter.getId())); 322 } 323 return Collections.unmodifiableSet(result); 324 } 325 326 /** 327 * @see org.opencms.loader.I_CmsTemplateContextProvider#getMenuLabel(java.util.Locale) 328 */ 329 public String getMenuLabel(Locale locale) { 330 331 CmsJsonMessageContainer container = getConfiguration().getMenuLabel(); 332 if (container != null) { 333 return container.key(locale); 334 } else { 335 return null; 336 } 337 } 338 339 /** 340 * @see org.opencms.loader.I_CmsTemplateContextProvider#getMenuPosition() 341 */ 342 public int getMenuPosition() { 343 344 return 1; 345 } 346 347 /** 348 * @see org.opencms.loader.I_CmsTemplateContextProvider#getOverrideCookieName() 349 */ 350 public String getOverrideCookieName() { 351 352 return m_cookieName; 353 } 354 355 /** 356 * @see org.opencms.loader.I_CmsTemplateContextProvider#getTemplateCompatibility(java.lang.String) 357 */ 358 public String getTemplateCompatibility(String currentContext) { 359 360 return getConfiguration().getTemplateCompatibility(currentContext); 361 } 362 363 /** 364 * @see org.opencms.loader.I_CmsTemplateContextProvider#getTemplateContext(org.opencms.file.CmsObject, javax.servlet.http.HttpServletRequest, org.opencms.file.CmsResource) 365 */ 366 public CmsTemplateContext getTemplateContext(CmsObject cms, HttpServletRequest request, CmsResource resource) { 367 368 Configuration config = getConfiguration(); 369 return config.getContextMap().get(TEMPLATE_KEY_SOURCE); 370 } 371 372 /** 373 * @see org.opencms.loader.I_CmsTemplateContextProvider#initialize(org.opencms.file.CmsObject, java.lang.String) 374 */ 375 public void initialize(CmsObject cms, String config) { 376 377 m_cms = cms; 378 if (config == null) { 379 config = ""; 380 } 381 config = config.trim(); 382 Map<String, String> parsedConfig = CmsStringUtil.splitAsMap(config, ",", "="); 383 m_configPath = parsedConfig.get(PARAM_CONFIG); 384 if (m_configPath == null) { 385 throw new RuntimeException( 386 "Missing parameter '" + PARAM_CONFIG + "' for template provider '" + getClass().getName() + "'"); 387 } 388 // Use MD5 of configuration path for cookie name, so users can switch templates independently for differently configured instances of the template provider 389 try { 390 MessageDigest md5 = MessageDigest.getInstance("MD5"); 391 md5.update(m_configPath.getBytes(StandardCharsets.UTF_8)); 392 md5.update((byte)0); 393 md5.update(VERSION.getBytes(StandardCharsets.UTF_8)); 394 byte[] md5bytes = md5.digest(); 395 m_cookieName = COOKIE_PREFIX + Hex.encodeHexString(md5bytes); 396 } catch (NoSuchAlgorithmException e) { 397 // shouldn't happen - MD5 must be in standard library 398 throw new RuntimeException(e); 399 } 400 } 401 402 /** 403 * @see org.opencms.loader.I_CmsTemplateContextProvider#isHiddenContext(java.lang.String) 404 */ 405 public boolean isHiddenContext(String key) { 406 407 return TEMPLATE_KEY_SOURCE.equals(key); 408 } 409 410 /** 411 * @see org.opencms.loader.I_CmsTemplateContextProvider#isIgnoreTemplateContextsSetting() 412 */ 413 public boolean isIgnoreTemplateContextsSetting() { 414 return true; 415 } 416 417 /** 418 * @see org.opencms.loader.I_CmsTemplateContextProvider#readCommonProperty(org.opencms.file.CmsObject, java.lang.String, java.lang.String) 419 */ 420 public String readCommonProperty(CmsObject cms, String propertyName, String fallbackValue) { 421 422 Configuration config = getConfiguration(); 423 try { 424 CmsProperty prop = cms.readPropertyObject( 425 config.getContextMap().get(TEMPLATE_KEY_SOURCE).getTemplatePath(), 426 propertyName, 427 false); 428 return prop.getValue(); 429 } catch (Exception e) { 430 LOG.error(e.getLocalizedMessage(), e); 431 return null; 432 } 433 } 434 435 /** 436 * @see org.opencms.loader.I_CmsTemplateContextProvider#shouldShowContextMenuOption(org.opencms.file.CmsObject) 437 */ 438 public boolean shouldShowContextMenuOption(CmsObject cms) { 439 440 return OpenCms.getRoleManager().hasRole(cms, CmsRole.DEVELOPER); 441 } 442 443 /** 444 * @see org.opencms.loader.I_CmsTemplateContextProvider#shouldShowElementTemplateContextSelection(org.opencms.file.CmsObject) 445 */ 446 public boolean shouldShowElementTemplateContextSelection(CmsObject cms) { 447 448 return false; 449 } 450 451 /** 452 * Helper method for loading the configuration from the VFS. 453 * 454 * @return the provider configuration 455 * @throws Exception if something goes wrong 456 */ 457 protected Configuration loadConfiguration() throws Exception { 458 459 CmsFile configFile = m_cms.readFile(m_configPath); 460 String configStr = new String(configFile.getContents(), StandardCharsets.UTF_8); 461 JSONTokener tok = new JSONTokener(configStr); 462 tok.setOrdered(true); 463 JSONObject configJson = new JSONObject(tok, true); 464 return new Configuration(configJson); 465 } 466 467}