CVE-2023-37913 (published 2023-10-25T18:17Z)

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Starting in version 3.5-milestone-1 and prior to versions 14.10.8 and 15.3-rc-1, triggering the office converter with a specially crafted file name allows writing the attachment's content to an attacker-controlled location on the server as long as the Java process has write access to that location. In particular in the combination with attachment moving, a feature introduced in XWiki 14.0, this is easy to reproduce but it also possible to reproduce in versions as old as XWiki 3.5 by uploading the attachment through the REST API which doesn't remove `/` or `\\` from the filename. As the mime type of the attachment doesn't matter for the exploitation, this could e.g., be used to replace the `jar`-file of an extension which would allow executing arbitrary Java code and thus impact the confidentiality, integrity and availability of the XWiki installation. This vulnerability has been patched in XWiki 14.10.8 and 15.3RC1. There are no known workarounds apart from disabling the office converter.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-03-10 17:26:25+01:00
msg: XWIKI-20715: Don't use user-provided filenames in office import

* Add a method to DocumentAccessBridge for setting the attachment
  content from an input stream
* Apply some basic cleaning to filenames in OfficeConverterFileStorage
* Use fixed filenames for input/output in the office importer and only
  keep the extension.
* Introduce an interface OfficeDocumentArtifact to represent artifacts
  with two implementations that are backed by a file or a byte array,
  respectively.
* Provide a map between true filename and the OfficeDocumentArtifact
  instead of just a set of files in the conversion result.
* Adapt code using the changed APIs and tests.
* Deprecate methods that used the `Set<File>` API for artifacts and move
  them to legacy.
* Replace the static prefix on images by the name of the input file.
* Add a test for the image prefix replacement.
* Use a real XHTMLMarkerResourceReferenceSerializer in ImageFilterTest
  as it simplifies the tests (and adds just a single class as
  dependency).
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: DocumentAccessBridge.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.NotImplementedException;
 import org.xwiki.component.annotation.Role;
 import org.xwiki.model.EntityType;
@@ -539,6 +540,23 @@ default InputStream getAttachmentContent(EntityReference reference) throws Excep
      */
     void setAttachmentContent(AttachmentReference attachmentReference, byte[] attachmentData) throws Exception;
 
+    /**
+     * Sets the content of a document attachment. If the document or the attachment does not exist, both will be created
+     * newly.
+     *
+     * @param attachmentReference the name of the attachment to access
+     * @param attachmentData Attachment content.
+     * @throws Exception If the storage cannot be accessed.
+     * @since 14.10.8
+     * @since 15.3RC1
+     */
+    @Unstable
+    default void setAttachmentContent(AttachmentReference attachmentReference, InputStream attachmentData)
+        throws Exception
+    {
+        setAttachmentContent(attachmentReference, IOUtils.toByteArray(attachmentData));
+    }
+
     /**
      * Sets the content of a document attachment. If the document or the attachment does not exist, both will be created
      * newly.

programming_language: Java
filename: CompatibilityOfficeDocument.java
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,10 @@
  */
 package org.xwiki.officeimporter.document;
 
