diff --git a/celements-filebase/src/main/java/com/celements/filebase/AttachmentRequest.java b/celements-filebase/src/main/java/com/celements/filebase/AttachmentRequest.java new file mode 100644 index 000000000..d135d4eb2 --- /dev/null +++ b/celements-filebase/src/main/java/com/celements/filebase/AttachmentRequest.java @@ -0,0 +1,6 @@ +package com.celements.filebase; + +import org.xwiki.model.reference.DocumentReference; + +record AttachmentRequest(DocumentReference docRef, String dirPath) { +} diff --git a/celements-filebase/src/main/java/com/celements/filebase/FileItemHelper.java b/celements-filebase/src/main/java/com/celements/filebase/FileItemHelper.java new file mode 100644 index 000000000..36d3b3667 --- /dev/null +++ b/celements-filebase/src/main/java/com/celements/filebase/FileItemHelper.java @@ -0,0 +1,76 @@ +package com.celements.filebase; + +import java.net.URLConnection; +import java.time.Instant; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.xwiki.model.reference.AttachmentReference; + +import com.celements.filebase.dto.FileItem; +import com.celements.model.reference.RefBuilder; +import com.celements.url.UrlService; +import com.xpn.xwiki.doc.XWikiAttachment; + +@Component +public class FileItemHelper { + + private static final int MAX_WIDTH = 800; + private static final int MAX_HEIGHT = 800; + + private final UrlService urlService; + + public FileItemHelper(UrlService urlService) { + this.urlService = Objects.requireNonNull(urlService); + } + + public FileItem toFileItem(String dirPath, XWikiAttachment att, String storage) { + String name = att.getFilename(); + AttachmentReference attachmentRef = RefBuilder.from(att.getDoc().getDocumentReference()) + .att(name).build(AttachmentReference.class); + String query = "celwidth=" + MAX_WIDTH + "&celheight=" + MAX_HEIGHT; + return new FileItem( + dirPath, + name, + extensionOf(name), + dirPath.endsWith("/") ? (dirPath + name) : (dirPath + "/" + name), + urlService.getURL(attachmentRef, "download"), + urlService.getURL(attachmentRef, "download", query), + storage, + "file", + (long) att.getFilesize(), + toUnixSeconds(att.getDate()), + guessMimeType(name), + "public"); + } + + public String guessMimeType(String filename) { + String mime = URLConnection.guessContentTypeFromName(filename); + return mime != null ? mime : "application/octet-stream"; + } + + public String extensionOf(String name) { + int i = name.lastIndexOf('.'); + return (i > 0) && (i < (name.length() - 1)) ? name.substring(i + 1).toLowerCase(Locale.ROOT) + : ""; + } + + public long toUnixSeconds(Date date) { + if (date == null) { + return Instant.now().getEpochSecond(); + } + return date.toInstant().getEpochSecond(); + } + + public String normalizeFileName(String path) { + String p = StringUtils.hasText(path) ? path.trim() : ""; + int lastSlash = p.lastIndexOf('/'); + if (lastSlash >= 0) { + return p.substring(lastSlash + 1); + } + return p; + } +} diff --git a/celements-filebase/src/main/java/com/celements/filebase/MediaLibController.java b/celements-filebase/src/main/java/com/celements/filebase/MediaLibController.java index eb8a759ac..195dacd70 100644 --- a/celements-filebase/src/main/java/com/celements/filebase/MediaLibController.java +++ b/celements-filebase/src/main/java/com/celements/filebase/MediaLibController.java @@ -2,10 +2,8 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URLConnection; -import java.time.Instant; import java.util.ArrayList; -import java.util.Date; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -29,10 +27,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; -import org.xwiki.model.reference.AttachmentReference; import org.xwiki.model.reference.DocumentReference; import com.celements.auth.user.User; @@ -56,28 +52,23 @@ import com.celements.model.access.exception.DocumentSaveException; import com.celements.model.context.ModelContext; import com.celements.model.object.xwiki.XWikiObjectEditor; -import com.celements.model.reference.RefBuilder; import com.celements.model.util.ModelUtils; import com.celements.rights.access.EAccessLevel; import com.celements.rights.access.IRightsAccessFacadeRole; import com.celements.spring.security.AuthenticatedBaseController; -import com.celements.url.UrlService; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; @RestController -@RestControllerAdvice @RequestMapping("/files") public class MediaLibController extends AuthenticatedBaseController { private static final Logger LOGGER = LoggerFactory.getLogger(MediaLibController.class); private static final String STORAGE = "local"; - private static final int MAX_WIDTH = 800; - private static final int MAX_HEIGHT = 800; private final IFileBaseServiceRole fileBaseService; - private final UrlService urlService; + private final FileItemHelper fileItemHelper; private final IModelAccessFacade modelAccess; private final IRightsAccessFacadeRole rightsAccess; private final ModelUtils modelUtils; @@ -86,17 +77,17 @@ public class MediaLibController extends AuthenticatedBaseController { @Inject public MediaLibController( IFileBaseServiceRole fileBaseService, - UrlService urlService, + FileItemHelper fileItemHelper, IModelAccessFacade modelAccess, IRightsAccessFacadeRole rightsAccess, ModelUtils modelUtils, ModelContext modelContext) { - this.fileBaseService = fileBaseService; - this.urlService = urlService; - this.modelAccess = modelAccess; - this.rightsAccess = rightsAccess; - this.modelUtils = modelUtils; - this.modelContext = modelContext; + this.fileBaseService = Objects.requireNonNull(fileBaseService); + this.fileItemHelper = Objects.requireNonNull(fileItemHelper); + this.modelAccess = Objects.requireNonNull(modelAccess); + this.rightsAccess = Objects.requireNonNull(rightsAccess); + this.modelUtils = Objects.requireNonNull(modelUtils); + this.modelContext = Objects.requireNonNull(modelContext); } /** @@ -119,7 +110,7 @@ public ListResponse list(@RequestParam(name = "path", required = false) String p try { List files = fileBaseService.getFilesNameMatch(new AllAttachmentMatcher()) .stream() - .map(att -> toFileItem(dirPath, att)) + .map(att -> fileItemHelper.toFileItem(dirPath, att, STORAGE)) // enforce XWiki rights (skip what user cannot access) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -159,7 +150,7 @@ public Object upload(@RequestParam("path") String path, exp); } } - return java.util.Collections.emptyMap(); + return Collections.emptyMap(); } throw new ResponseStatusException(HttpStatus.FORBIDDEN); } @@ -170,7 +161,6 @@ public Object upload(@RequestParam("path") String path, * body: { "items": [ { "path": "local://public/FileRepo/a.png", "type":"file" } * ] } * Response: updated list (same structure as GET /). - * :contentReference[oaicite:5]{index=5} */ @PostMapping(path = "/delete") @PreAuthorize("permitAll()") @@ -185,7 +175,7 @@ public ListResponse delete(@RequestBody DeleteRequest body) { if ((item == null) || !"file".equalsIgnoreCase(item.type())) { continue; } - String delFileName = normalizeFileName(item.path()); + String delFileName = fileItemHelper.normalizeFileName(item.path()); LOGGER.debug("add filename '{}' to delete list", delFileName); refs.add(delFileName); } @@ -214,7 +204,7 @@ public ListResponse search(@RequestParam("q") String query, List files = fileBaseService.getFilesNameMatch( att -> att.getFilename().toLowerCase(Locale.ROOT).contains(lower)) .stream() - .map(att -> toFileItem(dirPath, att)) + .map(att -> fileItemHelper.toFileItem(dirPath, att, STORAGE)) .filter(Objects::nonNull) .collect(Collectors.toList()); return new ListResponse(List.of(STORAGE), dirPath, false, files); @@ -256,9 +246,9 @@ public ResponseEntity filesForTag(@RequestParam("tagId") String tagId) { .filter(t -> modelUtils.serializeRefLocal(t.getTagRef()).equals(tagId)) .findFirst() .map(FileBaseTag::getTagFileList) - .orElse(java.util.Collections.emptyList()) + .orElse(Collections.emptyList()) .stream() - .map(ref -> normalizeFileName(ref.getName())) + .map(ref -> fileItemHelper.normalizeFileName(ref.getName())) .collect(Collectors.toList()); return ResponseEntity.ok(files); } @@ -373,7 +363,7 @@ private FileBaseTag findTag(String tagId) { } private String toAttachmentKey(String filePath) { - String filename = normalizeFileName(filePath); + String filename = fileItemHelper.normalizeFileName(filePath); try { XWikiAttachment att = fileBaseService.getFileNameEqual(filename); return modelUtils.serializeRefLocal(att.getDoc().getDocumentReference()) + "/" + filename; @@ -399,58 +389,12 @@ private String normalizeDirPath(String path) { return p; } - String normalizeFileName(String path) { - String p = StringUtils.hasText(path) ? path.trim() : (STORAGE + "://"); - var parts = p.split("://|/"); - return parts[Math.max(0, parts.length - 1)]; - } - - private FileItem toFileItem(String dirPath, XWikiAttachment att) { - String name = att.getFilename(); - AttachmentReference attachmentRef = RefBuilder.from(att.getDoc().getDocumentReference()) - .att(name).build(AttachmentReference.class); - String query = "celwidth=" + MAX_WIDTH + "&celheight=" + MAX_HEIGHT; - return new FileItem( - dirPath, - name, - extensionOf(name), - dirPath.endsWith("/") ? (dirPath + name) : (dirPath + "/" + name), - urlService.getURL(attachmentRef, "download"), - urlService.getURL(attachmentRef, "download", query), - STORAGE, - "file", - (long) att.getFilesize(), - toUnixSeconds(att.getDate()), - guessMimeType(name), - "public"); - } - - private String guessMimeType(String filename) { - String mime = URLConnection.guessContentTypeFromName(filename); - return (mime != null) ? mime : "application/octet-stream"; - } - - private String extensionOf(String name) { - int i = name.lastIndexOf('.'); - return ((i > 0) && (i < (name.length() - 1))) ? name.substring(i + 1).toLowerCase(Locale.ROOT) - : ""; - } - - private long toUnixSeconds(Date date) { - if (date == null) { - return Instant.now().getEpochSecond(); - } - return date.toInstant().getEpochSecond(); - } - - // inner classes moved to com.celements.filebase.dto package - @ExceptionHandler(ResponseStatusException.class) @PreAuthorize("permitAll()") public ResponseEntity handle(ResponseStatusException ex) { return ResponseEntity .status(ex.getStatus()) - .body(java.util.Map.of("message", + .body(Map.of("message", ex.getReason() != null ? ex.getReason() : "Request failed")); } @@ -477,6 +421,6 @@ public ResponseEntity handle(FileBaseTagRenameException exp) { private ResponseEntity internalServerError(String message) { return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(java.util.Map.of("message", message)); + .body(Map.of("message", message)); } } diff --git a/celements-filebase/src/main/java/com/celements/filebase/PageAttachmentsController.java b/celements-filebase/src/main/java/com/celements/filebase/PageAttachmentsController.java new file mode 100644 index 000000000..099867b48 --- /dev/null +++ b/celements-filebase/src/main/java/com/celements/filebase/PageAttachmentsController.java @@ -0,0 +1,249 @@ +package com.celements.filebase; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.server.ResponseStatusException; +import org.xwiki.model.reference.AttachmentReference; +import org.xwiki.model.reference.DocumentReference; + +import com.celements.auth.user.User; +import com.celements.filebase.dto.DeleteItem; +import com.celements.filebase.dto.DeleteRequest; +import com.celements.filebase.dto.FileItem; +import com.celements.filebase.dto.ListResponse; +import com.celements.model.access.IModelAccessFacade; +import com.celements.model.access.exception.DocumentLoadException; +import com.celements.model.access.exception.DocumentSaveException; +import com.celements.model.context.ModelContext; +import com.celements.rights.access.EAccessLevel; +import com.celements.rights.access.IRightsAccessFacadeRole; +import com.celements.spring.security.AuthenticatedBaseController; +import com.xpn.xwiki.doc.XWikiDocument; + +@RestController +@RequestMapping("/attachments/{spaceName}/{docName}") +public class PageAttachmentsController extends AuthenticatedBaseController { + + private static final Logger LOGGER = LoggerFactory.getLogger(PageAttachmentsController.class); + + private static final String STORAGE = "attachments"; + + private final IAttachmentServiceRole attService; + private final FileItemHelper fileItemHelper; + private final IModelAccessFacade modelAccess; + private final IRightsAccessFacadeRole rightsAccess; + private final ModelContext modelContext; + + @Inject + public PageAttachmentsController( + IAttachmentServiceRole attService, + FileItemHelper fileItemHelper, + IModelAccessFacade modelAccess, + IRightsAccessFacadeRole rightsAccess, + ModelContext modelContext) { + this.attService = Objects.requireNonNull(attService); + this.fileItemHelper = Objects.requireNonNull(fileItemHelper); + this.modelAccess = Objects.requireNonNull(modelAccess); + this.rightsAccess = Objects.requireNonNull(rightsAccess); + this.modelContext = Objects.requireNonNull(modelContext); + } + + @GetMapping("") + @PreAuthorize("permitAll()") + public ListResponse list( + @PathVariable String spaceName, + @PathVariable String docName, + @RequestParam(name = "path", required = false) String path) { + AttachmentRequest request = prepareRequest(spaceName, docName, path, EAccessLevel.VIEW); + try { + XWikiDocument doc = modelAccess.getOrCreateDocument(request.docRef()); + List files = attService.getAttachmentsNameMatch(doc, att -> true) + .stream() + .map(att -> fileItemHelper.toFileItem(request.dirPath(), att, STORAGE)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return new ListResponse(List.of(STORAGE), request.dirPath(), false, files); + } catch (DocumentLoadException exp) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Document load failed", exp); + } + } + + @PostMapping(path = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("permitAll()") + public Object upload( + @PathVariable String spaceName, + @PathVariable String docName, + @RequestParam(name = "path", required = false) String path, + @RequestParam("file") List files) { + AttachmentRequest request = prepareRequest(spaceName, docName, path, EAccessLevel.EDIT); + try { + XWikiDocument doc = modelAccess.getOrCreateDocument(request.docRef()); + for (MultipartFile file : files) { + if ((file == null) || file.isEmpty()) { + continue; + } + String original = file.getOriginalFilename(); + String fileName = StringUtils.hasText(original) ? original : "upload.bin"; + String safeName = attService.clearFileName(fileName); + try (InputStream in = file.getInputStream()) { + attService.addAttachment(doc, in, safeName, null, "Uploaded via VueFinder"); + } catch (IOException | AttachmentToBigException | AddingAttachmentContentFailedException exp) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Fileupload failed.", + exp); + } + } + return Collections.emptyMap(); + } catch (DocumentLoadException | DocumentSaveException exp) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Fileupload failed.", exp); + } + } + + @PostMapping("/delete") + @PreAuthorize("permitAll()") + public ListResponse delete( + @PathVariable String spaceName, + @PathVariable String docName, + @RequestBody DeleteRequest body) { + AttachmentRequest request = prepareRequest(spaceName, docName, body.path(), EAccessLevel.DELETE); + try { + XWikiDocument doc = modelAccess.getOrCreateDocument(request.docRef()); + List attsToDelete = new ArrayList<>(); + if (body.items() != null) { + for (DeleteItem item : body.items()) { + if (item == null || !"file".equalsIgnoreCase(item.type())) { + continue; + } + String delFileName = fileItemHelper.normalizeFileName(item.path()); + if (attService.existsAttachmentNameEqual(doc, delFileName)) { + attsToDelete.add(new AttachmentReference(delFileName, request.docRef())); + } + } + } + if (!attsToDelete.isEmpty()) { + attService.deleteAttachmentList(attsToDelete); + } + return list(spaceName, docName, body.path()); + } catch (DocumentLoadException exp) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Delete failed.", exp); + } + } + + @GetMapping("/search") + @PreAuthorize("permitAll()") + public ListResponse search( + @PathVariable String spaceName, + @PathVariable String docName, + @RequestParam("q") String query, + @RequestParam(name = "path", required = false) String path) { + AttachmentRequest request = prepareRequest(spaceName, docName, path, EAccessLevel.VIEW); + String lower = query.toLowerCase(Locale.ROOT); + try { + XWikiDocument doc = modelAccess.getOrCreateDocument(request.docRef()); + List files = attService.getAttachmentsNameMatch(doc, + att -> att.getFilename().toLowerCase(Locale.ROOT).contains(lower)) + .stream() + .map(att -> fileItemHelper.toFileItem(request.dirPath(), att, STORAGE)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return new ListResponse(List.of(STORAGE), request.dirPath(), false, files); + } catch (DocumentLoadException exp) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Search failed", exp); + } + } + + private AttachmentRequest prepareRequest( + String spaceName, + String docName, + String path, + EAccessLevel accessLevel) { + checkAuth(); + User user = modelContext.user().orElse(null); + if (user == null) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + DocumentReference docRef = new DocumentReference( + docName, + new org.xwiki.model.reference.SpaceReference(spaceName, modelContext.getWikiRef())); + if (!rightsAccess.hasAccessLevel(docRef, accessLevel, user)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + return new AttachmentRequest(docRef, normalizeDirPath(spaceName, docName, path)); + } + + private String normalizeDirPath(String spaceName, String docName, String path) { + String expectedPrefix = STORAGE + "://" + spaceName + "/" + docName; + String p = StringUtils.hasText(path) ? path.trim() : expectedPrefix; + if (!p.startsWith(STORAGE + "://")) { + p = expectedPrefix + "/" + p; + } + if (p.endsWith("/") && !p.equals(expectedPrefix)) { + p = p.substring(0, p.length() - 1); + } + return p; + } + + @ExceptionHandler(ResponseStatusException.class) + @PreAuthorize("permitAll()") + public ResponseEntity handle(ResponseStatusException ex) { + return ResponseEntity + .status(ex.getStatus()) + .body(Map.of("message", + ex.getReason() != null ? ex.getReason() : "Request failed")); + } + + @ExceptionHandler({ + MissingServletRequestParameterException.class, + MissingServletRequestPartException.class + }) + @PreAuthorize("permitAll()") + public ResponseEntity handleMissingParams( + Exception ex, + HttpServletRequest request) { + LOGGER.warn("Missing parameter/part: {}, Request URI: {}, Content-Type: {}", + ex.getMessage(), request.getRequestURI(), request.getContentType()); + try { + if (request instanceof MultipartHttpServletRequest multipartRequest) { + LOGGER.warn("Multipart parameter names: {}", multipartRequest.getParameterMap().keySet()); + LOGGER.warn("Multipart file names: {}", multipartRequest.getFileMap().keySet()); + } else { + LOGGER.warn("Request is not a MultipartHttpServletRequest. Parameter names: {}", + request.getParameterMap().keySet()); + } + } catch (Exception e) { + LOGGER.warn("Failed to log request details", e); + } + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", ex.getMessage())); + } +} diff --git a/celements-filebase/src/test/java/com/celements/filebase/MediaLibControllerTest.java b/celements-filebase/src/test/java/com/celements/filebase/MediaLibControllerTest.java index ce84404fc..42ee210c2 100644 --- a/celements-filebase/src/test/java/com/celements/filebase/MediaLibControllerTest.java +++ b/celements-filebase/src/test/java/com/celements/filebase/MediaLibControllerTest.java @@ -25,6 +25,7 @@ public class MediaLibControllerTest extends AbstractComponentTest { private MediaLibController mediaLibCtrl; + private FileItemHelper fileItemHelper; private IFileBaseServiceRole fileBaseServiceMock; private ModelContext modelContextMock; private UserService userServiceMock; @@ -38,18 +39,19 @@ public void prepare() throws Exception { modelContextMock = registerComponentMock(ModelContext.class); userServiceMock = registerComponentMock(UserService.class); userMock = createDefaultMock(User.class); + fileItemHelper = getBeanFactory().getBean(FileItemHelper.class); mediaLibCtrl = getBeanFactory().getBean(MediaLibController.class); } @Test public void testNormalizeFileName_file() { - String fileName = mediaLibCtrl.normalizeFileName("local://IMG-20250606-WA0000.jpg"); + String fileName = fileItemHelper.normalizeFileName("local://IMG-20250606-WA0000.jpg"); assertEquals("IMG-20250606-WA0000.jpg", fileName); } @Test public void testNormalizeFileName_subPath() { - String fileName = mediaLibCtrl.normalizeFileName("local://test/IMG-20250606-WA0000.jpg"); + String fileName = fileItemHelper.normalizeFileName("local://test/IMG-20250606-WA0000.jpg"); assertEquals("IMG-20250606-WA0000.jpg", fileName); } diff --git a/celements-filebase/src/test/java/com/celements/filebase/PageAttachmentsControllerTest.java b/celements-filebase/src/test/java/com/celements/filebase/PageAttachmentsControllerTest.java new file mode 100644 index 000000000..59f8a67b8 --- /dev/null +++ b/celements-filebase/src/test/java/com/celements/filebase/PageAttachmentsControllerTest.java @@ -0,0 +1,181 @@ +package com.celements.filebase; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.server.ResponseStatusException; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.SpaceReference; +import org.xwiki.model.reference.WikiReference; + +import com.celements.auth.user.User; +import com.celements.auth.user.UserService; +import com.celements.common.test.AbstractComponentTest; +import com.celements.filebase.dto.DeleteItem; +import com.celements.filebase.dto.DeleteRequest; +import com.celements.filebase.dto.ListResponse; +import com.celements.model.access.IModelAccessFacade; +import com.celements.model.context.ModelContext; +import com.celements.rights.access.EAccessLevel; +import com.celements.rights.access.IRightsAccessFacadeRole; +import com.celements.url.UrlService; +import com.xpn.xwiki.doc.XWikiAttachment; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.user.api.XWikiUser; + +public class PageAttachmentsControllerTest extends AbstractComponentTest { + + private PageAttachmentsController pageAttachmentsCtrl; + private FileItemHelper fileItemHelper; + private IAttachmentServiceRole attServiceMock; + private UrlService urlServiceMock; + private IModelAccessFacade modelAccessMock; + private IRightsAccessFacadeRole rightsAccessMock; + private ModelContext modelContextMock; + private UserService userServiceMock; + private User userMock; + + @Before + public void prepare() throws Exception { + SecurityContextHolder.getContext() + .setAuthentication(new TestingAuthenticationToken("test", "n/a", "ROLE_USER")); + attServiceMock = registerComponentMock(IAttachmentServiceRole.class); + urlServiceMock = registerComponentMock(UrlService.class); + modelAccessMock = registerComponentMock(IModelAccessFacade.class); + rightsAccessMock = registerComponentMock(IRightsAccessFacadeRole.class); + modelContextMock = registerComponentMock(ModelContext.class); + userServiceMock = registerComponentMock(UserService.class); + userMock = createDefaultMock(User.class); + fileItemHelper = getBeanFactory().getBean(FileItemHelper.class); + pageAttachmentsCtrl = getBeanFactory().getBean(PageAttachmentsController.class); + } + + @Test + public void testNormalizeFileName_file() { + String fileName = fileItemHelper.normalizeFileName("attachments://MySpace/MyDoc/IMG.jpg"); + assertEquals("IMG.jpg", fileName); + } + + @Test + public void test_list_allowed() throws Exception { + expectCheckAuth(); + expect(modelContextMock.user()).andReturn(Optional.of(userMock)).anyTimes(); + WikiReference wikiRef = new WikiReference("xwiki"); + expect(modelContextMock.getWikiRef()).andReturn(wikiRef).anyTimes(); + + DocumentReference docRef = new DocumentReference("MyDoc", new SpaceReference("MySpace", wikiRef)); + expect(rightsAccessMock.hasAccessLevel(eq(docRef), eq(EAccessLevel.VIEW), same(userMock))).andReturn(true); + + XWikiDocument docMock = createDefaultMock(XWikiDocument.class); + expect(modelAccessMock.getOrCreateDocument(eq(docRef))).andReturn(docMock); + + XWikiAttachment attMock = createDefaultMock(XWikiAttachment.class); + expect(attMock.getFilename()).andReturn("file.png").anyTimes(); + expect(attMock.getDoc()).andReturn(docMock).anyTimes(); + expect(attMock.getFilesize()).andReturn(100).anyTimes(); + expect(attMock.getDate()).andReturn(new Date()).anyTimes(); + + expect(attServiceMock.getAttachmentsNameMatch(same(docMock), anyObject())).andReturn(List.of(attMock)); + expect(docMock.getDocumentReference()).andReturn(docRef).anyTimes(); + expect(urlServiceMock.getURL(anyObject(), eq("download"))).andReturn("http://download"); + expect(urlServiceMock.getURL(anyObject(), eq("download"), anyString())).andReturn("http://preview"); + + replayDefault(); + ListResponse response = pageAttachmentsCtrl.list("MySpace", "MyDoc", "attachments://MySpace/MyDoc"); + verifyDefault(); + + assertNotNull(response); + assertEquals("attachments://MySpace/MyDoc", response.dirname()); + assertEquals(1, response.files().size()); + assertEquals("file.png", response.files().get(0).basename()); + } + + @Test + public void test_list_denied() throws Exception { + expectCheckAuth(); + expect(modelContextMock.user()).andReturn(Optional.of(userMock)).anyTimes(); + WikiReference wikiRef = new WikiReference("xwiki"); + expect(modelContextMock.getWikiRef()).andReturn(wikiRef).anyTimes(); + + DocumentReference docRef = new DocumentReference("MyDoc", new SpaceReference("MySpace", wikiRef)); + expect(rightsAccessMock.hasAccessLevel(eq(docRef), eq(EAccessLevel.VIEW), same(userMock))).andReturn(false); + + replayDefault(); + try { + pageAttachmentsCtrl.list("MySpace", "MyDoc", "attachments://MySpace/MyDoc"); + fail("Expected FORBIDDEN"); + } catch (ResponseStatusException rse) { + assertEquals(HttpStatus.FORBIDDEN, rse.getStatus()); + } + verifyDefault(); + } + + @Test + public void test_search_allowed() throws Exception { + expectCheckAuth(); + expect(modelContextMock.user()).andReturn(Optional.of(userMock)).anyTimes(); + WikiReference wikiRef = new WikiReference("xwiki"); + expect(modelContextMock.getWikiRef()).andReturn(wikiRef).anyTimes(); + + DocumentReference docRef = new DocumentReference("MyDoc", new SpaceReference("MySpace", wikiRef)); + expect(rightsAccessMock.hasAccessLevel(eq(docRef), eq(EAccessLevel.VIEW), same(userMock))).andReturn(true); + + XWikiDocument docMock = createDefaultMock(XWikiDocument.class); + expect(modelAccessMock.getOrCreateDocument(eq(docRef))).andReturn(docMock); + + expect(attServiceMock.getAttachmentsNameMatch(same(docMock), anyObject())).andReturn(List.of()); + + replayDefault(); + ListResponse response = pageAttachmentsCtrl.search("MySpace", "MyDoc", "test", "attachments://MySpace/MyDoc"); + verifyDefault(); + + assertNotNull(response); + assertEquals("attachments://MySpace/MyDoc", response.dirname()); + } + + @Test + public void test_delete_allowed() throws Exception { + expectCheckAuth(); + expect(modelContextMock.user()).andReturn(Optional.of(userMock)).anyTimes(); + WikiReference wikiRef = new WikiReference("xwiki"); + expect(modelContextMock.getWikiRef()).andReturn(wikiRef).anyTimes(); + + DocumentReference docRef = new DocumentReference("MyDoc", new SpaceReference("MySpace", wikiRef)); + expect(rightsAccessMock.hasAccessLevel(eq(docRef), eq(EAccessLevel.DELETE), same(userMock))).andReturn(true); + expect(rightsAccessMock.hasAccessLevel(eq(docRef), eq(EAccessLevel.VIEW), same(userMock))).andReturn(true); + + XWikiDocument docMock = createDefaultMock(XWikiDocument.class); + expect(modelAccessMock.getOrCreateDocument(eq(docRef))).andReturn(docMock).times(2); + + expect(attServiceMock.existsAttachmentNameEqual(same(docMock), eq("a.png"))).andReturn(true); + attServiceMock.deleteAttachmentList(anyObject()); + expectLastCall().andReturn(1); + + expect(attServiceMock.getAttachmentsNameMatch(same(docMock), anyObject())).andReturn(List.of()); + + replayDefault(); + + DeleteItem item = new DeleteItem("attachments://MySpace/MyDoc/a.png", "file"); + DeleteRequest request = new DeleteRequest("attachments://MySpace/MyDoc", List.of(item)); + + ListResponse response = pageAttachmentsCtrl.delete("MySpace", "MyDoc", request); + verifyDefault(); + assertNotNull(response); + } + + private void expectCheckAuth() throws Exception { + XWikiUser xuser = new XWikiUser("xwiki:User.test"); + expect(getXContext().getWiki().checkAuth(same(getXContext()))).andReturn(xuser).anyTimes(); + expect(userServiceMock.getUser(eq("xwiki:User.test"))).andReturn(userMock).anyTimes(); + } + +}