001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.servlet;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.net.URL;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.time.ZonedDateTime;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Enumeration;
031import java.util.Map;
032import java.util.Properties;
033import java.util.UUID;
034import java.util.regex.Pattern;
035
036import javax.servlet.ServletConfig;
037import javax.servlet.ServletContext;
038import javax.servlet.ServletException;
039import javax.servlet.ServletOutputStream;
040import javax.servlet.http.HttpServlet;
041import javax.servlet.http.HttpServletRequest;
042import javax.servlet.http.HttpServletResponse;
043import javax.servlet.http.HttpSession;
044import javax.xml.XMLConstants;
045import javax.xml.parsers.SAXParserFactory;
046import javax.xml.transform.OutputKeys;
047import javax.xml.transform.TransformerFactory;
048import javax.xml.transform.sax.SAXTransformerFactory;
049import javax.xml.transform.sax.TransformerHandler;
050import javax.xml.transform.stream.StreamResult;
051import javax.xml.transform.stream.StreamSource;
052import javax.xml.validation.Schema;
053import javax.xml.validation.SchemaFactory;
054
055import org.apache.avalon.excalibur.logger.LoggerManager;
056import org.apache.avalon.framework.configuration.Configuration;
057import org.apache.avalon.framework.configuration.ConfigurationException;
058import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
059import org.apache.avalon.framework.container.ContainerUtil;
060import org.apache.avalon.framework.context.DefaultContext;
061import org.apache.cocoon.Cocoon;
062import org.apache.cocoon.ConnectionResetException;
063import org.apache.cocoon.Constants;
064import org.apache.cocoon.ResourceNotFoundException;
065import org.apache.cocoon.environment.http.HttpContext;
066import org.apache.cocoon.environment.http.HttpEnvironment;
067import org.apache.cocoon.servlet.multipart.MultipartHttpServletRequest;
068import org.apache.cocoon.util.ClassUtils;
069import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
070import org.apache.cocoon.util.log.SLF4JLoggerManager;
071import org.apache.cocoon.xml.XMLUtils;
072import org.apache.commons.collections.EnumerationUtils;
073import org.apache.commons.io.FileUtils;
074import org.apache.commons.io.IOUtils;
075import org.apache.commons.lang3.StringUtils;
076import org.apache.commons.lang3.exception.ExceptionUtils;
077import org.apache.commons.lang3.time.StopWatch;
078import org.apache.log4j.Appender;
079import org.apache.log4j.LogManager;
080import org.apache.log4j.xml.DOMConfigurator;
081import org.apache.xml.serializer.OutputPropertiesFactory;
082import org.slf4j.Logger;
083import org.slf4j.LoggerFactory;
084import org.slf4j.MDC;
085import org.xml.sax.SAXException;
086import org.xml.sax.XMLReader;
087
088import org.ametys.core.ObservationConstants;
089import org.ametys.core.authentication.AuthenticateAction;
090import org.ametys.core.migration.MigrationEngine;
091import org.ametys.core.observation.Event;
092import org.ametys.core.observation.ObservationManager;
093import org.ametys.core.user.CurrentUserProvider;
094import org.ametys.core.user.UserIdentity;
095import org.ametys.core.util.DateUtils;
096import org.ametys.runtime.config.Config;
097import org.ametys.runtime.config.ConfigManager;
098import org.ametys.runtime.data.AmetysHomeLock;
099import org.ametys.runtime.data.AmetysHomeLockException;
100import org.ametys.runtime.log.MemoryAppender;
101import org.ametys.runtime.plugin.Init;
102import org.ametys.runtime.plugin.InitExtensionPoint;
103import org.ametys.runtime.plugin.PluginsManager;
104import org.ametys.runtime.plugin.PluginsManager.Status;
105import org.ametys.runtime.plugin.component.PluginsComponentManager;
106import org.ametys.runtime.plugins.admin.jvmstatus.ActiveSessionListener;
107import org.ametys.runtime.request.RequestFactory;
108import org.ametys.runtime.request.RequestListener;
109import org.ametys.runtime.request.RequestListenerManager;
110import org.ametys.runtime.util.AmetysHomeHelper;
111
112/**
113 * Main entry point for applications.<br>
114 * Overrides the CocoonServlet to add some initialization.<br>
115 */
116public class RuntimeServlet extends HttpServlet
117{
118    /** Constant for storing the {@link ServletConfig} in the Avalon context  */
119    public static final String CONTEXT_SERVLET_CONFIG = "servlet-config";
120    
121    /** Constant for storing the servlet context URL in the Avalon context  */
122    public static final String CONTEXT_CONTEXT_ROOT = "context-root";
123    
124    /** The cocoon.xconf URL */
125    public static final String COCOON_CONF_URL = "/org/ametys/runtime/cocoon/cocoon.xconf";
126    
127    /** Default max upload size (10 Mb) */
128    public static final int DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
129
130    /** The maintenance file name */
131    public static final String MAINTENANCE_FILENAME = "maintenance.xml";
132    
133    /** The config file name */
134    public static final String CONFIG_FILE_NAME = "config.xml";
135    
136    /** Name of the servlet initialization parameter for the ametys home property */
137    public static final String AMETYS_HOME_PROPERTY = "ametys.home.property";
138    
139    /** The default ametys home path (relative to the servlet context)  */
140    public static final String AMETYS_HOME_DEFAULT = "/WEB-INF/data";
141    
142    /** The optional location (relative to application context) of the file that configures the list of plugins/workspaces/kernel to get somewhere else */
143    public static final String EXTERNAL_LOCATIONS = "WEB-INF/param/external-locations.xml";
144
145    /** File name in the administrator folder of the ametys home */
146    public static final String INSTANCE_FILENAME = "instance-info.xml";
147    
148    /** The run modes */
149    public enum RunMode
150    {
151        /** Application is currently starting */
152        STARTING,
153        /** Application is currently migrating */
154        MIGRATING,
155        /** Running init methods */
156        INITIALIZING,
157        /** Normal execution mode */
158        NORMAL,
159        /** Maintenance mode (See #getMaintenanceStatus) */
160        MAINTENANCE,
161        /** Safe mode (See PluginsManager#getStatus) */
162        SAFEMODE,
163        /** Fatal error during startup */
164        FATALERROR,
165        /** Application is currently stopping */
166        STOPPING
167    }
168    
169    /** The maintenance status of Ametys */
170    public enum MaintenanceStatus
171    {
172        /** No maintenance activated */
173        NONE,
174        /** The maintenance was automatically switched on at startup due to a failed migration */
175        AUTOMATIC,
176        /** The maintenance was manually switched on */
177        FORCED
178    }
179    /** Informations about the maintenance status when forced 
180     * @param comment The comment to explain why the maintenance was started
181     * @param initiator The user that started the maintenance
182     * @param since The date the maintenance was stated */
183    public record ForcedMainteanceInformations(String comment, UserIdentity initiator, ZonedDateTime since) { /* empty */ }
184    
185    private static RunMode _runMode = RunMode.STARTING;
186    private static MaintenanceStatus _maintenanceStatus;
187    private static ForcedMainteanceInformations _forcedMainteanceInformations;
188    private static Logger _logger;
189    private static ServletContext _servletContext;
190    
191    private String _servletContextPath;
192    private URL _servletContextURL;
193    private File _ametysHome;
194    
195    private DefaultContext _avalonContext;
196    private HttpContext _context;
197    private Cocoon _cocoon;
198    private RequestFactory _requestFactory;
199    private File _workDir;
200    
201    private int _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE;
202    private File _uploadDir;
203    private File _cacheDir;
204    
205    private AmetysHomeLock _ametysHomeLock;
206    
207    private LoggerManager _loggerManager;
208    
209    private Throwable _exception;
210    
211    private Collection<Pattern> _allowedURLPattern = Arrays.asList(Pattern.compile("_admin($|/.*)"), Pattern.compile("plugins/[^/]+/resources/.*"));
212    
213    @Override
214    public void init() throws ServletException 
215    {
216        try
217        {
218            // Set this property in order to avoid a System.err.println (CatalogManager.java)
219            if (System.getProperty("xml.catalog.ignoreMissing") == null)
220            {
221                System.setProperty("xml.catalog.ignoreMissing", "true");
222            }
223            
224            _servletContext = getServletContext();
225            _servletContext.addListener(ActiveSessionListener.class);
226            _servletContextPath = _servletContext.getRealPath("/");
227            
228            _avalonContext = new DefaultContext();
229            _context = new HttpContext(_servletContext);
230            _avalonContext.put(Constants.CONTEXT_ENVIRONMENT_CONTEXT, _context);
231            _avalonContext.put(Constants.CONTEXT_DEFAULT_ENCODING, "UTF-8");
232            _avalonContext.put(CONTEXT_SERVLET_CONFIG, getServletConfig());
233            
234            _servletContextURL = new File(_servletContextPath).toURI().toURL();
235            _avalonContext.put(CONTEXT_CONTEXT_ROOT, _servletContextURL);
236        
237            URL configFile = getClass().getResource(COCOON_CONF_URL);
238            _avalonContext.put(Constants.CONTEXT_CONFIG_URL, configFile);
239            
240            _workDir = new File((File) _servletContext.getAttribute("javax.servlet.context.tempdir"), "cocoon-files");
241            _workDir.mkdirs();
242            _avalonContext.put(Constants.CONTEXT_WORK_DIR, _workDir);
243            
244            _cacheDir = new File(_workDir, "cache-dir");
245            _cacheDir.mkdirs();
246            _avalonContext.put(Constants.CONTEXT_CACHE_DIR, _cacheDir);
247    
248            // Create temp dir if it does not exist
249            File tmpDir = new File(System.getProperty("java.io.tmpdir"));
250            if (!tmpDir.exists())
251            {
252                FileUtils.forceMkdir(tmpDir);
253            }
254            
255            // Init the Ametys home directory and lock before setting the logger.
256            // The log folder is located in the Ametys home directory.
257            _initAmetysHome();
258            
259            // Configuration file
260            File config = FileUtils.getFile(_ametysHome, AmetysHomeHelper.AMETYS_HOME_CONFIG_DIR, CONFIG_FILE_NAME);
261            final String fileName = config.getCanonicalPath();
262            Config.setFilename(fileName);
263            
264            // Init logger
265            _initLogger();
266            
267            _initAmetys();
268        }
269        catch (Throwable t)
270        {
271            setRunMode(RunMode.FATALERROR);
272            
273            if (_logger != null)
274            {
275                _logger.error("Error while loading Ametys. Entering in error mode.", t);
276            }
277            else
278            {
279                System.out.println("Error while loading Ametys. Entering in error mode.");
280                t.printStackTrace();
281            }
282
283            _exception = t;
284            
285            _disposeCocoon();
286        }
287    }
288    
289    private void _initAmetysHome() throws AmetysHomeLockException
290    {
291        String ametysHomePath = null;
292        
293        boolean hasAmetysHomeProp = EnumerationUtils.toList(getInitParameterNames()).contains(AMETYS_HOME_PROPERTY);
294        if (!hasAmetysHomeProp)
295        {
296            System.out.println(String.format("[INFO] The '%s' servlet initialization parameter was not found. The Ametys home directory default value will be used '%s'",
297                    AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT));
298        }
299        else
300        {
301            String ametysHomeEnv = getInitParameter(AMETYS_HOME_PROPERTY);
302            if (StringUtils.isEmpty(ametysHomeEnv))
303            {
304                System.out.println(String.format("[WARN] The '%s' servlet initialization parameter appears to be empty. Ametys home directory default value will be used '%s'",
305                        AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT));
306            }
307            else
308            {
309                ametysHomePath = System.getenv(ametysHomeEnv);
310                if (StringUtils.isEmpty(ametysHomePath))
311                {
312                    System.out.println(String.format(
313                            "[WARN] The '%s' environment variable was not found or was empty. Ametys home directory default value will be used '%s'",
314                            ametysHomeEnv, AMETYS_HOME_DEFAULT));
315                }
316            }
317        }
318        
319        if (StringUtils.isEmpty(ametysHomePath))
320        {
321            ametysHomePath = _servletContext.getRealPath(AMETYS_HOME_DEFAULT);
322        }
323        
324        System.out.println("Acquiring lock on " + ametysHomePath);
325        
326        // Acquire the lock on Ametys home
327        _ametysHome = new File(ametysHomePath);
328        _ametysHome.mkdirs();
329        
330        _ametysHomeLock = new AmetysHomeLock(_ametysHome);
331        _ametysHomeLock.acquire();
332    }
333
334    /**
335     * Get the instance identifier
336     * @return The UUID of the Ametys instance or null if the id could not be generated at startup
337     * @throws IllegalStateException If the file cannot be read
338     */
339    public static String getInstanceId()
340    {
341        Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME);
342
343        if (Files.exists(statisticsFile))
344        {
345            try (InputStream is = Files.newInputStream(statisticsFile))
346            {
347                Configuration configuration = new DefaultConfigurationBuilder().build(is);
348                return configuration.getChild("unique-id").getValue();
349            }
350            catch (Exception e)
351            {
352                throw new IllegalStateException("Cannot read the ametys-home://" + INSTANCE_FILENAME + " file. Consider remove it and restart Ametys to recreate it", e);
353            }
354        }
355        
356        throw new IllegalStateException("The file ametys-home://" + INSTANCE_FILENAME + " does not exist while it should have created at startup");
357    }
358
359    private static void _createInstanceId()
360    {
361        try
362        {
363            Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME);
364    
365            if (Files.exists(statisticsFile))
366            {
367                return;
368            }
369            
370            String newId = UUID.randomUUID().toString();
371            
372            try (OutputStream os = Files.newOutputStream(statisticsFile))
373            {
374                // create a transformer for saving sax into a file
375                TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
376                
377                StreamResult result = new StreamResult(os);
378                th.setResult(result);
379    
380                // create the format of result
381                Properties format = new Properties();
382                format.put(OutputKeys.METHOD, "xml");
383                format.put(OutputKeys.INDENT, "yes");
384                format.put(OutputKeys.ENCODING, "UTF-8");
385                format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
386                th.getTransformer().setOutputProperties(format);
387    
388                th.startDocument();
389                XMLUtils.startElement(th, "instance");
390                XMLUtils.createElement(th, "unique-id", newId);
391                XMLUtils.endElement(th, "instance");
392                th.endDocument();
393            }
394        }
395        catch (Exception e)
396        {
397            _logger.error("Cannot create the instance id", e);
398        }
399    }
400
401    private void _initAmetys() throws Exception
402    {
403        setRunMode(RunMode.STARTING);
404        _maintenanceStatus = null;
405        _exception = null;
406
407        // WEB-INF/param/runtime.xml loading
408        _loadRuntimeConfig();
409
410        _createCocoon();
411        
412        // Upload initialization
413        _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE;
414        _uploadDir = new File(RuntimeConfig.getInstance().getAmetysHome(), "uploads");
415        
416        if (ConfigManager.getInstance().isComplete())
417        {
418            Long maxUploadSizeParam = Config.getInstance().getValue("runtime.upload.max-size");
419            if (maxUploadSizeParam != null)
420            {
421                // if the feature core/runtime.upload is deactivated, use the default value (10 Mb)
422                _maxUploadSize = maxUploadSizeParam.intValue();
423            }
424        }
425
426        _uploadDir.mkdirs();
427        _avalonContext.put(Constants.CONTEXT_UPLOAD_DIR, _uploadDir);
428        
429        _requestFactory = new RequestFactory(true, _uploadDir, false, true, _maxUploadSize, "UTF-8");
430        
431        // Generate the instance-info file
432        _createInstanceId();
433
434        PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager");
435        if (doMigrationAndInit(pluginCM))
436        {
437            restartCocoon(null);
438        }
439        else
440        {
441            if (PluginsManager.getInstance().isSafeMode())
442            {
443                setRunMode(RunMode.SAFEMODE);
444            }
445            else if (getMaintenanceStatus() != MaintenanceStatus.NONE)
446            {
447                setRunMode(RunMode.MAINTENANCE);
448            }
449            else
450            {
451                setRunMode(RunMode.NORMAL);
452            }
453        }
454    }
455    
456    private void _initLogger() 
457    {
458        // Configure Log4j
459        String logj4fFile = _servletContext.getRealPath("/WEB-INF/log4j.xml");
460        
461        // Hack to have context-relative log files, because of lack in configuration capabilities in log4j.
462        // If there are more than one Ametys in the same JVM, the property will be successively set for each instance, 
463        // so we heavily rely on DOMConfigurator beeing synchronous.
464        System.setProperty("ametys.home.dir", _ametysHome.getAbsolutePath());
465        DOMConfigurator.configure(logj4fFile);
466        System.clearProperty("ametys.home.dir");
467        
468        Appender appender = new MemoryAppender(); 
469        appender.setName(org.ametys.plugins.core.ui.log.LogManager.MEMORY_APPENDER_NAME);
470        LogManager.getRootLogger().addAppender(appender); 
471        Enumeration<org.apache.log4j.Logger> categories = LogManager.getCurrentLoggers(); 
472        while (categories.hasMoreElements()) 
473        { 
474            org.apache.log4j.Logger logger = categories.nextElement(); 
475            logger.addAppender(appender); 
476        } 
477        
478        _loggerManager = new SLF4JLoggerManager();
479        _logger = LoggerFactory.getLogger(getClass());
480    }
481    
482    private void _createCocoon() throws Exception
483    {
484        _avalonContext.put(Constants.CONTEXT_CLASS_LOADER, getClass().getClassLoader());
485        _avalonContext.put(Constants.CONTEXT_CLASSPATH, "");
486        
487        URL configFile = (URL) _avalonContext.get(Constants.CONTEXT_CONFIG_URL);
488        
489        _logger.info("Reloading from: {}", configFile.toExternalForm());
490        
491        Cocoon c = (Cocoon) ClassUtils.newInstance("org.apache.cocoon.Cocoon");
492        ContainerUtil.enableLogging(c, new SLF4JLoggerAdapter(_logger));
493        c.setLoggerManager(_loggerManager);
494        ContainerUtil.contextualize(c, _avalonContext);
495        ContainerUtil.initialize(c);
496
497        _cocoon = c;
498    }
499
500    /**
501     * Init migration and plugins
502     * @param pluginCM Plugins Component Manager
503     * @return true if a server restart is required
504     * @throws Exception Something went wrong
505     */
506    public static boolean doMigrationAndInit(PluginsComponentManager pluginCM) throws Exception
507    {
508        if (_initMigration(pluginCM))
509        {
510            return true;
511        }
512
513        _initPlugins(pluginCM);
514        
515        return false;
516    }
517    
518    private static void _initPlugins(PluginsComponentManager pluginCM) throws Exception
519    {
520        setRunMode(RunMode.INITIALIZING);
521
522        // If we're in safe mode 
523        if (!PluginsManager.getInstance().isSafeMode())
524        {
525            // Plugins Init class execution
526            InitExtensionPoint initExtensionPoint = (InitExtensionPoint) pluginCM.lookup(InitExtensionPoint.ROLE);
527            for (String id : initExtensionPoint.getExtensionsIds())
528            {
529                Init init = initExtensionPoint.getExtension(id);
530                init.init();
531            }
532            
533            // Application Init class execution if available
534            if (pluginCM.hasComponent(Init.ROLE))
535            {
536                Init init = (Init) pluginCM.lookup(Init.ROLE);
537                init.init();
538            }
539        }
540    }
541    
542    private static boolean _initMigration(PluginsComponentManager pluginCM) throws Exception
543    {
544        setRunMode(RunMode.MIGRATING);
545
546        MigrationEngine migrationEngine = (MigrationEngine) pluginCM.lookup(MigrationEngine.ROLE);
547        return migrationEngine.migrate();
548    }
549    
550    private void _loadRuntimeConfig() throws ServletException
551    {
552        Configuration runtimeConf = null;
553        try
554        {
555            // XML Schema validation
556            SAXParserFactory factory = SAXParserFactory.newInstance();
557            factory.setNamespaceAware(true);
558            
559            URL schemaURL = getClass().getResource("runtime-4.1.xsd");
560            Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL);
561            factory.setSchema(schema);
562            
563            XMLReader reader = factory.newSAXParser().getXMLReader();
564            DefaultConfigurationBuilder runtimeConfBuilder = new DefaultConfigurationBuilder(reader);
565            
566            File runtimeConfigFile = new File(_servletContextPath, "WEB-INF/param/runtime.xml");
567            try (InputStream runtime = new FileInputStream(runtimeConfigFile))
568            {
569                runtimeConf = runtimeConfBuilder.build(runtime, runtimeConfigFile.getAbsolutePath());
570            }
571        }
572        catch (Exception e)
573        {
574            _logger.error("Unable to load runtime file at 'WEB-INF/param/runtime.xml'. PluginsManager will enter in safe mode.", e);
575        }
576        
577        Configuration externalConf = null;
578        try
579        {
580            DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder();
581
582            File externalConfigFile = new File(_servletContextPath, EXTERNAL_LOCATIONS);
583            if (externalConfigFile.exists())
584            {
585                try (InputStream external = new FileInputStream(externalConfigFile))
586                {
587                    externalConf = externalConfBuilder.build(external, externalConfigFile.getAbsolutePath());
588                }
589            }
590        }
591        catch (Exception e)
592        {
593            _logger.error("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e);
594            throw new ServletException("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e);
595        }
596        
597        RuntimeConfig.configure(runtimeConf, externalConf, _ametysHome, _servletContextPath);
598    }
599
600    @Override
601    public void destroy() 
602    {
603        setRunMode(RunMode.STOPPING);
604        
605        if (_cocoon != null) 
606        {
607            _logger.debug("Servlet destroyed - disposing Cocoon");
608            _disposeCocoon();
609        }
610        
611        if (_ametysHomeLock != null)
612        {
613            _ametysHomeLock.release();
614            _ametysHomeLock = null;
615        }
616        
617        _avalonContext = null;
618        _logger = null;
619        _loggerManager = null;
620    }
621
622    
623    private final void _disposeCocoon() 
624    {
625        if (_cocoon != null) 
626        {
627            ContainerUtil.dispose(_cocoon);
628            _cocoon = null;
629        }
630    }
631    
632    private String _getRestartMessage()
633    {
634        if (_cocoon == null)
635        {
636            return "Ametys is restarting";
637        }
638        switch (getRunMode())
639        {
640            case RunMode.STARTING: return "Ametys is restarting";
641            case RunMode.MIGRATING: return "Ametys is migrating data";
642            case RunMode.INITIALIZING: return "Ametys is initializing";
643            default: return null;
644        }
645    }
646
647    @Override
648    public final void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
649    {
650        req.setCharacterEncoding("UTF-8");
651        
652        // Error mode
653        if (_exception != null)
654        {
655            _renderSpecialPage(req, res, _exception, "An error occured during Ametys initialization.", true);
656            return;
657        }
658        
659        // Restarting
660        String msg = _getRestartMessage();
661        if (msg != null)
662        {
663            _renderSpecialPage(req, res, null, msg, false);
664            return;
665        }
666        
667        String uri = _retrieveUri(req);
668        
669        if (PluginsManager.getInstance().isSafeMode())
670        {
671            // safe mode
672            String finalUri = uri;
673            boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches());
674            if (!allowed) 
675            {
676                res.addHeader("X-Ametys-SafeMode", "true");
677
678                Status status = PluginsManager.getInstance().getStatus();
679                
680                if (status == Status.NO_CONFIG)
681                {
682                    res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html");
683                    return;
684                }
685                else if (status == Status.CONFIG_INCOMPLETE)
686                {
687                    res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html");
688                    return;
689                }
690                else
691                {
692                    res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html");
693                    return;
694                }
695            }
696        }
697
698        UserIdentity authenticatedUser = null;
699        HttpSession session = req.getSession(false); 
700        if (session != null) 
701        { 
702            authenticatedUser = (UserIdentity) session.getAttribute(AuthenticateAction.SESSION_USERIDENTITY); 
703        }
704        
705        MDC.remove("user"); 
706                
707        if (authenticatedUser != null) 
708        { 
709            MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser));
710        }
711        
712        MDC.put("requestURI", req.getRequestURI());
713
714        StopWatch stopWatch = new StopWatch();
715        HttpServletRequest request = null;
716        try 
717        {
718            // used for timing the processing
719            stopWatch.start();
720
721            _fireRequestStarted(req);
722            
723            // get the request (wrapped if contains multipart-form data)
724            request = _requestFactory.getServletRequest(req);
725            
726            // Process the request
727            HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8");
728            env.enableLogging(new SLF4JLoggerAdapter(_logger));
729            
730            if (!_cocoon.process(env)) 
731            {
732                // We reach this when there is nothing in the processing change that matches
733                // the request. For example, no matcher matches.
734                _logger.error("The Cocoon engine failed to process the request.");
735                _renderSpecialPage(request, res, null, "Cocoon engine failed to process the request", true);
736            }
737        } 
738        catch (ResourceNotFoundException | ConnectionResetException | IOException e) 
739        {
740            _logger.warn(e.toString());
741            _renderSpecialPage(request, res, e, e.getMessage(), true);
742        }
743        catch (Throwable e)
744        {
745            _logger.error("Internal Cocoon Problem", e);
746            _renderSpecialPage(request, res, e, "Internal Cocoon Problem", true);
747        }
748        finally 
749        {
750            stopWatch.stop();
751            _logger.info("'{}' processed in {} ms.", uri, stopWatch.getDuration());
752            
753            try
754            {
755                if (request instanceof MultipartHttpServletRequest) 
756                {
757                    _logger.debug("Deleting uploaded file(s).");
758                    ((MultipartHttpServletRequest) request).cleanup();
759                }
760            } 
761            catch (IOException e)
762            {
763                _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e);
764            }
765        }
766
767        _fireRequestEnded(req);
768
769        if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload")))
770        {
771            restartCocoon(req);
772        }
773    }
774    
775    /**
776     * Restart cocoon
777     * @param req The http servlet request, used to read safeMode and normalMode
778     *            request attribute if possible. If null, safe mode will be
779     *            forced only if it was already forced.
780     */
781    public void restartCocoon(HttpServletRequest req)
782    {
783        try
784        {
785            ConfigManager.getInstance().dispose();
786            _disposeCocoon(); 
787            _servletContext.removeAttribute("PluginsComponentManager");
788            
789            // By default force safe mode if it was already forced
790            boolean wasForcedSafeMode = PluginsManager.getInstance().getStatus() == PluginsManager.Status.SAFE_MODE_FORCED;
791            boolean forceSafeMode = wasForcedSafeMode;
792            if (req != null)
793            {
794                // Also, checks if there is some specific request attributes
795                // Force safe mode if is explicitly requested
796                // Or force safe mode it was already forced unless normal mode is explicitly requested 
797                forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode"));
798                if (!forceSafeMode)
799                {
800                    forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode"));
801                }
802            }
803            
804            _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode);
805            
806            _initAmetys();
807        }
808        catch (Exception e)
809        {
810            setRunMode(RunMode.FATALERROR);
811
812            _logger.error("Error while reloading Ametys. Entering in error mode.", e);
813            _exception = e;
814        }
815    }
816    
817    private String _retrieveUri(HttpServletRequest req)
818    {
819        String uri = req.getServletPath();
820        
821        String pathInfo = req.getPathInfo();
822        if (pathInfo != null) 
823        {
824            uri += pathInfo;
825        }
826        
827        if (uri.length() > 0 && uri.charAt(0) == '/') 
828        {
829            uri = uri.substring(1);
830        }
831        return uri;
832    }
833    
834    @SuppressWarnings("unchecked")
835    private void _fireRequestStarted(HttpServletRequest req)
836    {
837        Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS);
838
839        if (listeners == null)
840        {
841            return;
842        }
843
844        for (RequestListener listener : listeners)
845        {
846            listener.requestStarted(req);
847        }
848    }
849
850    @SuppressWarnings("unchecked")
851    private void _fireRequestEnded(HttpServletRequest req)
852    {
853        Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS);
854
855        if (listeners == null)
856        {
857            return;
858        }
859
860        for (RequestListener listener : listeners)
861        {
862            listener.requestEnded(req);
863        }
864    }
865    
866    /**
867     * Set the maintenance status
868     * @param maintenanceStatus The new maintenance status. Cannot be null.
869     * @param forcedMainteanceInformations The informations if the mode is FORCED. Can be null.
870     */
871    public static void setMaintenanceStatus(MaintenanceStatus maintenanceStatus, ForcedMainteanceInformations forcedMainteanceInformations)
872    {
873        if (_maintenanceStatus != maintenanceStatus)
874        {
875            LoggerFactory.getLogger(Runtime.class).debug("Switching maintenance status from " + _maintenanceStatus + " to " + maintenanceStatus);
876        }
877
878        ForcedMainteanceInformations fixedForcedMainteanceInformations = null;
879        if (forcedMainteanceInformations != null)
880        {
881            fixedForcedMainteanceInformations = new ForcedMainteanceInformations(StringUtils.defaultString(forcedMainteanceInformations.comment).replaceAll("\r?\n", "<br/>"), forcedMainteanceInformations.initiator, forcedMainteanceInformations.since);
882        }
883        
884        try
885        {
886            File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME);
887            if (maintenanceStatus != MaintenanceStatus.FORCED)
888            {
889                if (maintenanceFile.exists())
890                {
891                    FileUtils.delete(maintenanceFile);
892                }
893            }
894            else 
895            {
896                // create the result where to write
897                maintenanceFile.getParentFile().mkdirs();
898                
899                try (OutputStream os = new FileOutputStream(maintenanceFile))
900                {
901                    // create a transformer for saving sax into a file
902                    TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
903                    
904                    StreamResult result = new StreamResult(os);
905                    th.setResult(result);
906    
907                    // create the format of result
908                    Properties format = new Properties();
909                    format.put(OutputKeys.METHOD, "xml");
910                    format.put(OutputKeys.INDENT, "yes");
911                    format.put(OutputKeys.ENCODING, "UTF-8");
912                    format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
913                    th.getTransformer().setOutputProperties(format);
914    
915                    th.startDocument();
916                    XMLUtils.startElement(th, "maintenance");
917                    if (fixedForcedMainteanceInformations != null)
918                    {
919                        XMLUtils.createElement(th, "comment", StringUtils.defaultString(fixedForcedMainteanceInformations.comment));
920                        XMLUtils.createElement(th, "initiator", StringUtils.defaultString(UserIdentity.userIdentityToString(fixedForcedMainteanceInformations.initiator)));
921                        if (fixedForcedMainteanceInformations.since != null)
922                        {
923                            XMLUtils.createElement(th, "since", DateUtils.zonedDateTimeToString(fixedForcedMainteanceInformations.since));
924                        }
925                    }
926                    XMLUtils.endElement(th, "maintenance");
927                    th.endDocument();
928                }
929            }
930        }
931        catch (Exception e)
932        {
933            throw new RuntimeException("Cannot change maintenance status", e);
934        }
935
936        _maintenanceStatus = maintenanceStatus;
937        _forcedMainteanceInformations = fixedForcedMainteanceInformations;
938        
939        try
940        {
941            PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager");
942            ObservationManager observationManager = (ObservationManager) pluginCM.lookup(ObservationManager.ROLE);
943            CurrentUserProvider currentUserProvider = (CurrentUserProvider) pluginCM.lookup(CurrentUserProvider.ROLE);
944            if (observationManager != null)
945            {
946                observationManager.notify(new Event(ObservationConstants.EVENT_RUNTIME_MAINTENANCE, currentUserProvider.getUser(), Map.of()));
947            }
948        }
949        catch (Exception e)
950        {
951            // Safe mode
952        }
953    }
954    
955    /**
956     * Get the current maintenance status
957     * @return The maintenance status
958     */
959    public static MaintenanceStatus getMaintenanceStatus()
960    {
961        if (_maintenanceStatus == null)
962        {
963            File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME);
964            
965            if (!maintenanceFile.exists())
966            {
967                _maintenanceStatus = MaintenanceStatus.NONE;
968            }
969            else
970            {
971                _maintenanceStatus = MaintenanceStatus.FORCED;
972                try
973                {
974                    Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(maintenanceFile);
975                    _forcedMainteanceInformations = new ForcedMainteanceInformations(configuration.getChild("comment").getValue(""), 
976                                                                                    UserIdentity.stringToUserIdentity(configuration.getChild("initiator").getValue(null)),
977                                                                                    DateUtils.parseZonedDateTime(configuration.getChild("since").getValue(null)));
978                }
979                catch (ConfigurationException | SAXException | IOException e)
980                {
981                    _logger.error("Cannot read the maintenance status file", e);
982                    _forcedMainteanceInformations = null;
983                }
984            }
985        }
986        return _maintenanceStatus; 
987    }
988    
989    /**
990     * When maintenance status is forced, this comment may explains why
991     * @return The comment. Can be null even in forced mode.
992     */
993    public static ForcedMainteanceInformations getMaintenanceStatusForcedInformations()
994    {
995        return _forcedMainteanceInformations;
996    }
997
998    /**
999     * Set the run mode
1000     * @param mode the running mode
1001     */
1002    public static void setRunMode(RunMode mode)
1003    {
1004        if (_runMode != mode)
1005        {
1006            LoggerFactory.getLogger(RuntimeServlet.class).debug("Switching run mode from " + _runMode + " to " + mode);
1007        }
1008        
1009        _runMode = mode;
1010    }
1011
1012    /**
1013     * Get the run mode
1014     * @return the current run mode
1015     */
1016    public static RunMode getRunMode()
1017    {
1018        return _runMode;
1019    }
1020
1021    private void _renderSpecialPage(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message, boolean error) throws ServletException
1022    {
1023        ServletConfig config = getServletConfig();
1024
1025        if (config == null)
1026        {
1027            throw new ServletException("Cannot access to ServletConfig");
1028        }
1029
1030        try
1031        {
1032            ServletOutputStream os = res.getOutputStream();
1033            String path = req.getRequestURI().substring(req.getContextPath().length());
1034    
1035            // Favicon associated with the error page.
1036            if (path.equals("/favicon.ico"))
1037            {
1038                try (InputStream is = getClass().getResourceAsStream("favicon.ico"))
1039                {
1040                    res.setStatus(200);
1041                    res.setContentType(config.getServletContext().getMimeType("favicon.ico"));
1042                    
1043                    IOUtils.copy(is, os);
1044                    
1045                    return;
1046                }
1047            }
1048    
1049            res.setStatus(error ? 500 : 200);
1050            res.setContentType("text/html; charset=UTF-8");
1051    
1052            SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
1053            TransformerHandler th;
1054            
1055            try (InputStream is = getClass().getResourceAsStream("fatal.xsl"))
1056            {
1057                StreamSource errorSource = new StreamSource(is);
1058                th = saxFactory.newTransformerHandler(errorSource);
1059            }
1060            
1061            Properties format = new Properties();
1062            format.put(OutputKeys.METHOD, "xml");
1063            format.put(OutputKeys.ENCODING, "UTF-8");
1064            format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat");
1065    
1066            th.getTransformer().setOutputProperties(format);
1067    
1068            th.getTransformer().setParameter("code", error ? 500 : 200);
1069            th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/"));
1070            th.getTransformer().setParameter("contextPath", req.getContextPath());
1071            
1072            StreamResult result = new StreamResult(os);
1073            th.setResult(result);
1074    
1075            th.startDocument();
1076    
1077            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report");
1078            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message");
1079            XMLUtils.data(th, message);
1080            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message");
1081            
1082            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace");
1083            if (throwable != null)
1084            {
1085                XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable));
1086            }
1087            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace");
1088            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report");
1089    
1090            th.endDocument();
1091        }
1092        catch (Exception e)
1093        {
1094            // Nothing we can do anymore ...
1095            throw new ServletException(e);
1096        }
1097    }
1098
1099    /*
1100     * private void _runMaintenanceMode(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { ServletConfig config = getServletConfig(); if (config == null) { throw new ServletException("Cannot access to ServletConfig"); } ServletOutputStream os = res.getOutputStream(); if (req.getRequestURI().startsWith("/WEB-INF/error/")) { // Service des fichiers statiques de la page d'erreur File f = new File(config.getServletContext().getRealPath(req.getRequestURI())); if (f.exists()) { res.setStatus(200); InputStream is = new FileInputStream(f); SourceUtil.copy(is, os); is.close(); } else { res.setStatus(404); } return; } res.setStatus(500); SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); InputStream is; File errorXSL = new File(config.getServletContext().getRealPath("/WEB-INF/error/error.xsl")); if (errorXSL.exists()) { is = new FileInputStream(errorXSL); } else { is = getClass().getResourceAsStream("/org/ametys/runtime/kernel/pages/error/error.xsl"); } try { StreamSource errorSource = new StreamSource(is); Templates templates = saxFactory.newTemplates(errorSource); TransformerHandler th = saxFactory.newTransformerHandler(templates); is.close(); StreamResult result = new StreamResult(os); th.setResult(result); th.startDocument(); th.startElement("", "error", "error", new AttributesImpl()); saxMaintenanceMessage(th); th.endElement("", "error", "error"); th.endDocument(); } catch (Exception ex) { throw new ServletException("Unable to send maintenance page", ex); } }
1101     */
1102
1103    /**
1104     * In maintenance mode, send error information as SAX events.<br>
1105     * 
1106     * @param ch the contentHandler receiving the message
1107     * @throws SAXException if an error occured while send SAX events
1108     */
1109    /*
1110     * protected void saxMaintenanceMessage(ContentHandler ch) throws SAXException { String maintenanceMessage = "The application is under maintenance. Please retry later."; ch.characters(maintenanceMessage.toCharArray(), 0, maintenanceMessage.length()); } private boolean _accept(HttpServletRequest req) { // FIX ME à ne pas mettre en dur return req.getRequestURI().startsWith("_admin"); }
1111     */
1112}