-
Notifications
You must be signed in to change notification settings - Fork 160
Derive media content types from file content, not the request #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
|
|
||
| import org.apache.commons.logging.Log; | ||
| import org.apache.commons.logging.LogFactory; | ||
| import org.apache.roller.weblogger.util.MediaTypePolicy; | ||
| import org.apache.roller.weblogger.WebloggerException; | ||
| import org.apache.roller.weblogger.business.MediaFileManager; | ||
| import org.apache.roller.weblogger.business.WebloggerFactory; | ||
|
|
@@ -125,7 +126,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) | |
| } | ||
|
|
||
| // if not from theme then see if resource is in weblog's upload dir | ||
| boolean fromUploadedMedia = false; | ||
| if (resourceStream == null) { | ||
| fromUploadedMedia = true; | ||
| try { | ||
| MediaFileManager mmgr = WebloggerFactory.getWeblogger() | ||
| .getMediaFileManager(); | ||
|
|
@@ -159,8 +162,19 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) | |
| } | ||
|
|
||
| // set the content type based on whatever is in our web.xml mime defs | ||
| response.setContentType(this.context.getMimeType(resourceRequest | ||
| .getResourcePath())); | ||
| String resourceType = this.context.getMimeType( | ||
| resourceRequest.getResourcePath()); | ||
| if (fromUploadedMedia) { | ||
| // Uploaded through the media library, so it is governed by the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch is also where customized-theme resources land: |
||
| // same policy as any other media response. | ||
| MediaTypePolicy.applyResponseHeaders(response, resourceType, | ||
| resourceRequest.getResourcePath()); | ||
| } else { | ||
| // A theme resource: authored as part of the theme and served as | ||
| // the type the theme intends, but never re-typed by the browser. | ||
| response.setHeader("X-Content-Type-Options", "nosniff"); | ||
| response.setContentType(resourceType); | ||
| } | ||
|
|
||
| try { | ||
| // ok, lets serve up the file | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| import org.apache.commons.lang3.StringUtils; | ||
| import org.apache.commons.logging.Log; | ||
| import org.apache.commons.logging.LogFactory; | ||
| import org.apache.roller.weblogger.util.MediaTypePolicy; | ||
| import org.apache.roller.weblogger.WebloggerException; | ||
| import org.apache.roller.weblogger.business.FileIOException; | ||
| import org.apache.roller.weblogger.business.MediaFileManager; | ||
|
|
@@ -124,7 +125,10 @@ public String save() { | |
|
|
||
| if (uploadedFile != null) { | ||
| mediaFile.setLength(this.uploadedFile.length()); | ||
| mediaFile.setContentType(this.uploadedFileContentType); | ||
| // Replacing the body re-decides the type, on the same | ||
| // terms as the original upload. | ||
| mediaFile.setContentType(MediaTypePolicy.storedTypeFor( | ||
| mediaFile.getName(), this.uploadedFileContentType)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On replace this derives the type from the record's (old) name rather than the uploaded replacement's name ( |
||
| manager.updateMediaFile(getActionWeblog(), mediaFile, | ||
| new FileInputStream(this.uploadedFile)); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. The ASF licenses this file to You | ||
| * under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||
| * implied. See the License for the specific language governing | ||
| * permissions and limitations under the License. For additional | ||
| * information regarding copyright in this work, please see the NOTICE | ||
| * file in the top level directory of this distribution. | ||
| */ | ||
|
|
||
| package org.apache.roller.weblogger.util; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashSet; | ||
| import java.util.Locale; | ||
| import java.util.Set; | ||
|
|
||
| import javax.servlet.http.HttpServletResponse; | ||
|
|
||
| /** | ||
| * Decides what type an uploaded file is stored as, and how it is served back. | ||
| * | ||
| * <p>A client uploading a file states a type, but the stored type is derived | ||
| * from the file name. The declared value is a hint only, consulted where the | ||
| * name yields nothing, and it cannot introduce a type the browser would | ||
| * execute. | ||
| * | ||
| * <p>Serving applies the second half. Only a short list of formats that | ||
| * browsers render passively are sent inline; everything else is sent as an | ||
| * attachment, and {@code nosniff} accompanies every response so browsers do | ||
| * not substitute their own type guess. | ||
| */ | ||
| public final class MediaTypePolicy { | ||
|
|
||
| private MediaTypePolicy() { | ||
| } | ||
|
|
||
| public static final String DEFAULT_TYPE = "application/octet-stream"; | ||
|
|
||
| /** | ||
| * Formats browsers render without executing anything the file carries. | ||
| * SVG is deliberately absent: it is an XML document that can carry script. | ||
| */ | ||
| private static final Set<String> INLINE_TYPES = Collections.unmodifiableSet( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth listing what this drops compared to |
||
| new HashSet<>(Arrays.asList( | ||
| "image/jpeg", "image/pjpeg", "image/png", "image/gif", | ||
| "image/bmp", "image/x-ms-bmp", "image/webp", "image/tiff", | ||
| "image/x-icon", "image/vnd.microsoft.icon", | ||
| "application/pdf"))); | ||
|
|
||
| /** Families served inline whatever the subtype. */ | ||
| private static final String[] INLINE_PREFIXES = {"audio/", "video/"}; | ||
|
|
||
| /** | ||
| * Types a browser may execute, or that can carry something it will. These | ||
| * are never adopted from a client's declaration. | ||
| */ | ||
| private static final Set<String> ACTIVE_TYPES = Collections.unmodifiableSet( | ||
| new HashSet<>(Arrays.asList( | ||
| "text/html", "application/xhtml+xml", "application/xhtml", | ||
| "image/svg+xml", "text/xml", "application/xml", | ||
| "text/javascript", "application/javascript", | ||
| "application/ecmascript", "text/ecmascript", | ||
| "text/vbscript", "application/x-shockwave-flash", | ||
| "text/xsl", "application/xslt+xml"))); | ||
|
|
||
| /** | ||
| * @param fileName the uploaded file's name | ||
| * @param declaredType the type the client said it was, may be null | ||
| * @return the type to store: derived from the name where that is | ||
| * conclusive, otherwise the declared type if it is not one a | ||
| * browser would act on, otherwise the generic binary type | ||
| */ | ||
| public static String storedTypeFor(String fileName, String declaredType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because active declared types collapse to |
||
| String derived = normalize(deriveFromName(fileName)); | ||
| if (isConclusive(derived)) { | ||
| return derived; | ||
| } | ||
|
|
||
| String declared = normalize(declaredType); | ||
| if (isConclusive(declared) && !isActive(declared)) { | ||
| return declared; | ||
| } | ||
|
|
||
| return DEFAULT_TYPE; | ||
| } | ||
|
|
||
| /** @return true when browsers render this type without executing it */ | ||
| public static boolean isInlineSafe(String contentType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| String type = normalize(contentType); | ||
| if (type == null) { | ||
| return false; | ||
| } | ||
| if (INLINE_TYPES.contains(type)) { | ||
| return true; | ||
| } | ||
| for (String prefix : INLINE_PREFIXES) { | ||
| if (type.startsWith(prefix)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** @return true when a browser may execute this type, or script inside it */ | ||
| public static boolean isActive(String contentType) { | ||
| String type = normalize(contentType); | ||
| if (type == null) { | ||
| return false; | ||
| } | ||
| return ACTIVE_TYPES.contains(type) || type.endsWith("+xml"); | ||
| } | ||
|
|
||
| /** | ||
| * Sets the type and the headers that govern how the response is treated. | ||
| * Anything outside the inline list is marked as an attachment. | ||
| */ | ||
| public static void applyResponseHeaders(HttpServletResponse response, | ||
| String contentType, String fileName) { | ||
| response.setHeader("X-Content-Type-Options", "nosniff"); | ||
|
|
||
| String type = normalize(contentType); | ||
| if (type == null) { | ||
| type = DEFAULT_TYPE; | ||
| } | ||
|
|
||
| if (isInlineSafe(type)) { | ||
| response.setContentType(type); | ||
| return; | ||
| } | ||
|
|
||
| // Served as bytes to be saved rather than a document to be rendered. | ||
| response.setContentType(DEFAULT_TYPE); | ||
| response.setHeader("Content-Disposition", | ||
| "attachment; filename=\"" + headerSafe(fileName) + "\""); | ||
| } | ||
|
|
||
| private static String deriveFromName(String fileName) { | ||
| if (fileName == null || fileName.trim().isEmpty()) { | ||
| return null; | ||
| } | ||
| try { | ||
| return Utilities.getContentTypeFromFileName(fileName); | ||
| } catch (Exception undetermined) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** @return the bare type in lower case, without parameters such as charset */ | ||
| private static String normalize(String contentType) { | ||
| if (contentType == null) { | ||
| return null; | ||
| } | ||
| String type = contentType.trim(); | ||
| int semicolon = type.indexOf(';'); | ||
| if (semicolon > -1) { | ||
| type = type.substring(0, semicolon).trim(); | ||
| } | ||
| return type.isEmpty() ? null : type.toLowerCase(Locale.ENGLISH); | ||
| } | ||
|
|
||
| private static boolean isConclusive(String type) { | ||
| return type != null && !DEFAULT_TYPE.equals(type); | ||
| } | ||
|
|
||
| /** | ||
| * @return the name with the characters that would end the quoted string or | ||
| * start another header removed, since it is placed in one | ||
| */ | ||
| private static String headerSafe(String fileName) { | ||
| if (fileName == null || fileName.trim().isEmpty()) { | ||
| return "download"; | ||
| } | ||
| String safe = fileName.replaceAll("[\\r\\n\"\\\\]", ""); | ||
| return safe.trim().isEmpty() ? "download" : safe; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| import org.apache.commons.logging.Log; | ||
| import org.apache.commons.logging.LogFactory; | ||
| import org.apache.roller.util.RollerConstants; | ||
| import org.apache.roller.weblogger.util.MediaTypePolicy; | ||
| import org.apache.roller.weblogger.business.MediaFileManager; | ||
| import org.apache.roller.weblogger.business.URLStrategy; | ||
| import org.apache.roller.weblogger.business.WeblogEntryManager; | ||
|
|
@@ -381,7 +382,7 @@ public Object newMediaObject(String blogid, String userid, String password, | |
| mf.setDirectory(root); | ||
| mf.setWeblog(website); | ||
| mf.setName(name); | ||
| mf.setContentType(type); | ||
| mf.setContentType(MediaTypePolicy.storedTypeFor(name, type)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously |
||
| mf.setInputStream(new ByteArrayInputStream(bits)); | ||
| mf.setLength(bits.length); | ||
| String fileLink = mf.getPermalink(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context.getMimeType()returnsnullfor extensions not mapped inweb.xml/the container, andapplyResponseHeadersturnsnullinto an octet-stream attachment. On mastersetContentType(null)left the type unset and the browser could still display the file; now any unmapped (or uppercase, on a case-sensitive container) extension downloads in the theme preview.