Skip to content
Snippets Groups Projects
Commit 66452840 authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-19270: Add support for performing the PDF export using a browser running...

XWIKI-19270: Add support for performing the PDF export using a browser running in a Docker container
* Fix the empty PDF ToC (look for the job status also on the jobs that are currently in execution)
* Allow only HTTP URLs for printing
* Set the base URL before printing to PDF to make sure the relative URLs in the generated PDF are resolved based on the original URL.

(cherry picked from commit dc737df3)
parent 41a74161
No related branches found
No related tags found
No related merge requests found
......@@ -35,6 +35,8 @@
import org.xwiki.export.pdf.job.PDFExportJobStatus;
import org.xwiki.export.pdf.job.PDFExportJobStatus.DocumentRenderingResult;
import org.xwiki.export.pdf.macro.PDFTocMacroParameters;
import org.xwiki.job.Job;
import org.xwiki.job.JobExecutor;
import org.xwiki.job.JobStatusStore;
import org.xwiki.job.event.status.JobStatus;
import org.xwiki.rendering.block.Block;
......@@ -62,9 +64,18 @@
@Singleton
public class PDFTocMacro extends AbstractMacro<PDFTocMacroParameters>
{
/**
* Used to retrieve the status of a finished job.
*/
@Inject
private JobStatusStore jobStatusStore;
/**
* Used to retrieve the status of the job currently being executed.
*/
@Inject
private JobExecutor jobExecutor;
@Inject
private DocumentAccessBridge documentAccessBridge;
......@@ -121,9 +132,18 @@ public boolean supportsInlineMode()
return false;
}
private PDFExportJobStatus getJobStatus(String jobId)
private PDFExportJobStatus getJobStatus(String jobIdString)
{
JobStatus jobStatus = this.jobStatusStore.getJobStatus(Arrays.asList(jobId.split("/")));
List<String> jobId = Arrays.asList(jobIdString.split("/"));
JobStatus jobStatus;
Job job = this.jobExecutor.getJob(jobId);
if (job == null) {
jobStatus = this.jobStatusStore.getJobStatus(jobId);
} else {
jobStatus = job.getStatus();
}
if (jobStatus instanceof PDFExportJobStatus) {
return (PDFExportJobStatus) jobStatus;
} else {
......
......@@ -46,6 +46,7 @@
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
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;
......@@ -195,18 +196,26 @@ private void waitForPageReady(Runtime runtime) throws IOException
/* generatePreview */ false, /* userGesture */ false, /* awaitPromise */ true,
/* throwOnSideEffect */ false, /* timeout */ REMOTE_DEBUGGING_TIMEOUT * 1000.0, /* disableBreaks */ true,
/* replMode */ false, /* allowUnsafeEvalBlockedByCSP */ false, /* uniqueContextId */ null);
checkEvaluation(evaluate, "Page ready.", "Failed to wait for page to be ready.",
"Timeout waiting for page to be ready.");
}
private void checkEvaluation(Evaluate evaluate, Object expectedValue, String evaluationException,
String unexpectedValueException) throws IOException
{
String messageTemplae = "%s Root cause: %s";
if (evaluate.getExceptionDetails() != null) {
RemoteObject exception = evaluate.getExceptionDetails().getException();
Object cause = exception.getDescription();
if (cause == null) {
// When the page ready promise as rejected.
// When the exception was thrown as a string or when a promise was rejected.
cause = exception.getValue();
}
throw new IOException("Failed to wait for page to be ready. Root cause: " + cause);
throw new IOException(String.format(messageTemplae, evaluationException, cause));
} else {
RemoteObject result = evaluate.getResult();
if (!"Page ready.".equals(result.getValue())) {
throw new IOException("Timeout waiting for page to be ready. Root cause: " + result.getValue());
if (!Objects.equals(expectedValue, result.getValue())) {
throw new IOException(String.format(messageTemplae, unexpectedValueException, result.getValue()));
}
}
}
......@@ -365,4 +374,28 @@ private void createBrowserDevToolsService() throws ChromeServiceException
throw new ChromeServiceException("Failed to connect to the browser web socket.", e);
}
}
/**
* Sets the base URL for the page loaded on the specified browser tab.
*
* @param tabDevToolsService the developer tools service associated with the target page
* @param baseURL the base URL to set
*/
public void setBaseURL(ChromeDevToolsService tabDevToolsService, URL baseURL) throws IOException
{
Runtime runtime = 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 = 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());
}
}
......@@ -129,15 +129,25 @@ public InputStream print(PDFExportJobRequest request) throws IOException
URL printPreviewURL = (URL) request.getContext().get(XWikiContextContextStore.PROP_REQUEST_URL);
this.logger.debug("Printing [{}]", printPreviewURL);
validatePrintPreviewURL(printPreviewURL);
// The headless Chrome web browser runs inside a Docker container where 'localhost' refers to the container
// itself. We have to update the domain from the given URL to point to the host running both the XWiki instance
// and the Docker container.
printPreviewURL = new URL(
URL dockerPrintPreviewURL = new URL(
printPreviewURL.toString().replace("://localhost", "://" + this.configuration.getChromeDockerHostName()));
ChromeDevToolsService devToolsService = this.chromeManager.createIncognitoTab();
this.chromeManager.setCookies(devToolsService, getCookies(request, printPreviewURL));
this.chromeManager.navigate(devToolsService, printPreviewURL);
this.chromeManager.setCookies(devToolsService, getCookies(request, dockerPrintPreviewURL));
this.chromeManager.navigate(devToolsService, dockerPrintPreviewURL);
if (!printPreviewURL.toString().equals(dockerPrintPreviewURL.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 defined only inside the
// Docker container where the PDF was generated. See PDFExportConfiguration#getChromeDockerHostName()
this.chromeManager.setBaseURL(devToolsService, printPreviewURL);
}
return this.chromeManager.printToPDF(devToolsService, () -> {
this.chromeManager.closeIncognitoTab(devToolsService);
});
......@@ -151,4 +161,16 @@ private List<CookieParam> getCookies(PDFExportJobRequest request, URL printPrevi
cookies.forEach(cookie -> cookie.setUrl(printPreviewURL.toString()));
return cookies;
}
private void validatePrintPreviewURL(URL printPreviewURL) throws IOException
{
if (printPreviewURL == null) {
throw new IOException("Print preview URL missing.");
}
String protocol = printPreviewURL.getProtocol();
if (!"http".equals(protocol) && !"https".equals(protocol)) {
throw new IOException(String.format("Unsupported protocol [%s].", protocol));
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment