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