From ae5c113c241a6926d8df5a893f3ee585561b3a79 Mon Sep 17 00:00:00 2001 From: Marius Dumitru Florea <marius@xwiki.com> Date: Mon, 20 Feb 2023 00:06:51 +0200 Subject: [PATCH] XWIKI-13977: Generate internal links when exporting multiple linked pages to PDF XWIKI-20563: Duplicate headings can lead to broken internal links in the generated PDF (cherry picked from commit 7bd586c075a13b660f167f1eb243e08fd3d5a86b) --- xwiki-platform-core/pom.xml | 13 +- .../browser/AbstractBrowserPDFPrinter.java | 7 - .../xwiki/export/pdf/browser/BrowserTab.java | 7 - .../pdf/internal/job/DocumentRenderer.java | 5 +- .../internal/job/PDFExportIdGenerator.java | 87 +++++++++++ .../export/pdf/job/PDFExportJobRequest.java | 28 ++++ .../export/pdf/job/PDFExportJobStatus.java | 37 +++++ .../pdf/browser/BrowserPDFPrinterTest.java | 1 - .../internal/job/DocumentRendererTest.java | 12 +- .../job/PDFExportIdGeneratorTest.java | 82 +++++++++++ .../export/pdf/internal/chrome/ChromeTab.java | 21 --- .../DefaultPDFExportJobRequestFactory.java | 27 +++- .../pdf/internal/chrome/ChromeTabTest.java | 32 +---- ...DefaultPDFExportJobRequestFactoryTest.java | 5 + .../resources/PDFExportIT/Anchors/Child.xml | 72 ++++++++++ .../resources/PDFExportIT/Anchors/WebHome.xml | 72 ++++++++++ .../xwiki/export/pdf/test/ui/PDFExportIT.java | 117 ++++++++++++++- .../main/resources/XWiki/PDFExport/Sheet.xml | 135 ++++++++++++++++-- 18 files changed, 674 insertions(+), 86 deletions(-) create mode 100644 xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/PDFExportIdGenerator.java create mode 100644 xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/PDFExportIdGeneratorTest.java create mode 100644 xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/Child.xml create mode 100644 xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/WebHome.xml diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml index d60f6ea5ba0..2b281b85afa 100644 --- a/xwiki-platform-core/pom.xml +++ b/xwiki-platform-core/pom.xml @@ -125,8 +125,17 @@ Single justification example: --> - - + <revapi.differences> + <justification>Removed unused method from unstable API.</justification> + <criticality>documented</criticality> + <differences> + <item> + <ignore>true</ignore> + <code>java.method.removed</code> + <old>method void org.xwiki.export.pdf.browser.BrowserTab::setBaseURL(java.net.URL) throws java.io.IOException</old> + </item> + </differences> + </revapi.differences> </analysisConfiguration> </configuration> </plugin> diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/AbstractBrowserPDFPrinter.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/AbstractBrowserPDFPrinter.java index c6b7758cc6f..45344940c8c 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/AbstractBrowserPDFPrinter.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/AbstractBrowserPDFPrinter.java @@ -80,13 +80,6 @@ public InputStream print(URL printPreviewURL) throws IOException throw new IOException("Failed to load the print preview URL: " + cookieFilterContext.getTargetURL()); } - if (!printPreviewURL.toString().equals(cookieFilterContext.getTargetURL().toString())) { - // Make sure the relative URLs are resolved based on the original print preview URL otherwise the user - // won't be able to open the links from the generated PDF because they use a host name accessible only - // from the browser that generated the PDF. See PDFExportConfiguration#getXWikiHost() - browserTab.setBaseURL(printPreviewURL); - } - return browserTab.printToPDF(() -> { browserTab.close(); }); diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/BrowserTab.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/BrowserTab.java index 00c2821160e..c972e996ed6 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/BrowserTab.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/browser/BrowserTab.java @@ -94,13 +94,6 @@ default boolean navigate(URL url) throws IOException */ String getSource(); - /** - * Sets the base URL for the currently loaded web page. - * - * @param baseURL the base URL to set - */ - void setBaseURL(URL baseURL) throws IOException; - /** * Print the current web page to PDF. * diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/DocumentRenderer.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/DocumentRenderer.java index a28ff6903ca..881404eb31e 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/DocumentRenderer.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/DocumentRenderer.java @@ -41,7 +41,6 @@ import org.xwiki.rendering.renderer.printer.WikiPrinter; import org.xwiki.rendering.syntax.Syntax; import org.xwiki.rendering.transformation.RenderingContext; -import org.xwiki.rendering.util.IdGenerator; /** * Component used to render documents. @@ -80,7 +79,7 @@ public class DocumentRenderer /** * Used to generate unique identifiers across multiple rendered documents. */ - private IdGenerator idGenerator = new IdGenerator(); + private PDFExportIdGenerator idGenerator = new PDFExportIdGenerator(); /** * Renders the specified document. @@ -111,7 +110,7 @@ public DocumentRenderingResult render(DocumentReference documentReference, boole DocumentModelBridge document = this.documentAccessBridge.getTranslatedDocumentInstance(documentReference); XDOM xdom = display(document, parameters, withTitle); String html = renderXDOM(xdom, targetSyntax); - return new DocumentRenderingResult(documentReference, xdom, html); + return new DocumentRenderingResult(documentReference, xdom, html, this.idGenerator.resetLocalIds()); } private XDOM display(DocumentModelBridge document, DocumentDisplayerParameters parameters, boolean withTitle) diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/PDFExportIdGenerator.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/PDFExportIdGenerator.java new file mode 100644 index 00000000000..bbb8e555595 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/internal/job/PDFExportIdGenerator.java @@ -0,0 +1,87 @@ +/* + * 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.export.pdf.internal.job; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.xwiki.rendering.util.IdGenerator; + +/** + * The id generator used when rendering wiki pages for PDF export. It collects a map of {@code localId -> globalId} for + * each rendered page (see {@link #resetLocalIds()}) that can be used on the client side to refactor external links into + * internal links. The local id is the id that is generated when rendering a single page, while the global id is the id + * generated when rendering multiple pages (and thus the generated id needs to be unique across all these pages). + * + * @version $Id$ + * @since 14.10.6 + * @since 15.1RC1 + */ +public class PDFExportIdGenerator extends IdGenerator +{ + private IdGenerator localIdGenerator = new IdGenerator(); + + /** + * Maps local IDs to global IDs. + */ + private Map<String, String> idMap = new HashMap<>(); + + @Override + public String generateUniqueId(String prefix, String text) + { + String globalId = super.generateUniqueId(prefix, text); + String localId = this.localIdGenerator.generateUniqueId(prefix, text); + this.idMap.put(localId, globalId); + return globalId; + } + + @Override + public void remove(String globalId) + { + super.remove(globalId); + this.idMap.entrySet().stream().filter(entry -> Objects.equals(entry.getValue(), globalId)).findFirst() + .ifPresent(entry -> { + String localId = entry.getKey(); + this.localIdGenerator.remove(localId); + this.idMap.remove(localId); + }); + } + + @Override + public void reset() + { + super.reset(); + resetLocalIds(); + } + + /** + * Reset the collected local IDs. Call this before each page rendering. + * + * @return the mapping between the local IDs and the global IDs + */ + public Map<String, String> resetLocalIds() + { + Map<String, String> idMapCopy = new HashMap<>(this.idMap); + this.localIdGenerator.reset(); + this.idMap.clear(); + return idMapCopy; + } +} diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobRequest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobRequest.java index a85030950ed..8da4420dea5 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobRequest.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobRequest.java @@ -19,6 +19,7 @@ */ package org.xwiki.export.pdf.job; +import java.net.URL; import java.util.Collections; import java.util.List; @@ -60,6 +61,8 @@ public class PDFExportJobRequest extends AbstractCheckRightsRequest private static final String PROPERTY_FILE_NAME = "fileName"; + private static final String PROPERTY_BASE_URL = "baseURL"; + /** * Default constructor. */ @@ -254,4 +257,29 @@ public void setFileName(String fileName) { setProperty(PROPERTY_FILE_NAME, fileName); } + + /** + * @return the base URL used to resolve relative URLs in the exported content + * @since 14.10.6 + * @since 15.1RC1 + */ + public URL getBaseURL() + { + return getProperty(PROPERTY_BASE_URL); + } + + /** + * Sets the base URL used to resolve relative URLs in the exported content. When the base URL is not set the + * relative URLs are by default resolved relative to the print preview URL which uses the {@code export} action and + * has a long query string that is specific to PDF export. This means relative URLs may be resolved using the + * {@code export} action and some strange query string if the base URL is not set. + * + * @param baseURL the base URL used to resolve URLs in the exported content + * @since 14.10.6 + * @since 15.1RC1 + */ + public void setBaseURL(URL baseURL) + { + setProperty(PROPERTY_BASE_URL, baseURL); + } } diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobStatus.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobStatus.java index d6d82ca99a8..f5dcd5420c3 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobStatus.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/main/java/org/xwiki/export/pdf/job/PDFExportJobStatus.java @@ -20,8 +20,10 @@ package org.xwiki.export.pdf.job; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.UUID; import org.xwiki.job.DefaultJobStatus; @@ -58,6 +60,8 @@ public static class DocumentRenderingResult private final String html; + private final Map<String, String> idMap; + /** * Create a new rendering result for the specified document. * @@ -66,10 +70,30 @@ public static class DocumentRenderingResult * @param html the HTML obtained by rendering the specified document */ public DocumentRenderingResult(DocumentReference documentReference, XDOM xdom, String html) + { + this(documentReference, xdom, html, Collections.emptyMap()); + } + + /** + * Create a new rendering result for the specified document. + * + * @param documentReference the document that has been rendered + * @param xdom the XDOM obtained by rendering the specified document + * @param html the HTML obtained by rendering the specified document + * @param idMap the mapping between local IDs (that would have been generated if the document were rendered + * alone) and global IDs (that were actually generated when the document was rendered together with + * the other documents included in the PDF export); this mapping can be used to convert external + * links into internal links + * @since 14.10.6 + * @since 15.1RC1 + */ + public DocumentRenderingResult(DocumentReference documentReference, XDOM xdom, String html, + Map<String, String> idMap) { this.documentReference = documentReference; this.xdom = xdom; this.html = html; + this.idMap = idMap; } /** @@ -97,6 +121,19 @@ public String getHTML() { return html; } + + /** + * @return the mapping between local IDs (that would have been generated if the document were rendered alone) + * and global IDs (that were actually generated when the document was rendered together with the other + * documents included in the PDF export); this mapping can be used to convert external links into + * internal links + * @since 14.10.6 + * @since 15.1RC1 + */ + public Map<String, String> getIdMap() + { + return Collections.unmodifiableMap(this.idMap); + } } private final List<DocumentRenderingResult> documentRenderingResults = new LinkedList<>(); diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/browser/BrowserPDFPrinterTest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/browser/BrowserPDFPrinterTest.java index 9d08975e7b4..321dfdbcd13 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/browser/BrowserPDFPrinterTest.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/browser/BrowserPDFPrinterTest.java @@ -147,7 +147,6 @@ public InputStream answer(InvocationOnMock invocation) throws Throwable assertEquals("172.12.0.3", this.cookieFilterContextCaptor.getValue().getBrowserIPAddress()); assertEquals(browserPrintPreviewURL, this.cookieFilterContextCaptor.getValue().getTargetURL()); - verify(this.browserTab).setBaseURL(printPreviewURL); verify(this.browserTab).close(); } diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/DocumentRendererTest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/DocumentRendererTest.java index 5f2ff7675ac..1aa8f315823 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/DocumentRendererTest.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/DocumentRendererTest.java @@ -20,6 +20,9 @@ package org.xwiki.export.pdf.internal.job; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import javax.inject.Named; @@ -49,7 +52,6 @@ import org.xwiki.test.junit5.mockito.MockComponent; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.same; @@ -115,7 +117,7 @@ void render() throws Exception public XDOM answer(InvocationOnMock invocation) throws Throwable { DocumentDisplayerParameters params = (DocumentDisplayerParameters) invocation.getArgument(1); - assertNotNull(params.getIdGenerator()); + params.getIdGenerator().generateUniqueId("H", "heading"); return params.isTitleDisplayed() ? titleXDOM : xdom; } }); @@ -135,6 +137,7 @@ public Void answer(InvocationOnMock invocation) assertEquals(1, xdom.getChildren().size()); assertSame(xdom, result.getXDOM()); assertEquals("some content", result.getHTML()); + assertEquals(Collections.singletonMap("Hheading", "Hheading"), result.getIdMap()); // Now render with title. result = this.documentRenderer.render(documentReference, true); @@ -144,5 +147,10 @@ public Void answer(InvocationOnMock invocation) assertEquals("Htest:Some.Page", title.getId()); assertEquals("test:Some.Page", title.getParameter("data-xwiki-document-reference")); assertEquals("title", ((WordBlock) title.getChildren().get(0)).getWord()); + + Map<String, String> expectedIdMap = new HashMap<>(); + expectedIdMap.put("Hheading", "Hheading-1"); + expectedIdMap.put("Hheading-1", "Hheading-2"); + assertEquals(expectedIdMap, result.getIdMap()); } } diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/PDFExportIdGeneratorTest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/PDFExportIdGeneratorTest.java new file mode 100644 index 00000000000..d4aa35b83d3 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-api/src/test/java/org/xwiki/export/pdf/internal/job/PDFExportIdGeneratorTest.java @@ -0,0 +1,82 @@ +/* + * 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.export.pdf.internal.job; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@link PDFExportIdGenerator}. + * + * @version $Id$ + */ +class PDFExportIdGeneratorTest +{ + private PDFExportIdGenerator idGenerator = new PDFExportIdGenerator(); + + @Test + void removeAndReset() + { + Map<String, String> expectedIdMap = new HashMap<>(); + + this.idGenerator.generateUniqueId("Alice"); + this.idGenerator.generateUniqueId("Bob"); + + expectedIdMap.put("IAlice", "IAlice"); + expectedIdMap.put("IBob", "IBob"); + assertEquals(expectedIdMap, this.idGenerator.resetLocalIds()); + + this.idGenerator.generateUniqueId("Alice"); + this.idGenerator.generateUniqueId("Bob"); + this.idGenerator.generateUniqueId("Carol"); + this.idGenerator.remove("IBob-1"); + + expectedIdMap.clear(); + expectedIdMap.put("IAlice", "IAlice-1"); + expectedIdMap.put("ICarol", "ICarol"); + assertEquals(expectedIdMap, this.idGenerator.resetLocalIds()); + + this.idGenerator.remove("IBob"); + this.idGenerator.generateUniqueId("Alice"); + this.idGenerator.generateUniqueId("Bob"); + this.idGenerator.generateUniqueId("Carol"); + + expectedIdMap.clear(); + expectedIdMap.put("IAlice", "IAlice-2"); + expectedIdMap.put("IBob", "IBob"); + expectedIdMap.put("ICarol", "ICarol-1"); + assertEquals(expectedIdMap, this.idGenerator.resetLocalIds()); + + this.idGenerator.reset(); + this.idGenerator.generateUniqueId("Alice"); + this.idGenerator.generateUniqueId("Bob"); + this.idGenerator.generateUniqueId("Carol"); + + expectedIdMap.clear(); + expectedIdMap.put("IAlice", "IAlice"); + expectedIdMap.put("IBob", "IBob"); + expectedIdMap.put("ICarol", "ICarol"); + assertEquals(expectedIdMap, this.idGenerator.resetLocalIds()); + } +} diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/chrome/ChromeTab.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/chrome/ChromeTab.java index 3dcc50cf108..d21644339f2 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/chrome/ChromeTab.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/chrome/ChromeTab.java @@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory; import org.xwiki.export.pdf.browser.BrowserTab; -import com.github.kklisura.cdt.protocol.commands.DOM; import com.github.kklisura.cdt.protocol.commands.Network; import com.github.kklisura.cdt.protocol.commands.Page; import com.github.kklisura.cdt.protocol.commands.Runtime; @@ -128,26 +127,6 @@ public String getSource() return page.getResourceContent(frame.getId(), frame.getUrl()).getContent(); } - @Override - public void setBaseURL(URL baseURL) throws IOException - { - LOGGER.debug("Setting base URL [{}].", baseURL); - Runtime runtime = this.tabDevToolsService.getRuntime(); - runtime.enable(); - - // Add the BASE tag to the page head. I couldn't find a way to create this node using the DOM domain, so I'm - // using JavaScript instead (i.e. the runtime domain). - Evaluate evaluate = runtime.evaluate("jQuery('<base/>').prependTo('head').length"); - checkEvaluation(evaluate, 1, "Failed to insert the BASE tag.", "Unexpected page HTML."); - - DOM dom = this.tabDevToolsService.getDOM(); - dom.enable(); - - // Look for the BASE tag we just added and set its href attribute in order to change the page base URL. - Integer baseNodeId = dom.querySelector(dom.getDocument().getNodeId(), "base"); - dom.setAttributeValue(baseNodeId, "href", baseURL.toString()); - } - @Override public InputStream printToPDF(Runnable cleanup) { diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactory.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactory.java index 2e7cdfff4bb..056a17aa3c8 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactory.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/main/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactory.java @@ -70,6 +70,10 @@ public class DefaultPDFExportJobRequestFactory implements PDFExportJobRequestFac { private static final String EXPORT = "export"; + private static final String PDF_QUERY_STRING = "pdfQueryString"; + + private static final String PDF_HASH = "pdfHash"; + @Inject private Provider<XWikiContext> xcontextProvider; @@ -134,6 +138,7 @@ private void setContextProperties(PDFExportJobRequest request) throws Exception pdfExportContext.put(XWikiContextContextStore.PROP_REQUEST_URL, printPreviewURL); request.setContext(pdfExportContext); + request.setBaseURL(getBaseURL()); } private void readPDFExportOptionsFromHTTPRequest(PDFExportJobRequest request) @@ -178,13 +183,13 @@ private URL getPrintPreviewURL(List<String> jobId) XWikiContext xcontext = this.xcontextProvider.get(); XWikiRequest httpRequest = xcontext.getRequest(); - String originalQueryString = Objects.toString(httpRequest.get("pdfQueryString"), ""); + String originalQueryString = Objects.toString(httpRequest.get(PDF_QUERY_STRING), ""); String queryString = getPrintPreviewQueryString(jobId, originalQueryString); // The request URL hash (fragment identifier) is not sent to the server but in the case of server-side PDF // export we need it as it can influence the behavior of the JavaScript code when the PDF template is loaded in // the headless web browser. In order to overcome this we receive the hash as a request parameter. - String hash = Objects.toString(httpRequest.get("pdfHash"), ""); + String hash = Objects.toString(httpRequest.get(PDF_HASH), ""); // We want the documents to be rendered with the same parameters (query string) and hash (anchor or fragment // identifier) as in print preview mode (what is used to generate the PDF in the end). For this the saved @@ -209,4 +214,22 @@ private String getPrintPreviewQueryString(List<String> jobId, String originalQue ); return URLEncodedUtils.format(printPreviewParams, StandardCharsets.UTF_8) + '&' + originalQueryString; } + + private URL getBaseURL() + { + XWikiContext xcontext = this.xcontextProvider.get(); + XWikiRequest httpRequest = xcontext.getRequest(); + + // Get the query string and hash that were used when the user triggered the PDF export. + String queryString = Objects.toString(httpRequest.get(PDF_QUERY_STRING), ""); + String hash = Objects.toString(httpRequest.get(PDF_HASH), ""); + + // We want the base URL to be the URL from where the user triggered the PDF export. + DocumentReference documentReference = xcontext.getDoc().getDocumentReference(); + return xcontext.getURLFactory().createExternalURL( + this.localStringEntityReferenceSerializer.serialize(documentReference.getLastSpaceReference()), + // We assume the action was view, since most of the time the PDF export is triggered from view mode. + documentReference.getName(), "view", queryString, hash, documentReference.getWikiReference().getName(), + xcontext); + } } diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/chrome/ChromeTabTest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/chrome/ChromeTabTest.java index 1cfd9e94e76..19f8d7c7468 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/chrome/ChromeTabTest.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/chrome/ChromeTabTest.java @@ -35,13 +35,11 @@ import org.mockito.Mock; import org.xwiki.test.junit5.mockito.ComponentTest; -import com.github.kklisura.cdt.protocol.commands.DOM; import com.github.kklisura.cdt.protocol.commands.IO; import com.github.kklisura.cdt.protocol.commands.Network; import com.github.kklisura.cdt.protocol.commands.Page; import com.github.kklisura.cdt.protocol.commands.Runtime; import com.github.kklisura.cdt.protocol.commands.Target; -import com.github.kklisura.cdt.protocol.types.dom.Node; import com.github.kklisura.cdt.protocol.types.io.Read; import com.github.kklisura.cdt.protocol.types.network.CookieParam; import com.github.kklisura.cdt.protocol.types.page.Frame; @@ -193,7 +191,7 @@ void navigateWithWait() throws Exception verify(this.runtime).enable(); } - + @Test void navigateWithWaitAndException() throws Exception { @@ -245,34 +243,6 @@ void getSource() assertEquals("source", this.chromeTab.getSource()); } - @Test - void setBaseURL() throws Exception - { - URL url = new URL("http://www.xwiki.org"); - - Evaluate evaluate = mock(Evaluate.class); - when(this.runtime.evaluate("jQuery('<base/>').prependTo('head').length")).thenReturn(evaluate); - - RemoteObject result = mock(RemoteObject.class); - when(evaluate.getResult()).thenReturn(result); - when(result.getValue()).thenReturn(1); - - DOM dom = mock(DOM.class); - when(this.tabDevToolsService.getDOM()).thenReturn(dom); - - Node document = mock(Node.class); - when(dom.getDocument()).thenReturn(document); - when(document.getNodeId()).thenReturn(12345); - - when(dom.querySelector(12345, "base")).thenReturn(6789); - - this.chromeTab.setBaseURL(url); - - verify(this.runtime).enable(); - verify(dom).enable(); - verify(dom).setAttributeValue(6789, "href", "http://www.xwiki.org"); - } - @Test void printToPDF() throws Exception { diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactoryTest.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactoryTest.java index d7c679bd830..0b172ec4513 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactoryTest.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-default/src/test/java/org/xwiki/export/pdf/internal/job/DefaultPDFExportJobRequestFactoryTest.java @@ -146,6 +146,10 @@ void createRequest() throws Exception when(this.xcontext.getURLFactory().createExternalURL("Some", "Page", "export", queryString, "foo", "test", this.xcontext)).thenReturn(printPreviewURL); + URL baseURL = new URL("http://localhost:8080/xwiki/bin/view/Some/Page?color=red#foo"); + when(this.xcontext.getURLFactory().createExternalURL("Some", "Page", "view", "color=red", "foo", "test", + this.xcontext)).thenReturn(baseURL); + List<String> supportedContextEntries = Arrays.asList("one", "two"); when(this.contextStoreManager.getSupportedEntries()).thenReturn(supportedContextEntries); @@ -177,6 +181,7 @@ void createRequest() throws Exception assertEquals(Collections.singleton("key"), requestParameters.keySet()); assertEquals(Collections.singletonList("value"), Arrays.asList(requestParameters.get("key"))); assertEquals(printPreviewURL, request.getContext().get("request.url")); + assertEquals(baseURL, request.getBaseURL()); assertEquals(selectedDocuments, request.getDocuments()); assertEquals(templateReference, request.getTemplate()); diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/Child.xml b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/Child.xml new file mode 100644 index 00000000000..32ce0dee680 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/Child.xml @@ -0,0 +1,72 @@ +<?xml version="1.1" encoding="UTF-8"?> + +<!-- + * 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. +--> + +<xwikidoc version="1.5" reference="PDFExportIT.Anchors.Child" locale=""> + <web>PDFExportIT.Anchors</web> + <name>Child</name> + <language/> + <defaultLanguage>en</defaultLanguage> + <translation>0</translation> + <creator>xwiki:XWiki.Admin</creator> + <parent>PDFExportIT.Anchors.WebHome</parent> + <author>xwiki:XWiki.Admin</author> + <contentAuthor>xwiki:XWiki.Admin</contentAuthor> + <version>1.1</version> + <title>Child</title> + <comment/> + <minorEdit>false</minorEdit> + <syntaxId>xwiki/2.1</syntaxId> + <hidden>false</hidden> + <content>== Description == + +* [[Self>>]] +* [[Usage>>||anchor="HUsage"]] +* [[Comments>>||anchor="Comments"]] +* [[History>>||queryString="viewer=history"]] +* [[Diff>>||queryString="viewer=changes" anchor="diff"]] + +{{velocity}} +* [[Edit>>path:$doc.getURL('edit')]] +* [[Edit Description>>path:$doc.getURL('edit')#HDescription]] +* [[Edit Wiki>>path:$doc.getURL('edit', 'editor=wiki')]] +* [[Edit Usage Wiki>>path:$doc.getURL('edit', 'editor=wiki')#HUsage]] +{{/velocity}} + +* [[Parent>>.WebHome]] +* [[Parent Usage>>.WebHome||anchor="HUsage"]] +* [[Parent Comments>>.WebHome||anchor="Comments"]] +* [[Parent History>>.WebHome||queryString="viewer=history"]] +* [[Parent Diff>>.WebHome||queryString="viewer=changes" anchor="diff"]] + +{{velocity}} +* [[Parent Edit>>path:$xwiki.getURL('.WebHome', 'edit')]] +* [[Parent Edit Description>>path:$xwiki.getURL('.WebHome', 'edit')#HDescription]] +* [[Parent Edit Wiki>>path:$xwiki.getURL('.WebHome', 'edit', 'editor=wiki')]] +* [[Parent Edit Usage Wiki>>path:$xwiki.getURL('.WebHome', 'edit', 'editor=wiki')#HUsage]] +{{/velocity}} + +== Usage == + +* [[Description>>||anchor="HDescription"]] +* [[Parent Description>>.WebHome||anchor="HDescription"]] +* [[Other Description>>PDFExportIT.Parent.WebHome||anchor="HDescription"]]</content> +</xwikidoc> diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/WebHome.xml b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/WebHome.xml new file mode 100644 index 00000000000..8fddaa804ec --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-content/src/main/resources/PDFExportIT/Anchors/WebHome.xml @@ -0,0 +1,72 @@ +<?xml version="1.1" encoding="UTF-8"?> + +<!-- + * 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. +--> + +<xwikidoc version="1.5" reference="PDFExportIT.Anchors.WebHome" locale=""> + <web>PDFExportIT.Anchors</web> + <name>WebHome</name> + <language/> + <defaultLanguage>en</defaultLanguage> + <translation>0</translation> + <creator>xwiki:XWiki.Admin</creator> + <parent>Main.WebHome</parent> + <author>xwiki:XWiki.Admin</author> + <contentAuthor>xwiki:XWiki.Admin</contentAuthor> + <version>1.1</version> + <title/> + <comment/> + <minorEdit>false</minorEdit> + <syntaxId>xwiki/2.1</syntaxId> + <hidden>false</hidden> + <content>== Description == + +* [[Self>>]] +* [[Usage>>||anchor="HUsage"]] +* [[Comments>>||anchor="Comments"]] +* [[History>>||queryString="viewer=history"]] +* [[Diff>>||queryString="viewer=changes" anchor="diff"]] + +{{velocity}} +* [[Edit>>path:$doc.getURL('edit')]] +* [[Edit Description>>path:$doc.getURL('edit')#HDescription]] +* [[Edit Wiki>>path:$doc.getURL('edit', 'editor=wiki')]] +* [[Edit Usage Wiki>>path:$doc.getURL('edit', 'editor=wiki')#HUsage]] +{{/velocity}} + +* [[Child>>.Child]] +* [[Child Usage>>.Child||anchor="HUsage"]] +* [[Child Comments>>.Child||anchor="Comments"]] +* [[Child History>>.Child||queryString="viewer=history"]] +* [[Child Diff>>.Child||queryString="viewer=changes" anchor="diff"]] + +{{velocity}} +* [[Child Edit>>path:$xwiki.getURL('.Child', 'edit')]] +* [[Child Edit Description>>path:$xwiki.getURL('.Child', 'edit')#HDescription]] +* [[Child Edit Wiki>>path:$xwiki.getURL('.Child', 'edit', 'editor=wiki')]] +* [[Child Edit Usage Wiki>>path:$xwiki.getURL('.Child', 'edit', 'editor=wiki')#HUsage]] +{{/velocity}} + +== Usage == + +* [[Description>>||anchor="HDescription"]] +* [[Child Description>>.Child||anchor="HDescription"]] +* [[Other Description>>PDFExportIT.Parent.WebHome||anchor="HDescription"]]</content> +</xwikidoc> diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-docker/src/test/it/org/xwiki/export/pdf/test/ui/PDFExportIT.java b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-docker/src/test/it/org/xwiki/export/pdf/test/ui/PDFExportIT.java index 7b98e157aef..f290d0c351c 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-docker/src/test/it/org/xwiki/export/pdf/test/ui/PDFExportIT.java +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-test/xwiki-platform-export-pdf-test-docker/src/test/it/org/xwiki/export/pdf/test/ui/PDFExportIT.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -178,8 +179,7 @@ void exportAsPDF(TestUtils setup, TestConfiguration testConfiguration) throws Ex // The content of the parent document has a link to the child document. Map<String, String> contentPageLinks = pdf.getLinksFromPage(2); assertEquals(1, contentPageLinks.size()); - assertEquals(setup.getURL(Arrays.asList("PDFExportIT", "Parent"), "Child") + "/", - contentPageLinks.get("child page.")); + assertEquals("Hxwiki:PDFExportIT.Parent.Child.WebHome", contentPageLinks.get("child page.")); // // Verify the page corresponding to the child document. @@ -431,6 +431,119 @@ void invalidTOCAnchors(TestUtils setup, TestConfiguration testConfiguration) thr } } + @Test + @Order(8) + void refactorAnchors(TestUtils setup, TestConfiguration testConfiguration) throws Exception + { + setup.login("John", "pass"); + + ViewPage viewPage = + setup.gotoPage(new LocalDocumentReference(Arrays.asList("PDFExportIT", "Anchors"), "WebHome")); + ExportTreeModal exportTreeModal = ExportTreeModal.open(viewPage, "PDF"); + // Include the child page in the export. + exportTreeModal.getPageTree().getNode("document:xwiki:PDFExportIT.Anchors.Child").select(); + exportTreeModal.export(); + PDFExportOptionsModal exportOptions = new PDFExportOptionsModal(); + + try (PDFDocument pdf = export(exportOptions, testConfiguration)) { + // + // Verify the anchors from the parent document. + // + + Map<String, String> expectedLinks = new LinkedHashMap<>(); + expectedLinks.put("Self", "Hxwiki:PDFExportIT.Anchors.WebHome"); + // Anchor to a section from this document. + expectedLinks.put("Usage", "HUsage"); + // Anchor to a section from this document that is not found in the PDF. + // expectedLinks.put("Comments", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/#Comments"); + // Anchors that use a query string cannot be made internal. + expectedLinks.put("History", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/?viewer=history#"); + expectedLinks.put("Diff", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/?viewer=changes#diff"); + // Anchors that don't target the view action cannot be made internal. + expectedLinks.put("Edit", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome"); + expectedLinks.put("Edit Description", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome#HDescription"); + expectedLinks.put("Edit Wiki", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome?editor=wiki"); + expectedLinks.put("Edit Usage Wiki", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome?editor=wiki#HUsage"); + + // Anchor to the child document that is included in the PDF. + expectedLinks.put("Child", "Hxwiki:PDFExportIT.Anchors.Child"); + // Anchor to a section from the child document that is included in the PDF. + expectedLinks.put("Child Usage", "HUsage-1"); + // Anchor to a section from the child document that is not found in the PDF. + expectedLinks.put("Child Comments", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child#Comments"); + // Anchors that use a query string cannot be made internal. + expectedLinks.put("Child History", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child?viewer=history"); + expectedLinks.put("Child Diff", + setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child?viewer=changes#diff"); + // Anchors that don't target the view action cannot be made internal. + expectedLinks.put("Child Edit", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child"); + expectedLinks.put("Child Edit Description", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child#HDescription"); + expectedLinks.put("Child Edit Wiki", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child?editor=wiki"); + expectedLinks.put("Child Edit Usage Wiki", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child?editor=wiki#HUsage"); + + // Anchor to another section from this document. + expectedLinks.put("Description", "HDescription"); + // Anchor to another section from the child document that is included in the PDF. + expectedLinks.put("Child Description", "HDescription-1"); + // Anchor to a section from a document that is not included in the PDF. + expectedLinks.put("Other Description", setup.getBaseBinURL() + "view/PDFExportIT/Parent/#HDescription"); + + assertEquals(expectedLinks, pdf.getLinksFromPage(2)); + + // + // Verify the anchors from the child document. + // + + expectedLinks.clear(); + expectedLinks.put("Self", "Hxwiki:PDFExportIT.Anchors.Child"); + // Anchor to a section from this document. + expectedLinks.put("Usage", "HUsage-1"); + // Anchor to a section from this document that is not found in the PDF. + expectedLinks.put("Comments", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child#Comments"); + // Anchors that use a query string cannot be made internal. + expectedLinks.put("History", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child?viewer=history#"); + expectedLinks.put("Diff", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/Child?viewer=changes#diff"); + // Anchors that don't target the view action cannot be made internal. + expectedLinks.put("Edit", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child"); + expectedLinks.put("Edit Description", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child#HDescription"); + expectedLinks.put("Edit Wiki", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child?editor=wiki"); + expectedLinks.put("Edit Usage Wiki", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/Child?editor=wiki#HUsage"); + + // Anchor to the parent document that is included in the PDF. + expectedLinks.put("Parent", "Hxwiki:PDFExportIT.Anchors.WebHome"); + // Anchor to a section from the parent document that is included in the PDF. + expectedLinks.put("Parent Usage", "HUsage"); + // Anchor to a section from the parent document that is not found in the PDF. + // expectedLinks.put("Parent Comments", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/#Comments"); + // Anchors that use a query string cannot be made internal. + expectedLinks.put("Parent History", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/?viewer=history"); + expectedLinks.put("Parent Diff", setup.getBaseBinURL() + "view/PDFExportIT/Anchors/?viewer=changes#diff"); + // Anchors that don't target the view action cannot be made internal. + expectedLinks.put("Parent Edit", setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome"); + expectedLinks.put("Parent Edit Description", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome#HDescription"); + expectedLinks.put("Parent Edit Wiki", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome?editor=wiki"); + expectedLinks.put("Parent Edit Usage Wiki", + setup.getBaseBinURL() + "edit/PDFExportIT/Anchors/WebHome?editor=wiki#HUsage"); + + // Anchor to another section from this document. + expectedLinks.put("Description", "HDescription-1"); + // Anchor to another section from the parent document that is included in the PDF. + expectedLinks.put("Parent Description", "HDescription"); + // Anchor to a section from a document that is not included in the PDF. + expectedLinks.put("Other Description", setup.getBaseBinURL() + "view/PDFExportIT/Parent/#HDescription"); + + assertEquals(expectedLinks, pdf.getLinksFromPage(3)); + } + } + private URL getHostURL(TestConfiguration testConfiguration) throws Exception { return new URL(String.format("http://%s:%d", testConfiguration.getServletEngine().getIP(), diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml index 7610f533bb6..bdabd95db92 100644 --- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml +++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml @@ -83,6 +83,7 @@ ## Inject the required skin extensions. #if ($pdfExportJobId) $!services.job.getJobStatus($pdfExportJobId).requiredSkinExtensions + #clientSidePDFExportConfiguration() #end ## Start the BODY tag. $htmlHeader.substring($headTagEnd, $bodyContentStart) @@ -104,6 +105,21 @@ </html> #end +#macro (clientSidePDFExportConfiguration) + #set ($clientSideConfig = { + 'documents': [], + 'baseURL': $services.job.getJobStatus($pdfExportJobId).request.baseURL + }) + #foreach ($renderingResult in $services.job.getJobStatus($pdfExportJobId).documentRenderingResults) + #set ($discard = $clientSideConfig.documents.add({ + 'reference': $services.model.serialize($renderingResult.documentReference, 'default'), + 'idMap': $renderingResult.idMap + })) + #end + <script id="pdfExportConfig" type="application/json">$jsontool.serialize($clientSideConfig).replace( + '<', '\u003C')</script> +#end + #macro (renderPDFElement $pdfTemplateObj $element) #unwrapXPropertyDisplay($tdoc.display($element, $pdfTemplateObj)) #end @@ -227,15 +243,94 @@ <cache>long</cache> </property> <property> - <code>require(['jquery', 'xwiki-page-ready'], function($, pageReady) { - const fixInternalAnchors = function(container) { - var baseURL = new URL(XWiki.currentDocument.getURL(), window.location.href).toString(); + <code>define('xwiki-export-pdf-config', ['jquery'], function($) { + const pdfExportConfig = { + documents: [], + baseURL: window.location.href + }; + try { + Object.assign(pdfExportConfig, JSON.parse($('#pdfExportConfig').text())); + } catch (e) { + console.error(e); + } + return pdfExportConfig; +}); + +require([ + 'jquery', + 'xwiki-page-ready', + 'xwiki-export-pdf-config' +], function($, pageReady, pdfExportConfig) { + const refactorAnchors = function() { + // Fix anchors that use relative URLs to point to the right document (that generated that anchor). + fixRelativeAnchors($('#xwikicontent')); + // Convert external anchors that target exported documents into internal anchor, whenever possible. + pdfExportConfig.documents?.forEach?.(document => { + const documentReference = XWiki.Model.resolve(document.reference, XWiki.EntityType.DOCUMENT, + XWiki.currentDocument.documentReference); + makeInternalAnchors($('#xwikicontent'), documentReference, document.idMap); + }); + // The anchors in the PDF table of contents are already using global fragment identifiers. + makeInternalAnchors($('.pdf-toc'), XWiki.currentDocument.documentReference); + }; + + /** + * When exporting multiple documents to PDF each of them can produce anchors with relative URLs that are by default + * resolved based on the current document used to trigger the PDF export, which is not what we want. Those relative + * anchors should target the documents that generated them. + * + * @param container where to look for relative anchors that need to be fixed + */ + const fixRelativeAnchors = function(container) { + container.find('a[href]').each(function() { + const anchor = $(this); + let documentReference = anchor.parentsUntil(container).last().prevAll('h1[data-xwiki-document-reference]') + .first().attr('data-xwiki-document-reference'); + if (documentReference) { + documentReference = XWiki.Model.resolve(documentReference, XWiki.EntityType.DOCUMENT, + XWiki.currentDocument.documentReference); + const document = new XWiki.Document(documentReference); + const documentURL = new URL(document.getURL(), pdfExportConfig.baseURL); + try { + anchor.attr('href', new URL(anchor.attr('href'), documentURL).href); + } catch (e) { + console.log('Failed to fix relative URL: ' + anchor.attr('href')); + } + } + }); + }; + + /** + * Look for external anchors that target the specified document (which is included in the PDF export) and convert them + * to internal anchors like this: + * <ul> + * <li>/xwiki/bin/view/Path/To/Document => #documentTitleHeadingId</li> + * <li>/xwiki/bin/view/Path/To/Document#localFragmentId => #globalFragmentId</li> + * <li>/xwiki/bin/view/Path/To/Document#globalFragmentId => #globalFragmentId</li> + * </ul> + */ + const makeInternalAnchors = function(container, documentReference, idMap) { + const document = new XWiki.Document(documentReference); + const documentURL = new URL(document.getURL(), pdfExportConfig.baseURL).href; container.find('a[href]').each(function() { const anchor = $(this); try { - const url = new URL(anchor.prop('href')); - if (url.hash && baseURL + url.hash === url.toString()) { - anchor.attr('href', url.hash); + const anchorURL = new URL(anchor.attr('href'), pdfExportConfig.baseURL); + // Drop the '#' from the end of the URL when the hash is empty. + anchorURL.hash = anchorURL.hash; + if (anchorURL.href === documentURL + anchorURL.hash) { + // Assume the anchor targets a document fragment specified by its global id. + let fragmentId = anchorURL.hash.substring(1); + if (!fragmentId) { + // The anchor targets the start of the document. + fragmentId = getDocumentFragmentId(documentReference); + } else if (idMap) { + // The anchor targets a document fragment specified by its local id. + fragmentId = idMap[fragmentId]; + } + if (fragmentId) { + anchor.attr('href', '#' + fragmentId); + } } } catch (e) { console.log('Failed to parse URL: ' + anchor.prop('href')); @@ -243,6 +338,19 @@ }); }; + /** + * Search for the heading that marks the start of the specified document in the rendering output and return its id, + * which can be used as a fragment identifier in internal links. + * + * @return the id of the heading that marks the start of the specified document + */ + const getDocumentFragmentId = function(documentReference) { + const stringDocRef = XWiki.Model.serialize(documentReference); + return $('#xwikicontent > h1[data-xwiki-document-reference]').filter(function() { + return $(this).attr('data-xwiki-document-reference') === stringDocRef; + }).attr('id'); + }; + const replaceCanvasesWithImages = function(container) { return $.makeArray(container.find('canvas')).reduce((replacePreviousCanvas, canvas) => { return replacePreviousCanvas.then(replaceCanvasWithImage.bind(null, canvas)).catch((e) => { @@ -331,7 +439,7 @@ // Adjust the exported content before performing the print layout. pageReady.afterPageReady(() => { - fixInternalAnchors($('#xwikicontent, .pdf-toc')); + refactorAnchors(); removeEmptyTableOfContents(); validateTableOfContentsAnchors(); }); @@ -357,9 +465,20 @@ } }); - // Revoke the object URLs previously created, after the print layout is ready. pageReady.afterPageReady(() => { + // Revoke the object URLs previously created, after the print layout is ready. objectURLs.forEach(url => URL.revokeObjectURL(url)); + + // Sets the base URL used to resolve relative URLs in the exported content. When the base URL is not set the + // relative URLs are by default resolved relative to the print preview URL which uses the export action and has a + // long query string that is specific to PDF export. This means relative URLs may be resolved using the export + // action and some strange query string if the base URL is not set. We set the base URL after the page is ready + // because we don't want to influence resource loading (images, CSS, JavaScript). This is especially important when + // the PDF is printed using a remote web browser (e.g. running inside a Docker container) in which case the URL used + // to access the print preview page can be different than the URL used by the user to trigger the PDF export (i.e. + // the base URL). We want the base URL to be taken into account only when resolving relative links in the generated + // PDF. + $('<base/>').attr('href', pdfExportConfig.baseURL).prependTo('head'); }); });</code> </property> -- GitLab