/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.celements.filebase;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.annotation.Requirement;
import org.xwiki.context.Execution;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import com.celements.filebase.matcher.IAttFileNameMatcherRole;
import com.celements.filebase.matcher.IAttachmentMatcher;
import com.celements.model.access.IModelAccessFacade;
import com.celements.model.access.exception.AttachmentNotExistsException;
import com.celements.model.access.exception.DocumentLoadException;
import com.celements.model.access.exception.DocumentNotExistsException;
import com.celements.model.access.exception.DocumentSaveException;
import com.celements.rights.access.EAccessLevel;
import com.celements.rights.access.IRightsAccessFacadeRole;
import com.celements.rights.access.exceptions.NoAccessRightsException;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Attachment;
import com.xpn.xwiki.api.Document;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.plugin.fileupload.FileUploadPlugin;
import com.xpn.xwiki.web.XWikiResponse;
/**
* This Service makes the methods in the XWiki UploadAction-Class accessible
* by other means. In celements we especially need them for the TokenBasedUpload because
* the addAttachment on the Document class is buggy.
*
* @author fabian
* since 2.28.0
*/
@Component
public class AttachmentService implements IAttachmentServiceRole {
private static Logger _LOGGER = LoggerFactory.getLogger(AttachmentService.class);
/** The prefix of the corresponding filename input field name. */
private static final String FILENAME_FIELD_NAME = "filename";
@Requirement
IModelAccessFacade modelAccess;
@Requirement
Execution execution;
@Requirement
IRightsAccessFacadeRole rightsAccess;
private XWikiContext getContext() {
return (XWikiContext) execution.getContext().getProperty("xwikicontext");
}
@Override
public XWikiAttachment addAttachment(XWikiDocument doc, byte[] data, String filename,
String username, String comment) throws AttachmentToBigException,
AddingAttachmentContentFailedException, DocumentSaveException {
ByteArrayInputStream dataStream = null;
try {
dataStream = new ByteArrayInputStream(data);
return addAttachment(doc, dataStream, filename, username, comment);
} finally {
IOUtils.closeQuietly(dataStream);
}
}
@Override
public XWikiAttachment addAttachment(XWikiDocument doc, InputStream in, String filename,
String username, String comment) throws AttachmentToBigException,
AddingAttachmentContentFailedException, DocumentSaveException {
// We do not want to change the document in xwiki cache in case an exception happens
// those not saved changes would be left in memory
XWikiDocument theDoc = (XWikiDocument) doc.clone();
// Read XWikiAttachment
XWikiAttachment attachment = null;
try {
attachment = getAttachmentNameEqual(theDoc, filename);
} catch (AttachmentNotExistsException e) {
_LOGGER.debug("adding new attachment with name [{}]", filename);
}
if (attachment == null) {
attachment = new XWikiAttachment();
theDoc.getAttachmentList().add(attachment);
}
try {
attachment.setContent(in);
} catch (IOException exp) {
throw new AddingAttachmentContentFailedException(exp);
}
attachment.setFilename(filename);
attachment.setAuthor(username);
// Add the attachment to the document
attachment.setDoc(theDoc);
doc.setMetaDataDirty(true);
doc.setContentDirty(true);
theDoc.setAuthor(username);
// Adding a comment with a link to the download URL
String nextRev = attachment.getNextVersion();
ArrayList<String> params = new ArrayList<String>();
params.add(filename);
params.add(theDoc.getAttachmentRevisionURL(filename, nextRev, getContext()));
if (comment == null) {
if (attachment.isImage(getContext())) {
comment = getContext().getMessageTool().get("core.comment.uploadImageComment", params);
} else {
comment = getContext().getMessageTool().get("core.comment.uploadAttachmentComment", params);
}
}
// Save the document.
try {
_LOGGER.debug("uploadAttachment: save document [" + theDoc.getDocumentReference()
+ "] after adding filename [" + filename + "] in revision [" + nextRev + "].");
modelAccess.saveDocument(theDoc, comment);
} catch (DocumentSaveException exp) {
// check Exception is ERROR_XWIKI_APP_JAVA_HEAP_SPACE when saving
// Attachment
XWikiException xwe = (XWikiException) exp.getCause();
if (xwe.getCode() == XWikiException.ERROR_XWIKI_APP_JAVA_HEAP_SPACE) {
throw new AttachmentToBigException(xwe);
}
throw exp;
}
return attachment;
}
/**
* Attach a file to the current document.
*
* @param fieldName
* the target file field
* @param filename
* @param fileupload
* the {@link FileUploadPlugin} holding the form data
* @param doc
* the target document
* @return {@code true} if the file was successfully attached, {@code false}
* otherwise.
* @throws XWikiException
* if the form data cannot be accessed, or if the database operation
* failed
*/
@Override
public boolean uploadAttachment(String fieldName, String filename, FileUploadPlugin fileupload,
XWikiDocument doc) throws XWikiException {
XWikiResponse response = getContext().getResponse();
String username = getContext().getUser();
_LOGGER.debug("uploadAttachment: fieldName [" + fieldName + "], filename [" + filename
+ "], context username [" + username + "], doc [" + doc.getDocumentReference() + "].");
if (doc.isNew()) {
doc.setCreator(username);
}
try {
InputStream in = fileupload.getFileItemInputStream(fieldName, getContext());
addAttachment(doc, in, filename, username, null);
} catch (AddingAttachmentContentFailedException | IOException exp) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_UPLOAD_FILE_EXCEPTION,
"Exception while reading uploaded parsed file", exp);
} catch (AttachmentToBigException exp) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
getContext().put("message", "javaheapspace");
return true;
} catch (DocumentSaveException exp) {
XWikiException xwe = (XWikiException) exp.getCause();
throw xwe;
}
return false;
}
/**
* @param attachToDoc
* @param fieldNamePrefix
* @return number of saved attachments
*/
@Override
public int uploadMultipleAttachments(XWikiDocument attachToDoc, String fieldNamePrefix) {
XWikiDocument doc = attachToDoc.clone();
// The document is saved for each attachment in the group.
FileUploadPlugin fileupload = (FileUploadPlugin) getContext().getWiki().getPlugin("fileupload",
getContext());
Map<String, String> fileNames = new HashMap<String, String>();
List<String> wrongFileNames = new ArrayList<String>();
List<String> failedFiles = new ArrayList<String>();
for (String fieldName : fileupload.getFileItemNames(getContext())) {
try {
if (fieldName.startsWith(fieldNamePrefix)) {
String fileName = getFileName(fieldName, fieldNamePrefix, fileupload);
if (fileName != null) {
fileNames.put(fileName, fieldName);
}
}
} catch (Exception ex) {
wrongFileNames.add(fileupload.getFileName(fieldName, getContext()));
}
}
if (_LOGGER.isTraceEnabled()) {
_LOGGER.trace("uploadMultipleAttachments: found fileNames [ key: " + Arrays.deepToString(
fileNames.keySet().toArray()) + " values : " + Arrays.deepToString(
fileNames.values().toArray()) + "].");
}
for (Entry<String, String> file : fileNames.entrySet()) {
try {
uploadAttachment(file.getValue(), file.getKey(), fileupload, doc);
} catch (Exception ex) {
_LOGGER.warn("Saving uploaded file failed", ex);
failedFiles.add(file.getKey());
}
}
int numSavedFiles = fileNames.size() - failedFiles.size();
_LOGGER.debug("Found files to upload: " + fileNames + ", Failed attachments: " + failedFiles
+ ", Wrong attachment names: " + wrongFileNames + ", saved files: " + numSavedFiles);
return numSavedFiles;
}
/**
* Extract the corresponding attachment name for a given file field. It can
* either be specified in a separate form input field, or it is extracted from
* the original filename.
*
* @param fieldName
* the target file field
* @param fieldNamePrefix
* the fieldName prefix
* @param fileupload
* the {@link FileUploadPlugin} holding the form data
* @return a valid attachment name
* @throws XWikiException
* if the form data cannot be accessed, or if the specified filename
* is invalid
*/
String getFileName(String fieldName, String fieldNamePrefix, FileUploadPlugin fileupload)
throws XWikiException {
String filenameField = FILENAME_FIELD_NAME + fieldName.substring(fieldNamePrefix.length());
String filename = null;
// Try to use the name provided by the user
filename = fileupload.getFileItemAsString(filenameField, getContext());
if (!StringUtils.isBlank(filename)) {
// TODO These should be supported, the URL should just contain escapes.
if (filename.indexOf("/") != -1 || filename.indexOf("\\") != -1 || filename.indexOf(
";") != -1) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_INVALID_CHARS, "Invalid filename: " + filename);
}
}
if (StringUtils.isBlank(filename)) {
// Try to get the actual filename on the client
String fname = fileupload.getFileName(fieldName, getContext());
if (StringUtils.indexOf(fname, "/") >= 0) {
fname = StringUtils.substringAfterLast(fname, "/");
}
if (StringUtils.indexOf(fname, "\\") >= 0) {
fname = StringUtils.substringAfterLast(fname, "\\");
}
filename = fname;
}
// Sometimes spaces are replaced with '+' by the browser.
filename = filename.replaceAll("\\+", " ");
if (StringUtils.isBlank(filename)) {
// The file field was left empty, ignore this
return null;
}
return clearFileName(filename);
}
@Override
public String clearFileName(String fileName) {
boolean stripDots = false;
boolean ascii = true;
String temp = fileName;
temp = temp.replaceAll("[\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u0100\u0102\u0104"
+ "\u01cd\u01de\u01e0\u01fa\u0200\u0202\u0226]", "A");
temp = temp.replaceAll(
"[\u00e0\u00e1\u00e2\u00e3\u00e4\u00e5\u0101\u0103\u0105\u01ce\u01df\u01e1"
+ "\u01fb\u0201\u0203\u0227]", "a");
temp = temp.replaceAll("[\u00c6\u01e2\u01fc]", "AE");
temp = temp.replaceAll("[\u00e6\u01e3\u01fd]", "ae");
temp = temp.replaceAll("[\u008c\u0152]", "OE");
temp = temp.replaceAll("[\u009c\u0153]", "oe");
temp = temp.replaceAll("[\u00c7\u0106\u0108\u010a\u010c]", "C");
temp = temp.replaceAll("[\u00e7\u0107\u0109\u010b\u010d]", "c");
temp = temp.replaceAll("[\u00d0\u010e\u0110]", "D");
temp = temp.replaceAll("[\u00f0\u010f\u0111]", "d");
temp = temp.replaceAll("[\u00c8\u00c9\u00ca\u00cb\u0112\u0114\u0116\u0118\u011a"
+ "\u0204\u0206\u0228]", "E");
temp = temp.replaceAll("[\u00e8\u00e9\u00ea\u00eb\u0113\u0115\u0117\u0119\u011b"
+ "\u01dd\u0205\u0207\u0229]", "e");
temp = temp.replaceAll("[\u011c\u011e\u0120\u0122\u01e4\u01e6\u01f4]", "G");
temp = temp.replaceAll("[\u011d\u011f\u0121\u0123\u01e5\u01e7\u01f5]", "g");
temp = temp.replaceAll("[\u0124\u0126\u021e]", "H");
temp = temp.replaceAll("[\u0125\u0127\u021f]", "h");
temp = temp.replaceAll("[\u00cc\u00cd\u00ce\u00cf\u0128\u012a\u012c\u012e\u0130"
+ "\u01cf\u0208\u020a]", "I");
temp = temp.replaceAll("[\u00ec\u00ed\u00ee\u00ef\u0129\u012b\u012d\u012f\u0131"
+ "\u01d0\u0209\u020b]", "i");
temp = temp.replaceAll("[\u0132]", "IJ");
temp = temp.replaceAll("[\u0133]", "ij");
temp = temp.replaceAll("[\u0134]", "J");
temp = temp.replaceAll("[\u0135]", "j");
temp = temp.replaceAll("[\u0136\u01e8]", "K");
temp = temp.replaceAll("[\u0137\u0138\u01e9]", "k");
temp = temp.replaceAll("[\u0139\u013b\u013d\u013f\u0141]", "L");
temp = temp.replaceAll("[\u013a\u013c\u013e\u0140\u0142\u0234]", "l");
temp = temp.replaceAll("[\u00d1\u0143\u0145\u0147\u014a\u01f8]", "N");
temp = temp.replaceAll("[\u00f1\u0144\u0146\u0148\u0149\u014b\u01f9\u0235]", "n");
temp = temp.replaceAll("[\u00d2\u00d3\u00d4\u00d5\u00d6\u00d8\u014c\u014e\u0150"
+ "\u01d1\u01ea\u01ec\u01fe\u020c\u020e\u022a\u022c\u022e\u0230]", "O");
temp = temp.replaceAll("[\u00f2\u00f3\u00f4\u00f5\u00f6\u00f8\u014d\u014f\u0151"
+ "\u01d2\u01eb\u01ed\u01ff\u020d\u020f\u022b\u022d\u022f\u0231]", "o");
temp = temp.replaceAll("[\u0156\u0158\u0210\u0212]", "R");
temp = temp.replaceAll("[\u0157\u0159\u0211\u0213]", "r");
temp = temp.replaceAll("[\u015a\u015c\u015e\u0160\u0218]", "S");
temp = temp.replaceAll("[\u015b\u015d\u015f\u0161\u0219]", "s");
temp = temp.replaceAll("[\u00de\u0162\u0164\u0166\u021a]", "T");
temp = temp.replaceAll("[\u00fe\u0163\u0165\u0167\u021b\u0236]", "t");
temp = temp.replaceAll("[\u00d9\u00da\u00db\u00dc\u0168\u016a\u016c\u016e\u0170"
+ "\u0172\u01d3\u01d5\u01d7\u01d9\u01db\u0214\u0216]", "U");
temp = temp.replaceAll("[\u00f9\u00fa\u00fb\u00fc\u0169\u016b\u016d\u016f\u0171"
+ "\u0173\u01d4\u01d6\u01d8\u01da\u01dc\u0215\u0217]", "u");
temp = temp.replaceAll("[\u0174]", "W");
temp = temp.replaceAll("[\u0175]", "w");
temp = temp.replaceAll("[\u00dd\u0176\u0178\u0232]", "Y");
temp = temp.replaceAll("[\u00fd\u00ff\u0177\u0233]", "y");
temp = temp.replaceAll("[\u0179\u017b\u017d]", "Z");
temp = temp.replaceAll("[\u017a\u017c\u017e]", "z");
temp = temp.replaceAll("[\u00df]", "SS");
temp = temp.replaceAll("[':,;\\\\/]", " ");
temp = temp.replaceAll(" ", "-");
fileName = temp;
fileName = fileName.replaceAll("\\s+", "");
fileName = fileName.replaceAll("[\\(\\)]", " ");
if (stripDots) {
fileName = fileName.replaceAll("[\\.]", "");
}
if (ascii) {
fileName = fileName.replaceAll("[^a-zA-Z0-9\\-_\\.]", "");
}
if (fileName.length() > 250) {
fileName = fileName.substring(0, 250);
}
return fileName;
}
public int deleteAttachmentList(List<AttachmentReference> attachmentRefList) {
int nrDeleted = 0;
if (attachmentRefList != null) {
nrDeleted = deleteAttachmentMap(buildAttachmentsToDeleteMap(attachmentRefList));
}
return nrDeleted;
}
Map<DocumentReference, List<String>> buildAttachmentsToDeleteMap(
List<AttachmentReference> attachmentRefList) {
Map<DocumentReference, List<String>> attachmentMap = new HashMap<DocumentReference, List<String>>();
for (AttachmentReference attRef : attachmentRefList) {
DocumentReference docRef = attRef.getDocumentReference();
if (attachmentMap.containsKey(docRef)) {
List<String> attList = attachmentMap.get(docRef);
attList.add(attRef.getName());
attachmentMap.put(docRef, attList);
} else {
List<String> attList = new ArrayList<String>();
attList.add(attRef.getName());
attachmentMap.put(docRef, attList);
}
}
return attachmentMap;
}
int deleteAttachmentMap(Map<DocumentReference, List<String>> attachmentMap) {
int nrDeleted = 0;
for (DocumentReference docRef : attachmentMap.keySet()) {
int nrDeletedOnDoc = 0;
try {
XWikiDocument doc = modelAccess.getDocument(docRef);
// Analogue to class DeleteAttachmentAction
String versionCommentList = "";
for (String filename : attachmentMap.get(docRef)) {
try {
XWikiAttachment attachment = getAttachmentNameEqual(doc, filename);
versionCommentList += ", " + filename;
doc.deleteAttachment(attachment, getContext());
nrDeletedOnDoc++;
} catch (AttachmentNotExistsException anee) {
_LOGGER.warn("Tried to delete not existing attachment [{}] on doc [{}]", filename, doc,
anee);
}
}
if (nrDeletedOnDoc > 0) {
doc.setAuthor(getContext().getUser());
// Set "deleted attachment" as the version comment.
doc.setComment(getContext().getMessageTool().get("core.comment."
+ "deleteAttachmentComment", Arrays.asList(versionCommentList.substring(2))));
// Needed to counter a side effect of XWIKI-1982: the attachment is deleted from
// the newdoc.originalDoc as well
doc.setOriginalDocument(doc);
// Also save the document and attachment metadata
modelAccess.saveDocument(doc, "deleted " + nrDeletedOnDoc + " attachments");
}
nrDeleted += nrDeletedOnDoc;
} catch (XWikiException | DocumentLoadException | DocumentNotExistsException
| DocumentSaveException xwe) {
_LOGGER.error("Exception deleting Attachments on doc '{}'", docRef, xwe);
}
}
return nrDeleted;
}
@Override
public boolean existsAttachmentNameEqual(XWikiDocument document, String filename) {
try {
getAttachmentNameEqual(document, filename);
return true;
} catch (AttachmentNotExistsException attNotExistExp) {
_LOGGER.trace("existsAttachmentNameEqual not exist", attNotExistExp);
return false;
}
}
@Override
public boolean existsAttachmentNameEqual(AttachmentReference attachmentRef)
throws DocumentLoadException {
try {
getAttachmentNameEqual(attachmentRef);
return true;
} catch (AttachmentNotExistsException | DocumentNotExistsException attNotExistExp) {
_LOGGER.trace("existsAttachmentNameEqual not exist", attNotExistExp);
return false;
}
}
@Override
public XWikiAttachment getAttachmentNameEqual(XWikiDocument document, String filename)
throws AttachmentNotExistsException {
return modelAccess.getAttachmentNameEqual(document, filename);
}
@Override
public XWikiAttachment getAttachmentNameEqual(AttachmentReference attachmentRef)
throws DocumentLoadException, AttachmentNotExistsException, DocumentNotExistsException {
DocumentReference docRef = new DocumentReference(attachmentRef.extractReference(
EntityType.DOCUMENT));
XWikiDocument doc = modelAccess.getDocument(docRef);
return getAttachmentNameEqual(doc, attachmentRef.getName());
}
@Override
public XWikiAttachment getAttachmentFirstNameMatch(XWikiDocument document,
IAttFileNameMatcherRole attMatcher) throws AttachmentNotExistsException {
String filename = attMatcher.getFileNamePattern();
try {
return getAttachmentNameEqual(document, filename);
} catch (AttachmentNotExistsException anee) {
List<XWikiAttachment> attList = getAttachmentsNameMatch(document, attMatcher);
if (!attList.isEmpty()) {
return attList.get(0);
}
throw anee;
}
}
@Override
public List<XWikiAttachment> getAttachmentsNameMatch(XWikiDocument document,
IAttachmentMatcher attMatcher) {
List<XWikiAttachment> attList = new ArrayList<>();
if (attMatcher != null) {
for (XWikiAttachment attach : document.getAttachmentList()) {
if ((attach != null) && attMatcher.accept(attach)) {
attList.add(attach);
}
}
}
return attList;
}
@Override
public Attachment getApiAttachment(XWikiAttachment attachment) throws NoAccessRightsException {
XWikiDocument doc = attachment.getDoc();
if (rightsAccess.hasAccessLevel(doc.getDocumentReference(), EAccessLevel.VIEW)) {
return getApiAttachmentWithoutRightChecks(attachment);
}
throw new NoAccessRightsException(doc.getDocumentReference(), getContext().getXWikiUser(),
EAccessLevel.VIEW);
}
@Override
public Attachment getApiAttachmentWithoutRightChecks(XWikiAttachment attachment) {
Document fbDoc = attachment.getDoc().newDocument(getContext());
return new Attachment(fbDoc, attachment, getContext());
}
}