+import java.io.File;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 public interface CompatibilityOfficeDocument
 {
@@ -30,8 +33,25 @@
      * as artifacts.
      *
      * @return a map containing artifacts for this document.
-     * @deprecated Since 13.1RC1 use {@link OfficeDocument#getArtifactsFiles()}.
+     * @deprecated Since 13.1RC1 use {@link #getArtifactsFiles()}.
      */
     @Deprecated
     Map<String, byte[]> getArtifacts();
+
+    /**
+     * Returns the files corresponding to all the artifacts for this office document, except the conversion of the
+     * document itself.
+     * Artifacts are generated during the import operation if the original office document contains embedded
+     * non-textual elements. Also, some office formats (like presentations) result in multiple output files when
+     * converted into html. In this case all these output files will be considered as artifacts.
+     *
+     * @return the set of artifacts related to this office document.
+     * @since 13.1RC1
+     * @deprecated Use {@link OfficeDocument#getArtifactsMap()} instead.
+     */
+    @Deprecated(since = "15.3RC1, 14.10.8")
+    default Set<File> getArtifactsFiles()
+    {
+        return Collections.emptySet();
+    }
 }

programming_language: Java
filename: XDOMOfficeDocumentCompatibilityAspect.aj
change_type: ModificationType.MODIFY
diff: @@ -20,16 +20,21 @@
 package org.xwiki.officeimporter.document;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.apache.commons.io.IOUtils;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.officeimporter.OfficeImporterException;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 import org.xwiki.rendering.block.XDOM;
 
 public privileged aspect XDOMOfficeDocumentCompatibilityAspect
@@ -37,6 +42,9 @@ public privileged aspect XDOMOfficeDocumentCompatibilityAspect
     @Deprecated
     private Map<String, byte[]> XDOMOfficeDocument.artifacts;
 
+    @Deprecated
+    private Set<File> XDOMOfficeDocument.fileArtifacts;
+
     /**
      * Creates a new {@link XDOMOfficeDocument}.
      *
@@ -48,10 +56,33 @@ public privileged aspect XDOMOfficeDocumentCompatibilityAspect
     @Deprecated
     public XDOMOfficeDocument.new(XDOM xdom, Map<String, byte[]> artifacts, ComponentManager componentManager)
     {
-        this(xdom, Collections.emptySet(), componentManager, null);
+        this(xdom, artifacts.entrySet().stream()
+                .map(entry -> new ByteArrayOfficeDocumentArtifact(entry.getKey(), entry.getValue()))
+                .collect(Collectors.toMap(ByteArrayOfficeDocumentArtifact::getName, Function.identity())),
+            componentManager, null);
         this.artifacts = artifacts;
     }
 
+    /**
+     * Creates a new {@link XDOMOfficeDocument}.
+     *
+     * @param xdom {@link XDOM} corresponding to office document content.
+     * @param artifactFiles artifacts for this office document.
+     * @param componentManager {@link ComponentManager} used to lookup for various renderers.
+     * @param converterResult the {@link OfficeConverterResult} used to build that object.
+     * @since 13.1RC1
+     * @deprecated Use {@link #XDOMOfficeDocument(XDOM, Map, ComponentManager, OfficeConverterResult)} instead.
+     */
+    @Deprecated(since = "14.10.8, 15.3RC1")
+    public XDOMOfficeDocument.new(XDOM xdom, Set<File> artifactFiles, ComponentManager componentManager,
+        OfficeConverterResult converterResult)
+    {
+        this(xdom, artifactFiles.stream().collect(Collectors.toMap(File::getName,
+                file -> new FileOfficeDocumentArtifact(file.getName(), file))),
+            componentManager, converterResult);
+        this.fileArtifacts = artifactFiles;
+    }
+
     /**
      * Overrides {@link CompatibilityOfficeDocument#getArtifacts()}.
      */
@@ -60,20 +91,26 @@ public privileged aspect XDOMOfficeDocumentCompatibilityAspect
     {
         if (this.artifacts == null) {
             this.artifacts = new HashMap<>();
-            FileInputStream fis = null;
-
-            for (File file : this.artifactFiles) {
-                try {
-                    fis = new FileInputStream(file);
-                    this.artifacts.put(file.getName(), IOUtils.toByteArray(fis));
-                } catch (IOException e) {
+            for (Map.Entry<String, OfficeDocumentArtifact> mapItem : this.artifactsMap.entrySet()) {
+                OfficeDocumentArtifact artifact = mapItem.getValue();
+                String fileName = mapItem.getKey();
+                try (InputStream is = artifact.getContentInputStream()) {
+                    this.artifacts.put(fileName, IOUtils.toByteArray(is));
+                } catch (OfficeImporterException | IOException e) {
                     // FIXME
                     e.printStackTrace();
-                } finally {
-                    IOUtils.closeQuietly(fis);
                 }
             }
         }
         return this.artifacts;
     }
+
+    /**
+     * Overrides {@link CompatibilityOfficeDocument#getArtifactsFiles()}.
+     */
+    @Deprecated
+    public Set<File> XDOMOfficeDocument.getArtifactsFiles()
+    {
+        return this.fileArtifacts != null ? this.fileArtifacts : Collections.emptySet();
+    }
 }

programming_language: Java
filename: XHTMLOfficeDocumentCompatibilityAspect.aj
change_type: ModificationType.MODIFY
diff: @@ -20,22 +20,30 @@
 package org.xwiki.officeimporter.document;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.apache.commons.io.IOUtils;
 import org.w3c.dom.Document;
+import org.xwiki.officeimporter.OfficeImporterException;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 
 public privileged aspect XHTMLOfficeDocumentCompatibilityAspect
 {
     @Deprecated
     private Map<String, byte[]> XHTMLOfficeDocument.artifacts;
 
+    @Deprecated
+    private Set<File> XHTMLOfficeDocument.fileArtifacts;
+
     /**
      * Creates a new {@link XHTMLOfficeDocument}.
      *
@@ -46,10 +54,31 @@ public privileged aspect XHTMLOfficeDocumentCompatibilityAspect
     @Deprecated
     public XHTMLOfficeDocument.new(Document document, Map<String, byte[]> artifacts)
     {
-        this(document, Collections.emptySet(), null);
+        this(document, artifacts.entrySet().stream()
+                .map(entry -> new ByteArrayOfficeDocumentArtifact(entry.getKey(), entry.getValue()))
+                .collect(Collectors.toMap(ByteArrayOfficeDocumentArtifact::getName, Function.identity())),
+            null);
         this.artifacts = artifacts;
     }
 
+    /**
+     * Creates a new {@link XHTMLOfficeDocument}.
+     *
+     * @param document the w3c dom representing the office document.
+     * @param artifactFiles artifacts for this office document.
+     * @param converterResult the {@link OfficeConverterResult} used to build that object.
+     * @since 13.1RC1
+     * @deprecated Use {@link #XHTMLOfficeDocument(Document, Map, OfficeConverterResult)} instead.
+     */
+    @Deprecated(since = "14.10.8, 15.3RC1")
+    public XHTMLOfficeDocument.new(Document document, Set<File> artifactFiles, OfficeConverterResult converterResult)
+    {
+        this(document, artifactFiles.stream().collect(Collectors.toMap(File::getName,
+                file -> new FileOfficeDocumentArtifact(file.getName(), file))),
+            converterResult);
+        this.fileArtifacts = artifactFiles;
+    }
+
     /**
      * Overrides {@link CompatibilityOfficeDocument#getArtifacts()}.
      */
@@ -58,20 +87,26 @@ public privileged aspect XHTMLOfficeDocumentCompatibilityAspect
     {
         if (this.artifacts == null) {
             this.artifacts = new HashMap<>();
-            FileInputStream fis = null;
-
-            for (File file : this.artifactFiles) {
-                try {
-                    fis = new FileInputStream(file);
-                    this.artifacts.put(file.getName(), IOUtils.toByteArray(fis));
-                } catch (IOException e) {
+            for (Map.Entry<String, OfficeDocumentArtifact> mapItem : this.artifactsMap.entrySet()) {
+                OfficeDocumentArtifact artifact = mapItem.getValue();
+                String fileName = mapItem.getKey();
+                try (InputStream is = artifact.getContentInputStream()) {
+                    this.artifacts.put(fileName, IOUtils.toByteArray(is));
+                } catch (OfficeImporterException | IOException e) {
                     // FIXME
                     e.printStackTrace();
-                } finally {
-                    IOUtils.closeQuietly(fis);
                 }
             }
         }
         return this.artifacts;
     }
+
+    /**
+     * Overrides {@link CompatibilityOfficeDocument#getArtifactsFiles()}.
+     */
+    @Deprecated(since = "14.10.8, 15.3RC1")
+    public Set<File> XHTMLOfficeDocument.getArtifactsFiles()
+    {
+        return this.fileArtifacts != null ? this.fileArtifacts : Collections.emptySet();
+    }
 }

programming_language: Java
filename: OfficeDocument.java
change_type: ModificationType.MODIFY
diff: @@ -20,12 +20,12 @@
 package org.xwiki.officeimporter.document;
 
 import java.io.Closeable;
-import java.io.File;
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Set;
+import java.util.Map;
 
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.stability.Unstable;
 
 /**
  * Represents an office document being imported.
@@ -57,13 +57,17 @@ public interface OfficeDocument extends Closeable
      * Artifacts are generated during the import operation if the original office document contains embedded
      * non-textual elements. Also, some office formats (like presentations) result in multiple output files when
      * converted into html. In this case all these output files will be considered as artifacts.
+     * <p>
+     * The key of the map is the artifact name.
      *
-     * @return the set of artifacts related to this office document.
-     * @since 13.1RC1
+     * @return the map of artifact names to artifacts related to this office document
+     * @since 14.10.8
+     * @since 15.3RC1
      */
-    default Set<File> getArtifactsFiles()
+    @Unstable
+    default Map<String, OfficeDocumentArtifact> getArtifactsMap()
     {
-        return Collections.emptySet();
+        return Collections.emptyMap();
     }
 
     /**

programming_language: Java
filename: OfficeDocumentArtifact.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,46 @@
+/*
+ * 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 org.xwiki.officeimporter.document;
+
+import java.io.InputStream;
+
+import org.xwiki.officeimporter.OfficeImporterException;
+import org.xwiki.stability.Unstable;
+
+/**
+ * An artifact for an office document.
+ *
+ * @version $Id$
+ * @since 14.10.8
+ * @since 15.3RC1
+ */
+@Unstable
+public interface OfficeDocumentArtifact
+{
+    /**
+     * @return the name of the artifact
+     */
+    String getName();
+
+    /**
+     * @return an input stream for reading the artifact's content
+     */
+    InputStream getContentInputStream() throws OfficeImporterException;
+}

programming_language: Java
filename: XDOMOfficeDocument.java
change_type: ModificationType.MODIFY
diff: @@ -19,9 +19,8 @@
  */
 package org.xwiki.officeimporter.document;
 
-import java.io.File;
 import java.io.IOException;
-import java.util.Set;
+import java.util.Map;
 
 import org.apache.commons.lang3.StringUtils;
 import org.xwiki.component.manager.ComponentLookupException;
@@ -35,6 +34,7 @@
 import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
 import org.xwiki.rendering.renderer.printer.WikiPrinter;
 import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.stability.Unstable;
 
 /**
  * An {@link OfficeDocument} backed by an {@link XDOM} document.
@@ -52,7 +52,7 @@ public class XDOMOfficeDocument implements OfficeDocument
     /**
      * Artifacts for this office document.
      */
-    private Set<File> artifactFiles;
+    private final Map<String, OfficeDocumentArtifact> artifactsMap;
 
     /**
      * {@link ComponentManager} used to lookup for various renderers.
@@ -65,16 +65,18 @@ public class XDOMOfficeDocument implements OfficeDocument
      * Creates a new {@link XDOMOfficeDocument}.
      *
      * @param xdom {@link XDOM} corresponding to office document content.
-     * @param artifactFiles artifacts for this office document.
+     * @param artifacts artifacts for this office document.
      * @param componentManager {@link ComponentManager} used to lookup for various renderers.
      * @param converterResult the {@link OfficeConverterResult} used to build that object.
-     * @since 13.1RC1
+     * @since 14.10.8
+     * @since 15.3-rc-1
      */
-    public XDOMOfficeDocument(XDOM xdom, Set<File> artifactFiles, ComponentManager componentManager,
-        OfficeConverterResult converterResult)
+    @Unstable
+    public XDOMOfficeDocument(XDOM xdom, Map<String, OfficeDocumentArtifact> artifacts,
+        ComponentManager componentManager, OfficeConverterResult converterResult)
     {
         this.xdom = xdom;
-        this.artifactFiles = artifactFiles;
+        this.artifactsMap = artifacts;
         this.componentManager = componentManager;
         this.converterResult = converterResult;
     }
@@ -111,9 +113,9 @@ public String getContentAsString(String syntaxId)
     }
 
     @Override
-    public Set<File> getArtifactsFiles()
+    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
     {
-        return this.artifactFiles;
+        return this.artifactsMap;
     }
 
     /**

programming_language: Java
filename: XHTMLOfficeDocument.java
change_type: ModificationType.MODIFY
diff: @@ -19,12 +19,12 @@
  */
 package org.xwiki.officeimporter.document;
 
-import java.io.File;
 import java.io.IOException;
-import java.util.Set;
+import java.util.Map;
 
 import org.w3c.dom.Document;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.stability.Unstable;
 import org.xwiki.xml.html.HTMLUtils;
 
 /**
@@ -43,7 +43,7 @@ public class XHTMLOfficeDocument implements OfficeDocument
     /**
      * Artifacts for this office document.
      */
-    private Set<File> artifactFiles;
+    private Map<String, OfficeDocumentArtifact> artifactsMap;
 
     private OfficeConverterResult converterResult;
 
@@ -51,14 +51,17 @@ public class XHTMLOfficeDocument implements OfficeDocument
      * Creates a new {@link XHTMLOfficeDocument}.
      *
      * @param document the w3c dom representing the office document.
-     * @param artifactFiles artifacts for this office document.
+     * @param artifacts artifacts for this office document.
      * @param converterResult the {@link OfficeConverterResult} used to build that object.
-     * @since 13.1RC1
+     * @since 14.10.8
+     * @since 15.3RC1
      */
-    public XHTMLOfficeDocument(Document document, Set<File> artifactFiles, OfficeConverterResult converterResult)
+    @Unstable
+    public XHTMLOfficeDocument(Document document, Map<String, OfficeDocumentArtifact> artifacts,
+        OfficeConverterResult converterResult)
     {
         this.document = document;
-        this.artifactFiles = artifactFiles;
+        this.artifactsMap = artifacts;
         this.converterResult = converterResult;
     }
 
@@ -75,9 +78,9 @@ public String getContentAsString()
     }
 
     @Override
-    public Set<File> getArtifactsFiles()
+    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
     {
-        return this.artifactFiles;
+        return this.artifactsMap;
     }
 
     @Override

programming_language: Java
filename: ModelBridge.java
change_type: ModificationType.MODIFY
diff: @@ -19,20 +19,20 @@
  */
 package org.xwiki.officeimporter.internal;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.util.Set;
+import java.io.InputStream;
+import java.util.Map;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
-import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.slf4j.Logger;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.officeimporter.OfficeImporterException;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
 import org.xwiki.security.authorization.Right;
@@ -117,7 +117,7 @@ public void save(XDOMOfficeDocument doc, DocumentReference documentReference, St
         }
 
         // Finally attach all the artifacts into target document.
-        attachArtifacts(doc.getArtifactsFiles(), documentReference);
+        attachArtifacts(doc.getArtifactsMap(), documentReference);
     }
 
     /**
@@ -126,17 +126,17 @@ public void save(XDOMOfficeDocument doc, DocumentReference documentReference, St
      * @param artifactFiles set of artifact files.
      * @param targetDocumentReference target wiki page into which artifacts are to be attached
      */
-    private void attachArtifacts(Set<File> artifactFiles, DocumentReference targetDocumentReference)
+    private void attachArtifacts(Map<String, OfficeDocumentArtifact> artifactFiles,
+        DocumentReference targetDocumentReference)
     {
-        for (File artifact : artifactFiles) {
-            AttachmentReference attachmentReference =
-                new AttachmentReference(artifact.getName(), targetDocumentReference);
-            try (FileInputStream fis = new FileInputStream(artifact)) {
-                this.docBridge.setAttachmentContent(attachmentReference, IOUtils.toByteArray(fis));
+        artifactFiles.forEach((filename, artifact) -> {
+            AttachmentReference attachmentReference = new AttachmentReference(filename, targetDocumentReference);
+            try (InputStream is = artifact.getContentInputStream()) {
+                this.docBridge.setAttachmentContent(attachmentReference, is);
             } catch (Exception ex) {
-                // Log the error and skip the artifact.
-                this.logger.error("Error while attaching artifact.", ex);
+                // Log the error as warning and skip the artifact.
+                this.logger.warn("Error while attaching artifact: [{}].", ExceptionUtils.getRootCauseMessage(ex));
             }
-        }
+        });
     }
 }

programming_language: Java
filename: DefaultPresentationBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -25,10 +25,8 @@
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
-import java.nio.file.Files;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -52,7 +50,10 @@
 import org.xwiki.officeimporter.builder.PresentationBuilder;
 import org.xwiki.officeimporter.converter.OfficeConverterException;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
+import org.xwiki.officeimporter.internal.converter.OfficeConverterFileStorage;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.ExpandedMacroBlock;
@@ -60,6 +61,7 @@
 import org.xwiki.rendering.listener.MetaData;
 import org.xwiki.rendering.parser.Parser;
 import org.xwiki.rendering.renderer.BlockRenderer;
+import org.xwiki.xml.XMLUtils;
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.html.HTMLCleanerConfiguration;
 import org.xwiki.xml.html.HTMLUtils;
@@ -119,18 +121,14 @@ public class DefaultPresentationBuilder implements PresentationBuilder
     public XDOMOfficeDocument build(InputStream officeFileStream, String officeFileName,
         DocumentReference documentReference) throws OfficeImporterException
     {
-        // Accents seems to cause issues in some conditions
-        // See https://jira.xwiki.org/browse/XWIKI-14692
-        String cleanedOfficeFileName = StringUtils.stripAccents(officeFileName);
-
         // Invoke the office document converter.
-        OfficeConverterResult officeConverterResult = importPresentation(officeFileStream, cleanedOfficeFileName);
+        OfficeConverterResult officeConverterResult = importPresentation(officeFileStream, officeFileName);
 
-        Pair<String, Set<File>> htmlPresentationResult = null;
+        Pair<String, Map<String, OfficeDocumentArtifact>> htmlPresentationResult;
         // Create presentation HTML.
         try {
             htmlPresentationResult = buildPresentationHTML(officeConverterResult,
-                StringUtils.substringBeforeLast(cleanedOfficeFileName, "."));
+                StringUtils.substringBeforeLast(officeFileName, "."));
         } catch (IOException e) {
             throw new OfficeImporterException("Error while preparing the presentation artifacts.", e);
         }
@@ -157,15 +155,15 @@ public XDOMOfficeDocument build(InputStream officeFileStream, String officeFileN
     protected OfficeConverterResult importPresentation(InputStream officeFileStream, String officeFileName)
         throws OfficeImporterException
     {
-        Map<String, InputStream> inputStreams = new HashMap<String, InputStream>();
-        inputStreams.put(officeFileName, officeFileStream);
+        String inputFileName = OfficeConverterFileStorage.getSafeInputFilenameFromExtension(officeFileName);
+        Map<String, InputStream> inputStreams = Map.of(inputFileName, officeFileStream);
         try {
             // The office converter uses the output file name extension to determine the output format/syntax.
             // The returned artifacts are of three types: imgX.jpg (slide screen shot), imgX.html (HTML page that
             // display the corresponding slide screen shot) and textX.html (HTML page that display the text extracted
             // from the corresponding slide). We use "img0.html" as the output file name because the corresponding
             // artifact displays a screen shot of the first presentation slide.
-            return this.officeServer.getConverter().convertDocument(inputStreams, officeFileName, "img0.html");
+            return this.officeServer.getConverter().convertDocument(inputStreams, inputFileName, "img0.html");
         } catch (OfficeConverterException e) {
             String message = "Error while converting document [%s] into html.";
             throw new OfficeImporterException(String.format(message, officeFileName), e);
@@ -183,22 +181,19 @@ protected OfficeConverterResult importPresentation(InputStream officeFileStream,
      * @param nameSpace the prefix to add in front of all slide image names to prevent name conflicts
      * @return the presentation HTML
      */
-    protected Pair<String, Set<File>> buildPresentationHTML(OfficeConverterResult officeConverterResult, String nameSpace)
-        throws IOException
+    protected Pair<String, Map<String, OfficeDocumentArtifact>> buildPresentationHTML(
+        OfficeConverterResult officeConverterResult, String nameSpace) throws IOException
     {
-        Set<File> artifactFiles = new HashSet<>();
+        Map<String, OfficeDocumentArtifact> artifactFiles = new HashMap<>();
         // Iterate all the slides.
         Set<File> conversionOutputFiles = officeConverterResult.getAllFiles();
-        File outputDirectory = officeConverterResult.getOutputDirectory();
         Map<Integer, String> filenames = new HashMap<>();
         for (File conversionOutputFile : conversionOutputFiles) {
             Matcher matcher = SLIDE_FORMAT.matcher(conversionOutputFile.getName());
             if (matcher.matches()) {
                 String number = matcher.group("number");
                 String slideImageName = String.format("%s-slide%s.jpg", nameSpace, number);
-                File artifact = new File(outputDirectory, slideImageName);
-                artifactFiles.add(artifact);
-                Files.copy(conversionOutputFile.toPath(), artifact.toPath());
+                artifactFiles.put(slideImageName, new FileOfficeDocumentArtifact(slideImageName, conversionOutputFile));
                 // Append slide image to the presentation HTML.
                 String slideImageURL = null;
                 try {
@@ -219,7 +214,7 @@ protected Pair<String, Set<File>> buildPresentationHTML(OfficeConverterResult of
         }
         // We sort by number so that the filenames are ordered by slide number.
         String presentationHTML = filenames.entrySet().stream().sorted(Map.Entry.comparingByKey())
-            .map(entry -> String.format("<p><img src=\"%s\"/></p>", entry.getValue()))
+            .map(entry -> String.format("<p><img src=\"%s\"/></p>", XMLUtils.escapeAttributeValue(entry.getValue())))
             .collect(Collectors.joining());
         return Pair.of(presentationHTML, artifactFiles);
     }

programming_language: Java
filename: DefaultXDOMOfficeDocumentBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -100,7 +100,7 @@ public XDOMOfficeDocument build(XHTMLOfficeDocument xhtmlOfficeDocument) throws
         } catch (ParseException ex) {
             throw new OfficeImporterException("Error: Could not parse xhtml office content.", ex);
         }
-        return new XDOMOfficeDocument(xdom, xhtmlOfficeDocument.getArtifactsFiles(), this.componentManager,
+        return new XDOMOfficeDocument(xdom, xhtmlOfficeDocument.getArtifactsMap(), this.componentManager,
             xhtmlOfficeDocument.getConverterResult());
     }
 }

programming_language: Java
filename: DefaultXHTMLOfficeDocumentBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -21,23 +21,17 @@
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.FileReader;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.tika.parser.html.HtmlEncodingDetector;
 import org.w3c.dom.Document;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.model.reference.DocumentReference;
@@ -46,7 +40,11 @@
 import org.xwiki.officeimporter.builder.XHTMLOfficeDocumentBuilder;
 import org.xwiki.officeimporter.converter.OfficeConverterException;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XHTMLOfficeDocument;
+import org.xwiki.officeimporter.internal.converter.OfficeConverterFileStorage;
+import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.html.HTMLCleanerConfiguration;
@@ -80,42 +78,43 @@ public class DefaultXHTMLOfficeDocumentBuilder implements XHTMLOfficeDocumentBui
     @Named("openoffice")
     private HTMLCleaner officeHtmlCleaner;
 
-    /**
-     * Used to determine the encoding of the HTML byte array produced by the office server.
-     */
-    private HtmlEncodingDetector htmlEncodingDetector = new HtmlEncodingDetector();
-
     @Override
     public XHTMLOfficeDocument build(InputStream officeFileStream, String officeFileName, DocumentReference reference,
         boolean filterStyles) throws OfficeImporterException
     {
-        // Accents seems to cause issues in some conditions
-        // See https://jira.xwiki.org/browse/XWIKI-14692
-        String cleanedOfficeFileName = StringUtils.stripAccents(officeFileName);
+        String inputFileName = OfficeConverterFileStorage.getSafeInputFilenameFromExtension(officeFileName);
 
         // Invoke the office document converter.
-        Map<String, InputStream> inputStreams = new HashMap<String, InputStream>();
-        inputStreams.put(cleanedOfficeFileName, officeFileStream);
+        Map<String, InputStream> inputStreams = new HashMap<>();
+        inputStreams.put(inputFileName, officeFileStream);
         // The office converter uses the output file name extension to determine the output format/syntax.
-        String outputFileName = StringUtils.substringBeforeLast(cleanedOfficeFileName, ".") + ".html";
+        String outputFileName = "output.html";
         OfficeConverterResult officeConverterResult;
         try {
             officeConverterResult =
-                this.officeServer.getConverter().convertDocument(inputStreams, cleanedOfficeFileName, outputFileName);
+                this.officeServer.getConverter().convertDocument(inputStreams, inputFileName, outputFileName);
         } catch (OfficeConverterException ex) {
             String message = "Error while converting document [%s] into html.";
             throw new OfficeImporterException(String.format(message, officeFileName), ex);
         }
 
-        Document xhtmlDoc = this.cleanAndCreateFile(reference, filterStyles, officeConverterResult);
-        Set<File> artifacts = this.handleArtifacts(xhtmlDoc, officeConverterResult);
+        // Replace the prefix "output_html" that JODConverter/LibreOffice prepend based on the output file name by
+        // prefix based on the user-provided input name
+        String replacePrefix = "output_html_";
+        String replacementPrefix = StringUtils.substringBeforeLast(officeFileName, ".") + "_";
+
+        Document xhtmlDoc = this.cleanAndCreateFile(reference, filterStyles, officeConverterResult,
+            replacePrefix, replacementPrefix);
+        Map<String, OfficeDocumentArtifact> artifacts = this.handleArtifacts(xhtmlDoc, officeConverterResult,
+            replacePrefix, replacementPrefix);
 
         // Return a new XHTMLOfficeDocument instance.
         return new XHTMLOfficeDocument(xhtmlDoc, artifacts, officeConverterResult);
     }
 
     private Document cleanAndCreateFile(DocumentReference reference, boolean filterStyles,
-        OfficeConverterResult officeConverterResult) throws OfficeImporterException
+        OfficeConverterResult officeConverterResult, String replacePrefix, String replacementPrefix)
+        throws OfficeImporterException
     {
         // Prepare the parameters for HTML cleaning.
         Map<String, String> params = new HashMap<String, String>();
@@ -123,6 +122,10 @@ private Document cleanAndCreateFile(DocumentReference reference, boolean filterS
         // Extract the images that are embedded through the Data URI scheme and add them to the other artifacts so that
         // they end up as attachments.
         params.put("attachEmbeddedImages", "true");
+        // Replace the prefix of the static output filename by the replacement based on the user-provided input
+        // filename.
+        params.put("replaceImagePrefix", replacePrefix);
+        params.put("replacementImagePrefix", replacementPrefix);
         if (filterStyles) {
             params.put("filterStyles", "strict");
         }
@@ -141,25 +144,27 @@ private Document cleanAndCreateFile(DocumentReference reference, boolean filterS
         return this.officeHtmlCleaner.clean(html, configuration);
     }
 
-    private Set<File> handleArtifacts(Document xhtmlDoc, OfficeConverterResult officeConverterResult)
-        throws OfficeImporterException
+    private Map<String, OfficeDocumentArtifact> handleArtifacts(Document xhtmlDoc,
+        OfficeConverterResult officeConverterResult, String replacePrefix, String replacementPrefix)
     {
-        Set<File> artifacts = new HashSet<>(officeConverterResult.getAllFiles());
-        artifacts.remove(officeConverterResult.getOutputFile());
+        Map<String, OfficeDocumentArtifact> artifacts = new HashMap<>();
+        for (File file : officeConverterResult.getAllFiles()) {
+            // Rename the file if it starts with the static prefix similar to the image filter.
+            String filename = file.getName();
+            if (StringUtils.startsWith(filename, replacePrefix)) {
+                filename = replacementPrefix + StringUtils.removeStart(filename, replacePrefix);
+            }
+            artifacts.put(filename, new FileOfficeDocumentArtifact(file.getName(), file));
+        }
+        // Remove the output file from the artifacts
+        artifacts.remove(officeConverterResult.getOutputFile().getName());
 
         @SuppressWarnings("unchecked")
         Map<String, byte[]> embeddedImages = (Map<String, byte[]>) xhtmlDoc.getUserData("embeddedImages");
         if (embeddedImages != null) {
-            File outputDirectory = officeConverterResult.getOutputDirectory();
             for (Map.Entry<String, byte[]> embeddedImage : embeddedImages.entrySet()) {
-                File outputFile = new File(outputDirectory, embeddedImage.getKey());
-                try (FileOutputStream fos = new FileOutputStream(outputFile)) {
-                    IOUtils.write(embeddedImage.getValue(), fos);
-                } catch (IOException e) {
-                    throw new OfficeImporterException(
-                        String.format("Error when writing embedded image file [%s]", outputFile.getAbsolutePath()), e);
-                }
-                artifacts.add(outputFile);
+                String fileName = embeddedImage.getKey();
+                artifacts.put(fileName, new ByteArrayOfficeDocumentArtifact(fileName, embeddedImage.getValue()));
             }
         }
         return artifacts;

programming_language: Java
filename: DefaultOfficeConverter.java
change_type: ModificationType.MODIFY
diff: @@ -92,6 +92,9 @@ public DefaultOfficeConverterResult convertDocument(Map<String, InputStream> inp
             OfficeConverterFileStorage storage = new OfficeConverterFileStorage(this.workDir, inputFileName,
                 outputFileName);
 
+            // Check that the potentially cleaned filename is actually in the input streams.
+            this.checkInputStream(inputStreams, storage.getInputFile().getName());
+
             // Write out all the input streams.
             for (Map.Entry<String, InputStream> entry : inputStreams.entrySet()) {
                 File temp = new File(storage.getInputDir(), entry.getKey());

programming_language: Java
filename: OfficeConverterFileStorage.java
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,9 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.UUID;
+import java.util.regex.Pattern;
 
+import org.apache.commons.compress.utils.FileNameUtils;
 import org.apache.commons.lang3.StringUtils;
 
 /**
@@ -34,6 +36,20 @@
  */
 public class OfficeConverterFileStorage
 {
+    /**
+     * Pattern for matching a file extension that is safe, i.e., contains between 1 and 20 alphanumeric
+     * characters. The upper limit is arbitrary but should fit all extensions and is there to ensure that the
+     * filename won't be too long.
+     */
+    private static final Pattern SAFE_EXTENSION = Pattern.compile("^[a-zA-Z0-9]{1,20}$");
+
+    /**
+     * Pattern for matching characters not allowed in filenames. Currently just matches directory separators.
+     */
+    private static final Pattern DISALLOWED_CHARACTERS = Pattern.compile("[/\\\\]");
+
+    private static final String INPUT = "input";
+
     /**
      * Top-level temporary working directory.
      */
@@ -75,11 +91,11 @@ public OfficeConverterFileStorage(File parentDir, String inputFileName, String o
         // Realize the temporary directory hierarchy.
         this.rootDir = new File(parentDir, UUID.randomUUID().toString());
         if (this.rootDir.mkdir()) {
-            this.inputDir = new File(this.rootDir, "input");
+            this.inputDir = new File(this.rootDir, INPUT);
             this.outputDir = new File(this.rootDir, "output");
             if (this.inputDir.mkdir() && this.outputDir.mkdir()) {
-                this.inputFile = new File(this.inputDir, StringUtils.stripAccents(inputFileName));
-                this.outputFile = new File(this.outputDir, StringUtils.stripAccents(outputFileName));
+                this.inputFile = new File(this.inputDir, cleanFilename(inputFileName));
+                this.outputFile = new File(this.outputDir, cleanFilename(outputFileName));
                 success = true;
             }
         }
@@ -91,6 +107,47 @@ public OfficeConverterFileStorage(File parentDir, String inputFileName, String o
         }
     }
 
+    /**
+     * Gets a filename that is safe to use as input filename for a conversion operation.
+     * <p>
+     * The extension is kept from the input filename if it is alphanumeric and contains between 1 and 20 characters.
+     *
+     * @param filename the filename for getting the extension
+     * @return the input filename
+     */
+    public static String getSafeInputFilenameFromExtension(String filename)
+    {
+        String extension = FileNameUtils.getExtension(filename);
+        if (!SAFE_EXTENSION.matcher(extension).matches()) {
+            extension = "";
+        }
+
+        return StringUtils.isBlank(extension) ? INPUT : INPUT + "." + extension;
+    }
+
+    /**
+     * Clean a file name for use as input or output name.
+     *
+     * @param name the filename to clean
+     * @return the cleaned name, shortened to 255 characters if needed
+     */
+    public static String cleanFilename(String name)
+    {
+        String result = DISALLOWED_CHARACTERS.matcher(StringUtils.stripAccents(name)).replaceAll("_");
+
+        // Make sure that the filename is not blank. Don't use an extension so maybe content guessing works.
+        if (StringUtils.isBlank(result)) {
+            result = "fallback";
+        }
+
+        // If the filename is too long, keep the part at the end as it contains the extension.
+        if (result.length() > 255) {
+            result = result.substring(result.length() - 255);
+        }
+
+        return result;
+    }
+
     /**
      * @return {@link File} representing the input directory where the main input document as well as any other
      *         dependent artifacts should be located.

programming_language: Java
filename: ByteArrayOfficeDocumentArtifact.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,88 @@
+/*
+ * 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 org.xwiki.officeimporter.internal.document;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
+
+/**
+ * A {@link OfficeDocumentArtifact} backed by a byte array.
+ *
+ * @version $Id$
+ * @since 14.10.8
+ * @since 15.3RC1
+ */
+public class ByteArrayOfficeDocumentArtifact implements OfficeDocumentArtifact
+{
+    private final String name;
+
+    private final byte[] content;
+
+    /**
+     * Construct a new {@link OfficeDocumentArtifact} backed by a byte array.
+     *
+     * @param name the name of the artifact
+     * @param content the content as byte array
+     */
+    public ByteArrayOfficeDocumentArtifact(String name, byte[] content)
+    {
+        this.name = name;
+        this.content = content;
+    }
+
+    @Override
+    public String getName()
+    {
+        return this.name;
+    }
+
+    @Override
+    public InputStream getContentInputStream()
+    {
+        return new ByteArrayInputStream(this.content);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ByteArrayOfficeDocumentArtifact that = (ByteArrayOfficeDocumentArtifact) o;
+
+        return new EqualsBuilder().append(getName(), that.getName())
+            .append(this.content, that.content).isEquals();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return new HashCodeBuilder(17, 37).append(getName()).toHashCode();
+    }
+}

programming_language: Java
filename: FileOfficeDocumentArtifact.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,95 @@
+/*
+ * 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 org.xwiki.officeimporter.internal.document;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.xwiki.officeimporter.OfficeImporterException;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
+
+/**
+ * A {@link OfficeDocumentArtifact} backed by a file.
+ *
+ * @version $Id$
+ * @since 14.10.8
+ * @since 15.3RC1
+ */
+public class FileOfficeDocumentArtifact implements OfficeDocumentArtifact
+{
+    private final String name;
+
+    private final File content;
+
+    /**
+     * Construct a new {@link OfficeDocumentArtifact} backed by a file.
+     *
+     * @param name the name of the artifact
+     * @param content the content of the artifact
+     */
+    public FileOfficeDocumentArtifact(String name, File content)
+    {
+        this.name = name;
+        this.content = content;
+    }
+
+    @Override
+    public String getName()
+    {
+        return this.name;
+    }
+
+    @Override
+    public InputStream getContentInputStream() throws OfficeImporterException
+    {
+        try {
+            return new FileInputStream(this.content);
+        } catch (FileNotFoundException e) {
+            throw new OfficeImporterException("Artifact file not found", e);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        FileOfficeDocumentArtifact that = (FileOfficeDocumentArtifact) o;
+
+        return new EqualsBuilder().append(getName(), that.getName())
+            .append(this.content, that.content).isEquals();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return new HashCodeBuilder(17, 37).append(getName()).append(this.content).toHashCode();
+    }
+}

programming_language: Java
filename: ImageFilter.java
change_type: ModificationType.MODIFY
diff: @@ -112,11 +112,15 @@ public void filter(Document htmlDocument, Map<String, String> cleaningParams)
             htmlDocument.setUserData(EMBEDDED_IMAGES, new HashMap<String, byte[]>(), null);
         }
 
+        String replaceAttachmentPrefix = cleaningParams.get("replaceImagePrefix");
+        String replacementAttachmentPrefix = cleaningParams.get("replacementImagePrefix");
+
         List<Element> images = filterDescendants(htmlDocument.getDocumentElement(), new String[] {TAG_IMG});
         for (Element image : images) {
             Attr source = image.getAttributeNode(ATTRIBUTE_SRC);
             if (source != null && targetDocumentReference != null) {
-                filterImageSource(source, targetDocumentReference);
+                filterImageSource(source, targetDocumentReference, replaceAttachmentPrefix,
+                    replacementAttachmentPrefix);
             }
 
             // The 'align' attribute of images creates a lot of problems. First,the office server has a problem with
@@ -127,11 +131,12 @@ public void filter(Document htmlDocument, Map<String, String> cleaningParams)
         }
     }
 
-    private void filterImageSource(Attr source, DocumentReference targetDocumentReference)
+    private void filterImageSource(Attr source, DocumentReference targetDocumentReference, String replacePrefix,
+        String replacementPrefix)
     {
         String fileName = null;
         try {
-            fileName = getFileName(source);
+            fileName = getFileName(source, replacePrefix, replacementPrefix);
         } catch (Exception e) {
             this.logger.warn("Failed to extract the image file name. Root cause is [{}]",
                 ExceptionUtils.getRootCauseMessage(e));
@@ -155,7 +160,7 @@ private void filterImageSource(Attr source, DocumentReference targetDocumentRefe
         image.getParentNode().insertBefore(afterComment, image.getNextSibling());
     }
 
-    private String getFileName(Attr source) throws MimeTypeException
+    private String getFileName(Attr source, String replacePrefix, String replacementPrefix) throws MimeTypeException
     {
         String value = source.getValue();
         String fileName = null;
@@ -194,13 +199,25 @@ private String getFileName(Attr source) throws MimeTypeException
                 fileName = fileName.replaceAll("\\+", "%2B");
                 // We have to decode the image file name in case it contains URL special characters.
                 fileName = URLDecoder.decode(fileName, UTF_8);
-
-                // '@' must also be escaped in order to resolve properly the path in XWiki
-                fileName = fileName.replaceAll("@", "\\\\@");
             } catch (Exception e) {
                 // This shouldn't happen. Use the encoded image file name.
             }
+
+            fileName = replacePrefix(replacePrefix, replacementPrefix, fileName);
+            // '@' must also be escaped in order to resolve properly the path in XWiki
+            fileName = fileName.replace("@", "\\@");
         }
         return fileName;
     }
+
+    private static String replacePrefix(String replacePrefix, String replacementPrefix, String fileName)
+    {
+        String result = fileName;
+
+        if (StringUtils.startsWith(fileName, replacePrefix) && replacementPrefix != null) {
+            result = replacementPrefix + StringUtils.removeStart(fileName, replacePrefix);
+        }
+
+        return result;
+    }
 }

programming_language: Java
filename: DefaultXDOMOfficeDocumentSplitter.java
change_type: ModificationType.MODIFY
diff: @@ -19,12 +19,9 @@
  */
 package org.xwiki.officeimporter.internal.splitter;
 
-import java.io.File;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -35,6 +32,7 @@
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.officeimporter.OfficeImporterException;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
 import org.xwiki.officeimporter.splitter.OfficeDocumentSplitterParameters;
 import org.xwiki.officeimporter.splitter.TargetDocumentDescriptor;
@@ -105,10 +103,10 @@ public Map<TargetDocumentDescriptor, XDOMOfficeDocument> split(XDOMOfficeDocumen
             }
 
             // Rewire artifacts.
-            Set<File> artifactsFiles = relocateArtifacts(doc, officeDocument);
+            Map<String, OfficeDocumentArtifact> artifactsMap = relocateArtifacts(doc, officeDocument);
 
             // Create the resulting XDOMOfficeDocument.
-            XDOMOfficeDocument splitDocument = new XDOMOfficeDocument(doc.getXdom(), artifactsFiles, componentManager,
+            XDOMOfficeDocument splitDocument = new XDOMOfficeDocument(doc.getXdom(), artifactsMap, componentManager,
                 officeDocument.getConverterResult());
             result.put(targetDocumentDescriptor, splitDocument);
         }
@@ -117,27 +115,25 @@ public Map<TargetDocumentDescriptor, XDOMOfficeDocument> split(XDOMOfficeDocumen
     }
 
     /**
-     * Move artifacts (i.e. embedded images) from the original office document to a specific wiki document corresponding
-     * to a section. Only the artifacts from that section are moved.
+     * Copy artifacts (i.e. embedded images) from the original office document to a specific wiki document corresponding
+     * to a section. Only the artifacts from that section are copied.
      * 
      * @param sectionDoc the newly created wiki document corresponding to a section of the original office document
      * @param officeDocument the office document being splitted into wiki documents
      * @return the relocated artifacts
      */
-    private Set<File> relocateArtifacts(WikiDocument sectionDoc, XDOMOfficeDocument officeDocument)
+    private Map<String, OfficeDocumentArtifact> relocateArtifacts(WikiDocument sectionDoc,
+        XDOMOfficeDocument officeDocument)
     {
-        Set<File> artifacts = officeDocument.getArtifactsFiles();
-        Set<File> result = new HashSet<>();
+        Map<String, OfficeDocumentArtifact> artifacts = officeDocument.getArtifactsMap();
+        Map<String, OfficeDocumentArtifact> result = new HashMap<>();
         List<ImageBlock> imageBlocks =
             sectionDoc.getXdom().getBlocks(new ClassBlockMatcher(ImageBlock.class), Axes.DESCENDANT);
-        if (!imageBlocks.isEmpty()) {
-            Map<String, File> fileMap = new HashMap<>();
-            artifacts.forEach(item -> fileMap.put(item.getName(), item));
-            for (ImageBlock imageBlock : imageBlocks) {
-                String imageReference = imageBlock.getReference().getReference();
-                File file = fileMap.get(imageReference);
-                result.add(file);
-                artifacts.remove(file);
+        for (ImageBlock imageBlock : imageBlocks) {
+            String imageReference = imageBlock.getReference().getReference();
+            OfficeDocumentArtifact artifact = artifacts.get(imageReference);
+            if (artifact != null) {
+                result.put(imageReference, artifact);
             }
         }
         return result;

programming_language: Java
filename: XDOMOfficeDocumentTest.java
change_type: ModificationType.MODIFY
diff: @@ -79,6 +79,6 @@ private XDOMOfficeDocument createOfficeDocument(String content, String syntax) t
     {
         Parser parser = this.componentManager.getInstance(Parser.class, syntax);
         XDOM xdom = parser.parse(new StringReader(content));
-        return new XDOMOfficeDocument(xdom, Collections.emptySet(), this.componentManager, null);
+        return new XDOMOfficeDocument(xdom, Collections.emptyMap(), this.componentManager, null);
     }
 }

programming_language: Java
filename: ModelBridgeTest.java
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,10 @@
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.InputStream;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.Test;
@@ -30,6 +33,7 @@
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.rendering.syntax.SyntaxType;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
@@ -39,6 +43,10 @@
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -81,7 +89,15 @@ void saveWithOverwrite() throws Exception
 
         when(this.contextualAuthorizationManager.hasAccess(Right.EDIT, documentReference)).thenReturn(true);
         when(doc.getContentAsString(syntaxId)).thenReturn(content);
-        when(doc.getArtifactsFiles()).thenReturn(Collections.singleton(artifact));
+        when(doc.getArtifactsMap())
+            .thenReturn(Collections.singletonMap(fileName, new FileOfficeDocumentArtifact(fileName, artifact)));
+        // Store all attachment contents that are set on the mock.
+        Map<AttachmentReference, byte[]> attachmentContents = new HashMap<>();
+        doAnswer((invocation) -> attachmentContents.put(
+            invocation.getArgument(0, AttachmentReference.class),
+            IOUtils.toByteArray(invocation.getArgument(1, InputStream.class))
+        )).when(this.documentAccessBridge)
+            .setAttachmentContent(any(AttachmentReference.class), any(InputStream.class));
 
         this.modelBridge.save(doc, documentReference, syntaxId, parentReference, title, false);
 
@@ -90,8 +106,10 @@ void saveWithOverwrite() throws Exception
             false);
         verify(documentAccessBridge).setDocumentParentReference(documentReference, parentReference);
         verify(documentAccessBridge).setDocumentTitle(documentReference, title);
-        verify(documentAccessBridge).setAttachmentContent(new AttachmentReference(fileName, documentReference),
-            fileContent);
+        assertEquals(1, attachmentContents.size());
+        AttachmentReference expectedAttachmentReference = new AttachmentReference(fileName, documentReference);
+        assertEquals(expectedAttachmentReference, attachmentContents.keySet().iterator().next());
+        assertArrayEquals(fileContent, attachmentContents.get(expectedAttachmentReference));
     }
 
     @Test

programming_language: Java
filename: DefaultPresentationBuilderTest.java
change_type: ModificationType.MODIFY
diff: @@ -28,7 +28,9 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import javax.inject.Named;
@@ -46,7 +48,9 @@
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.officeimporter.converter.OfficeConverter;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
+import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.ExpandedMacroBlock;
@@ -151,7 +155,7 @@ void build() throws Exception
 
         when(officeConverterResult.getAllFiles()).thenReturn(allFiles);
 
-        when(this.officeConverter.convertDocument(Collections.singletonMap("file.odp", officeFileStream), "file.odp",
+        when(this.officeConverter.convertDocument(Collections.singletonMap("input.odp", officeFileStream), "input.odp",
             "img0.html")).thenReturn(officeConverterResult);
 
         HTMLCleanerConfiguration config = mock(HTMLCleanerConfiguration.class);
@@ -170,9 +174,11 @@ void build() throws Exception
         XDOMOfficeDocument result = this.presentationBuilder.build(officeFileStream, "file.odp", documentReference);
 
         verify(config).setParameters(Collections.singletonMap("targetDocument", "wiki:Path.To.Page"));
-        Set<File> expectedArtifacts = slideNumbers.stream().map(slideNumber ->
-            new File(this.outputDirectory, String.format("file-slide%d.jpg", slideNumber))).collect(Collectors.toSet());
-        assertEquals(expectedArtifacts, result.getArtifactsFiles());
+        Map<String, OfficeDocumentArtifact> expectedArtifacts = slideNumbers.stream()
+            .map(slideNumber -> new FileOfficeDocumentArtifact(String.format("file-slide%d.jpg", slideNumber),
+                new File(this.outputDirectory, String.format("img%d.jpg", slideNumber))))
+            .collect(Collectors.toMap(OfficeDocumentArtifact::getName, Function.identity()));
+        assertEquals(expectedArtifacts, result.getArtifactsMap());
 
         assertEquals("wiki:Path.To.Page", result.getContentDocument().getMetaData().getMetaData(MetaData.BASE));
 

programming_language: Java
filename: DefaultXDOMOfficeDocumentBuilderTest.java
change_type: ModificationType.MODIFY
diff: @@ -57,7 +57,7 @@
  * @since 2.1M1
  */
 @ComponentTest
-public class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
+class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
 {
     /**
      * The name of an input file to be used in tests.
@@ -91,12 +91,13 @@ public void setUp() throws Exception
      * Test {@link OfficeDocument} building.
      */
     @Test
-    public void xdomOfficeDocumentBuilding() throws Exception
+    void xdomOfficeDocumentBuilding() throws Exception
     {
         // Create & register a mock document converter to by-pass the office server.
         final InputStream mockOfficeFileStream = new ByteArrayInputStream(new byte[1024]);
-        final Map<String, InputStream> mockInput = new HashMap<String, InputStream>();
-        mockInput.put(INPUT_FILE_NAME, mockOfficeFileStream);
+        final Map<String, InputStream> mockInput = new HashMap<>();
+        String internalInputFilename = "input.doc";
+        mockInput.put(internalInputFilename, mockOfficeFileStream);
         OfficeConverterResult converterResult = mock(OfficeConverterResult.class);
         when(converterResult.getOutputDirectory()).thenReturn(this.outputDirectory);
         File outputFile = new File(this.outputDirectory, OUTPUT_FILE_NAME);
@@ -112,7 +113,7 @@ public void xdomOfficeDocumentBuilding() throws Exception
         final DocumentReference documentReference = new DocumentReference("xwiki", "Main", "Test");
 
         when(mockOfficeServer.getConverter()).thenReturn(mockDocumentConverter);
-        when(mockDocumentConverter.convertDocument(mockInput, INPUT_FILE_NAME, OUTPUT_FILE_NAME))
+        when(mockDocumentConverter.convertDocument(mockInput, internalInputFilename, "output.html"))
             .thenReturn(converterResult);
         when(mockDocumentReferenceResolver.resolve("xwiki:Main.Test")).thenReturn(documentReference);
         when(mockDefaultStringEntityReferenceSerializer.serialize(documentReference)).thenReturn("xwiki:Main.Test");
@@ -121,7 +122,7 @@ public void xdomOfficeDocumentBuilding() throws Exception
             xdomOfficeDocumentBuilder.build(mockOfficeFileStream, INPUT_FILE_NAME, documentReference, true);
         assertEquals("xwiki:Main.Test", document.getContentDocument().getMetaData().getMetaData(MetaData.BASE));
         assertEquals("**Hello There**", document.getContentAsString());
-        assertEquals(0, document.getArtifactsFiles().size());
+        assertEquals(0, document.getArtifactsMap().size());
 
         verify(mockOfficeServer).getConverter();
     }

programming_language: Java
filename: DefaultXHTMLOfficeDocumentBuilderTest.java
change_type: ModificationType.MODIFY
diff: @@ -24,6 +24,7 @@
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collections;
@@ -42,6 +43,7 @@
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.officeimporter.converter.OfficeConverter;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XHTMLOfficeDocument;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.test.junit5.XWikiTempDir;
@@ -51,9 +53,14 @@
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.html.HTMLCleanerConfiguration;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * Test case for {@link DefaultXHTMLOfficeDocumentBuilder}.
@@ -62,7 +69,7 @@
  * @since 2.1M1
  */
 @ComponentTest
-public class DefaultXHTMLOfficeDocumentBuilderTest
+class DefaultXHTMLOfficeDocumentBuilderTest
 {
     @InjectMockComponents
     private DefaultXHTMLOfficeDocumentBuilder officeDocumentBuilder;
@@ -90,7 +97,7 @@ public void configure() throws Exception
     }
 
     @Test
-    public void xhtmlOfficeDocumentBuilding() throws Exception
+    void xhtmlOfficeDocumentBuilding() throws Exception
     {
         DocumentReference documentReference = new DocumentReference("wiki", Arrays.asList("Path", "To"), "Page");
         when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn("wiki:Path.To.Page");
@@ -112,12 +119,18 @@ public void xhtmlOfficeDocumentBuilding() throws Exception
         try (FileOutputStream fos = new FileOutputStream(outputFile)) {
             IOUtils.write("HTML content".getBytes(), fos);
         }
+        String otherArtifactContent = "Other content";
+        try (FileOutputStream fos = new FileOutputStream(otherArtifact)) {
+            IOUtils.write(otherArtifactContent.getBytes(), fos);
+        }
+
         when(converterResult.getAllFiles()).thenReturn(allFiles);
 
-        when(this.officeConverter.convertDocument(Collections.singletonMap("file.odt", officeFileStream), "file.odt",
-            "file.html")).thenReturn(converterResult);
+        when(this.officeConverter.convertDocument(Collections.singletonMap("input.odt", officeFileStream), "input.odt",
+            "output.html")).thenReturn(converterResult);
 
-        Map<String, byte[]> embeddedImages = Collections.singletonMap("image.png", "Image content".getBytes());
+        byte[] imageContent = "Image content".getBytes();
+        Map<String, byte[]> embeddedImages = Collections.singletonMap("image.png", imageContent);
         Document xhtmlDoc = mock(Document.class);
         when(xhtmlDoc.getUserData("embeddedImages")).thenReturn(embeddedImages);
 
@@ -132,13 +145,18 @@ public void xhtmlOfficeDocumentBuilding() throws Exception
         params.put("targetDocument", "wiki:Path.To.Page");
         params.put("attachEmbeddedImages", "true");
         params.put("filterStyles", "strict");
+        params.put("replaceImagePrefix", "output_html_");
+        params.put("replacementImagePrefix", "file_");
         verify(config).setParameters(params);
 
         assertEquals(xhtmlDoc, result.getContentDocument());
 
-        Set<File> expectedFileArtifacts = new HashSet<>();
-        expectedFileArtifacts.add(otherArtifact);
-        expectedFileArtifacts.add(new File(this.outputDirectory, "image.png"));
-        assertEquals(expectedFileArtifacts, result.getArtifactsFiles());
+        Map<String, OfficeDocumentArtifact> actualArtifacts = result.getArtifactsMap();
+        OfficeDocumentArtifact actualOtherArtifact = actualArtifacts.get("file.txt");
+        assertEquals(otherArtifactContent,
+            IOUtils.toString(actualOtherArtifact.getContentInputStream(), StandardCharsets.UTF_8));
+        assertTrue(actualArtifacts.containsKey("image.png"));
+        OfficeDocumentArtifact imageArtifact = actualArtifacts.get("image.png");
+        assertArrayEquals(imageContent, IOUtils.toByteArray(imageArtifact.getContentInputStream()));
     }
 }

programming_language: Java
filename: OfficeConverterFileStorageStorageTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,79 @@
+/*
+ * 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 org.xwiki.officeimporter.internal.converter;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.xwiki.test.junit5.XWikiTempDir;
+import org.xwiki.test.junit5.XWikiTempDirExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit test for {@link OfficeConverterFileStorage}.
+ *
+ * @version $Id$
+ */
+@ExtendWith(XWikiTempDirExtension.class)
+class OfficeConverterFileStorageStorageTest
+{
+    @XWikiTempDir
+    private File tmpDir;
+
+    @Test
+    void invalidCharacters() throws IOException
+    {
+        OfficeConverterFileStorage storage =
+            new OfficeConverterFileStorage(this.tmpDir, "{/in\\§ä.ext", "{/out\\$ä.out");
+
+        assertEquals("{_in_§a.ext", storage.getInputFile().getName());
+        assertEquals("{_out_$a.out", storage.getOutputFile().getName());
+        assertEquals(storage.getInputDir(), storage.getInputFile().getParentFile());
+        assertEquals(storage.getOutputDir(), storage.getOutputFile().getParentFile());
+
+        assertTrue(storage.getInputDir().isDirectory());
+        assertTrue(storage.getOutputDir().isDirectory());
+    }
+
+    @Test
+    void longFilename() throws IOException
+    {
+        String longName = " ".repeat(300) + ".ext";
+        OfficeConverterFileStorage storage = new OfficeConverterFileStorage(this.tmpDir, longName, longName);
+
+        String expectedName = " ".repeat(251) + ".ext";
+        assertEquals(expectedName, storage.getInputFile().getName());
+        assertEquals(expectedName, storage.getOutputFile().getName());
+    }
+
+    @Test
+    void fallback() throws IOException
+    {
+        OfficeConverterFileStorage storage = new OfficeConverterFileStorage(this.tmpDir, "", "");
+
+        String fallback = "fallback";
+        assertEquals(fallback, storage.getInputFile().getName());
+        assertEquals(fallback, storage.getOutputFile().getName());
+    }
+}

programming_language: Java
filename: ImageFilterTest.java
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -37,16 +38,18 @@
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.rendering.internal.renderer.xhtml.XHTMLMarkerResourceReferenceSerializer;
 import org.xwiki.rendering.listener.reference.ResourceReference;
 import org.xwiki.rendering.listener.reference.ResourceType;
-import org.xwiki.rendering.renderer.reference.ResourceReferenceSerializer;
+import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.MockComponent;
 
 import com.github.ooxi.jdatauri.DataUri;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * Unit tests for {@link ImageFilter}.
@@ -54,15 +57,12 @@
  * @version $Id$
  */
 @ComponentTest
+@ComponentList({ XHTMLMarkerResourceReferenceSerializer.class })
 public class ImageFilterTest extends AbstractHTMLFilterTest
 {
     @MockComponent
     private DocumentAccessBridge dab;
 
-    @MockComponent
-    @Named("xhtmlmarker")
-    private ResourceReferenceSerializer xhtmlMarkerSerializer;
-
     @MockComponent
     @Named("currentmixed")
     private DocumentReferenceResolver<String> currentMixedResolver;
@@ -91,7 +91,6 @@ public void filterAddsImageMarkers()
 
         ResourceReference resourceReference = new ResourceReference("-foo--bar.png-", ResourceType.ATTACHMENT);
         resourceReference.setTyped(false);
-        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|-foo--bar.png-");
 
         filterAndAssertOutput("<img src=\"../../some/path/-foo--b%61r.png-\"/>",
             Collections.singletonMap("targetDocument", "Path.To.Page"),
@@ -110,7 +109,6 @@ public void filterAddsImageMarkersSpecialCharacters() throws UnsupportedEncoding
         ResourceReference resourceReference = new ResourceReference(imageName, ResourceType.ATTACHMENT);
         resourceReference.setTyped(false);
         String imageNameEscaped = "foo&+_bar\\\\@.png";
-        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|" + imageName);
 
         filterAndAssertOutput(String.format( "<img src=\"../../some/path/%s\"/>", imageNameUrl),
             Collections.singletonMap("targetDocument", "Path.To.Page"),
@@ -136,7 +134,6 @@ public void filterCollectsEmbeddedImages()
 
         ResourceReference resourceReference = new ResourceReference("foo.png", ResourceType.ATTACHMENT);
         resourceReference.setTyped(false);
-        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|foo.png");
 
         String fileName =
             DataUri.parse("data:image/jpeg;base64,GgoAAAAN==", Charset.forName("UTF-8")).hashCode() + ".jpg";
@@ -145,7 +142,6 @@ public void filterCollectsEmbeddedImages()
 
         resourceReference = new ResourceReference(fileName, ResourceType.ATTACHMENT);
         resourceReference.setTyped(false);
-        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|" + fileName);
 
         Map<String, String> parameters = new HashMap<String, String>();
         parameters.put("targetDocument", "Path.To.Page");
@@ -163,4 +159,25 @@ public void filterCollectsEmbeddedImages()
         Map<String, byte[]> embeddedImages = (Map<String, byte[]>) document.getUserData("embeddedImages");
         assertEquals(new HashSet<>(Arrays.asList("foo.png", fileName)), embeddedImages.keySet());
     }
+
+    @Test
+    void rewriteImagePrefix() throws UnsupportedEncodingException
+    {
+        Map<String, String> configuration = Map.of(
+            "targetDocument", "Path.To.Page",
+            "replaceImagePrefix", "output_",
+            "replacementImagePrefix", "re@placement_"
+        );
+
+        String imageName = "re\\@placement_image.png";
+        String encodedImageName = URLEncoder.encode(imageName.replace("\\@", "@"), StandardCharsets.UTF_8);
+        AttachmentReference attachmentReference = new AttachmentReference(imageName, this.documentReference);
+        when(this.dab.getAttachmentURL(attachmentReference, false)).thenReturn("/path/to/" + encodedImageName);
+
+        filterAndAssertOutput("<img src=\"output_image.png\" />", configuration,
+            "<!--startimage:false|-|attach|-|re\\\\@placement_image.png-->"
+                + "<img src=\"/path/to/re%40placement_image.png\"/><!--stopimage-->");
+
+        verify(this.dab).getAttachmentURL(attachmentReference, false);
+    }
 }

programming_language: Java
filename: DefaultXDOMOfficeDocumentSplitterTest.java
change_type: ModificationType.MODIFY
diff: @@ -87,7 +87,7 @@ void documentSplitting() throws Exception
 
         // Create xdom office document.
         XDOMOfficeDocument officeDocument =
-            new XDOMOfficeDocument(xdom, Collections.emptySet(), this.componentManager, null);
+            new XDOMOfficeDocument(xdom, Collections.emptyMap(), this.componentManager, null);
         final DocumentReference baseDocument = new DocumentReference("xwiki", "Test", "Test");
 
         // Add expectations to mock document name serializer.

programming_language: Java
filename: OfficeImporterIT.java
change_type: ModificationType.MODIFY
diff: @@ -129,7 +129,7 @@ private void verifyImports(TestInfo info)
         resultPage = importFile(testName, "ooffice.3.0/Test.odt");
         assertTrue(StringUtils.contains(resultPage.getContent(), "This is a test document."));
         WikiEditPage wikiEditPage = resultPage.editWiki();
-        String regex = "(?<imageName>Test_html_[\\w]+\\.png)";
+        String regex = "(?<imageName>Test_[\\w]+\\.png)";
         Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
         Matcher matcher = pattern.matcher(wikiEditPage.getContent());
         assertTrue(matcher.find());
@@ -159,27 +159,28 @@ private void verifyImports(TestInfo info)
         resultPage = importFile(testName, "ooffice.3.0/Test_accents & é$ù !-_.+();@=.odt");
         assertTrue(StringUtils.contains(resultPage.getContent(), "This is a test document."));
         wikiEditPage = resultPage.editWiki();
-        regex = "(?<imageName>Test_accents & e\\$u !-_\\.\\+\\(\\);\\\\@=_html_[\\w]+\\.png)";
+        regex = "(?<imageName>Test_accents & é\\$ù !-_\\.\\+\\(\\);\\\\@=_\\w+\\.png)";
         pattern = Pattern.compile(regex, Pattern.MULTILINE);
-        matcher = pattern.matcher(wikiEditPage.getContent());
-        assertTrue(matcher.find());
+        String wikiContent = wikiEditPage.getContent();
+        matcher = pattern.matcher(wikiContent);
+        assertTrue(matcher.find(), String.format("Pattern [%s] not found in [%s]", regex, wikiContent));
         imageName = matcher.group("imageName");
         resultPage = wikiEditPage.clickCancel();
         attachmentsPane = new AttachmentsViewPage().openAttachmentsDocExtraPane();
         assertEquals(4, attachmentsPane.getNumberOfAttachments());
         // the \ before the @ needs to be removed as it's not in the filename
-        assertTrue(attachmentsPane.attachmentExistsByFileName(imageName.replaceAll("\\\\@", "@")));
+        assertTrue(attachmentsPane.attachmentExistsByFileName(imageName.replace("\\@", "@")));
         deletePage(testName);
 
         // Test power point file with accents
         resultPage = importFile(testName, "msoffice.97-2003/Test_accents & é$ù !-_.+();@=.ppt");
         attachmentsPane = new AttachmentsViewPage().openAttachmentsDocExtraPane();
-        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & e$u !-_.+();@=-slide0.jpg"));
-        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & e$u !-_.+();@=-slide1.jpg"));
-        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & e$u !-_.+();@=-slide2.jpg"));
-        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & e$u !-_.+();@=-slide3.jpg"));
+        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & é$ù !-_.+();@=-slide0.jpg"));
+        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & é$ù !-_.+();@=-slide1.jpg"));
+        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & é$ù !-_.+();@=-slide2.jpg"));
+        assertTrue(attachmentsPane.attachmentExistsByFileName("Test_accents & é$ù !-_.+();@=-slide3.jpg"));
         wikiEditPage = resultPage.editWiki();
-        assertTrue(wikiEditPage.getContent().contains("[[image:Test_accents & e$u !-_.+();\\@=-slide0.jpg]]"));
+        assertTrue(wikiEditPage.getContent().contains("[[image:Test_accents & é$ù !-_.+();\\@=-slide0.jpg]]"));
         resultPage = wikiEditPage.clickCancel();
         deletePage(testName);
     }

programming_language: Java
filename: DefaultOfficeResourceViewer.java
change_type: ModificationType.MODIFY
diff: @@ -20,12 +20,10 @@
 package org.xwiki.office.viewer.internal;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -60,6 +58,7 @@
 import org.xwiki.officeimporter.builder.PresentationBuilder;
 import org.xwiki.officeimporter.builder.XDOMOfficeDocumentBuilder;
 import org.xwiki.officeimporter.converter.OfficeConverter;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.properties.ConverterManager;
@@ -200,30 +199,25 @@ public class DefaultOfficeResourceViewer implements OfficeResourceViewer, Initia
      * images that are view artifacts.
      * 
      * @param xdom the XDOM whose image blocks are to be processed
-     * @param artifactFiles specify which of the image blocks should be processed; only the image blocks
+     * @param artifactMap specify which of the image blocks should be processed; only the image blocks
      *          that were generated during the office import process should be processed
      * @param ownerDocumentReference specifies the document that owns the office file
      * @param parameters the build parameters. Note that currently only {@code filterStyles} is supported and if "true"
      *            it means that styles will be filtered to the maximum and the focus will be put on importing only the
      * @return the set of temporary files corresponding to image artifacts
      */
-    private Set<File> processImages(XDOM xdom, Set<File> artifactFiles, DocumentReference ownerDocumentReference,
-        Map<String, ?> parameters)
+    private Set<File> processImages(XDOM xdom, Map<String, OfficeDocumentArtifact> artifactMap,
+        DocumentReference ownerDocumentReference, Map<String, ?> parameters)
     {
         // Process all image blocks.
         Set<File> temporaryFiles = new HashSet<>();
         List<ImageBlock> imgBlocks = xdom.getBlocks(new ClassBlockMatcher(ImageBlock.class), Block.Axes.DESCENDANT);
         if (!imgBlocks.isEmpty()) {
-            Map<String, File> fileMap = new HashMap<>();
-            for (File file : artifactFiles) {
-                fileMap.put(file.getName(), file);
-            }
-
             for (ImageBlock imgBlock : imgBlocks) {
                 String imageReference = imgBlock.getReference().getReference();
 
                 // Check whether there is a corresponding artifact.
-                if (fileMap.containsKey(imageReference)) {
+                if (artifactMap.containsKey(imageReference)) {
                     try {
                         List<String> resourcePath =
                             Arrays.asList(String.valueOf(parameters.hashCode()), imageReference);
@@ -231,10 +225,13 @@ private Set<File> processImages(XDOM xdom, Set<File> artifactFiles, DocumentRefe
                             new TemporaryResourceReference(MODULE_NAME, resourcePath, ownerDocumentReference);
 
                         // Write the image into a temporary file.
-                        File artifact = fileMap.get(imageReference);
+                        OfficeDocumentArtifact artifact = artifactMap.get(imageReference);
 
-                        File tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
-                            new FileInputStream(artifact));
+                        File tempFile;
+                        try (InputStream inputStream = artifact.getContentInputStream()) {
+                            tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
+                                inputStream);
+                        }
 
                         // Create a URL image reference which links to above temporary image file.
                         String temporaryResourceURL =
@@ -429,7 +426,7 @@ private OfficeDocumentView getView(ResourceReference reference, AttachmentRefere
                 // specified by the owner document reference. This way we ensure the path to the temporary files
                 // doesn't contain redundant information and so it remains as small as possible (considering that the
                 // path length is limited on some environments).
-                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsFiles(),
+                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsMap(),
                     attachmentReference.getDocumentReference(), parameters);
                 view = new AttachmentOfficeDocumentView(reference, attachmentReference, attachmentVersion, xdom,
                     temporaryFiles);
@@ -457,7 +454,7 @@ private OfficeDocumentView getView(ResourceReference resourceReference, Map<Stri
             try (XDOMOfficeDocument xdomOfficeDocument = createXDOM(ownerDocument, resourceReference, parameters))
             {
                 XDOM xdom = xdomOfficeDocument.getContentDocument();
-                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsFiles(), ownerDocument,
+                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsMap(), ownerDocument,
                     parameters);
                 view = new OfficeDocumentView(resourceReference, xdom, temporaryFiles);
 

programming_language: Java
filename: DefaultOfficeResourceViewerTest.java
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,6 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.ArrayList;
@@ -34,7 +33,6 @@
 import javax.inject.Named;
 import javax.inject.Provider;
 
-import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.xwiki.bridge.DocumentAccessBridge;
@@ -50,6 +48,7 @@
 import org.xwiki.officeimporter.converter.OfficeConverter;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
+import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.properties.ConverterManager;
 import org.xwiki.rendering.block.Block;
@@ -281,7 +280,7 @@ void viewExistingOfficeAttachmentWithCacheMiss(MockitoComponentManager component
         when(documentAccessBridge.getAttachmentContent(ATTACHMENT_REFERENCE)).thenReturn(attachmentContent);
 
         XDOMOfficeDocument xdomOfficeDocument =
-            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
+            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
         when(
             officeDocumentBuilder.build(attachmentContent, ATTACHMENT_REFERENCE.getName(),
                 ATTACHMENT_REFERENCE.getDocumentReference(), false)).thenReturn(xdomOfficeDocument);
@@ -311,7 +310,7 @@ void viewTemporaryUploadedOfficeAttachmentWithCacheMiss(MockitoComponentManager
         when(attachment.getContentInputStream(this.context)).thenReturn(attachmentContent);
 
         XDOMOfficeDocument xdomOfficeDocument =
-            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
+            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
         when(
             officeDocumentBuilder.build(attachmentContent, ATTACHMENT_REFERENCE.getName(),
                 ATTACHMENT_REFERENCE.getDocumentReference(), false)).thenReturn(xdomOfficeDocument);
@@ -365,7 +364,7 @@ void viewTemporaryUploadedOfficeAttachmentWithCacheHit(MockitoComponentManager c
         when(attachment.getContentInputStream(this.context)).thenReturn(attachmentContent);
 
         XDOMOfficeDocument xdomOfficeDocument =
-            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
+            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
         when(
             officeDocumentBuilder.build(attachmentContent, ATTACHMENT_REFERENCE.getName(),
                 ATTACHMENT_REFERENCE.getDocumentReference(), false)).thenReturn(xdomOfficeDocument);
@@ -440,7 +439,7 @@ void viewANewVersionOfAnExistingOfficeAttachment(MockitoComponentManager compone
         when(documentAccessBridge.getAttachmentContent(ATTACHMENT_REFERENCE)).thenReturn(attachmentContent);
 
         XDOMOfficeDocument xdomOfficeDocument =
-            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
+            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
         when(
             officeDocumentBuilder.build(attachmentContent, ATTACHMENT_REFERENCE.getName(),
                 ATTACHMENT_REFERENCE.getDocumentReference(), false)).thenReturn(xdomOfficeDocument);
@@ -471,28 +470,25 @@ void viewPresentation(MockitoComponentManager componentManager) throws Exception
         ByteArrayInputStream attachmentContent = new ByteArrayInputStream(new byte[256]);
         when(documentAccessBridge.getAttachmentContent(attachmentReference)).thenReturn(attachmentContent);
 
-        ResourceReference imageReference = new ResourceReference("slide0.png", ResourceType.URL);
+        String imageName = "slide0.png";
+        ResourceReference imageReference = new ResourceReference(imageName, ResourceType.URL);
         ExpandedMacroBlock galleryMacro =
             new ExpandedMacroBlock("gallery", Collections.singletonMap("width", "300px"), null, false);
         galleryMacro.addChild(new ImageBlock(imageReference, true));
         XDOM xdom = new XDOM(Collections.<Block>singletonList(galleryMacro));
 
-        File artifact = new File(this.tempDir, "slide0.png");
-        try (FileOutputStream fos = new FileOutputStream(artifact)) {
-            IOUtils.write(new byte[8], fos);
-        }
         OfficeConverterResult converterResult = mock(OfficeConverterResult.class);
-        XDOMOfficeDocument xdomOfficeDocument = new XDOMOfficeDocument(xdom, Collections.singleton(artifact),
-            componentManager, converterResult);
+        XDOMOfficeDocument xdomOfficeDocument = new XDOMOfficeDocument(xdom, Collections.singletonMap(imageName,
+            new ByteArrayOfficeDocumentArtifact(imageName, new byte[8])), componentManager, converterResult);
 
         when(presentationBuilder.build(attachmentContent, attachmentReference.getName(), documentReference))
             .thenReturn(xdomOfficeDocument);
 
         Map<String, ?> viewParameters = Collections.singletonMap("ownerDocument", documentReference);
         TemporaryResourceReference temporaryResourceReference = new TemporaryResourceReference("officeviewer",
-            Arrays.asList(String.valueOf(viewParameters.hashCode()), "slide0.png"), documentReference);
+            Arrays.asList(String.valueOf(viewParameters.hashCode()), imageName), documentReference);
 
-        ExtendedURL extendedURL = new ExtendedURL(Arrays.asList("url", "to", "slide0.png"));
+        ExtendedURL extendedURL = new ExtendedURL(Arrays.asList("url", "to", imageName));
         when(this.resourceReferenceSerializer.serialize(temporaryResourceReference)).thenReturn(extendedURL);
 
         XDOM output = this.officeResourceViewer.createView(attachResourceRef, viewParameters);

programming_language: Java
filename: DefaultDocumentAccessBridge.java
change_type: ModificationType.MODIFY
diff: @@ -623,6 +623,17 @@ public void setAttachmentContent(AttachmentReference attachmentReference, byte[]
         setAttachmentContent(doc, attachmentReference.getName(), attachmentData, xcontext);
     }
 
+    @Override
+    public void setAttachmentContent(AttachmentReference attachmentReference, InputStream attachmentData)
+        throws Exception
+    {
+        XWikiContext xcontext = getContext();
+        XWikiDocument doc = xcontext.getWiki().getDocument(attachmentReference.getDocumentReference(), xcontext);
+
+        setAttachmentContent(doc, attachmentReference.getName(), attachmentData, xcontext);
+    }
+
+
     @Override
     @Deprecated
     public void setAttachmentContent(String documentReference, String attachmentFilename, byte[] attachmentData)
@@ -636,6 +647,13 @@ public void setAttachmentContent(String documentReference, String attachmentFile
 
     private void setAttachmentContent(XWikiDocument doc, String attachmentFilename, byte[] attachmentData,
         XWikiContext xcontext) throws Exception
+    {
+        setAttachmentContent(doc, attachmentFilename,
+            new ByteArrayInputStream(attachmentData != null ? attachmentData : new byte[0]), xcontext);
+    }
+
+    private void setAttachmentContent(XWikiDocument doc, String attachmentFilename, InputStream attachmentData,
+        XWikiContext xcontext) throws Exception
     {
         if (doc.getAttachment(attachmentFilename) == null) {
             doc.setComment("Add new attachment " + attachmentFilename);
@@ -643,8 +661,7 @@ private void setAttachmentContent(XWikiDocument doc, String attachmentFilename,
             doc.setComment("Update attachment " + attachmentFilename);
         }
 
-        doc.setAttachment(attachmentFilename,
-            new ByteArrayInputStream(attachmentData != null ? attachmentData : new byte[0]), xcontext);
+        doc.setAttachment(attachmentFilename, attachmentData, xcontext);
 
         doc.setAuthorReference(getCurrentUserReference());
         if (doc.isNew()) {

programming_language: Java
filename: DefaultDocumentAccessBridgeTest.java
change_type: ModificationType.MODIFY
diff: @@ -19,14 +19,22 @@
  */
 package com.xpn.xwiki.doc;
 
+import java.io.ByteArrayInputStream;
+
+import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
@@ -57,4 +65,26 @@ void testGetUrlEmptyDocument(MockitoOldcore oldcore)
         assertEquals(expectedURL, this.documentAccessBridge.getURL("", action, "", ""));
         assertEquals(expectedURL, this.documentAccessBridge.getURL(null, action, "", ""));
     }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void setAttachmentContent(boolean useStream, MockitoOldcore oldcore) throws Exception
+    {
+        DocumentReference documentReference = new DocumentReference("Wiki", "Space", "Page");
+        String fileName = "image.png";
+        AttachmentReference attachmentReference = new AttachmentReference(fileName, documentReference);
+        byte[] attachmentContent = new byte[] { 42, 23 };
+        if (useStream) {
+            this.documentAccessBridge.setAttachmentContent(attachmentReference,
+                new ByteArrayInputStream(attachmentContent));
+        } else {
+            this.documentAccessBridge.setAttachmentContent(attachmentReference, attachmentContent);
+        }
+        XWikiDocument document = oldcore.getSpyXWiki().getDocument(documentReference, oldcore.getXWikiContext());
+        XWikiAttachment attachment = document.getAttachment(fileName);
+        assertNotNull(attachment);
+        assertEquals(fileName, attachment.getFilename());
+        assertArrayEquals(attachmentContent,
+            IOUtils.toByteArray(attachment.getAttachmentContent(oldcore.getXWikiContext()).getContentInputStream()));
+    }
 }

programming_language: Java
filename: OfficeAttachmentImporter.java
change_type: ModificationType.MODIFY
diff: @@ -19,8 +19,6 @@
  */
 package org.xwiki.wysiwyg.internal.importer;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.Map;
 import java.util.Optional;
@@ -30,7 +28,6 @@
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
-import org.apache.commons.io.IOUtils;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.model.reference.AttachmentReference;
@@ -39,6 +36,7 @@
 import org.xwiki.officeimporter.OfficeImporterException;
 import org.xwiki.officeimporter.builder.PresentationBuilder;
 import org.xwiki.officeimporter.builder.XDOMOfficeDocumentBuilder;
+import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
 import org.xwiki.officeimporter.document.XDOMOfficeDocument;
 import org.xwiki.officeimporter.server.OfficeServer;
 import org.xwiki.officeimporter.server.OfficeServer.ServerState;
@@ -167,11 +165,12 @@ private String convertAttachmentContent(AttachmentReference attachmentReference,
             xdomOfficeDocument = documentBuilder.build(officeFileStream, officeFileName, targetDocRef, filterStyles);
         }
         // Attach the images extracted from the imported office document to the target wiki document.
-        for (File artifact : xdomOfficeDocument.getArtifactsFiles()) {
-
-            AttachmentReference artifactReference = new AttachmentReference(artifact.getName(), targetDocRef);
-            try (FileInputStream fis = new FileInputStream(artifact)) {
-                documentAccessBridge.setAttachmentContent(artifactReference, IOUtils.toByteArray(fis));
+        for (Map.Entry<String, OfficeDocumentArtifact> entry : xdomOfficeDocument.getArtifactsMap().entrySet()) {
+            String filename = entry.getKey();
+            OfficeDocumentArtifact artifact = entry.getValue();
+            AttachmentReference artifactReference = new AttachmentReference(filename, targetDocRef);
+            try (InputStream is = artifact.getContentInputStream()) {
+                this.documentAccessBridge.setAttachmentContent(artifactReference, is);
             }
         }
         String result = xdomOfficeDocument.getContentAsString("annotatedxhtml/1.0");

programming_language: Java
filename: OfficeAttachmentImporterTest.java
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,8 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 /**
  * Unit tests for {@link OfficeAttachmentImporter}.
@@ -155,7 +156,7 @@ void toHTML() throws Exception
         XDOMOfficeDocument xdomOfficeDocument = mock(XDOMOfficeDocument.class);
         when(documentBuilder.build(attachmentContent, "my.doc", ATTACHMENT_REFERENCE.getDocumentReference(), true))
             .thenReturn(xdomOfficeDocument);
-        when(xdomOfficeDocument.getArtifactsFiles()).thenReturn(Collections.emptySet());
+        when(xdomOfficeDocument.getArtifactsMap()).thenReturn(Collections.emptyMap());
         when(xdomOfficeDocument.getContentAsString("annotatedxhtml/1.0")).thenReturn("test");
 
         Map<String, Object> parameters = Collections.singletonMap("filterStyles", "true");
@@ -179,7 +180,7 @@ void toHTMLTemporaryUploadedAttachment() throws Exception
         XDOMOfficeDocument xdomOfficeDocument = mock(XDOMOfficeDocument.class);
         when(documentBuilder.build(attachmentContent, "my.doc", ATTACHMENT_REFERENCE.getDocumentReference(), true))
             .thenReturn(xdomOfficeDocument);
-        when(xdomOfficeDocument.getArtifactsFiles()).thenReturn(Collections.emptySet());
+        when(xdomOfficeDocument.getArtifactsMap()).thenReturn(Collections.emptyMap());
         when(xdomOfficeDocument.getContentAsString("annotatedxhtml/1.0")).thenReturn("test");
 
         Map<String, Object> parameters = Collections.singletonMap("filterStyles", "true");

programming_language: Java