CVE-2023-37913 (Java, published 2023-10-25T18:17Z)
Description: 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.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/xwiki/xwiki-platform
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 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).


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());