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.configuration;
029
030import org.opencms.file.CmsObject;
031import org.opencms.letsencrypt.CmsLetsEncryptConfiguration;
032import org.opencms.letsencrypt.CmsLetsEncryptConfiguration.Trigger;
033import org.opencms.letsencrypt.CmsSiteConfigToLetsEncryptConfigConverter;
034import org.opencms.main.CmsLog;
035import org.opencms.main.OpenCms;
036import org.opencms.report.CmsLogReport;
037import org.opencms.site.CmsAlternativeSiteRootMapping;
038import org.opencms.site.CmsSSLMode;
039import org.opencms.site.CmsSite;
040import org.opencms.site.CmsSiteManagerImpl;
041import org.opencms.site.CmsSiteMatcher;
042import org.opencms.site.CmsSiteMatcher.RedirectMode;
043
044import java.util.ArrayList;
045import java.util.HashSet;
046import java.util.Iterator;
047import java.util.List;
048import java.util.Locale;
049import java.util.Map;
050import java.util.Optional;
051import java.util.SortedMap;
052import java.util.TreeMap;
053import java.util.concurrent.ScheduledFuture;
054import java.util.concurrent.TimeUnit;
055
056import javax.xml.parsers.ParserConfigurationException;
057
058import org.apache.commons.digester3.CallMethodRule;
059import org.apache.commons.digester3.Digester;
060import org.apache.commons.digester3.NodeCreateRule;
061import org.apache.commons.digester3.ObjectCreateRule;
062import org.apache.commons.digester3.Rule;
063
064import org.dom4j.Element;
065import org.w3c.dom.NodeList;
066import org.xml.sax.Attributes;
067
068/**
069 * Class to read and write the OpenCms site configuration.<p>
070 */
071public class CmsSitesConfiguration extends A_CmsXmlConfiguration implements I_CmsXmlConfigurationWithUpdateHandler {
072
073    /** The "error" attribute. */
074    public static final String A_ERROR = "error";
075
076    /** The "errorPage" attribute. */
077    public static final String A_ERROR_PAGE = "errorPage";
078
079    /** The "exclusive" attribute. */
080    public static final String A_EXCLUSIVE = "exclusive";
081
082    /** The attribute name for the alias offset. */
083    public static final String A_OFFSET = "offset";
084
085    /** The "position" attribute. */
086    public static final String A_POSITION = "position";
087
088    /** The "redirect" attribute. */
089    public static final String A_REDIRECT = "redirect";
090
091    /** The "server" attribute. */
092    public static final String A_SERVER = "server";
093
094    /** The ssl mode attribute.*/
095    public static final String A_SSL = "sslmode";
096
097    /** Attribute name for the subsiteSelection option. */
098    public static final String A_SUBSITE_SELECTION = "subsiteSelection";
099
100    /** The "title" attribute. */
101    public static final String A_TITLE = "title";
102
103    /** The "usePermanentRedirects" attribute. */
104    public static final String A_USE_PERMANENT_REDIRECTS = "usePermanentRedirects";
105
106    /** The "webserver" attribute. */
107    public static final String A_WEBSERVER = "webserver";
108
109    /** The name of the DTD for this configuration. */
110    public static final String CONFIGURATION_DTD_NAME = "opencms-sites.dtd";
111
112    /** The name of the default XML file for this configuration. */
113    public static final String DEFAULT_XML_FILE_NAME = "opencms-sites.xml";
114
115    /** The node name for the alias node. */
116    public static final String N_ALIAS = "alias";
117
118    /** The node name for the default-uri node. */
119    public static final String N_DEFAULT_URI = "default-uri";
120
121    /** New secure modes node. */
122    public static final String N_OLD_STYLE_SECURE_SERVER = "oldStyleSecureServer";
123
124    /** The node name for the parameters. */
125    public static final String N_PARAMETERS = "parameters";
126
127    /** The node name for the secure site. */
128    public static final String N_SECURE = "secure";
129
130    /** Shared folder node name. */
131    public static final String N_SHARED_FOLDER = "shared-folder";
132
133    /** The node name for the sites node. */
134    public static final String N_SITES = "sites";
135
136    /** The node name which indicates if apache should be configurable in sitemanager. */
137    public static final String N_WEBSERVERSCRIPTING = "webserver-scripting";
138
139    /** Configuration node name. */
140    public static final String N_WEBSERVERSCRIPTING_CONFIGTEMPLATE = "configtemplate";
141
142    /** Configuration node name. */
143    public static final String N_WEBSERVERSCRIPTING_FILENAMEPREFIX = "filenameprefix";
144
145    /** Configuration node name. */
146    public static final String N_WEBSERVERSCRIPTING_LOGGINGDIR = "loggingdir";
147
148    /** Configuration node name. */
149    public static final String N_WEBSERVERSCRIPTING_SECURETEMPLATE = "securetemplate";
150
151    /** Configuration node name. */
152    public static final String N_WEBSERVERSCRIPTING_TARGETPATH = "targetpath";
153
154    /** Configuration node name. */
155    public static final String N_WEBSERVERSCRIPTING_WEBSERVERSCRIPT = "webserverscript";
156
157    /** The node name for the workplace-server node. */
158    public static final String N_WORKPLACE_SERVER = "workplace-server";
159
160    /** The CmsObject with admin privileges. */
161    private CmsObject m_adminCms;
162
163    /** The configured site manager. */
164    private CmsSiteManagerImpl m_siteManager;
165
166    /** Future for the LetsEncrypt async update. */
167    private ScheduledFuture<?> m_updateFuture;
168
169    /**
170     * @see org.opencms.configuration.I_CmsXmlConfiguration#addXmlDigesterRules(org.apache.commons.digester3.Digester)
171     */
172    public void addXmlDigesterRules(Digester digester) {
173
174        // add site configuration rule
175        digester.addObjectCreate("*/" + N_SITES, CmsSiteManagerImpl.class);
176        digester.addCallMethod("*/" + N_SITES + "/" + N_WORKPLACE_SERVER, "addWorkplaceServer", 2);
177        digester.addCallParam("*/" + N_SITES + "/" + N_WORKPLACE_SERVER, 0);
178        digester.addCallParam("*/" + N_SITES + "/" + N_WORKPLACE_SERVER, 1, A_SSL);
179        digester.addCallMethod("*/" + N_SITES + "/" + N_DEFAULT_URI, "setDefaultUri", 0);
180        digester.addCallMethod("*/" + N_SITES + "/" + N_OLD_STYLE_SECURE_SERVER, "setOldStyleSecureServerAllowed", 0);
181
182        String configApachePath = "*/" + N_SITES + "/" + N_WEBSERVERSCRIPTING;
183        digester.addCallMethod(configApachePath, "setWebServerScripting", 6);
184        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_WEBSERVERSCRIPT, 0);
185        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_TARGETPATH, 1);
186        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_CONFIGTEMPLATE, 2);
187        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_SECURETEMPLATE, 3);
188        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_FILENAMEPREFIX, 4);
189        digester.addCallParam(configApachePath + "/" + N_WEBSERVERSCRIPTING_LOGGINGDIR, 5);
190
191        digester.addSetNext("*/" + N_SITES, "setSiteManager");
192
193        // add site configuration rule
194        String siteXpath = "*/" + N_SITES + "/" + N_SITE;
195        digester.addRule(
196            siteXpath,
197            new CallMethodRule(
198                "addSiteInternally",
199                15,
200                new Class[] {
201                    String.class,
202                    String.class,
203                    String.class,
204                    String.class,
205                    String.class,
206                    String.class,
207                    String.class,
208                    String.class,
209                    String.class,
210                    String.class,
211                    String.class,
212                    String.class,
213                    SortedMap.class,
214                    List.class,
215                    Optional.class}) {
216
217                @Override
218                public void begin(String namespace, String name, Attributes attributes) throws Exception {
219
220                    super.begin(namespace, name, attributes);
221                    getDigester().peekParams()[12] = new TreeMap();
222                    getDigester().peekParams()[13] = new ArrayList();
223                    getDigester().peekParams()[14] = Optional.empty(); // non-string parameters must be initialized to a non-null value, so we have to use Optional
224                }
225            });
226        digester.addCallParam(siteXpath, 0, A_SERVER);
227        digester.addCallParam(siteXpath, 1, A_URI);
228        digester.addCallParam(siteXpath, 2, A_TITLE);
229        digester.addCallParam(siteXpath, 3, A_POSITION);
230        digester.addCallParam(siteXpath, 4, A_ERROR_PAGE);
231        digester.addCallParam(siteXpath, 5, A_WEBSERVER);
232        digester.addCallParam(siteXpath, 6, A_SSL);
233        digester.addCallParam("*/" + N_SITES + "/" + N_SITE + "/" + N_SECURE, 7, A_SERVER);
234        digester.addCallParam("*/" + N_SITES + "/" + N_SITE + "/" + N_SECURE, 8, A_EXCLUSIVE);
235        digester.addCallParam("*/" + N_SITES + "/" + N_SITE + "/" + N_SECURE, 9, A_ERROR);
236        digester.addCallParam("*/" + N_SITES + "/" + N_SITE + "/" + N_SECURE, 10, A_USE_PERMANENT_REDIRECTS);
237        digester.addCallParam(siteXpath, 11, A_SUBSITE_SELECTION);
238
239        digester.addRule(siteXpath + "/" + N_PARAMETERS, new ObjectCreateRule(TreeMap.class) {
240
241            @Override
242            public void end(String namespace, String name) throws Exception {
243
244                getDigester().peekParams()[12] = getDigester().peek();
245                super.end(namespace, name);
246            }
247        });
248        digester.addCallMethod(siteXpath + "/" + N_PARAMETERS + "/" + N_PARAM, "put", 2);
249        digester.addCallParam(siteXpath + "/" + N_PARAMETERS + "/" + N_PARAM, 0, A_NAME);
250        digester.addCallParam(siteXpath + "/" + N_PARAMETERS + "/" + N_PARAM, 1);
251
252        digester.addRule("*/" + N_SITES + "/" + N_SITE + "/" + N_ALIAS, new Rule() {
253
254            @Override
255            public void begin(String namespace, String name, Attributes attributes) throws Exception {
256
257                String server = attributes.getValue(A_SERVER);
258                String redirect = attributes.getValue(A_REDIRECT);
259                String offset = attributes.getValue(A_OFFSET);
260                CmsSiteMatcher matcher = CmsSiteManagerImpl.createAliasSiteMatcher(server, redirect, offset);
261                Object[] params = getDigester().peekParams();
262                ((ArrayList)params[13]).add(matcher);
263
264            }
265        });
266
267        try {
268            digester.addRule(
269                "*/" + N_SITES + "/" + N_SITE + "/" + CmsAlternativeSiteRootMapping.N_ALTERNATIVE_SITE_ROOT_MAPPING,
270                new NodeCreateRule() {
271
272                    @Override
273                    public void end(String namespace, String name) throws Exception {
274
275                        org.w3c.dom.Element elem = (org.w3c.dom.Element)digester.peek();
276                        String uri = elem.getAttribute(I_CmsXmlConfiguration.A_URI);
277                        String titlePrefix = elem.getAttribute(CmsAlternativeSiteRootMapping.A_TITLE_SUFFIX);
278                        NodeList nodes = elem.getElementsByTagName(CmsAlternativeSiteRootMapping.N_PATH);
279                        List<String> paths = new ArrayList<>();
280                        for (int i = 0; i < nodes.getLength(); i++) {
281                            org.w3c.dom.Element pathElem = (org.w3c.dom.Element)nodes.item(i);
282                            String path = pathElem.getTextContent().trim();
283                            paths.add(path);
284                        }
285                        CmsAlternativeSiteRootMapping mapping = new CmsAlternativeSiteRootMapping(
286                            uri,
287                            paths,
288                            titlePrefix);
289                        getDigester().peekParams()[14] = Optional.of(mapping);
290                        super.end(namespace, name);
291                    }
292
293                });
294        } catch (ParserConfigurationException e) {
295            throw new RuntimeException(e);
296        }
297        digester.addCallMethod("*/" + N_SITES + "/" + N_SHARED_FOLDER, "setSharedFolder", 0);
298
299    }
300
301    /**
302     * @see org.opencms.configuration.I_CmsXmlConfiguration#generateXml(org.dom4j.Element)
303     */
304    public Element generateXml(Element parent) {
305
306        // create <sites> node
307        Element sitesElement = parent.addElement(N_SITES);
308        if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
309            m_siteManager = OpenCms.getSiteManager();
310        }
311        Map<String, CmsSSLMode> workplaceMap = m_siteManager.getWorkplaceServersMap();
312        for (String server : workplaceMap.keySet()) {
313            Element workplaceElement = sitesElement.addElement(N_WORKPLACE_SERVER).addText(server);
314            workplaceElement.addAttribute(A_SSL, workplaceMap.get(server).getXMLValue());
315        }
316        sitesElement.addElement(N_DEFAULT_URI).addText(m_siteManager.getDefaultUri());
317        String sharedFolder = m_siteManager.getSharedFolder();
318        if (sharedFolder != null) {
319            sitesElement.addElement(N_SHARED_FOLDER).addText(sharedFolder);
320        }
321        String oldStyleSecureAllowed = String.valueOf(m_siteManager.isOldStyleSecureServerAllowed());
322        sitesElement.addElement(N_OLD_STYLE_SECURE_SERVER).addText(oldStyleSecureAllowed);
323        if (m_siteManager.isConfigurableWebServer()) {
324            Element configServer = sitesElement.addElement(N_WEBSERVERSCRIPTING);
325            Map<String, String> configServerMap = m_siteManager.getWebServerConfig();
326            configServer.addElement(N_WEBSERVERSCRIPTING_WEBSERVERSCRIPT).addText(
327                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_WEBSERVERSCRIPT));
328            configServer.addElement(N_WEBSERVERSCRIPTING_TARGETPATH).addText(
329                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_TARGETPATH));
330            configServer.addElement(N_WEBSERVERSCRIPTING_CONFIGTEMPLATE).addText(
331                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_CONFIGTEMPLATE));
332            configServer.addElement(N_WEBSERVERSCRIPTING_SECURETEMPLATE).addText(
333                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_SECURETEMPLATE));
334            configServer.addElement(N_WEBSERVERSCRIPTING_FILENAMEPREFIX).addText(
335                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_FILENAMEPREFIX));
336            configServer.addElement(N_WEBSERVERSCRIPTING_LOGGINGDIR).addText(
337                configServerMap.get(CmsSiteManagerImpl.WEB_SERVER_CONFIG_LOGGINGDIR));
338        }
339        Iterator<CmsSite> siteIterator = new HashSet<CmsSite>(m_siteManager.getSites().values()).iterator();
340        while (siteIterator.hasNext()) {
341            CmsSite site = siteIterator.next();
342            // create <site server="" uri=""/> subnode(s)
343            Element siteElement = sitesElement.addElement(N_SITE);
344
345            siteElement.addAttribute(A_SERVER, site.getSiteMatcher().toString());
346            siteElement.addAttribute(A_URI, site.getSiteRoot().concat("/"));
347            siteElement.addAttribute(A_TITLE, site.getTitle());
348            siteElement.addAttribute(A_POSITION, Float.toString(site.getPosition()));
349            siteElement.addAttribute(A_ERROR_PAGE, site.getErrorPage());
350            siteElement.addAttribute(A_WEBSERVER, String.valueOf(site.isWebserver()));
351            siteElement.addAttribute(A_SSL, site.getSSLMode().getXMLValue());
352            siteElement.addAttribute(A_SUBSITE_SELECTION, "" + site.isSubsiteSelectionEnabled());
353
354            // create <secure server=""/> subnode
355            if (site.hasSecureServer()) {
356                Element secureElem = siteElement.addElement(N_SECURE);
357                secureElem.addAttribute(A_SERVER, site.getSecureUrl());
358
359                secureElem.addAttribute(A_EXCLUSIVE, String.valueOf(site.isExclusiveUrl()));
360                secureElem.addAttribute(A_ERROR, String.valueOf(site.isExclusiveError()));
361                if (site.usesPermanentRedirects()) {
362                    secureElem.addAttribute(A_USE_PERMANENT_REDIRECTS, Boolean.TRUE.toString());
363                }
364            }
365
366            if ((site.getParameters() != null) && !site.getParameters().isEmpty()) {
367                Element parametersElem = siteElement.addElement(N_PARAMETERS);
368                for (Map.Entry<String, String> entry : site.getParameters().entrySet()) {
369                    Element paramElem = parametersElem.addElement(N_PARAM);
370                    paramElem.addAttribute(A_NAME, entry.getKey());
371                    paramElem.addText(entry.getValue());
372                }
373            }
374
375            // create <alias server=""/> subnode(s)
376            Iterator<CmsSiteMatcher> aliasIterator = site.getAliases().iterator();
377            while (aliasIterator.hasNext()) {
378                CmsSiteMatcher matcher = aliasIterator.next();
379                Element aliasElement = siteElement.addElement(N_ALIAS);
380                aliasElement.addAttribute(A_SERVER, matcher.getUrl());
381
382                RedirectMode redirectMode = matcher.getRedirectMode();
383
384                String redirectModeStr = null;
385                switch (redirectMode) {
386
387                    case permanent:
388                        redirectModeStr = "permanent";
389                        break;
390                    case temporary:
391
392                        redirectModeStr = "true";
393                        break;
394                    case none:
395                    default:
396                        redirectModeStr = "false";
397                        break;
398                }
399                aliasElement.addAttribute(A_REDIRECT, redirectModeStr);
400
401                if (matcher.getTimeOffset() != 0) {
402                    aliasElement.addAttribute(A_OFFSET, "" + (matcher.getTimeOffset() / 1000));
403                }
404            }
405            java.util.Optional<CmsAlternativeSiteRootMapping> altSiteRoot = site.getAlternativeSiteRootMapping();
406            if (altSiteRoot.isPresent()) {
407                altSiteRoot.get().appendXml(siteElement);
408            }
409        }
410        return sitesElement;
411
412    }
413
414    /**
415     * @see org.opencms.configuration.I_CmsXmlConfiguration#getDtdFilename()
416     */
417    public String getDtdFilename() {
418
419        return CONFIGURATION_DTD_NAME;
420    }
421
422    /**
423     * Returns the site manager.<p>
424     *
425     * @return the site manager
426     */
427    public CmsSiteManagerImpl getSiteManager() {
428
429        return m_siteManager;
430    }
431
432    /**
433     * @see org.opencms.configuration.I_CmsXmlConfigurationWithUpdateHandler#handleUpdate()
434     */
435    public synchronized void handleUpdate() throws Exception {
436
437        CmsLetsEncryptConfiguration config = OpenCms.getLetsEncryptConfig();
438        if ((config != null) && config.isValidAndEnabled() && (config.getTrigger() == Trigger.siteConfig)) {
439
440            // the configuration may be written several times in quick succession. We want to update when this
441            // happens for the last time, not the first, so we use a scheduled task.
442
443            if (m_updateFuture != null) {
444                m_updateFuture.cancel(false);
445                m_updateFuture = null;
446            }
447            m_updateFuture = OpenCms.getExecutor().schedule(new Runnable() {
448
449                @SuppressWarnings("synthetic-access")
450                public void run() {
451
452                    m_updateFuture = null;
453                    CmsLogReport report = new CmsLogReport(
454                        Locale.ENGLISH,
455                        org.opencms.letsencrypt.CmsSiteConfigToLetsEncryptConfigConverter.class);
456                    CmsSiteConfigToLetsEncryptConfigConverter converter = new CmsSiteConfigToLetsEncryptConfigConverter(
457                        config);
458                    converter.run(report, OpenCms.getSiteManager());
459
460                    // TODO Auto-generated method stub
461
462                }
463            }, 5, TimeUnit.SECONDS);
464        }
465
466    }
467
468    /**
469     * @see org.opencms.configuration.I_CmsXmlConfigurationWithUpdateHandler#setCmsObject(org.opencms.file.CmsObject)
470     */
471    public void setCmsObject(CmsObject cms) {
472
473        m_adminCms = cms;
474    }
475
476    /**
477     * Sets the site manager.<p>
478     *
479     * @param siteManager the site manager to set
480     */
481    public void setSiteManager(CmsSiteManagerImpl siteManager) {
482
483        m_siteManager = siteManager;
484        if (CmsLog.INIT.isInfoEnabled()) {
485            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SITE_CONFIG_FINISHED_0));
486        }
487    }
488
489    /**
490     * @see org.opencms.configuration.A_CmsXmlConfiguration#initMembers()
491     */
492    @Override
493    protected void initMembers() {
494
495        setXmlFileName(DEFAULT_XML_FILE_NAME);
496    }
497}