• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 10961332694

20 Sep 2024 02:41PM UTC coverage: 63.741% (-0.02%) from 63.759%
10961332694

push

github

ibacher
TRUNK-6261: Set the appropriate MIME-type for files served by initialization filter (#4717)

0 of 10 new or added lines in 1 file covered. (0.0%)

5 existing lines in 2 files now uncovered.

21691 of 34030 relevant lines covered (63.74%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

5.19
/web/src/main/java/org/openmrs/web/filter/StartupFilter.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.web.filter;
11

12
import java.io.File;
13
import java.io.FileInputStream;
14
import java.io.FileNotFoundException;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.io.InputStreamReader;
18
import java.lang.annotation.Annotation;
19
import java.lang.reflect.Field;
20
import java.nio.charset.StandardCharsets;
21
import java.nio.file.Files;
22
import java.nio.file.Path;
23
import java.nio.file.Paths;
24
import java.util.Collections;
25
import java.util.HashMap;
26
import java.util.List;
27
import java.util.Locale;
28
import java.util.Map;
29
import java.util.Properties;
30

31
import javax.servlet.Filter;
32
import javax.servlet.FilterChain;
33
import javax.servlet.FilterConfig;
34
import javax.servlet.ServletContext;
35
import javax.servlet.ServletException;
36
import javax.servlet.ServletRequest;
37
import javax.servlet.ServletResponse;
38
import javax.servlet.http.HttpServletRequest;
39
import javax.servlet.http.HttpServletResponse;
40

41
import org.apache.commons.lang3.ArrayUtils;
42
import org.apache.velocity.VelocityContext;
43
import org.apache.velocity.app.VelocityEngine;
44
import org.apache.velocity.runtime.RuntimeConstants;
45
import org.apache.velocity.runtime.log.CommonsLogLogChute;
46
import org.apache.velocity.tools.Scope;
47
import org.apache.velocity.tools.ToolContext;
48
import org.apache.velocity.tools.ToolManager;
49
import org.apache.velocity.tools.config.DefaultKey;
50
import org.apache.velocity.tools.config.FactoryConfiguration;
51
import org.apache.velocity.tools.config.ToolConfiguration;
52
import org.apache.velocity.tools.config.ToolboxConfiguration;
53
import org.codehaus.jackson.map.ObjectMapper;
54
import org.openmrs.OpenmrsCharacterEscapes;
55
import org.openmrs.api.APIException;
56
import org.openmrs.api.context.Context;
57
import org.openmrs.util.*;
58
import org.openmrs.web.WebConstants;
59
import org.openmrs.web.filter.initialization.InitializationFilter;
60
import org.openmrs.web.filter.update.UpdateFilter;
61
import org.openmrs.web.filter.util.FilterUtil;
62
import org.openmrs.web.filter.util.LocalizationTool;
63
import org.slf4j.Logger;
64
import org.slf4j.LoggerFactory;
65
import org.springframework.http.MediaType;
66

67
/**
68
 * Abstract class used when a small wizard is needed before Spring, jsp, etc has been started up.
69
 *
70
 * @see UpdateFilter
71
 * @see InitializationFilter
72
 */
73
public abstract class StartupFilter implements Filter {
1✔
74
        
75
        private static final Logger log = LoggerFactory.getLogger(StartupFilter.class);
1✔
76
        
77
        protected static VelocityEngine velocityEngine = null;
1✔
78
        
79
        public static final String AUTO_RUN_OPENMRS = "auto_run_openmrs";
80
        
81
        /**
82
         * Set by the {@link #init(FilterConfig)} method so that we have access to the current
83
         * {@link ServletContext}
84
         */
85
        protected FilterConfig filterConfig = null;
1✔
86
        
87
        /**
88
         * Records errors that will be displayed to the user
89
         */
90
        protected Map<String, Object[]> errors = new HashMap<>();
1✔
91
        
92
        /**
93
         * Messages that will be displayed to the user
94
         */
95
        protected Map<String, Object[]> msgs = new HashMap<>();
1✔
96
        
97
        /**
98
         * Used for configuring tools within velocity toolbox
99
         */
100
        private ToolContext toolContext = null;
1✔
101
        
102
        /**
103
         * The web.xml file sets this {@link StartupFilter} to be the first filter for all requests.
104
         *
105
         * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
106
         *      javax.servlet.FilterChain)
107
         */
108
        @Override
109
        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
110
                throws IOException, ServletException {
111
                if (skipFilter((HttpServletRequest) request)) {
×
112
                        chain.doFilter(request, response);
×
113
                } else {
114
                        
115
                        HttpServletRequest httpRequest = (HttpServletRequest) request;
×
116
                        HttpServletResponse httpResponse = (HttpServletResponse) response;
×
117
                        
118
                        String servletPath = httpRequest.getServletPath();
×
119
                        // for all /images and /initfilter/scripts files, write the path
120
                        // (the "/initfilter" part is needed so that the openmrs_static_context-servlet.xml file doesn't
121
                        //  get instantiated early, before the locale messages are all set up)
122
                        if (servletPath.startsWith("/images") || servletPath.startsWith("/initfilter/scripts")) {
×
123
                                // strip out the /initfilter part
124
                                servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view");
×
125
                                // writes the actual file path to the response
126
                                Path filePath = Paths.get(filterConfig.getServletContext().getRealPath(servletPath)).normalize();
×
127
                                Path fullFilePath = filePath;
×
128
                                
129
                                if (httpRequest.getPathInfo() != null) {
×
130
                                        fullFilePath = fullFilePath.resolve(httpRequest.getPathInfo());
×
131
                                        if (!(fullFilePath.normalize().startsWith(filePath))) {
×
132
                                                log.warn("Detected attempted directory traversal in request for {}", httpRequest.getPathInfo());
×
133
                                                return;
×
134
                                        }
135
                                }
136
                                
NEW
137
                                String contentType = httpRequest.getServletContext().getMimeType(fullFilePath.toString());
×
NEW
138
                                if (contentType == null || contentType.isEmpty()) {
×
139
                                        try {
NEW
140
                                                contentType = Files.probeContentType(fullFilePath);
×
NEW
141
                                        } catch (IOException ignored) {}
×
142
                                }
143

144
                                MediaType mediaType;
NEW
145
                                if (contentType != null && !contentType.isEmpty()) {
×
NEW
146
                                        mediaType = MediaType.parseMediaType(contentType);
×
147
                                } else {
NEW
148
                                        mediaType = MediaType.APPLICATION_OCTET_STREAM;
×
149
                                }
150
                                
NEW
151
                                response.setContentType(mediaType.toString());
×
152
                                
NEW
153
                                try (InputStream fis = new FileInputStream(fullFilePath.normalize().toFile())) {
×
NEW
154
                                        OpenmrsUtil.copyFile(fis, httpResponse.getOutputStream());
×
155
                                }
156
                                catch (FileNotFoundException e) {
×
157
                                        log.error("Unable to find file: {}", filePath, e);
×
158
                                }
159
                                catch (IOException e) {
×
160
                                        log.warn("An error occurred while handling file {}", filePath, e);
×
161
                                }
×
162
                        } else if (servletPath.startsWith("/scripts")) {
×
163
                                log.error(
×
164
                                    "Calling /scripts during the initializationfilter pages will cause the openmrs_static_context-servlet.xml to initialize too early and cause errors after startup.  Use '/initfilter"
165
                                            + servletPath + "' instead.");
166
                        }
167
                        // for anything but /initialsetup
168
                        else if (!httpRequest.getServletPath().equals("/" + WebConstants.SETUP_PAGE_URL)
×
169
                                && !httpRequest.getServletPath().equals("/" + AUTO_RUN_OPENMRS)) {
×
170
                                // send the user to the setup page
171
                                httpResponse.sendRedirect("/" + WebConstants.WEBAPP_NAME + "/" + WebConstants.SETUP_PAGE_URL);
×
172
                        } else {
173
                                
174
                                if ("GET".equals(httpRequest.getMethod())) {
×
175
                                        doGet(httpRequest, httpResponse);
×
176
                                } else if ("POST".equals(httpRequest.getMethod())) {
×
177
                                        // only clear errors before POSTS so that redirects can show errors too.
178
                                        errors.clear();
×
179
                                        msgs.clear();
×
180
                                        doPost(httpRequest, httpResponse);
×
181
                                }
182
                        }
183
                        // Don't continue down the filter chain otherwise Spring complains
184
                        // that it hasn't been set up yet.
185
                        // The jsp and servlet filter are also on this chain, so writing to
186
                        // the response directly here is the only option
187
                }
188
        }
×
189
        
190
        /**
191
         * Convenience method to set up the velocity context properly
192
         */
193
        private void initializeVelocity() {
194
                if (velocityEngine == null) {
×
195
                        velocityEngine = new VelocityEngine();
×
196
                        
197
                        Properties props = new Properties();
×
198
                        props.setProperty(RuntimeConstants.RUNTIME_LOG, "startup_wizard_vel.log");
×
199
                        // Linux requires setting logging properties to initialize Velocity Context.
200
                        props.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
×
201
                            "org.apache.velocity.runtime.log.CommonsLogLogChute");
202
                        props.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME, "initial_wizard_velocity");
×
203
                        
204
                        // so the vm pages can import the header/footer
205
                        props.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
×
206
                        props.setProperty("class.resource.loader.description", "Velocity Classpath Resource Loader");
×
207
                        props.setProperty("class.resource.loader.class",
×
208
                            "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
209
                        
210
                        try {
211
                                velocityEngine.init(props);
×
212
                        }
213
                        catch (Exception e) {
×
214
                                log.error("velocity init failed, because: {}", e, e);
×
215
                        }
×
216
                }
217
        }
×
218
        
219
        /**
220
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests
221
         *
222
         * @param httpRequest
223
         * @param httpResponse
224
         */
225
        protected abstract void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
226
                throws IOException, ServletException;
227
        
228
        /**
229
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests
230
         *
231
         * @param httpRequest
232
         * @param httpResponse
233
         */
234
        protected abstract void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
235
                throws IOException, ServletException;
236
        
237
        /**
238
         * All private attributes on this class are returned to the template via the velocity context and
239
         * reflection
240
         *
241
         * @param templateName the name of the velocity file to render. This name is prepended with
242
         *            {@link #getTemplatePrefix()}
243
         * @param referenceMap
244
         * @param httpResponse
245
         */
246
        protected void renderTemplate(String templateName, Map<String, Object> referenceMap, HttpServletResponse httpResponse)
247
                throws IOException {
248
                // first we should get velocity tools context for current client request (within
249
                // his http session) and merge that tools context with basic velocity context
250
                if (referenceMap == null) {
×
251
                        return;
×
252
                }
253
                
254
                Object locale = referenceMap.get(FilterUtil.LOCALE_ATTRIBUTE);
×
255
                ToolContext velocityToolContext = getToolContext(
×
256
                    locale != null ? locale.toString() : Context.getLocale().toString());
×
257
                VelocityContext velocityContext = new VelocityContext(velocityToolContext);
×
258
                
259
                for (Map.Entry<String, Object> entry : referenceMap.entrySet()) {
×
260
                        velocityContext.put(entry.getKey(), entry.getValue());
×
261
                }
×
262
                
263
                Object model = getUpdateFilterModel();
×
264
                
265
                // put each of the private varibles into the template for convenience
266
                for (Field field : model.getClass().getDeclaredFields()) {
×
267
                        try {
268
                                field.setAccessible(true);
×
269
                                velocityContext.put(field.getName(), field.get(model));
×
270
                        }
271
                        catch (IllegalArgumentException | IllegalAccessException e) {
×
272
                                log.error("Error generated while getting field value: " + field.getName(), e);
×
273
                        }
×
274
                }
275
                
276
                String fullTemplatePath = getTemplatePrefix() + templateName;
×
277
                InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream(fullTemplatePath);
×
278
                if (templateInputStream == null) {
×
279
                        throw new IOException("Unable to find " + fullTemplatePath);
×
280
                }
281
                
282
                velocityContext.put("errors", errors);
×
283
                velocityContext.put("msgs", msgs);
×
284
                
285
                // explicitly set the content type for the response because some servlet containers are assuming text/plain
286
                httpResponse.setContentType("text/html");
×
287
                
288
                try {
289
                        velocityEngine.evaluate(velocityContext, httpResponse.getWriter(), this.getClass().getName(),
×
290
                            new InputStreamReader(templateInputStream, StandardCharsets.UTF_8));
291
                }
292
                catch (Exception e) {
×
293
                        throw new APIException("Unable to process template: " + fullTemplatePath, e);
×
294
                }
×
295
        }
×
296
        
297
        /**
298
         * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
299
         */
300
        @Override
301
        public void init(FilterConfig filterConfig) throws ServletException {
302
                this.filterConfig = filterConfig;
×
303
                initializeVelocity();
×
304
        }
×
305
        
306
        /**
307
         * @see javax.servlet.Filter#destroy()
308
         */
309
        @Override
310
        public void destroy() {
311
        }
×
312
        
313
        /**
314
         * This string is prepended to all templateNames passed to
315
         * {@link #renderTemplate(String, Map, HttpServletResponse)}
316
         *
317
         * @return string to prepend as the path for the templates
318
         */
319
        protected String getTemplatePrefix() {
320
                return "org/openmrs/web/filter/";
×
321
        }
322
        
323
        /**
324
         * The model that is used as the backer for all pages in this startup wizard. Should never return
325
         * null.
326
         *
327
         * @return the stored formbacking/model object
328
         */
329
        protected abstract Object getUpdateFilterModel();
330
        
331
        /**
332
         * If this returns true, this filter fails early and quickly. All logic is skipped and startup and
333
         * usage continue normally.
334
         *
335
         * @return true if this filter can be skipped
336
         */
337
        public abstract boolean skipFilter(HttpServletRequest request);
338

339
        /**
340
         * Convenience method to read the last 5 log lines from the MemoryAppender
341
         * 
342
         * The log lines will be added to the "logLines" key
343
         * 
344
         * @param result A map to be returned as a JSON document
345
         */
346
        protected void addLogLinesToResponse(Map<String, Object> result) {
347
                MemoryAppender appender = OpenmrsUtil.getMemoryAppender();
×
348
                if (appender != null) {
×
349
                        List<String> logLines = appender.getLogLines();
×
350
                        
351
                        // truncate the list to the last five so we don't overwhelm jquery
352
                        if (logLines.size() > 5) {
×
353
                                logLines = logLines.subList(logLines.size() - 5, logLines.size());
×
354
                        }
355
                        
356
                        result.put("logLines", logLines);
×
357
                } else {
×
358
                        result.put("logLines", Collections.emptyList());
×
359
                }
360
        }
×
361
        
362
        /**
363
         * Convenience method to convert the given object to a JSON string. Supports Maps, Lists, Strings,
364
         * Boolean, Double
365
         *
366
         * @param object object to convert to json
367
         * @return JSON string to be eval'd in javascript
368
         */
369
        protected String toJSONString(Object object) {
370
                ObjectMapper mapper = new ObjectMapper();
×
371
                mapper.getJsonFactory().setCharacterEscapes(new OpenmrsCharacterEscapes());
×
372
                try {
373
                        return mapper.writeValueAsString(object);
×
374
                }
375
                catch (IOException e) {
×
376
                        log.error("Failed to convert object to JSON");
×
377
                        throw new APIException(e);
×
378
                }
379
        }
380
        
381
        /**
382
         * Gets tool context for specified locale parameter. If context does not exists, it creates new
383
         * context, configured for that locale. Otherwise, it changes locale property of
384
         * {@link LocalizationTool} object, that is being contained in tools context
385
         *
386
         * @param locale the string with locale parameter for configuring tools context
387
         * @return the tool context object
388
         */
389
        public ToolContext getToolContext(String locale) {
390
                Locale systemLocale = LocaleUtility.fromSpecification(locale);
×
391
                //Defaults to en if systemLocale is null or invalid e.g en_GBs
392
                if (systemLocale == null || !ArrayUtils.contains(Locale.getAvailableLocales(), systemLocale)) {
×
393
                        systemLocale = Locale.ENGLISH;
×
394
                }
395
                // If tool context has not been configured yet
396
                if (toolContext == null) {
×
397
                        // first we are creating manager for tools, factory for configuring tools 
398
                        // and empty configuration object for velocity tool box
399
                        ToolManager velocityToolManager = new ToolManager();
×
400
                        FactoryConfiguration factoryConfig = new FactoryConfiguration();
×
401
                        // since we are using one tool box for all request within wizard
402
                        // we should propagate toolbox's scope on all application 
403
                        ToolboxConfiguration toolbox = new ToolboxConfiguration();
×
404
                        toolbox.setScope(Scope.APPLICATION);
×
405
                        // next we are directly configuring custom localization tool by
406
                        // setting its class name, locale property etc.
407
                        ToolConfiguration localizationTool = new ToolConfiguration();
×
408
                        localizationTool.setClassname(LocalizationTool.class.getName());
×
409
                        localizationTool.setProperty(ToolContext.LOCALE_KEY, systemLocale);
×
410
                        localizationTool.setProperty(LocalizationTool.BUNDLES_KEY, "messages");
×
411
                        // and finally we are adding just configured tool into toolbox
412
                        // and creating tool context for this toolbox
413
                        toolbox.addTool(localizationTool);
×
414
                        factoryConfig.addToolbox(toolbox);
×
415
                        velocityToolManager.configure(factoryConfig);
×
416
                        toolContext = velocityToolManager.createContext();
×
417
                        toolContext.setUserCanOverwriteTools(true);
×
418
                } else {
×
419
                        // if it already has been configured, we just pull out our custom localization tool 
420
                        // from tool context, then changing its locale property and putting this tool back to the context
421
                        // First, we need to obtain the value of default key annotation of our localization tool
422
                        // class using reflection
423
                        Annotation annotation = LocalizationTool.class.getAnnotation(DefaultKey.class);
×
424
                        DefaultKey defaultKeyAnnotation = (DefaultKey) annotation;
×
425
                        String key = defaultKeyAnnotation.value();
×
426
                        //
427
                        LocalizationTool localizationTool = (LocalizationTool) toolContext.get(key);
×
428
                        localizationTool.setLocale(systemLocale);
×
429
                        toolContext.put(key, localizationTool);
×
430
                }
431
                return toolContext;
×
432
        }
433
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc