CVE-2023-37913: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: 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.
CWE 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.
CVSS v3 base score: 8.8
Published: 2023-10-25T18:17Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentAccessBridge.java, xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/CompatibilityOfficeDocument.java, xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XDOMOfficeDocumentCompatibilityAspect.aj, xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XHTMLOfficeDocumentCompatibilityAspect.aj, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocument.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocumentArtifact.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XDOMOfficeDocument.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XHTMLOfficeDocument.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/ModelBridge.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilder.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilder.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilder.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/DefaultOfficeConverter.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorage.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/ByteArrayOfficeDocumentArtifact.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/FileOfficeDocumentArtifact.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/filter/ImageFilter.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitter.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/document/XDOMOfficeDocumentTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/ModelBridgeTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilderTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilderTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilderTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorageStorageTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/filter/ImageFilterTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitterTest.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-test/xwiki-platform-office-test-docker/src/test/it/org/xwiki/officeimporter/test/ui/OfficeImporterIT.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/main/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewer.java, xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/test/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewerTest.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridge.java, xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridgeTest.java, xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/main/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporter.java, xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/test/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporterTest.java
Commit date: 2023-03-31 17:42:50 +0200
Commit message: 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).

Diff: diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentAccessBridge.java b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentAccessBridge.java
index 62adfd6ec7610..de8dd4b02ed09 100644
--- a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentAccessBridge.java
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentAccessBridge.java
@@ -21,10 +21,11 @@
 
 import java.io.InputStream;
 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;
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
@@ -537,10 +538,27 @@ default InputStream getAttachmentContent(EntityReference reference) throws Excep
      * @throws Exception If the storage cannot be accessed.
      * @since 2.2.1
      */
     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.
      * 
      * @param documentReference the reference to the target document name
diff --git a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/CompatibilityOfficeDocument.java b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/CompatibilityOfficeDocument.java
index 39ad1b3c88850..832c6e594d4b4 100644
--- a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/CompatibilityOfficeDocument.java
+++ b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/CompatibilityOfficeDocument.java
@@ -17,21 +17,41 @@
  * 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.File;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 public interface CompatibilityOfficeDocument
 {
     /**
      * Returns all the artifacts for this office document. 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 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();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XDOMOfficeDocumentCompatibilityAspect.aj b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XDOMOfficeDocumentCompatibilityAspect.aj
index e34e49997f0a6..e055f0088169b 100644
--- a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XDOMOfficeDocumentCompatibilityAspect.aj
+++ b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XDOMOfficeDocumentCompatibilityAspect.aj
@@ -18,27 +18,35 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 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
 {
     @Deprecated
     private Map<String, byte[]> XDOMOfficeDocument.artifacts;
 
+    @Deprecated
+    private Set<File> XDOMOfficeDocument.fileArtifacts;
+
     /**
      * Creates a new {@link XDOMOfficeDocument}.
      *
      * @param xdom {@link XDOM} corresponding to office document content.
      * @param artifacts artifacts for this office document.
@@ -46,34 +54,63 @@ public privileged aspect XDOMOfficeDocumentCompatibilityAspect
      * @deprecated Since 13.1RC1 use {@link #XDOMOfficeDocument(XDOM, Set, ComponentManager, OfficeConverterResult)}.
      */
     @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()}.
      */
     @Deprecated
     public Map<String, byte[]> XDOMOfficeDocument.getArtifacts()
     {
         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();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XHTMLOfficeDocumentCompatibilityAspect.aj b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XHTMLOfficeDocumentCompatibilityAspect.aj
index df04dc9d04393..797ca45afe7a7 100644
--- a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XHTMLOfficeDocumentCompatibilityAspect.aj
+++ b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-office/xwiki-platform-legacy-office-importer/src/main/aspect/org/xwiki/officeimporter/document/XHTMLOfficeDocumentCompatibilityAspect.aj
@@ -18,60 +18,95 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 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}.
      *
      * @param document the w3c dom representing the office document.
      * @param artifacts artifacts for this office document.
      * @deprecated Since 13.1RC1 use {@link #XHTMLOfficeDocument(Document, Set, OfficeConverterResult)}.
      */
     @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()}.
      */
     @Deprecated
     public Map<String, byte[]> XHTMLOfficeDocument.getArtifacts()
     {
         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();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocument.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocument.java
index a036635b4a2a9..c4f4b4729f91f 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocument.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocument.java
@@ -18,16 +18,16 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 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.
  * 
  * @version $Id$
@@ -55,17 +55,21 @@ public interface OfficeDocument extends Closeable
      * 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.
+     * <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();
     }
 
     /**
      * @return the converter result.
      * @since 13.1RC1
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocumentArtifact.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocumentArtifact.java
new file mode 100644
index 0000000000000..692e70b4129eb
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/OfficeDocumentArtifact.java
@@ -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;
+}
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XDOMOfficeDocument.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XDOMOfficeDocument.java
index 94bf5f2b89316..2e7f04e1f307a 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XDOMOfficeDocument.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XDOMOfficeDocument.java
@@ -17,13 +17,12 @@
  * 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.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;
 import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.officeimporter.converter.OfficeConverterResult;
@@ -33,10 +32,11 @@
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.renderer.BlockRenderer;
 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.
  * 
  * @version $Id$
@@ -50,11 +50,11 @@ public class XDOMOfficeDocument implements OfficeDocument
     private XDOM xdom;
 
     /**
      * Artifacts for this office document.
      */
-    private Set<File> artifactFiles;
+    private final Map<String, OfficeDocumentArtifact> artifactsMap;
 
     /**
      * {@link ComponentManager} used to lookup for various renderers.
      */
     private ComponentManager componentManager;
@@ -63,20 +63,22 @@ 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;
     }
 
     @Override
@@ -109,13 +111,13 @@ public String getContentAsString(String syntaxId)
         }
         return null;
     }
 
     @Override
-    public Set<File> getArtifactsFiles()
+    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
     {
-        return this.artifactFiles;
+        return this.artifactsMap;
     }
 
     /**
      * Tries to extract a title suitable for this document. This is done by navigating the internal {@link XDOM} and
      * finding a matching header block.
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XHTMLOfficeDocument.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XHTMLOfficeDocument.java
index 2425e8e960a39..863f9a95c13fb 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XHTMLOfficeDocument.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/document/XHTMLOfficeDocument.java
@@ -17,16 +17,16 @@
  * 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.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;
 
 /**
  * A {@link OfficeDocument} backed by a w3c {@link Document} containing xhtml markup.
  *
@@ -41,26 +41,29 @@ public class XHTMLOfficeDocument implements OfficeDocument
     private Document document;
 
     /**
      * Artifacts for this office document.
      */
-    private Set<File> artifactFiles;
+    private Map<String, OfficeDocumentArtifact> artifactsMap;
 
     private OfficeConverterResult converterResult;
 
     /**
      * 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;
     }
 
     @Override
     public Document getContentDocument()
@@ -73,13 +76,13 @@ public String getContentAsString()
     {
         return HTMLUtils.toString(this.document);
     }
 
     @Override
-    public Set<File> getArtifactsFiles()
+    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
     {
-        return this.artifactFiles;
+        return this.artifactsMap;
     }
 
     @Override
     public void close() throws IOException
     {
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/ModelBridge.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/ModelBridge.java
index 466e512981566..d698d03c1256a 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/ModelBridge.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/ModelBridge.java
@@ -17,24 +17,24 @@
  * 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;
 
-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;
 
 /**
@@ -115,28 +115,28 @@ public void save(XDOMOfficeDocument doc, DocumentReference documentReference, St
                 this.docBridge.setDocumentTitle(documentReference, docTitle);
             }
         }
 
         // Finally attach all the artifacts into target document.
-        attachArtifacts(doc.getArtifactsFiles(), documentReference);
+        attachArtifacts(doc.getArtifactsMap(), documentReference);
     }
 
     /**
      * Utility method for attaching artifacts into a wiki page.
      * 
      * @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));
             }
-        }
+        });
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilder.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilder.java
index c36ae0f90ade9..8a8b85716c211 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilder.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilder.java
@@ -23,14 +23,12 @@
 import java.io.IOException;
 import java.io.InputStream;
 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;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -50,18 +48,22 @@
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.officeimporter.OfficeImporterException;
 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;
 import org.xwiki.rendering.block.XDOM;
 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;
 
 /**
@@ -117,22 +119,18 @@ public class DefaultPresentationBuilder implements PresentationBuilder
 
     @Override
     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);
         }
 
         // Clear and adjust presentation HTML (slide image URLs are updated to point to the corresponding attachments).
@@ -155,19 +153,19 @@ public XDOMOfficeDocument build(InputStream officeFileStream, String officeFileN
      * @throws OfficeImporterException if converting the office presentation fails
      */
     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);
         }
     }
@@ -181,26 +179,23 @@ protected OfficeConverterResult importPresentation(InputStream officeFileStream,
      * @param officeConverterResult the map of presentation artifacts; this method removes some of the presentation
      *            artifacts and renames others so be aware of the side effects
      * @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 {
                     // We need to encode the slide image name in case it contains special URL characters.
                     slideImageURL = URLEncoder.encode(slideImageName, "UTF-8");
@@ -217,11 +212,11 @@ protected Pair<String, Set<File>> buildPresentationHTML(OfficeConverterResult of
                 filenames.put(Integer.parseInt(number), slideImageURL);
             }
         }
         // 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);
     }
 
     /**
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilder.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilder.java
index 156e5d3bc6683..e819f8069a750 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilder.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilder.java
@@ -98,9 +98,9 @@ public XDOMOfficeDocument build(XHTMLOfficeDocument xhtmlOfficeDocument) throws
         try {
             xdom = this.xHtmlParser.parse(new StringReader(HTMLUtils.toString(xhtmlDoc)));
         } 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());
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilder.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilder.java
index 4ce33fa33d645..357dbc651c6d4 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilder.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilder.java
@@ -19,36 +19,34 @@
  */
 package org.xwiki.officeimporter.internal.builder;
 
 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;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.officeimporter.OfficeImporterException;
 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;
 
 /**
@@ -78,53 +76,58 @@ public class DefaultXHTMLOfficeDocumentBuilder implements XHTMLOfficeDocumentBui
      */
     @Inject
     @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>();
         params.put("targetDocument", this.entityReferenceSerializer.serialize(reference));
         // 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");
         }
 
         // Parse and clean the HTML output.
@@ -139,29 +142,31 @@ private Document cleanAndCreateFile(DocumentReference reference, boolean filterS
                 String.format("The output file cannot be found: [%s].", officeConverterResult.getOutputFile()), e);
         }
         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;
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/DefaultOfficeConverter.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/DefaultOfficeConverter.java
index b1c5cab498a47..06950fb6c1d92 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/DefaultOfficeConverter.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/DefaultOfficeConverter.java
@@ -90,10 +90,13 @@ public DefaultOfficeConverterResult convertDocument(Map<String, InputStream> inp
         try {
             // Prepare temporary storage.
             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());
                 try (FileOutputStream fos = new FileOutputStream(temp)) {
                     IOUtils.copy(entry.getValue(), fos);
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorage.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorage.java
index 45b961decf804..232fc75b638c3 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorage.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorage.java
@@ -20,11 +20,13 @@
 package org.xwiki.officeimporter.internal.converter;
 
 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;
 
 /**
  * Keeps track of file system storage used by {@link org.xwiki.officeimporter.converter.OfficeConverter} for a
  * particular conversion.
@@ -32,10 +34,24 @@
  * @version $Id$
  * @since 2.2M2
  */
 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.
      */
     private File rootDir;
 
@@ -73,15 +89,15 @@ public OfficeConverterFileStorage(File parentDir, String inputFileName, String o
         boolean success = false;
 
         // 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;
             }
         }
 
         // Cleanup & signal if an error is encountered.
@@ -89,10 +105,51 @@ public OfficeConverterFileStorage(File parentDir, String inputFileName, String o
             cleanUp();
             throw new IOException("Could not create temporary directory hierarchy.");
         }
     }
 
+    /**
+     * 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.
      */
     public File getInputDir()
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/ByteArrayOfficeDocumentArtifact.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/ByteArrayOfficeDocumentArtifact.java
new file mode 100644
index 0000000000000..a08bd852e3af1
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/ByteArrayOfficeDocumentArtifact.java
@@ -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();
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/FileOfficeDocumentArtifact.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/FileOfficeDocumentArtifact.java
new file mode 100644
index 0000000000000..ca63048234491
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/document/FileOfficeDocumentArtifact.java
@@ -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();
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/filter/ImageFilter.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/filter/ImageFilter.java
index 1a51fcc3171f5..3019a4640ee11 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/filter/ImageFilter.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/filter/ImageFilter.java
@@ -110,30 +110,35 @@ public void filter(Document htmlDocument, Map<String, String> cleaningParams)
         boolean attachEmbeddedImages = Boolean.valueOf(cleaningParams.get("attachEmbeddedImages"));
         if (attachEmbeddedImages) {
             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
             // center aligning images (it aligns them to left). Next, the office server uses <br clear"xxx"> for
             // avoiding content wrapping around images which is not valid XHTML. There for, to be consistent and simple
             // we will remove the 'align' attribute of all the images so that they are all left aligned.
             image.removeAttribute(ATTRIBUTE_ALIGN);
         }
     }
 
-    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));
             this.logger.debug("Full stacktrace is: ", e);
         }
