/*
* 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.xpn.xwiki.doc;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.inject.Provider;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.tika.Tika;
import org.apache.tika.mime.MediaType;
import org.dom4j.Element;
import org.dom4j.io.DocumentResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.suigeneris.jrcs.rcs.Archive;
import org.suigeneris.jrcs.rcs.Version;
import org.xwiki.filter.input.InputSource;
import org.xwiki.filter.input.StringInputSource;
import org.xwiki.filter.instance.input.DocumentInstanceInputProperties;
import org.xwiki.filter.output.DefaultWriterOutputTarget;
import org.xwiki.filter.output.OutputTarget;
import org.xwiki.filter.xar.output.XAROutputProperties;
import org.xwiki.filter.xml.output.DefaultResultOutputTarget;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.AttachmentReferenceResolver;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.merge.MergeConfiguration;
import com.xpn.xwiki.doc.merge.MergeResult;
import com.xpn.xwiki.internal.filter.XWikiDocumentFilterUtils;
import com.xpn.xwiki.internal.xml.XMLWriter;
import com.xpn.xwiki.user.api.XWikiRightService;
import com.xpn.xwiki.web.Utils;
public class XWikiAttachment implements Cloneable
{
private static final Logger LOGGER = LoggerFactory.getLogger(XWikiAttachment.class);
/**
* {@link Tika} is thread safe and the configuration initialization is quite expensive so we keep it (and it's
* blocking all others detections when initializing despite the fact that they will still all redo the complete
* initialization).
*/
// TODO: move that in a singleton component to be shared between everything that needs it
private static final Tika TIKA = new Tika();
/**
* Used to convert a Document Reference to string (compact form without the wiki part if it matches the current
* wiki).
*/
private static EntityReferenceSerializer<String> getCompactWikiEntityReferenceSerializer()
{
return Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "compactwiki");
}
private static EntityReferenceResolver<String> getXClassEntityReferenceResolver()
{
return Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "xclass");
}
/**
* Used to normalize references.
*/
private DocumentReferenceResolver<EntityReference> getExplicitReferenceDocumentReferenceResolver()
{
return Utils.getComponent(DocumentReferenceResolver.TYPE_REFERENCE, "explicit");
}
private AttachmentReferenceResolver<String> getCurentAttachmentReferenceResolver()
{
return Utils.getComponent(AttachmentReferenceResolver.TYPE_STRING, "current");
}
private XWikiDocument doc;
private long size;
private String mimeType;
private String filename;
private String author;
private DocumentReference authorReference;
private Version version;
private String comment;
private Date date;
private XWikiAttachmentContent attachment_content;
private XWikiAttachmentArchive attachment_archive;
private boolean isMetaDataDirty = false;
private AttachmentReference reference;
private boolean forceSetFilesize;
public XWikiAttachment(XWikiDocument doc, String filename)
{
this();
setDoc(doc);
setFilename(filename);
// We know it's not Hibernate
this.forceSetFilesize = true;
}
public XWikiAttachment()
{
this.size = 0;
this.filename = "";
this.comment = "";
this.date = new Date();
// It might be Hibernate
this.forceSetFilesize = false;
}
public AttachmentReference getReference()
{
if (this.reference == null) {
if (this.doc != null) {
this.reference = new AttachmentReference(this.filename, this.doc.getDocumentReference());
} else {
// Try with current
return getCurentAttachmentReferenceResolver().resolve(this.filename);
}
}
return this.reference;
}
public long getId()
{
if (this.doc == null) {
return this.filename.hashCode();
} else {
return (this.doc.getFullName() + "/" + this.filename).hashCode();
}
}
public void setDocId(long id)
{
}
public long getDocId()
{
return this.doc.getId();
}
public void setId(long id)
{
}
@Override
public Object clone()
{
XWikiAttachment attachment = null;
try {
attachment = getClass().newInstance();
} catch (Exception e) {
// This should not happen
LOGGER.error("exception while attach.clone", e);
}
attachment.author = this.author;
attachment.authorReference = this.authorReference;
attachment.setComment(getComment());
attachment.setDate(getDate());
attachment.setFilename(getFilename());
attachment.setMimeType(getMimeType());
attachment.setLongSize(getLongSize());
attachment.setRCSVersion(getRCSVersion());
attachment.setMetaDataDirty(isMetaDataDirty());
if (getAttachment_content() != null) {
attachment.setAttachment_content((XWikiAttachmentContent) getAttachment_content().clone());
attachment.getAttachment_content().setAttachment(attachment);
}
if (getAttachment_archive() != null) {
attachment.setAttachment_archive((XWikiAttachmentArchive) getAttachment_archive().clone());
attachment.getAttachment_archive().setAttachment(attachment);
}
attachment.setDoc(getDoc());
return attachment;
}
/**
* @return the number of bytes in this attachment content
* @deprecated since 9.0RC1, use {@link #getLongSize()} instead
*/
@Deprecated
public int getFilesize()
{
long longSize = getLongSize();
return longSize > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) longSize;
}
/**
* Set cached filesize of the attachment that will be stored as metadata.
*
* @param filesize the number of bytes in this attachment content
* @deprecated since 9.0RC1, use {@link #setLongSize(long)} instead
*/
@Deprecated
public void setFilesize(int filesize)
{
// There is no way to tell Hibernate to not call #setFilesize and we don't want to break the size if it's bigger
// than an int (#setFilesize is usually called after setLongSize by Hibernate)
if (filesize >= 0 || filesize < Integer.MAX_VALUE || this.forceSetFilesize) {
setLongSize(filesize);
}
}
/**
* @return the metadata holding the number of bytes in this attachment content
* @since 9.0RC1
*/
public long getLongSize()
{
return this.size;
}
/**
* The size is automatically calculated from the attachment content so this method is mostly internal API that
* should not be used.
*
* @param size the metadata holding the number of bytes in this attachment content
* @since 9.0RC1
*/
public void setLongSize(long size)
{
if (size != this.size) {
setMetaDataDirty(true);
}
this.size = size;
}
/**
* @param context current XWikiContext
* @return the real filesize in byte of the attachment. We cannot trust the metadata that may be publicly changed.
* @throws XWikiException
* @since 2.3M2
* @deprecated since 9.0RC1, use {@link #getContentLongSize(XWikiContext)} instead
*/
@Deprecated
public int getContentSize(XWikiContext context) throws XWikiException
{
long longSize = getContentLongSize(context);
return longSize > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) longSize;
}
/**
* @param context current XWikiContext
* @return the real filesize in byte of the attachment. We cannot trust the metadata that may be publicly changed.
* @throws XWikiException
* @since 9.0RC1
*/
public long getContentLongSize(XWikiContext context) throws XWikiException
{
if (this.attachment_content == null && context != null) {
this.doc.loadAttachmentContent(this, context);
}
return this.attachment_content.getLongSize();
}
public String getFilename()
{
return this.filename;
}
public void setFilename(String filename)
{
if (ObjectUtils.notEqual(getFilename(), filename)) {
setMetaDataDirty(true);
this.filename = filename;
}
this.reference = null;
}
/**
* @since 6.4M1
*/
public DocumentReference getAuthorReference()
{
if (this.authorReference == null) {
if (this.doc != null) {
this.authorReference = userStringToReference(this.author);
} else {
// Don't store the reference when generated based on context (it might become wrong when actually
// setting the document)
return userStringToReference(this.author);
}
}
return this.authorReference;
}
/**
* @since 6.4M1
*/
public void setAuthorReference(DocumentReference authorReference)
{
if (ObjectUtils.notEqual(authorReference, getAuthorReference())) {
setMetaDataDirty(true);
}
this.authorReference = authorReference;
this.author = null;
// Log this since it's probably a mistake so that we find who is doing bad things
if (this.authorReference != null && this.authorReference.getName().equals(XWikiRightService.GUEST_USER)) {
LOGGER.warn("A reference to XWikiGuest user has been set instead of null. This is probably a mistake.",
new Exception("See stack trace"));
}
}
/**
* Note that this method cannot be removed for now since it's used by Hibernate for saving a XWikiDocument.
*
* @deprecated since 6.4M1 use {@link #getAuthorReference()} instead
*/
@Deprecated
public String getAuthor()
{
if (this.author == null) {
this.author = userReferenceToString(getAuthorReference());
}
return this.author != null ? this.author : "";
}
/**
* Note that this method cannot be removed for now since it's used by Hibernate for loading a XWikiDocument.
*
* @deprecated since 6.4M1 use {@link #setAuthorReference} instead
*/
@Deprecated
public void setAuthor(String author)
{
if (!Objects.equals(getAuthor(), author)) {
this.author = author;
this.authorReference = null;
setMetaDataDirty(true);
}
}
public String getVersion()
{
return getRCSVersion().toString();
}
public void setVersion(String version)
{
this.version = new Version(version);
}
public String getNextVersion()
{
if (this.version == null) {
return "1.1";
} else {
return ((Version) this.version.clone()).next().toString();
}
}
public Version getRCSVersion()
{
if (this.version == null) {
return new Version("1.1");
}
return (Version) this.version.clone();
}
public void setRCSVersion(Version version)
{
this.version = version;
}
public String getComment()
{
return this.comment != null ? this.comment : "";
}
public void setComment(String comment)
{
if (ObjectUtils.notEqual(getComment(), comment)) {
setMetaDataDirty(true);
this.comment = comment;
}
}
public XWikiDocument getDoc()
{
return this.doc;
}
public void setDoc(XWikiDocument doc)
{
if (this.doc != doc) {
this.doc = doc;
this.reference = null;
if (isMetaDataDirty() && doc != null) {
doc.setMetaDataDirty(true);
}
if (getAttachment_content() != null) {
getAttachment_content().setOwnerDocument(doc);
}
}
}
public Date getDate()
{
return this.date;
}
public void setDate(Date date)
{
// Make sure we drop milliseconds for consistency with the database
if (date != null) {
date.setTime((date.getTime() / 1000) * 1000);
}
this.date = date;
}
public boolean isContentDirty()
{
if (this.attachment_content == null) {
return false;
} else {
return this.attachment_content.isContentDirty();
}
}
public void incrementVersion()
{
if (this.version == null) {
this.version = new Version("1.1");
} else {
this.version = this.version.next();
}
}
public boolean isMetaDataDirty()
{
return this.isMetaDataDirty;
}
public void setMetaDataDirty(boolean metaDataDirty)
{
this.isMetaDataDirty = metaDataDirty;
if (metaDataDirty && this.doc != null) {
this.doc.setMetaDataDirty(true);
}
}
/**
* Retrieve an attachment as an XML string. You should prefer
* {@link #toXML(com.xpn.xwiki.internal.xml.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext)} to avoid
* memory loads when appropriate.
*
* @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
* @param bWithVersions if true, all archived versions are also included
* @param context current XWikiContext
* @return a string containing an XML representation of the attachment
* @throws XWikiException when an error occurs during wiki operations
*/
public String toStringXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
throws XWikiException
{
try {
StringWriter writer = new StringWriter();
toXML(new DefaultWriterOutputTarget(writer), bWithAttachmentContent, bWithVersions, true, context);
return writer.toString();
} catch (IOException e) {
LOGGER.error("Failed to write attachment XML", e);
return "";
}
}
/**
* Retrieve XML representation of attachment's metadata into an {@link Element}.
*
* @return a {@link Element} containing an XML representation of the attachment without content
* @throws XWikiException when an error occurs during wiki operations
*/
public Element toXML(XWikiContext context) throws XWikiException
{
return toXML(false, false, context);
}
/**
* Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
*
* @param wr the XMLWriter to write to
* @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
* @param bWithVersions if true, all archive version is also included
* @param context current XWikiContext
* @throws IOException when an error occurs during streaming operation
* @throws XWikiException when an error occurs during xwiki operation
* @since 2.3M2
*/
public void toXML(XMLWriter wr, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
throws IOException, XWikiException
{
// IMPORTANT: we don't use directly XMLWriter's SAX apis here because it's not really working well
DocumentResult domResult = new DocumentResult();
toXML(new DefaultResultOutputTarget(domResult), bWithAttachmentContent, bWithVersions, true, context);
wr.write(domResult.getDocument().getRootElement());
}
/**
* Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
*
* @param out the output where to write the XML
* @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
* @param bWithVersions if true, all archive version is also included
* @param format true if the XML should be formated
* @param context current XWikiContext
* @throws IOException when an error occurs during streaming operation
* @throws XWikiException when an error occurs during xwiki operation
* @since 9.0RC1
*/
public void toXML(OutputTarget out, boolean bWithAttachmentContent, boolean bWithVersions, boolean format,
XWikiContext context) throws IOException, XWikiException
{
// Input
DocumentInstanceInputProperties documentProperties = new DocumentInstanceInputProperties();
documentProperties.setWithWikiAttachmentsContent(bWithAttachmentContent);
documentProperties.setWithJRCSRevisions(bWithVersions);
documentProperties.setWithRevisions(false);
// Output
XAROutputProperties xarProperties = new XAROutputProperties();
xarProperties.setPreserveVersion(bWithVersions);
xarProperties.setEncoding(context.getWiki().getEncoding());
xarProperties.setFormat(format);
try {
Utils.getComponent(XWikiDocumentFilterUtils.class).exportEntity(this, out, xarProperties,
documentProperties);
} catch (Exception e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
"Error parsing xml", e, null);
}
}
/**
* Retrieve XML representation of attachment's metadata into an {@link Element}. You should prefer
* {@link #toXML(com.xpn.xwiki.internal.xml.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext)} to avoid
* memory loads when appropriate.
*
* @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
* @param bWithVersions if true, all archived versions are also included
* @param context current XWikiContext
* @return an {@link Element} containing an XML representation of the attachment
* @throws XWikiException when an error occurs during wiki operations
* @since 2.3M2
*/
public Element toXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
throws XWikiException
{
DocumentResult domResult = new DocumentResult();
try {
toXML(new DefaultResultOutputTarget(domResult), bWithAttachmentContent, bWithVersions, true, context);
} catch (IOException e) {
throw new RuntimeException(e);
}
return domResult.getDocument().getRootElement();
}
public void fromXML(String source) throws XWikiException
{
if (!source.isEmpty()) {
fromXML(new StringInputSource(source));
}
}
/**
* @param source the XML source to parse
* @throws XWikiException when failing to parse the XML
* @since 9.0RC1
*/
public void fromXML(InputSource source) throws XWikiException
{
try {
Utils.getComponent(XWikiDocumentFilterUtils.class).importEntity(this, source);
} catch (Exception e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
"Error parsing xml", e, null);
}
// The setters we're calling above will set the metadata dirty flag to true since they're changing the
// attachment's identity. However since this method is about loading the attachment from XML it shouldn't be
// considered as dirty.
setMetaDataDirty(false);
}
public void fromXML(Element docel) throws XWikiException
{
// Serialize the Document (could not find a way to convert a dom4j Element into a usable StAX source)
StringWriter writer = new StringWriter();
try {
org.dom4j.io.XMLWriter domWriter = new org.dom4j.io.XMLWriter(writer);
domWriter.write(docel);
domWriter.flush();
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
"Error parsing xml", e, null);
}
// Actually parse the XML
fromXML(writer.toString());
}
public XWikiAttachmentContent getAttachment_content()
{
return this.attachment_content;
}
public void setAttachment_content(XWikiAttachmentContent attachment_content)
{
this.attachment_content = attachment_content;
if (attachment_content != null) {
attachment_content.setOwnerDocument(this.doc);
}
}
public XWikiAttachmentArchive getAttachment_archive()
{
return this.attachment_archive;
}
public void setAttachment_archive(XWikiAttachmentArchive attachment_archive)
{
this.attachment_archive = attachment_archive;
}
/**
* Retrive the content of this attachment as a byte array.
*
* @param context current XWikiContext
* @return a byte array containing the binary data content of the attachment
* @throws XWikiException when an error occurs during wiki operation
* @deprecated use {@link #getContentInputStream(XWikiContext)} instead
*/
@Deprecated
public byte[] getContent(XWikiContext context) throws XWikiException
{
if (this.attachment_content == null && context != null) {
this.doc.loadAttachmentContent(this, context);
}
return this.attachment_content.getContent();
}
/**
* Retrieve the content of this attachment as an input stream.
*
* @param context current XWikiContext
* @return an InputStream to consume for receiving the content of this attachment
* @throws XWikiException when an error occurs during wiki operation
* @since 2.3M2
*/
public InputStream getContentInputStream(XWikiContext context) throws XWikiException
{
if (this.attachment_content == null && context != null) {
if (Objects.equals(this.getVersion(), this.getLatestStoredVersion(context))) {
// Load the attachment content from the xwikiattachment_content table.
this.doc.loadAttachmentContent(this, context);
} else {
// Load the attachment content from the xwikiattachment_archive table.
// We don't use #getAttachmentRevision() because it checks if the requested version equals the version
// of the target attachment (XWIKI-1938).
XWikiAttachment archivedVersion =
this.loadArchive(context).getRevision(this, this.getVersion(), context);
XWikiAttachmentContent content =
archivedVersion != null ? archivedVersion.getAttachment_content() : null;
if (content != null) {
this.setAttachment_content(content);
} else {
// Fall back on the version of the content stored in the xwikiattachment_content table.
this.doc.loadAttachmentContent(this, context);
}
}
}
return this.attachment_content.getContentInputStream();
}
/**
* The {@code xwikiattachment_content} table stores only the latest version of an attachment (which is identified by
* the attachment file name and the owner document reference). The rest of the attachment versions are stored in
* {@code xwikiattachment_archive} table. This method can be used to determine from where to load the attachment
* content.
*
* @param context the XWiki context
* @return the latest stored version of this attachment
*/
private String getLatestStoredVersion(XWikiContext context)
{
try {
XWikiDocument ownerDocumentLastestVersion =
context.getWiki().getDocument(this.doc.getDocumentReference(), context);
XWikiAttachment latestStoredVersion = ownerDocumentLastestVersion.getAttachment(this.filename);
return latestStoredVersion != null ? latestStoredVersion.getVersion() : null;
} catch (XWikiException e) {
LOGGER.warn(ExceptionUtils.getRootCauseMessage(e));
return null;
}
}
/**
* @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
*/
@Deprecated
public Archive getArchive()
{
if (this.attachment_archive == null) {
return null;
} else {
return this.attachment_archive.getRCSArchive();
}
}
/**
* @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
*/
@Deprecated
public void setArchive(Archive archive)
{
if (this.attachment_archive == null) {
this.attachment_archive = new XWikiAttachmentArchive();
this.attachment_archive.setAttachment(this);
}
this.attachment_archive.setRCSArchive(archive);
}
public void setArchive(String data) throws XWikiException
{
if (this.attachment_archive == null) {
this.attachment_archive = new XWikiAttachmentArchive();
this.attachment_archive.setAttachment(this);
}
this.attachment_archive.setArchive(data);
}
public synchronized Version[] getVersions()
{
try {
return getAttachment_archive().getVersions();
} catch (Exception ex) {
LOGGER.warn("Cannot retrieve versions of attachment [{}@{}]: {}",
new Object[] { getFilename(), getDoc().getDocumentReference(), ex.getMessage() });
return new Version[] { new Version(this.getVersion()) };
}
}
/**
* Get the list of all versions up to the current. We assume versions go from 1.1 to the current one This allows not
* to read the full archive file.
*
* @return a list of Version from 1.1 to the current version.
* @throws XWikiException never happens.
*/
public List<Version> getVersionList() throws XWikiException
{
final List<Version> list = new ArrayList<Version>();
final String currentVersion = this.version.toString();
Version v = new Version("1.1");
for (;;) {
list.add(v);
if (v.toString().equals(currentVersion)) {
break;
}
v = v.next();
}
return list;
}
/**
* Set the content of an attachment from a byte array.
*
* @param data a byte array with the binary content of the attachment
* @deprecated use {@link #setContent(java.io.InputStream, int)} instead
*/
@Deprecated
public void setContent(byte[] data)
{
if (this.attachment_content == null) {
this.attachment_content = new XWikiAttachmentContent();
this.attachment_content.setAttachment(this);
}
this.attachment_content.setContent(data);
}
/**
* Set the content of an attachment from an InputStream.
*
* @param is the input stream that will be read
* @param length the length in byte to read
* @throws IOException when an error occurs during streaming operation
* @since 2.3M2
*/
public void setContent(InputStream is, int length) throws IOException
{
if (this.attachment_content == null) {
this.attachment_content = new XWikiAttachmentContent();
this.attachment_content.setAttachment(this);
}
this.attachment_content.setContent(is, length);
}
/**
* Set the content of the attachment from an InputStream.
*
* @param is the input stream that will be read
* @throws IOException when an error occurs during streaming operation
* @since 2.6M1
*/
public void setContent(InputStream is) throws IOException
{
if (this.attachment_content == null) {
this.attachment_content = new XWikiAttachmentContent(this);
}
this.attachment_content.setContent(is);
}
public void loadContent(XWikiContext context) throws XWikiException
{
if (this.attachment_content == null) {
try {
context.getWiki().getAttachmentStore().loadAttachmentContent(this, context, true);
} catch (Exception ex) {
LOGGER.warn(
"Failed to load content for attachment [{}@{}]. "
+ "This attachment is broken, please consider re-uploading it. Internal error: {}",
new Object[] { getFilename(), (this.doc != null) ? this.doc.getDocumentReference() : "<unknown>",
ex.getMessage() });
}
}
}
public XWikiAttachmentArchive loadArchive(XWikiContext context) throws XWikiException
{
if (this.attachment_archive == null) {
try {
this.attachment_archive =
context.getWiki().getAttachmentVersioningStore().loadArchive(this, context, true);
} catch (Exception ex) {
LOGGER.warn(
"Failed to load archive for attachment [{}@{}]. "
+ "This attachment is broken, please consider re-uploading it. Internal error: {}",
new Object[] { getFilename(), (this.doc != null) ? this.doc.getDocumentReference() : "<unknown>",
ex.getMessage() });
}
}
return this.attachment_archive;
}
public void updateContentArchive(XWikiContext context) throws XWikiException
{
if (this.attachment_content == null) {
return;
}
loadArchive(context).updateArchive(context);
}
/**
* Return the stored media type. If none is stored try to detects the media type of this attachment's content using
* {@link Tika}. We first try to determine the media type based on the file name extension and if the extension is
* unknown we try to determine the media type by reading the first bytes of the attachment content.
*
* @param xcontext the XWiki context
* @return the media type of this attachment's content
*/
public String getMimeType(XWikiContext xcontext)
{
String type = getMimeType();
if (StringUtils.isEmpty(type)) {
type = extractMimeType(xcontext);
}
return type;
}
/**
* Return the stored media type.
*
* @return the media type of this attachment's content
* @since 7.1M1
*/
public String getMimeType()
{
return this.mimeType;
}
/**
* @param mimeType the explicit mime type of the file
* @since 7.1M1
*/
public void setMimeType(String mimeType)
{
this.mimeType = mimeType;
}
/**
* Extract the mime type from the file name and content and remember it to be stored.
*
* @param xcontext the {@link XWikiContext} use to load the content if it's not already loaded
* @since 7.1M1
*/
public void resetMimeType(XWikiContext xcontext)
{
this.mimeType = extractMimeType(xcontext);
}
private String extractMimeType(XWikiContext xcontext)
{
// We try name-based detection and then fall back on content-based detection. We don't do this in a single step,
// by passing both the content and the file name to Tika, because the default detector looks at the content
// first which can be an issue for large attachments. Our approach is less accurate but has better performance.
String mediaType = getFilename() != null ? TIKA.detect(getFilename()) : MediaType.OCTET_STREAM.toString();
if (MediaType.OCTET_STREAM.toString().equals(mediaType)) {
try {
// Content-based detection is more accurate but it may require loading the attachment content in memory
// (from the database) if it hasn't been cached as a temporary file yet. This can be an issue for large
// attachments when database storage is used. Only the first bytes are normally read but still this
// process is slower than name-based detection.
//
// We wrap the content input stream in a BufferedInputStream to make sure that all the detectors can
// read the content even if the input stream is configured to auto close when it reaches the end. This
// can happen for small files if AutoCloseInputStream is used, which supports the mark and reset methods
// so Tika uses it directly. In this case, the input stream is automatically closed after the first
// detector reads it so the next detector fails to read it.
mediaType = TIKA.detect(new BufferedInputStream(getContentInputStream(xcontext)));
} catch (Exception e) {
LOGGER.warn("Failed to read the content of [{}] in order to detect its mime type.", getReference());
}
}
return mediaType;
}
public boolean isImage(XWikiContext context)
{
String contenttype = getMimeType(context);
if (contenttype.startsWith("image/")) {
return true;
} else {
return false;
}
}
public XWikiAttachment getAttachmentRevision(String rev, XWikiContext context) throws XWikiException
{
if (StringUtils.equals(rev, this.getVersion())) {
return this;
}
return loadArchive(context).getRevision(this, rev, context);
}
/**
* Apply the provided attachment so that the current one contains the same informations and indicate if it was
* necessary to modify it in any way.
*
* @param attachment the attachment to apply
* @return true if the attachment has been modified
* @since 5.3M2
*/
public boolean apply(XWikiAttachment attachment)
{
boolean modified = false;
if (getLongSize() != attachment.getLongSize()) {
setLongSize(attachment.getLongSize());
modified = true;
}
if (StringUtils.equals(getMimeType(), attachment.getMimeType())) {
setMimeType(attachment.getMimeType());
modified = true;
}
try {
if (!IOUtils.contentEquals(getContentInputStream(null), attachment.getContentInputStream(null))) {
setContent(attachment.getContentInputStream(null));
modified = true;
}
} catch (Exception e) {
LOGGER.error("Failed to compare content of attachments", e);
}
return modified;
}
public boolean equalsData(XWikiAttachment otherAttachment, XWikiContext xcontext) throws XWikiException
{
try {
if (getLongSize() == otherAttachment.getLongSize()) {
InputStream is = getContentInputStream(xcontext);
try {
InputStream otherIS = otherAttachment.getContentInputStream(xcontext);
try {
return IOUtils.contentEquals(is, otherIS);
} finally {
otherIS.close();
}
} finally {
is.close();
}
}
} catch (Exception e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_XWIKI_UNKNOWN,
"Failed to compare attachments", e);
}
return false;
}
public void merge(XWikiAttachment previousAttachment, XWikiAttachment nextAttachment,
MergeConfiguration configuration, XWikiContext xcontext, MergeResult mergeResult)
{
try {
if (equalsData(previousAttachment, xcontext)) {
this.apply(nextAttachment);
mergeResult.setModified(true);
} else {
if (this.equals(nextAttachment)) {
// Already modified as expected in the DB, lets assume the user is prescient
mergeResult.getLog().warn("Attachment [{}] already modified", getReference());
}
}
} catch (Exception e) {
mergeResult.getLog().error("Failed to merge attachment [{}]", this, e);
}
}
/**
* @param userReference the user {@link DocumentReference} to convert to {@link String}
* @return the user as String
*/
private String userReferenceToString(DocumentReference userReference)
{
String userString;
if (userReference != null) {
userString = getCompactWikiEntityReferenceSerializer().serialize(userReference, getReference());
} else {
userString = XWikiRightService.GUEST_USER_FULLNAME;
}
return userString;
}
/**
* @param userString the user {@link String} to convert to {@link DocumentReference}
* @return the user as {@link DocumentReference}
*/
private DocumentReference userStringToReference(String userString)
{
DocumentReference userReference;
if (StringUtils.isEmpty(userString)) {
userReference = null;
} else {
userReference = getExplicitReferenceDocumentReferenceResolver()
.resolve(getXClassEntityReferenceResolver().resolve(userString, EntityType.DOCUMENT), getReference());
if (userReference.getName().equals(XWikiRightService.GUEST_USER)) {
userReference = null;
}
}
return userReference;
}
private XWikiContext getXWikiContext()
{
Provider<XWikiContext> xcontextProvider = Utils.getComponent(XWikiContext.TYPE_PROVIDER);
if (xcontextProvider != null) {
return xcontextProvider.get();
}
return null;
}
}