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}