@@ -153,11 +158,11 @@ private void filterImageSource(Attr source, DocumentReference targetDocumentRefe
         Element image = source.getOwnerElement();
         image.getParentNode().insertBefore(beforeComment, image);
         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;
         @SuppressWarnings("unchecked")
         Map<String, byte[]> embeddedImages =
@@ -192,15 +197,27 @@ private String getFileName(Attr source) throws MimeTypeException
                 Finally '@' is used in XWiki Syntax so it needs to be escaped to build the link properly.
                  */
                 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;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitter.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitter.java
index a215f77e04b17..2c071edc78220 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitter.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/main/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitter.java
@@ -17,26 +17,24 @@
  * 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.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;
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.xwiki.component.annotation.Component;
 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;
 import org.xwiki.officeimporter.splitter.XDOMOfficeDocumentSplitter;
 import org.xwiki.refactoring.WikiDocument;
@@ -103,43 +101,41 @@ public Map<TargetDocumentDescriptor, XDOMOfficeDocument> split(XDOMOfficeDocumen
             if (doc.getParent() != null) {
                 targetDocumentDescriptor.setParentReference(doc.getParent().getDocumentReference());
             }
 
             // 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);
         }
 
         return result;
     }
 
     /**
-     * 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;
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/document/XDOMOfficeDocumentTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/document/XDOMOfficeDocumentTest.java
index 7ecf5a845688b..fe1917a701e17 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/document/XDOMOfficeDocumentTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/document/XDOMOfficeDocumentTest.java
@@ -77,8 +77,8 @@ void titleExtraction() throws Exception
      */
     private XDOMOfficeDocument createOfficeDocument(String content, String syntax) throws Exception
     {
         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);
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/ModelBridgeTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/ModelBridgeTest.java
index 861df38886833..8516fde200128 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/ModelBridgeTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/ModelBridgeTest.java
@@ -19,28 +19,36 @@
  */
 package org.xwiki.officeimporter.internal;
 
 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;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.bridge.DocumentModelBridge;
 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;
 import org.xwiki.security.authorization.Right;
 import org.xwiki.test.junit5.XWikiTempDir;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 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;
 
 /**
@@ -79,21 +87,31 @@ void saveWithOverwrite() throws Exception
             IOUtils.write(fileContent, fos);
         }
 
         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);
 
         verify(documentAccessBridge).setDocumentSyntaxId(documentReference, syntaxId);
         verify(documentAccessBridge).setDocumentContent(documentReference, content, "Created by office importer.",
             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
     void saveWithAppend() throws Exception
     {
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilderTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilderTest.java
index 4976735ad3c00..7bcd6e43507d8 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilderTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultPresentationBuilderTest.java
@@ -26,11 +26,13 @@
 import java.nio.file.Files;
 import java.util.Arrays;
 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;
 
 import org.apache.commons.codec.binary.StringUtils;
@@ -44,11 +46,13 @@
 import org.xwiki.bridge.DocumentModelBridge;
 import org.xwiki.model.reference.DocumentReference;
 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;
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.block.match.ClassBlockMatcher;
@@ -149,11 +153,11 @@ void build() throws Exception
             Files.createFile(file.toPath());
         }
 
         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);
         when(this.officeHTMLCleaner.getDefaultConfiguration()).thenReturn(config);
 
@@ -168,13 +172,15 @@ void build() throws Exception
         when(this.xhtmlParser.parse(any(Reader.class))).thenReturn(galleryContent);
 
         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));
 
         List<ExpandedMacroBlock> macros =
             result.getContentDocument().getBlocks(new ClassBlockMatcher(ExpandedMacroBlock.class), Block.Axes.CHILD);
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilderTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilderTest.java
index f1b0bbff1cf2d..bfba201c71f17 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilderTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXDOMOfficeDocumentBuilderTest.java
@@ -55,11 +55,11 @@
  *
  * @version $Id$
  * @since 2.1M1
  */
 @ComponentTest
-public class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
+class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
 {
     /**
      * The name of an input file to be used in tests.
      */
     private static final String INPUT_FILE_NAME = "office.doc";
@@ -89,16 +89,17 @@ 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);
         when(converterResult.getOutputFile()).thenReturn(outputFile);
         Files.createFile(outputFile.toPath());
@@ -110,19 +111,19 @@ public void xdomOfficeDocumentBuilding() throws Exception
 
         final OfficeConverter mockDocumentConverter = mock(OfficeConverter.class);
         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");
 
         XDOMOfficeDocument document =
             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();
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilderTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilderTest.java
index 7079610a3c506..14569ce1d91d5 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilderTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/builder/DefaultXHTMLOfficeDocumentBuilderTest.java
@@ -22,10 +22,11 @@
 import java.io.ByteArrayInputStream;
 import java.io.File;
 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;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -40,31 +41,37 @@
 import org.w3c.dom.Document;
 import org.xwiki.model.reference.DocumentReference;
 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;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 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}.
  *
  * @version $Id$
  * @since 2.1M1
  */
 @ComponentTest
-public class DefaultXHTMLOfficeDocumentBuilderTest
+class DefaultXHTMLOfficeDocumentBuilderTest
 {
     @InjectMockComponents
     private DefaultXHTMLOfficeDocumentBuilder officeDocumentBuilder;
 
     @MockComponent
@@ -88,11 +95,11 @@ public void configure() throws Exception
     {
         when(this.officeServer.getConverter()).thenReturn(this.officeConverter);
     }
 
     @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");
 
         InputStream officeFileStream = new ByteArrayInputStream("office content".getBytes());
@@ -110,16 +117,22 @@ public void xhtmlOfficeDocumentBuilding() throws Exception
             Files.createFile(file.toPath());
         }
         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);
 
         HTMLCleanerConfiguration config = mock(HTMLCleanerConfiguration.class);
         when(this.officeHTMLCleaner.getDefaultConfiguration()).thenReturn(config);
@@ -130,15 +143,20 @@ public void xhtmlOfficeDocumentBuilding() throws Exception
 
         Map<String, String> params = new HashMap<String, String>();
         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()));
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorageStorageTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorageStorageTest.java
new file mode 100644
index 0000000000000..3670857cde9d7
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/converter/OfficeConverterFileStorageStorageTest.java
@@ -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());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/filter/ImageFilterTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/filter/ImageFilterTest.java
index b4e8531854238..56cb3790480bd 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/filter/ImageFilterTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/filter/ImageFilterTest.java
@@ -20,10 +20,11 @@
 package org.xwiki.officeimporter.internal.filter;
 
 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;
 import java.util.HashSet;
 import java.util.Map;
@@ -35,36 +36,35 @@
 import org.w3c.dom.Document;
 import org.xwiki.bridge.DocumentAccessBridge;
 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}.
  * 
  * @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;
 
     private DocumentReference documentReference = new DocumentReference("wiki", Arrays.asList("Path.To"), "Page");
@@ -89,11 +89,10 @@ public void filterAddsImageMarkers()
         AttachmentReference attachmentReference = new AttachmentReference("-foo--bar.png-", this.documentReference);
         when(this.dab.getAttachmentURL(attachmentReference, false)).thenReturn("/path/to/foo.png");
 
         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"),
             "<!--startimage:false|-|attach|-|-foo-\\-bar.png-\\--><img src=\"/path/to/foo.png\"/><!--stopimage-->");
     }
@@ -108,11 +107,10 @@ public void filterAddsImageMarkersSpecialCharacters() throws UnsupportedEncoding
         when(this.dab.getAttachmentURL(attachmentReference, false)).thenReturn("/path/to/" + encodedImageName);
 
         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"),
             String.format("<!--startimage:false|-|attach|-|%s--><img src=\"/path/to/%s\"/><!--stopimage-->",
                 imageNameEscaped, encodedImageName));
@@ -134,20 +132,18 @@ public void filterCollectsEmbeddedImages()
         AttachmentReference attachmentReference = new AttachmentReference("foo.png", this.documentReference);
         when(this.dab.getAttachmentURL(attachmentReference, false)).thenReturn("/path/to/foo.png");
 
         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";
         attachmentReference = new AttachmentReference(fileName, this.documentReference);
         when(this.dab.getAttachmentURL(attachmentReference, false)).thenReturn("/path/to/" + fileName);
 
         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");
         parameters.put("attachEmbeddedImages", "true");
 
@@ -161,6 +157,27 @@ public void filterCollectsEmbeddedImages()
 
         @SuppressWarnings("unchecked")
         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);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitterTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitterTest.java
index 3e313063fc541..94f12f9e16ea9 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitterTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-importer/src/test/java/org/xwiki/officeimporter/internal/splitter/DefaultXDOMOfficeDocumentSplitterTest.java
@@ -85,11 +85,11 @@ void documentSplitting() throws Exception
         buffer.append("Content").append('\n');
         XDOM xdom = xwikiSyntaxParser.parse(new StringReader(buffer.toString()));
 
         // 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.
         when(this.mockCompactWikiStringEntityReferenceSerializer.serialize(baseDocument)).thenReturn("Test.Test");
         when(this.mockCompactWikiStringEntityReferenceSerializer
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-test/xwiki-platform-office-test-docker/src/test/it/org/xwiki/officeimporter/test/ui/OfficeImporterIT.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-test/xwiki-platform-office-test-docker/src/test/it/org/xwiki/officeimporter/test/ui/OfficeImporterIT.java
index e7a413a5a58f5..3fcf13dc770af 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-test/xwiki-platform-office-test-docker/src/test/it/org/xwiki/officeimporter/test/ui/OfficeImporterIT.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-test/xwiki-platform-office-test-docker/src/test/it/org/xwiki/officeimporter/test/ui/OfficeImporterIT.java
@@ -127,11 +127,11 @@ private void verifyImports(TestInfo info)
 
         // Test ODT file
         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());
         String imageName = matcher.group("imageName");
         resultPage = wikiEditPage.clickCancel();
@@ -157,31 +157,32 @@ private void verifyImports(TestInfo info)
 
         // Test ODT file with accents
         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);
     }
 
     public void verifySplitByHeadings(TestInfo info)
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/main/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewer.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/main/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewer.java
index 1cfa9fea12656..e157af4931788 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/main/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewer.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/main/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewer.java
@@ -18,16 +18,14 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 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;
 import java.util.Optional;
 import java.util.Set;
@@ -58,10 +56,11 @@
 import org.xwiki.office.viewer.OfficeResourceViewer;
 import org.xwiki.officeimporter.OfficeImporterException;
 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;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.ExpandedMacroBlock;
@@ -198,45 +197,43 @@ public class DefaultOfficeResourceViewer implements OfficeResourceViewer, Initia
     /**
      * Processes all the image blocks in the given XDOM and changes image URL to point to a temporary file for those
      * 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);
                         TemporaryResourceReference temporaryResourceReference =
                             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 =
                             this.resourceReferenceSerializer.serialize(temporaryResourceReference).serialize();
                         ResourceReference urlImageReference =
@@ -427,11 +424,11 @@ private OfficeDocumentView getView(ResourceReference reference, AttachmentRefere
                 XDOM xdom = xdomOfficeDocument.getContentDocument();
                 // We use only the file name from the resource reference because the rest of the information is
                 // 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);
 
                 this.attachmentCache.set(cacheKey, view);
@@ -455,11 +452,11 @@ private OfficeDocumentView getView(ResourceReference resourceReference, Map<Stri
         // If a view in not available, build one and cache it.
         if (view == null) {
             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);
 
                 this.externalCache.set(cacheKey, view);
             }
diff --git a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/test/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewerTest.java b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/test/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewerTest.java
index 21d4c1a86d9fa..da547581c8eb8 100644
--- a/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/test/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewerTest.java
+++ b/xwiki-platform-core/xwiki-platform-office/xwiki-platform-office-viewer/src/test/java/org/xwiki/office/viewer/internal/DefaultOfficeResourceViewerTest.java
@@ -19,11 +19,10 @@
  */
 package org.xwiki.office.viewer.internal;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -32,11 +31,10 @@
 import java.util.Optional;
 
 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;
 import org.xwiki.cache.Cache;
 import org.xwiki.cache.CacheManager;
@@ -48,10 +46,11 @@
 import org.xwiki.officeimporter.builder.PresentationBuilder;
 import org.xwiki.officeimporter.builder.XDOMOfficeDocumentBuilder;
 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;
 import org.xwiki.rendering.block.ExpandedMacroBlock;
 import org.xwiki.rendering.block.ImageBlock;
@@ -279,11 +278,11 @@ void viewExistingOfficeAttachmentWithCacheMiss(MockitoComponentManager component
 
         ByteArrayInputStream attachmentContent = new ByteArrayInputStream(new byte[256]);
         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);
 
         this.officeResourceViewer.createView(ATTACHMENT_RESOURCE_REFERENCE, DEFAULT_VIEW_PARAMETERS);
@@ -309,11 +308,11 @@ void viewTemporaryUploadedOfficeAttachmentWithCacheMiss(MockitoComponentManager
 
         ByteArrayInputStream attachmentContent = new ByteArrayInputStream(new byte[256]);
         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);
 
         this.officeResourceViewer.createView(ATTACHMENT_RESOURCE_REFERENCE, DEFAULT_VIEW_PARAMETERS);
@@ -363,11 +362,11 @@ void viewTemporaryUploadedOfficeAttachmentWithCacheHit(MockitoComponentManager c
 
         ByteArrayInputStream attachmentContent = new ByteArrayInputStream(new byte[256]);
         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);
 
         this.officeResourceViewer.createView(ATTACHMENT_RESOURCE_REFERENCE, DEFAULT_VIEW_PARAMETERS);
@@ -438,11 +437,11 @@ void viewANewVersionOfAnExistingOfficeAttachment(MockitoComponentManager compone
 
         ByteArrayInputStream attachmentContent = new ByteArrayInputStream(new byte[256]);
         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);
 
         assertNotNull(this.officeResourceViewer.createView(ATTACHMENT_RESOURCE_REFERENCE, DEFAULT_VIEW_PARAMETERS));
@@ -469,32 +468,29 @@ void viewPresentation(MockitoComponentManager componentManager) throws Exception
         when(documentAccessBridge.getAttachmentVersion(attachmentReference)).thenReturn("3.2");
 
         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);
 
         ImageBlock imageBlock =
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridge.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridge.java
index e5fb8263b1567..6486d92c2add6 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridge.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridge.java
@@ -621,10 +621,21 @@ public void setAttachmentContent(AttachmentReference attachmentReference, byte[]
         XWikiDocument doc = xcontext.getWiki().getDocument(attachmentReference.getDocumentReference(), xcontext);
 
         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)
         throws Exception
     {
@@ -634,19 +645,25 @@ public void setAttachmentContent(String documentReference, String attachmentFile
         setAttachmentContent(doc, attachmentFilename, attachmentData, xcontext);
     }
 
     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);
         } else {
             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()) {
             doc.setCreatorReference(getCurrentUserReference());
         }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridgeTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridgeTest.java
index 5588a60cbba00..3375b85cbdee7 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridgeTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/DefaultDocumentAccessBridgeTest.java
@@ -17,18 +17,26 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package com.xpn.xwiki.doc;
 
+import java.io.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;
 
 /**
@@ -55,6 +63,28 @@ void testGetUrlEmptyDocument(MockitoOldcore oldcore)
                 eq(oldcore.getXWikiContext()));
 
         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()));
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/main/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporter.java b/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/main/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporter.java
index e59cc8fc6835e..21968f9356e40 100644
--- a/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/main/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporter.java
+++ b/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/main/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporter.java
@@ -17,30 +17,28 @@
  * 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.wysiwyg.internal.importer;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.Map;
 import java.util.Optional;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 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;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 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;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
 import org.xwiki.security.authorization.Right;
@@ -165,15 +163,16 @@ private String convertAttachmentContent(AttachmentReference attachmentReference,
             xdomOfficeDocument = presentationBuilder.build(officeFileStream, officeFileName, targetDocRef);
         } else {
             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");
         xdomOfficeDocument.close();
         return result;
diff --git a/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/test/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporterTest.java b/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/test/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporterTest.java
index ec6857aeeff0c..8c3675c4f2246 100644
--- a/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/test/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporterTest.java
+++ b/xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-api/src/test/java/org/xwiki/wysiwyg/internal/importer/OfficeAttachmentImporterTest.java
@@ -47,11 +47,12 @@
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiAttachment;
 
 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}.
  * 
  * @version $Id$
@@ -153,11 +154,11 @@ void toHTML() throws Exception
         when(this.documentAccessBridge.getAttachmentContent(ATTACHMENT_REFERENCE)).thenReturn(attachmentContent);
 
         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");
         assertEquals("test", officeAttachmentImporter.toHTML(ATTACHMENT_REFERENCE, parameters));
     }
@@ -177,11 +178,11 @@ void toHTMLTemporaryUploadedAttachment() throws Exception
         when(attachment.getContentInputStream(this.context)).thenReturn(attachmentContent);
 
         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");
         assertEquals("test", officeAttachmentImporter.toHTML(ATTACHMENT_REFERENCE, parameters));
     }

Vulnerable code:      * @deprecated Since 13.1RC1 use {@link OfficeDocument#getArtifactsFiles()}.
import java.io.FileInputStream;
        this(xdom, Collections.emptySet(), componentManager, null);
            FileInputStream fis = null;

            for (File file : this.artifactFiles) {
                try {
                    fis = new FileInputStream(file);
                    this.artifacts.put(file.getName(), IOUtils.toByteArray(fis));
                } catch (IOException e) {
                } finally {
                    IOUtils.closeQuietly(fis);
import java.io.FileInputStream;
        this(document, Collections.emptySet(), null);
            FileInputStream fis = null;

            for (File file : this.artifactFiles) {
                try {
                    fis = new FileInputStream(file);
                    this.artifacts.put(file.getName(), IOUtils.toByteArray(fis));
                } catch (IOException e) {
                } finally {
                    IOUtils.closeQuietly(fis);
import java.io.File;
import java.util.Set;
     * @return the set of artifacts related to this office document.
     * @since 13.1RC1
    default Set<File> getArtifactsFiles()
        return Collections.emptySet();
import java.io.File;
import java.util.Set;
    private Set<File> artifactFiles;
     * @param artifactFiles artifacts for this office document.
     * @since 13.1RC1
    public XDOMOfficeDocument(XDOM xdom, Set<File> artifactFiles, ComponentManager componentManager,
        OfficeConverterResult converterResult)
        this.artifactFiles = artifactFiles;
    public Set<File> getArtifactsFiles()
        return this.artifactFiles;
import java.io.File;
import java.util.Set;
    private Set<File> artifactFiles;
     * @param artifactFiles artifacts for this office document.
     * @since 13.1RC1
    public XHTMLOfficeDocument(Document document, Set<File> artifactFiles, OfficeConverterResult converterResult)
        this.artifactFiles = artifactFiles;
    public Set<File> getArtifactsFiles()
        return this.artifactFiles;
import java.io.File;
import java.io.FileInputStream;
import java.util.Set;
import org.apache.commons.io.IOUtils;
        attachArtifacts(doc.getArtifactsFiles(), documentReference);
    private void attachArtifacts(Set<File> 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));
                // Log the error and skip the artifact.
                this.logger.error("Error while attaching artifact.", ex);
        }
import java.nio.file.Files;
import java.util.HashSet;
        // Accents seems to cause issues in some conditions
        // See https://jira.xwiki.org/browse/XWIKI-14692
        String cleanedOfficeFileName = StringUtils.stripAccents(officeFileName);

        OfficeConverterResult officeConverterResult = importPresentation(officeFileStream, cleanedOfficeFileName);
        Pair<String, Set<File>> htmlPresentationResult = null;
                StringUtils.substringBeforeLast(cleanedOfficeFileName, "."));
        Map<String, InputStream> inputStreams = new HashMap<String, InputStream>();
        inputStreams.put(officeFileName, officeFileStream);
            return this.officeServer.getConverter().convertDocument(inputStreams, officeFileName, "img0.html");
    protected Pair<String, Set<File>> buildPresentationHTML(OfficeConverterResult officeConverterResult, String nameSpace)
        throws IOException
        Set<File> artifactFiles = new HashSet<>();
        File outputDirectory = officeConverterResult.getOutputDirectory();
                File artifact = new File(outputDirectory, slideImageName);
                artifactFiles.add(artifact);
                Files.copy(conversionOutputFile.toPath(), artifact.toPath());
            .map(entry -> String.format("<p><img src=\"%s\"/></p>", entry.getValue()))
        return new XDOMOfficeDocument(xdom, xhtmlOfficeDocument.getArtifactsFiles(), this.componentManager,
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.tika.parser.html.HtmlEncodingDetector;
    /**
     * Used to determine the encoding of the HTML byte array produced by the office server.
     */
    private HtmlEncodingDetector htmlEncodingDetector = new HtmlEncodingDetector();

        // Accents seems to cause issues in some conditions
        // See https://jira.xwiki.org/browse/XWIKI-14692
        String cleanedOfficeFileName = StringUtils.stripAccents(officeFileName);
        Map<String, InputStream> inputStreams = new HashMap<String, InputStream>();
        inputStreams.put(cleanedOfficeFileName, officeFileStream);
        String outputFileName = StringUtils.substringBeforeLast(cleanedOfficeFileName, ".") + ".html";
                this.officeServer.getConverter().convertDocument(inputStreams, cleanedOfficeFileName, outputFileName);
        Document xhtmlDoc = this.cleanAndCreateFile(reference, filterStyles, officeConverterResult);
        Set<File> artifacts = this.handleArtifacts(xhtmlDoc, officeConverterResult);
        OfficeConverterResult officeConverterResult) throws OfficeImporterException
    private Set<File> handleArtifacts(Document xhtmlDoc, OfficeConverterResult officeConverterResult)
        throws OfficeImporterException
        Set<File> artifacts = new HashSet<>(officeConverterResult.getAllFiles());
        artifacts.remove(officeConverterResult.getOutputFile());
            File outputDirectory = officeConverterResult.getOutputDirectory();
                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);
            this.inputDir = new File(this.rootDir, "input");
                this.inputFile = new File(this.inputDir, StringUtils.stripAccents(inputFileName));
                this.outputFile = new File(this.outputDir, StringUtils.stripAccents(outputFileName));
                filterImageSource(source, targetDocumentReference);
    private void filterImageSource(Attr source, DocumentReference targetDocumentReference)
            fileName = getFileName(source);
    private String getFileName(Attr source) throws MimeTypeException

                // '@' must also be escaped in order to resolve properly the path in XWiki
                fileName = fileName.replaceAll("@", "\\\\@");
import java.io.File;
import java.util.HashSet;
import java.util.Set;
            Set<File> artifactsFiles = relocateArtifacts(doc, officeDocument);
            XDOMOfficeDocument splitDocument = new XDOMOfficeDocument(doc.getXdom(), artifactsFiles, componentManager,
     * 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.
    private Set<File> relocateArtifacts(WikiDocument sectionDoc, XDOMOfficeDocument officeDocument)
        Set<File> artifacts = officeDocument.getArtifactsFiles();
        Set<File> result = new HashSet<>();
        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);
        return new XDOMOfficeDocument(xdom, Collections.emptySet(), this.componentManager, null);
        when(doc.getArtifactsFiles()).thenReturn(Collections.singleton(artifact));
        verify(documentAccessBridge).setAttachmentContent(new AttachmentReference(fileName, documentReference),
            fileContent);
        when(this.officeConverter.convertDocument(Collections.singletonMap("file.odp", officeFileStream), "file.odp",
        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());
public class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
    public void xdomOfficeDocumentBuilding() throws Exception
        final Map<String, InputStream> mockInput = new HashMap<String, InputStream>();
        mockInput.put(INPUT_FILE_NAME, mockOfficeFileStream);
        when(mockDocumentConverter.convertDocument(mockInput, INPUT_FILE_NAME, OUTPUT_FILE_NAME))
        assertEquals(0, document.getArtifactsFiles().size());
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class DefaultXHTMLOfficeDocumentBuilderTest
    public void xhtmlOfficeDocumentBuilding() throws Exception
        when(this.officeConverter.convertDocument(Collections.singletonMap("file.odt", officeFileStream), "file.odt",
            "file.html")).thenReturn(converterResult);
        Map<String, byte[]> embeddedImages = Collections.singletonMap("image.png", "Image content".getBytes());
        Set<File> expectedFileArtifacts = new HashSet<>();
        expectedFileArtifacts.add(otherArtifact);
        expectedFileArtifacts.add(new File(this.outputDirectory, "image.png"));
        assertEquals(expectedFileArtifacts, result.getArtifactsFiles());
import org.xwiki.rendering.renderer.reference.ResourceReferenceSerializer;
import static org.mockito.Mockito.*;
    @MockComponent
    @Named("xhtmlmarker")
    private ResourceReferenceSerializer xhtmlMarkerSerializer;

        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|-foo--bar.png-");
        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|" + imageName);
        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|foo.png");
        when(this.xhtmlMarkerSerializer.serialize(resourceReference)).thenReturn("false|-|attach|-|" + fileName);
            new XDOMOfficeDocument(xdom, Collections.emptySet(), this.componentManager, null);
        String regex = "(?<imageName>Test_html_[\\w]+\\.png)";
        regex = "(?<imageName>Test_accents & e\\$u !-_\\.\\+\\(\\);\\\\@=_html_[\\w]+\\.png)";
        matcher = pattern.matcher(wikiEditPage.getContent());
        assertTrue(matcher.find());
        assertTrue(attachmentsPane.attachmentExistsByFileName(imageName.replaceAll("\\\\@", "@")));
        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(wikiEditPage.getContent().contains("[[image:Test_accents & e$u !-_.+();\\@=-slide0.jpg]]"));
import java.io.FileInputStream;
import java.util.HashMap;
     * @param artifactFiles specify which of the image blocks should be processed; only the image blocks
    private Set<File> processImages(XDOM xdom, Set<File> artifactFiles, DocumentReference ownerDocumentReference,
        Map<String, ?> parameters)
            Map<String, File> fileMap = new HashMap<>();
            for (File file : artifactFiles) {
                fileMap.put(file.getName(), file);
            }

                if (fileMap.containsKey(imageReference)) {
                        File artifact = fileMap.get(imageReference);
                        File tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
                            new FileInputStream(artifact));
                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsFiles(),
                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsFiles(), ownerDocument,
import java.io.FileOutputStream;
import org.apache.commons.io.IOUtils;
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptySet(), componentManager, null);
        ResourceReference imageReference = new ResourceReference("slide0.png", ResourceType.URL);
        File artifact = new File(this.tempDir, "slide0.png");
        try (FileOutputStream fos = new FileOutputStream(artifact)) {
            IOUtils.write(new byte[8], fos);
        }
        XDOMOfficeDocument xdomOfficeDocument = new XDOMOfficeDocument(xdom, Collections.singleton(artifact),
            componentManager, converterResult);
            Arrays.asList(String.valueOf(viewParameters.hashCode()), "slide0.png"), documentReference);
        ExtendedURL extendedURL = new ExtendedURL(Arrays.asList("url", "to", "slide0.png"));
        doc.setAttachment(attachmentFilename,
            new ByteArrayInputStream(attachmentData != null ? attachmentData : new byte[0]), xcontext);
