/*
* (C) Copyright 2015-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
* Estelle Giuly <egiuly@nuxeo.com>
*/
package org.nuxeo.ecm.core.io.download;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.impl.UserPrincipal;
import org.nuxeo.ecm.core.api.impl.blob.FileBlob;
import org.nuxeo.ecm.core.api.local.ClientLoginModule;
import org.nuxeo.ecm.core.api.local.LoginStack;
import org.nuxeo.ecm.core.io.download.DownloadServiceImpl.Action;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.LocalDeploy;
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
@Deploy({ "org.nuxeo.ecm.core.io", "org.nuxeo.ecm.core.cache" })
public class TestDownloadService {
@Inject
protected DownloadService downloadService;
@Test
public void testBasicDownload() throws Exception {
// blob to download
String blobValue = "Hello World";
Blob blob = Blobs.createBlob(blobValue);
blob.setFilename("myFile.txt");
blob.setDigest("12345");
// prepare mocks
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getMethod()).thenReturn("GET");
HttpServletResponse response = mock(HttpServletResponse.class);
ServletOutputStream sos = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
out.write(b);
}
};
PrintWriter printWriter = new PrintWriter(sos);
when(response.getOutputStream()).thenReturn(sos);
when(response.getWriter()).thenReturn(printWriter);
// send download request
downloadService.downloadBlob(request, response, null, null, blob, null, null);
// check that the blob gets returned
assertEquals(blobValue, out.toString());
}
@Test
public void testETagHeaderNone() throws Exception {
doTestETagHeader(null);
}
@Test
public void testETagHeaderNotMatched() throws Exception {
doTestETagHeader(FALSE);
}
@Test
public void testETagHeaderMatched() throws Exception {
doTestETagHeader(TRUE);
}
protected void doTestETagHeader(Boolean match) throws Exception {
// Given a blob
String blobValue = "Hello World";
Blob blob = Blobs.createBlob(blobValue);
blob.setFilename("myFile.txt");
blob.setDigest("12345");
String digestToTest;
if (match == null) {
digestToTest = null;
} else if (TRUE.equals(match)) {
digestToTest = "12345";
} else {
digestToTest = "78787";
}
// When I send a request a given digest
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeader("If-None-Match")).thenReturn('"' + digestToTest + '"');
when(req.getMethod()).thenReturn("GET");
HttpServletResponse resp = mock(HttpServletResponse.class);
ServletOutputStream sos = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
out.write(b);
}
};
@SuppressWarnings("resource")
PrintWriter printWriter = new PrintWriter(sos);
when(resp.getOutputStream()).thenReturn(sos);
when(resp.getWriter()).thenReturn(printWriter);
downloadService.downloadBlob(req, resp, null, null, blob, null, null);
verify(req, atLeast(1)).getHeader("If-None-Match");
// Then the response differs if the digest match
if (TRUE.equals(match)) {
assertEquals(0, out.toByteArray().length);
verify(resp).sendError(HttpServletResponse.SC_NOT_MODIFIED);
} else {
assertEquals(blobValue, out.toString());
verify(resp).setHeader("ETag", '"' + blob.getDigest() + '"');
}
}
@Test
public void testETagHeaderNoDigest() throws Exception {
String blobValue = "Hello World";
Blob blob = Blobs.createBlob(blobValue);
blob.setFilename("myFile.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeader("If-None-Match")).thenReturn("\"b10a8db164e0754105b7a99be72e3fe5\"");
when(req.getMethod()).thenReturn("GET");
HttpServletResponse resp = mock(HttpServletResponse.class);
ServletOutputStream sos = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
out.write(b);
}
};
@SuppressWarnings("resource")
PrintWriter printWriter = new PrintWriter(sos);
when(resp.getOutputStream()).thenReturn(sos);
when(resp.getWriter()).thenReturn(printWriter);
downloadService.downloadBlob(req, resp, null, null, blob, null, "test");
verify(req, atLeastOnce()).getHeader("If-None-Match");
assertEquals(0, out.toByteArray().length);
verify(resp).sendError(HttpServletResponse.SC_NOT_MODIFIED);
}
@Test
@LocalDeploy("org.nuxeo.ecm.core.io.test:OSGI-INF/test-download-service-permission.xml")
public void testDownloadPermission() throws Exception {
// blob to download
String blobValue = "Hello World";
Blob blob = Blobs.createBlob(blobValue);
blob.setFilename("myfile.txt");
blob.setDigest("12345");
// mock request
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getMethod()).thenReturn("GET");
// mock response
HttpServletResponse response = mock(HttpServletResponse.class);
ServletOutputStream sos = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
out.write(b);
}
};
@SuppressWarnings("resource")
PrintWriter printWriter = new PrintWriter(sos);
when(response.getOutputStream()).thenReturn(sos);
when(response.getWriter()).thenReturn(printWriter);
// mock document
DocumentModel doc = mock(DocumentModel.class);
when(doc.getPropertyValue("dc:format")).thenReturn("pdf");
// extended infos with rendition
String reason = "rendition";
Map<String, Serializable> extendedInfos = Collections.singletonMap("rendition", "myrendition");
// principal
NuxeoPrincipal principal = new UserPrincipal("bob", Collections.singletonList("members"), false, false);
// do tests while logged in
LoginStack loginStack = ClientLoginModule.getThreadLocalLogin();
loginStack.push(principal, null, null);
try {
// send download request for file:content, should be denied
downloadService.downloadBlob(request, response, doc, "file:content", blob, null, reason, extendedInfos);
assertEquals("", out.toString());
verify(response, atLeastOnce()).sendError(403, "Permission denied");
// but another xpath is allowed, per the javascript rule
downloadService.downloadBlob(request, response, doc, "other:blob", blob, null, reason, extendedInfos);
assertEquals(blobValue, out.toString());
} finally {
loginStack.pop();
}
}
@Test
public void testTransientCleanup() throws IOException {
// transfert temporary file into a blob
Path path = Files.createTempFile("pfouh","pfouh");
FileBlob blob = new FileBlob("pfouh");
Files.move(path, blob.getFile().toPath(), REPLACE_EXISTING);
// store the blob for downloading
String key = downloadService.storeBlobs(Collections.singletonList(blob));
// mock request
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getMethod()).thenReturn("GET");
// mock response
HttpServletResponse response = mock(HttpServletResponse.class);
ServletOutputStream sos = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
out.write(b);
}
};
@SuppressWarnings("resource")
PrintWriter printWriter = new PrintWriter(sos);
when(response.getOutputStream()).thenReturn(sos);
when(response.getWriter()).thenReturn(printWriter);
NuxeoPrincipal principal = new UserPrincipal("bob", Collections.singletonList("members"), false, false);
// do tests while logged in
LoginStack loginStack = ClientLoginModule.getThreadLocalLogin();
loginStack.push(principal, null, null);
try {
downloadService.downloadBlob(request, response, key, "download");
} finally {
loginStack.pop();
}
// the file is gone
assertFalse(blob.getFile().exists());
}
@Test
public void testGetDownloadPathAndAction() {
DownloadServiceImpl downloadServiceImpl = new DownloadServiceImpl();
String path = "nxfile/default/3727ef6b-cf8c-4f27-ab2c-79de0171a2c8/files:files/0/file/image.png";
Pair<String, Action> pair = downloadServiceImpl.getDownloadPathAndAction(path);
assertEquals("default/3727ef6b-cf8c-4f27-ab2c-79de0171a2c8/files:files/0/file/image.png", pair.getLeft());
assertEquals(Action.DOWNLOAD_FROM_DOC, pair.getRight());
path = "plop/default/3727ef6b-cf8c-4f27-ab2c-79de0171a2c8/files:files/0/file/image.png";
pair = downloadServiceImpl.getDownloadPathAndAction(path);
assertNull(pair);
}
@Test
public void testResolveBlobFromDownloadUrl() throws IOException {
String repositoryName = "test";
CoreSession session = CoreInstance.openCoreSession(repositoryName);
Framework.getProperties().setProperty("nuxeo.url", "http://localhost:8080/nuxeo");
DocumentModel doc = session.createDocumentModel("/", "James-Bond", "File");
doc.setProperty("dublincore", "title", "Diamonds are forever");
FileBlob blob = new FileBlob("Synopsis");
String blobFilename = "synopsis.txt";
blob.setFilename(blobFilename);
Map<String, Object> fileMap = new HashMap<>();
fileMap.put("file", blob);
List<Map<String, Object>> docFiles = new ArrayList<>();
docFiles.add(fileMap);
doc.setProperty("files", "files", docFiles);
doc = session.createDocument(doc);
session.save();
String url = "http://localhost:8080/nuxeo/nxfile/" + repositoryName + "/" + doc.getId() + "/files:files/0/file/"
+ blobFilename;
Blob resolvedBlob = downloadService.resolveBlobFromDownloadUrl(url);
assertEquals(blob, resolvedBlob);
session.close();
}
}