/* * 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 com.xpn.xwiki.pdf.impl; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.List; import java.util.Map; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.model.reference.DocumentReference; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.internal.model.LegacySpaceResolver; import com.xpn.xwiki.web.Utils; import com.xpn.xwiki.web.XWikiServletURLFactory; /** * Special URL Factory used during exports, which stores referenced attachments and resources on the filesystem, in a * temporary folder, so that they can be included in the export result. The returned URLs point to these resources as * {@code file://} links, and not as {@code http://} links. * * @version $Id: 933bcf4d94a01236f8630d2e0226999025024959 $ * @since 5.0RC1 */ public class FileSystemURLFactory extends XWikiServletURLFactory { /** Logging helper object. */ private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemURLFactory.class); /** Segment separator used in the collision-free key generation. */ private static final char SEPARATOR = '/'; private LegacySpaceResolver legacySpaceResolver = Utils.getComponent(LegacySpaceResolver.class); @Override public URL createAttachmentURL(String filename, String spaces, String name, String action, String querystring, String wiki, XWikiContext context) { try { return getURL(wiki, spaces, name, filename, null, context); } catch (Exception ex) { LOGGER.warn("Failed to save image for PDF export", ex); return super.createAttachmentURL(filename, spaces, name, action, null, wiki, context); } } @Override public URL createAttachmentRevisionURL(String filename, String spaces, String name, String revision, String wiki, XWikiContext context) { try { return getURL(wiki, spaces, name, filename, revision, context); } catch (Exception ex) { LOGGER.warn("Failed to save image for PDF export: " + ex.getMessage()); return super.createAttachmentRevisionURL(filename, spaces, name, revision, wiki, context); } } @Override public URL createSkinURL(String filename, String skin, XWikiContext context) { try { Map<String, File> usedFiles = getFileMapping(context); String key = getSkinfileKey(filename, skin); if (!usedFiles.containsKey(key)) { if (!copyResource("/skins/" + skin + '/' + filename, key, usedFiles, context)) { // The resource does not exist, just return a http:// URL return super.createSkinURL(filename, skin, context); } } return usedFiles.get(key).toURI().toURL(); } catch (Exception ex) { // Shouldn't happen return super.createSkinURL(filename, skin, context); } } @Override public URL createResourceURL(String filename, boolean forceSkinAction, XWikiContext context) { try { Map<String, File> usedFiles = getFileMapping(context); String key = getResourceKey(filename); if (!usedFiles.containsKey(key)) { if (!copyResource("/resources/" + filename, key, usedFiles, context)) { return super.createResourceURL(filename, forceSkinAction, context); } } return usedFiles.get(key).toURI().toURL(); } catch (Exception ex) { // Shouldn't happen return super.createResourceURL(filename, forceSkinAction, context); } } @Override public String getURL(URL url, XWikiContext context) { if (url == null) { return ""; } return url.toString(); } /** * Store the requested attachment on the filesystem and return a {@code file://} URL where FOP can access that file. * * @param wiki the name of the owner document's wiki * @param spaces a serialized space reference which can contain one or several spaces (e.g. "space1.space2"). If * a space name contains a dot (".") it must be passed escaped as in "space1\.with\.dot.space2" * @param name the name of the owner document * @param filename the name of the attachment * @param revision an optional attachment version * @param context the current request context * @return a {@code file://} URL where the attachment has been stored * @throws Exception if the attachment can't be retrieved from the database and stored on the filesystem */ private URL getURL(String wiki, String spaces, String name, String filename, String revision, XWikiContext context) throws Exception { Map<String, File> usedFiles = getFileMapping(context); List<String> spaceNames = this.legacySpaceResolver.resolve(spaces); String key = getAttachmentKey(spaceNames, name, filename, revision); if (!usedFiles.containsKey(key)) { File file = getTemporaryFile(key, context); LOGGER.debug("Temporary PDF export file [{}]", file.toString()); XWikiDocument doc = context.getWiki().getDocument(new DocumentReference( StringUtils.defaultString(wiki, context.getWikiId()), spaceNames, name), context); XWikiAttachment attachment = doc.getAttachment(filename); if (StringUtils.isNotEmpty(revision)) { attachment = attachment.getAttachmentRevision(revision, context); } FileOutputStream fos = new FileOutputStream(file); IOUtils.copy(attachment.getContentInputStream(context), fos); fos.close(); usedFiles.put(key, file); } return usedFiles.get(key).toURI().toURL(); } /** * Copy a resource from the filesystem into a temporary file and map this resulting file to the requested resource * location. * * @param resourceName the name of the file to copy, possibly including a path to it, for example * {@code icons/silk/add.png} * @param key the collision-free identifier of the resource * @param usedFiles the mapping of resource keys to temporary files where to put the resulting temporary file * @param context the current request context * @return {@code true} if copying the resource succeeded and the new temporary file was mapped to the resource key, * {@code false} otherwise */ private boolean copyResource(String resourceName, String key, Map<String, File> usedFiles, XWikiContext context) { try { InputStream data = context.getWiki().getResourceAsStream(resourceName); if (data != null) { // Copy the resource to a temporary file File file = getTemporaryFile(key, context); FileOutputStream fos = new FileOutputStream(file); IOUtils.copy(data, fos); fos.close(); usedFiles.put(key, file); return true; } } catch (Exception ex) { // Can't access the resource, let's hope FOP can handle the http:// URL } return false; } /** * Computes a safe identifier for an attachment, guaranteed to be collision-free. * * @param name the name of the owner document * @param filename the name of the attachment * @param revision an optional attachment version * @return an identifier for this attachment */ private String getAttachmentKey(List<String> spaceNames, String name, String filename, String revision) { StringBuilder builder = new StringBuilder(); try { builder.append("attachment").append(SEPARATOR); for (String spaceName : spaceNames) { builder.append(URLEncoder.encode(spaceName, XWiki.DEFAULT_ENCODING)); builder.append(SEPARATOR); } builder.append(URLEncoder.encode(name, XWiki.DEFAULT_ENCODING)).append(SEPARATOR); builder.append(URLEncoder.encode(filename, XWiki.DEFAULT_ENCODING)).append(SEPARATOR); builder.append(URLEncoder.encode(StringUtils.defaultString(revision), XWiki.DEFAULT_ENCODING)); return builder.toString(); } catch (UnsupportedEncodingException e) { // This should never happen, UTF-8 is always available throw new RuntimeException(String.format("Failed to compute unique Attachment key for spaces [%s[, " + "page [%s], filename [%s], revision [%s], while exporting.", StringUtils.join(spaceNames, ", "), name, filename, revision), e); } } /** * Computes a safe identifier for a resource file, guaranteed to be collision-free. * * @param filename the name of the file, possibly including a path to it, for example {@code icons/silk/add.png} * @return an identifier for this file */ private String getResourceKey(String filename) { try { return "resource" + SEPARATOR + URLEncoder.encode(filename, XWiki.DEFAULT_ENCODING); } catch (UnsupportedEncodingException ex) { // This should never happen, UTF-8 is always available return filename; } } /** * Computes a safe identifier for a skin filename, guaranteed to be collision-free. * * @param filename the name of the file, possibly including a path to it, for example {@code css/colors/black.css} * @param skin the name of the skin where the file is expected to be * @return an identifier for this file */ private String getSkinfileKey(String filename, String skin) { try { return "skin" + SEPARATOR + URLEncoder.encode(skin, XWiki.DEFAULT_ENCODING) + SEPARATOR + URLEncoder.encode(filename, XWiki.DEFAULT_ENCODING); } catch (UnsupportedEncodingException ex) { // This should never happen, UTF-8 is always available return skin + SEPARATOR + filename; } } /** * Retrieve the Map that relates resource keys to their corresponding temporary file. * * @param context the current request context * @return the mapping as it was found in the context (read-write) */ private Map<String, File> getFileMapping(XWikiContext context) { @SuppressWarnings("unchecked") Map<String, File> usedFiles = (Map<String, File>) context.get("pdfexport-file-mapping"); return usedFiles; } /** * Create a new temporary file for the given resource key and return it. * * @param key the resource key, needed for getting the file extension, if any * @param context the current request context * @return a new empty file * @throws java.io.IOException if creating the file fails */ private File getTemporaryFile(String key, XWikiContext context) throws IOException { File tempdir = (File) context.get("pdfexportdir"); String prefix = "pdf"; String suffix = "." + FilenameUtils.getExtension(key); try { return File.createTempFile(prefix, suffix, tempdir); } catch (IOException e) { throw new IOException("Failed to create temporary PDF export file with prefix [" + prefix + "], suffix [" + suffix + "] in directory [" + tempdir + "]", e); } } }