import java.io.File;
import java.io.FileInputStream;
import org.apache.commons.io.IOUtils;
        for (File artifact : xdomOfficeDocument.getArtifactsFiles()) {

            AttachmentReference artifactReference = new AttachmentReference(artifact.getName(), targetDocRef);
            try (FileInputStream fis = new FileInputStream(artifact)) {
                documentAccessBridge.setAttachmentContent(artifactReference, IOUtils.toByteArray(fis));
import static org.mockito.Mockito.*;
        when(xdomOfficeDocument.getArtifactsFiles()).thenReturn(Collections.emptySet());
        when(xdomOfficeDocument.getArtifactsFiles()).thenReturn(Collections.emptySet());
Fixed code: import org.apache.commons.io.IOUtils;
    /**
     * 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));
    }

import java.io.File;
import java.util.Collections;
import java.util.Set;
     * @deprecated Since 13.1RC1 use {@link #getArtifactsFiles()}.

    /**
     * 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();
    }
import java.io.InputStream;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.xwiki.officeimporter.OfficeImporterException;
import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
    @Deprecated
    private Set<File> XDOMOfficeDocument.fileArtifacts;

        this(xdom, artifacts.entrySet().stream()
                .map(entry -> new ByteArrayOfficeDocumentArtifact(entry.getKey(), entry.getValue()))
                .collect(Collectors.toMap(ByteArrayOfficeDocumentArtifact::getName, Function.identity())),
            componentManager, null);
    /**
     * 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;
    }

            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) {

    /**
     * Overrides {@link CompatibilityOfficeDocument#getArtifactsFiles()}.
     */
    @Deprecated
    public Set<File> XDOMOfficeDocument.getArtifactsFiles()
    {
        return this.fileArtifacts != null ? this.fileArtifacts : Collections.emptySet();
    }
import java.io.InputStream;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.xwiki.officeimporter.OfficeImporterException;
import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
    @Deprecated
    private Set<File> XHTMLOfficeDocument.fileArtifacts;

        this(document, artifacts.entrySet().stream()
                .map(entry -> new ByteArrayOfficeDocumentArtifact(entry.getKey(), entry.getValue()))
                .collect(Collectors.toMap(ByteArrayOfficeDocumentArtifact::getName, Function.identity())),
            null);
    /**
     * 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;
    }

            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) {

    /**
     * Overrides {@link CompatibilityOfficeDocument#getArtifactsFiles()}.
     */
    @Deprecated(since = "14.10.8, 15.3RC1")
    public Set<File> XHTMLOfficeDocument.getArtifactsFiles()
    {
        return this.fileArtifacts != null ? this.fileArtifacts : Collections.emptySet();
    }
import java.util.Map;
import org.xwiki.stability.Unstable;
     * <p>
     * The key of the map is the artifact name.
     * @return the map of artifact names to artifacts related to this office document
     * @since 14.10.8
     * @since 15.3RC1
    @Unstable
    default Map<String, OfficeDocumentArtifact> getArtifactsMap()
        return Collections.emptyMap();
/*
 * 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;
}
import java.util.Map;
import org.xwiki.stability.Unstable;
    private final Map<String, OfficeDocumentArtifact> artifactsMap;
     * @param artifacts artifacts for this office document.
     * @since 14.10.8
     * @since 15.3-rc-1
    @Unstable
    public XDOMOfficeDocument(XDOM xdom, Map<String, OfficeDocumentArtifact> artifacts,
        ComponentManager componentManager, OfficeConverterResult converterResult)
        this.artifactsMap = artifacts;
    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
        return this.artifactsMap;
import java.util.Map;
import org.xwiki.stability.Unstable;
    private Map<String, OfficeDocumentArtifact> artifactsMap;
     * @param artifacts artifacts for this office document.
     * @since 14.10.8
     * @since 15.3RC1
    @Unstable
    public XHTMLOfficeDocument(Document document, Map<String, OfficeDocumentArtifact> artifacts,
        OfficeConverterResult converterResult)
        this.artifactsMap = artifacts;
    public Map<String, OfficeDocumentArtifact> getArtifactsMap()
        return this.artifactsMap;
import java.io.InputStream;
import java.util.Map;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
        attachArtifacts(doc.getArtifactsMap(), documentReference);
    private void attachArtifacts(Map<String, OfficeDocumentArtifact> artifactFiles,
        DocumentReference targetDocumentReference)
        artifactFiles.forEach((filename, artifact) -> {
            AttachmentReference attachmentReference = new AttachmentReference(filename, targetDocumentReference);
            try (InputStream is = artifact.getContentInputStream()) {
                this.docBridge.setAttachmentContent(attachmentReference, is);
                // Log the error as warning and skip the artifact.
                this.logger.warn("Error while attaching artifact: [{}].", ExceptionUtils.getRootCauseMessage(ex));
        });
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.converter.OfficeConverterFileStorage;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
import org.xwiki.xml.XMLUtils;
        OfficeConverterResult officeConverterResult = importPresentation(officeFileStream, officeFileName);
        Pair<String, Map<String, OfficeDocumentArtifact>> htmlPresentationResult;
                StringUtils.substringBeforeLast(officeFileName, "."));
        String inputFileName = OfficeConverterFileStorage.getSafeInputFilenameFromExtension(officeFileName);
        Map<String, InputStream> inputStreams = Map.of(inputFileName, officeFileStream);
            return this.officeServer.getConverter().convertDocument(inputStreams, inputFileName, "img0.html");
    protected Pair<String, Map<String, OfficeDocumentArtifact>> buildPresentationHTML(
        OfficeConverterResult officeConverterResult, String nameSpace) throws IOException
        Map<String, OfficeDocumentArtifact> artifactFiles = new HashMap<>();
                artifactFiles.put(slideImageName, new FileOfficeDocumentArtifact(slideImageName, conversionOutputFile));
            .map(entry -> String.format("<p><img src=\"%s\"/></p>", XMLUtils.escapeAttributeValue(entry.getValue())))
        return new XDOMOfficeDocument(xdom, xhtmlOfficeDocument.getArtifactsMap(), this.componentManager,
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.converter.OfficeConverterFileStorage;
import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
        String inputFileName = OfficeConverterFileStorage.getSafeInputFilenameFromExtension(officeFileName);
        Map<String, InputStream> inputStreams = new HashMap<>();
        inputStreams.put(inputFileName, officeFileStream);
        String outputFileName = "output.html";
                this.officeServer.getConverter().convertDocument(inputStreams, inputFileName, outputFileName);
        // 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);
        OfficeConverterResult officeConverterResult, String replacePrefix, String replacementPrefix)
        throws OfficeImporterException
        // 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);
    private Map<String, OfficeDocumentArtifact> handleArtifacts(Document xhtmlDoc,
        OfficeConverterResult officeConverterResult, String replacePrefix, String replacementPrefix)
        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());
                String fileName = embeddedImage.getKey();
                artifacts.put(fileName, new ByteArrayOfficeDocumentArtifact(fileName, embeddedImage.getValue()));
            // Check that the potentially cleaned filename is actually in the input streams.
            this.checkInputStream(inputStreams, storage.getInputFile().getName());

import java.util.regex.Pattern;
import org.apache.commons.compress.utils.FileNameUtils;
    /**
     * 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";

            this.inputDir = new File(this.rootDir, INPUT);
                this.inputFile = new File(this.inputDir, cleanFilename(inputFileName));
                this.outputFile = new File(this.outputDir, cleanFilename(outputFileName));
    /**
     * 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;
    }

/*
 * 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();
    }
}
/*
 * 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();
    }
}
        String replaceAttachmentPrefix = cleaningParams.get("replaceImagePrefix");
        String replacementAttachmentPrefix = cleaningParams.get("replacementImagePrefix");

                filterImageSource(source, targetDocumentReference, replaceAttachmentPrefix,
                    replacementAttachmentPrefix);
    private void filterImageSource(Attr source, DocumentReference targetDocumentReference, String replacePrefix,
        String replacementPrefix)
            fileName = getFileName(source, replacePrefix, replacementPrefix);
    private String getFileName(Attr source, String replacePrefix, String replacementPrefix) throws MimeTypeException

            fileName = replacePrefix(replacePrefix, replacementPrefix, fileName);
            // '@' must also be escaped in order to resolve properly the path in XWiki
            fileName = fileName.replace("@", "\\@");

    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;
    }
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
            Map<String, OfficeDocumentArtifact> artifactsMap = relocateArtifacts(doc, officeDocument);
            XDOMOfficeDocument splitDocument = new XDOMOfficeDocument(doc.getXdom(), artifactsMap, componentManager,
     * 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.
    private Map<String, OfficeDocumentArtifact> relocateArtifacts(WikiDocument sectionDoc,
        XDOMOfficeDocument officeDocument)
        Map<String, OfficeDocumentArtifact> artifacts = officeDocument.getArtifactsMap();
        Map<String, OfficeDocumentArtifact> result = new HashMap<>();
        for (ImageBlock imageBlock : imageBlocks) {
            String imageReference = imageBlock.getReference().getReference();
            OfficeDocumentArtifact artifact = artifacts.get(imageReference);
            if (artifact != null) {
                result.put(imageReference, artifact);
        return new XDOMOfficeDocument(xdom, Collections.emptyMap(), this.componentManager, null);
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
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;
        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));
        assertEquals(1, attachmentContents.size());
        AttachmentReference expectedAttachmentReference = new AttachmentReference(fileName, documentReference);
        assertEquals(expectedAttachmentReference, attachmentContents.keySet().iterator().next());
        assertArrayEquals(fileContent, attachmentContents.get(expectedAttachmentReference));
import java.util.Map;
import java.util.function.Function;
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
import org.xwiki.officeimporter.internal.document.FileOfficeDocumentArtifact;
        when(this.officeConverter.convertDocument(Collections.singletonMap("input.odp", officeFileStream), "input.odp",
        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());
class DefaultXDOMOfficeDocumentBuilderTest extends AbstractOfficeImporterTest
    void xdomOfficeDocumentBuilding() throws Exception
        final Map<String, InputStream> mockInput = new HashMap<>();
        String internalInputFilename = "input.doc";
        mockInput.put(internalInputFilename, mockOfficeFileStream);
        when(mockDocumentConverter.convertDocument(mockInput, internalInputFilename, "output.html"))
        assertEquals(0, document.getArtifactsMap().size());
import java.nio.charset.StandardCharsets;
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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;
class DefaultXHTMLOfficeDocumentBuilderTest
    void xhtmlOfficeDocumentBuilding() throws Exception
        String otherArtifactContent = "Other content";
        try (FileOutputStream fos = new FileOutputStream(otherArtifact)) {
            IOUtils.write(otherArtifactContent.getBytes(), fos);
        }

        when(this.officeConverter.convertDocument(Collections.singletonMap("input.odt", officeFileStream), "input.odt",
            "output.html")).thenReturn(converterResult);
        byte[] imageContent = "Image content".getBytes();
        Map<String, byte[]> embeddedImages = Collections.singletonMap("image.png", imageContent);
        params.put("replaceImagePrefix", "output_html_");
        params.put("replacementImagePrefix", "file_");
        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()));
/*
 * 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());
    }
}
import java.nio.charset.StandardCharsets;
import org.xwiki.rendering.internal.renderer.xhtml.XHTMLMarkerResourceReferenceSerializer;
import org.xwiki.test.annotation.ComponentList;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ComponentList({ XHTMLMarkerResourceReferenceSerializer.class })

    @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);
    }
            new XDOMOfficeDocument(xdom, Collections.emptyMap(), this.componentManager, null);
        String regex = "(?<imageName>Test_[\\w]+\\.png)";
        regex = "(?<imageName>Test_accents & é\\$ù !-_\\.\\+\\(\\);\\\\@=_\\w+\\.png)";
        String wikiContent = wikiEditPage.getContent();
        matcher = pattern.matcher(wikiContent);
        assertTrue(matcher.find(), String.format("Pattern [%s] not found in [%s]", regex, wikiContent));
        assertTrue(attachmentsPane.attachmentExistsByFileName(imageName.replace("\\@", "@")));
        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"));
        assertTrue(wikiEditPage.getContent().contains("[[image:Test_accents & é$ù !-_.+();\\@=-slide0.jpg]]"));
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
     * @param artifactMap specify which of the image blocks should be processed; only the image blocks
    private Set<File> processImages(XDOM xdom, Map<String, OfficeDocumentArtifact> artifactMap,
        DocumentReference ownerDocumentReference, Map<String, ?> parameters)
                if (artifactMap.containsKey(imageReference)) {
                        OfficeDocumentArtifact artifact = artifactMap.get(imageReference);
                        File tempFile;
                        try (InputStream inputStream = artifact.getContentInputStream()) {
                            tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
                                inputStream);
                        }
                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsMap(),
                Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifactsMap(), ownerDocument,
import org.xwiki.officeimporter.internal.document.ByteArrayOfficeDocumentArtifact;
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
            new XDOMOfficeDocument(new XDOM(new ArrayList<Block>()), Collections.emptyMap(), componentManager, null);
        String imageName = "slide0.png";
        ResourceReference imageReference = new ResourceReference(imageName, ResourceType.URL);
        XDOMOfficeDocument xdomOfficeDocument = new XDOMOfficeDocument(xdom, Collections.singletonMap(imageName,
            new ByteArrayOfficeDocumentArtifact(imageName, new byte[8])), componentManager, converterResult);
            Arrays.asList(String.valueOf(viewParameters.hashCode()), imageName), documentReference);
        ExtendedURL extendedURL = new ExtendedURL(Arrays.asList("url", "to", imageName));
    @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);
    }


    {
        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
        doc.setAttachment(attachmentFilename, attachmentData, xcontext);
import java.io.ByteArrayInputStream;

import org.apache.commons.io.IOUtils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.xwiki.model.reference.AttachmentReference;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

    @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()));
    }
import org.xwiki.officeimporter.document.OfficeDocumentArtifact;
        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);
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
        when(xdomOfficeDocument.getArtifactsMap()).thenReturn(Collections.emptyMap());
        when(xdomOfficeDocument.getArtifactsMap()).thenReturn(Collections.emptyMap());