diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 3ae0320bf9..dd54cdc101 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -979,6 +979,7 @@ + @@ -990,6 +991,7 @@ + @@ -1001,6 +1003,7 @@ + . + * #L% + */ +package org.alfresco.opencmis; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Set; + +/** + * Wraps an OpenCMIS HttpServletResponse for specific mapping to the Alfresco implementation of OpenCMIS. + * + * @author janv + */ +public class CMISHttpServletResponse implements HttpServletResponse +{ + protected HttpServletResponse httpResp; + + protected Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf + + private final static String HDR_CONTENT_DISPOSITION = "Content-Disposition"; + + public CMISHttpServletResponse(WebScriptResponse res, Set nonAttachContentTypes) + { + httpResp = WebScriptServletRuntime.getHttpServletResponse(res); + this.nonAttachContentTypes = nonAttachContentTypes; + } + + @Override + public void addCookie(Cookie cookie) + { + httpResp.addCookie(cookie); + } + + @Override + public boolean containsHeader(String name) + { + return httpResp.containsHeader(name); + } + + @Override + public String encodeURL(String url) + { + return httpResp.encodeURL(url); + } + + @Override + public String encodeRedirectURL(String url) + { + return httpResp.encodeRedirectURL(url); + } + + @Override + public String encodeUrl(String url) + { + return encodeUrl(url); + } + + @Override + public String encodeRedirectUrl(String url) + { + return httpResp.encodeRedirectUrl(url); + } + + @Override + public void sendError(int sc, String msg) throws IOException + { + httpResp.sendError(sc, msg); + } + + @Override + public void sendError(int sc) throws IOException + { + httpResp.sendError(sc); + } + + @Override + public void sendRedirect(String location) throws IOException + { + httpResp.sendRedirect(location); + } + + @Override + public void setDateHeader(String name, long date) + { + httpResp.setDateHeader(name, date); + } + + @Override + public void addDateHeader(String name, long date) + { + httpResp.addDateHeader(name, date); + } + + @Override + public void setHeader(String name, String value) + { + httpResp.setHeader(name, getStringHeaderValue(name, value, httpResp.getContentType())); + } + + @Override + public void addHeader(String name, String value) + { + httpResp.addHeader(name, getStringHeaderValue(name, value, httpResp.getContentType())); + } + + private String getStringHeaderValue(String name, String value, String contentType) + { + if (HDR_CONTENT_DISPOSITION.equals(name)) + { + if (! nonAttachContentTypes.contains(contentType)) + { + if (value.startsWith("inline")) + { + // force attachment + value = value.replace("inline", "attachment"); + } + else if (! value.startsWith("attachment")) + { + throw new AlfrescoRuntimeException("Unexpected - attachment header could not be set: "+name+" = "+value); + } + } + } + + return value; + } + + @Override + public void setIntHeader(String name, int value) + { + httpResp.setIntHeader(name, value); + } + + @Override + public void addIntHeader(String name, int value) + { + httpResp.addIntHeader(name, value); + } + + @Override + public void setStatus(int sc) + { + httpResp.setStatus(sc); + } + + @Override + public void setStatus(int sc, String sm) + { + httpResp.setStatus(sc, sm); + } + + @Override + public int getStatus() + { + return httpResp.getStatus(); + } + + @Override + public String getHeader(String name) + { + return httpResp.getHeader(name); + } + + @Override + public Collection getHeaders(String name) + { + return httpResp.getHeaders(name); + } + + @Override + public Collection getHeaderNames() + { + return httpResp.getHeaderNames(); + } + + @Override + public String getCharacterEncoding() + { + return httpResp.getCharacterEncoding(); + } + + @Override + public String getContentType() + { + return httpResp.getContentType(); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException + { + return httpResp.getOutputStream(); + } + + @Override + public PrintWriter getWriter() throws IOException + { + return httpResp.getWriter(); + } + + @Override + public void setCharacterEncoding(String charset) + { + httpResp.setCharacterEncoding(charset); + } + + @Override + public void setContentLength(int len) + { + httpResp.setContentLength(len); + } + + @Override + public void setContentType(String type) + { + httpResp.setContentType(type); + } + + @Override + public void setBufferSize(int size) + { + httpResp.setBufferSize(size); + } + + @Override + public int getBufferSize() + { + return httpResp.getBufferSize(); + } + + @Override + public void flushBuffer() throws IOException + { + httpResp.flushBuffer(); + } + + @Override + public void resetBuffer() + { + httpResp.resetBuffer(); + } + + @Override + public boolean isCommitted() + { + return httpResp.isCommitted(); + } + + @Override + public void reset() + { + httpResp.reset(); + } + + @Override + public void setLocale(Locale loc) + { + httpResp.setLocale(loc); + } + + @Override + public Locale getLocale() + { + return httpResp.getLocale(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/opencmis/CMISServletDispatcher.java b/source/java/org/alfresco/opencmis/CMISServletDispatcher.java index 07fd479666..d803a91951 100644 --- a/source/java/org/alfresco/opencmis/CMISServletDispatcher.java +++ b/source/java/org/alfresco/opencmis/CMISServletDispatcher.java @@ -27,13 +27,17 @@ package org.alfresco.opencmis; import java.io.IOException; import java.io.InputStream; +import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -44,10 +48,12 @@ import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.ServletRegistration; import javax.servlet.SessionCookieConfig; import javax.servlet.SessionTrackingMode; import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; @@ -84,6 +90,8 @@ public abstract class CMISServletDispatcher implements CMISDispatcher protected CmisVersion cmisVersion; protected TenantAdminService tenantAdminService; + private Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf + public void setTenantAdminService(TenantAdminService tenantAdminService) { this.tenantAdminService = tenantAdminService; @@ -129,6 +137,11 @@ public abstract class CMISServletDispatcher implements CMISDispatcher this.cmisVersion = CmisVersion.fromValue(cmisVersion); } + public void setNonAttachContentTypes(Set nonAttachWhiteList) + { + this.nonAttachContentTypes = nonAttachWhiteList; + } + protected synchronized Descriptor getCurrentDescriptor() { if(this.currentDescriptor == null) @@ -191,16 +204,22 @@ public abstract class CMISServletDispatcher implements CMISDispatcher return httpReqWrapper; } + protected CMISHttpServletResponse getHttpResponse(WebScriptResponse res) + { + CMISHttpServletResponse httpResWrapper = new CMISHttpServletResponse(res, nonAttachContentTypes); + + return httpResWrapper; + } + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException { try { - HttpServletResponse httpResp = WebScriptServletRuntime.getHttpServletResponse(res); - - // fake a servlet request. + // wrap request & response + CMISHttpServletResponse httpResWrapper = getHttpResponse(res); CMISHttpServletRequest httpReqWrapper = getHttpRequest(req); - servlet.service(httpReqWrapper, httpResp); + servlet.service(httpReqWrapper, httpResWrapper); } catch(ServletException e) { diff --git a/source/test-java/org/alfresco/rest/api/tests/TestCMIS.java b/source/test-java/org/alfresco/rest/api/tests/TestCMIS.java index 8ccfa469c6..91b4f92382 100644 --- a/source/test-java/org/alfresco/rest/api/tests/TestCMIS.java +++ b/source/test-java/org/alfresco/rest/api/tests/TestCMIS.java @@ -2343,7 +2343,100 @@ public class TestCMIS extends EnterpriseTestApi } assertTrue("The aspects should have P:cm:generalclassifiable", mandatoryAspects.contains("P:cm:generalclassifiable")); } - + + @Test + public void testContentDisposition_MNT_17477() throws Exception + { + final TestNetwork network1 = getTestFixture().getRandomNetwork(); + + String username = "user" + System.currentTimeMillis(); + PersonInfo personInfo = new PersonInfo(username, username, username, TEST_PASSWORD, null, null, null, null, null, null, null); + TestPerson person1 = network1.createUser(personInfo); + String person1Id = person1.getId(); + + publicApiClient.setRequestContext(new RequestContext(network1.getId(), person1Id)); + CmisSession cmisSession = publicApiClient.createPublicApiCMISSession(Binding.browser, CMIS_VERSION_11, AlfrescoObjectFactoryImpl.class.getName()); + + Folder folder = (Folder)cmisSession.getObjectByPath("/Shared"); + + // + // Upload test JPG document + // + String name = GUID.generate() + ".jpg"; + + Map properties = new HashMap<>(); + { + properties.put(PropertyIds.OBJECT_TYPE_ID, TYPE_CMIS_DOCUMENT); + properties.put(PropertyIds.NAME, name); + } + + ContentStreamImpl fileContent = new ContentStreamImpl(); + { + fileContent.setMimeType(MimetypeMap.MIMETYPE_IMAGE_JPEG); + fileContent.setStream(this.getClass().getResourceAsStream("/test.jpg")); + } + + Document doc = folder.createDocument(properties, fileContent, VersioningState.MAJOR); + String docId = doc.getId(); + + // note: Content-Disposition can be "inline or "attachment" for content types that are white-listed (eg. specific image types & pdf) + + HttpResponse response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/browser/root/Shared/"+name, null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("inline")); + assertEquals(200, response.getStatusCode()); + + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/browser/root/Shared/"+name+"?download=inline", null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("inline")); + assertEquals(200, response.getStatusCode()); + + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/browser/root/Shared/"+name+"?download=attachment", null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("attachment")); + assertEquals(200, response.getStatusCode()); + + // note: AtomPub binding (via OpenCMIS) does not support "download" query parameter + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/atom/content?id="+docId, null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("attachment")); + assertEquals(200, response.getStatusCode()); + + // + // Create test HTML document + // + name = GUID.generate() + ".html"; + + properties = new HashMap<>(); + { + properties.put(PropertyIds.OBJECT_TYPE_ID, TYPE_CMIS_DOCUMENT); + properties.put(PropertyIds.NAME, name); + } + + fileContent = new ContentStreamImpl(); + { + ContentWriter writer = new FileContentWriter(TempFileProvider.createTempFile(GUID.generate(), ".html")); + writer.putContent("Hello world"); + ContentReader reader = writer.getReader(); + fileContent.setMimeType(MimetypeMap.MIMETYPE_HTML); + fileContent.setStream(reader.getContentInputStream()); + } + + doc = folder.createDocument(properties, fileContent, VersioningState.MAJOR); + docId = doc.getId(); + + // note: Content-Disposition will always be "attachment" for content types that are not white-listed + + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/browser/root/Shared/"+name, null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("attachment;")); + assertEquals(200, response.getStatusCode()); + + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/browser/root/Shared/"+name+"?download=inline", null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("attachment;")); + assertEquals(200, response.getStatusCode()); + + // note: AtomPub binding (via OpenCMIS) does not support "download" query parameter + response = publicApiClient.get(network1.getId()+"/public/cmis/versions/1.1/atom/content?id="+docId, null); + assertTrue(response.getHeaders().get("Content-Disposition").startsWith("attachment;")); + assertEquals(200, response.getStatusCode()); + } + /** * Test delete version on versions other than latest (most recent) version (MNT-17228) */