1. Project Clover database Sat Feb 2 2019 06:45:20 CET
  2. Package com.xpn.xwiki.web

File ExportURLFactory.java

 

Coverage histogram

../../../../img/srcFileCovDistChart8.png
56% of files have more coverage

Code metrics

48
166
21
1
572
359
58
0.35
7.9
21
2.76

Classes

Class Line # Actions
ExportURLFactory 65 166 0% 58 51
0.782978778.3%
 

Contributing tests

This file is covered by 3 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.web;
21   
22    import java.io.File;
23    import java.io.FileOutputStream;
24    import java.io.IOException;
25    import java.io.InputStream;
26    import java.net.URISyntaxException;
27    import java.net.URL;
28    import java.nio.charset.StandardCharsets;
29    import java.util.ArrayList;
30    import java.util.Collection;
31    import java.util.HashMap;
32    import java.util.HashSet;
33    import java.util.List;
34    import java.util.Map;
35    import java.util.Set;
36    import java.util.regex.Matcher;
37    import java.util.regex.Pattern;
38   
39    import javax.inject.Provider;
40   
41    import org.apache.commons.io.FileUtils;
42    import org.apache.commons.lang3.StringUtils;
43    import org.slf4j.Logger;
44    import org.slf4j.LoggerFactory;
45    import org.xwiki.component.util.DefaultParameterizedType;
46    import org.xwiki.model.reference.AttachmentReference;
47    import org.xwiki.model.reference.DocumentReference;
48    import org.xwiki.model.reference.DocumentReferenceResolver;
49    import org.xwiki.model.reference.EntityReferenceSerializer;
50    import org.xwiki.url.filesystem.FilesystemExportContext;
51   
52    import com.xpn.xwiki.XWikiContext;
53    import com.xpn.xwiki.XWikiException;
54    import com.xpn.xwiki.doc.XWikiAttachment;
55    import com.xpn.xwiki.doc.XWikiDocument;
56    import com.xpn.xwiki.internal.model.LegacySpaceResolver;
57   
58    /**
59    * Handle URL generation in rendered wiki pages. This implementation makes sure that generated URLs will be file URLs
60    * pointing to the local filesystem, for exported content (like skin, attachment and pages). This is needed for example
61    * for the HTML export.
62    *
63    * @version $Id: 8c30c3b6c6a8d35518584d52f3e14bd1a0e654d1 $
64    */
 
65    public class ExportURLFactory extends XWikiServletURLFactory
66    {
67    /**
68    * Logging tool.
69    */
70    protected static final Logger LOGGER = LoggerFactory.getLogger(ExportURLFactory.class);
71   
72    private static final SkinAction SKINACTION = new SkinAction();
73   
74    // TODO: use real css parser
75    private static Pattern CSSIMPORT = Pattern.compile("^\\s*@import\\s*\"(.*)\"\\s*;$", Pattern.MULTILINE);
76   
77    private LegacySpaceResolver legacySpaceResolver = Utils.getComponent(LegacySpaceResolver.class);
78   
79    private EntityReferenceSerializer<String> fsPathEntityReferenceSerializer =
80    Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "fspath");
81   
82    /**
83    * Pages for which to convert URL to local.
84    *
85    * @deprecated since 6.2RC1, use {link #getExportURLFactoryContext} instead
86    */
87    @Deprecated
88    protected Set<String> exportedPages = new HashSet<>();
89   
90    /**
91    * Directory where to export attachment.
92    *
93    * @deprecated since 6.2RC1, use {link #getExportURLFactoryContext} instead
94    */
95    @Deprecated
96    protected File exportDir;
97   
98    private FilesystemExportContext exportContext;
99   
100    /**
101    * ExportURLFactory constructor.
102    */
 
103  5 toggle public ExportURLFactory()
104    {
105    }
106   
107    /**
108    * @since 7.2M1
109    */
 
110  1188 toggle public FilesystemExportContext getFilesystemExportContext()
111    {
112  1188 return this.exportContext;
113    }
114   
115    /**
116    * @return the list skins names used.
117    * @deprecated since 6.2RC1, use {@link #getFilesystemExportContext()}
118    */
 
119  0 toggle @Deprecated
120    public Collection<String> getNeededSkins()
121    {
122  0 return getFilesystemExportContext().getNeededSkins();
123    }
124   
125    /**
126    * @return the list of custom skin files.
127    * @deprecated since 6.2RC1, use {@link #getFilesystemExportContext()}
128    */
 
129  0 toggle @Deprecated
130    public Collection<String> getExportedSkinFiles()
131    {
132  0 return getFilesystemExportContext().getExportedSkinFiles();
133    }
134   
135    /**
136    * Init the url factory.
137    *
138    * @param exportedPages the pages that will be exported.
139    * @param exportDir the directory where to copy exported objects (attachments).
140    * @param exportContext the context for the export
141    * @param context the XWiki context.
142    * @since 8.4.5
143    * @since 9.0
144    */
 
145  5 toggle public void init(Collection<DocumentReference> exportedPages, File exportDir, FilesystemExportContext exportContext,
146    XWikiContext context)
147    {
148  5 super.init(context);
149   
150  5 this.exportContext = exportContext;
151   
152  5 if (exportDir != null) {
153  3 getFilesystemExportContext().setExportDir(exportDir);
154   
155    // Backward-compatibility, also set the exportDir deprecated variable.
156  3 this.exportDir = getFilesystemExportContext().getExportDir();
157    }
158   
159  5 if (exportedPages != null) {
160  4 for (DocumentReference pageReference : exportedPages) {
161  5 getFilesystemExportContext().addExportedPage(
162    this.fsPathEntityReferenceSerializer.serialize(pageReference));
163   
164    // Backward-compatibility, also set the exportedPages deprecated variable.
165  5 EntityReferenceSerializer<String> defaultEntityReferenceSerializer =
166    Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
167  5 this.exportedPages.add(defaultEntityReferenceSerializer.serialize(pageReference));
168    }
169    }
170    }
171   
172    /**
173    * Init the url factory.
174    *
175    * @param exportedPages the pages that will be exported.
176    * @param exportDir the directory where to copy exported objects (attachments).
177    * @param context the XWiki context.
178    * @deprecated starting with 8.4.5/9.0, use {@link #init(Collection, File, FilesystemExportContext, XWikiContext)}
179    */
 
180  0 toggle @Deprecated
181    public void init(Collection<String> exportedPages, File exportDir, XWikiContext context)
182    {
183  0 Provider<FilesystemExportContext> exportContextProvider = Utils.getComponent(
184    new DefaultParameterizedType(null, Provider.class, FilesystemExportContext.class));
185   
186  0 DocumentReferenceResolver<String> resolver =
187    Utils.getComponent(DocumentReferenceResolver.TYPE_STRING, "current");
188   
189  0 List<DocumentReference> references = new ArrayList<>();
190  0 for (String exportedPage : exportedPages) {
191  0 references.add(resolver.resolve(exportedPage));
192    }
193   
194  0 init(references, exportDir, exportContextProvider.get(), context);
195    }
196   
 
197  8 toggle @Override
198    public URL createSkinURL(String filename, String skin, XWikiContext context)
199    {
200  8 try {
201  8 getFilesystemExportContext().addNeededSkin(skin);
202   
203  8 StringBuilder newPath = new StringBuilder("file://");
204   
205    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
206  8 adjustCSSPath(newPath);
207   
208  8 newPath.append("skins/");
209  8 newPath.append(skin);
210   
211  8 addFileName(newPath, filename, false, context);
212   
213  8 return new URL(newPath.toString());
214    } catch (Exception e) {
215  0 LOGGER.error("Failed to create skin URL", e);
216    }
217   
218  0 return super.createSkinURL(filename, skin, context);
219    }
220   
 
221  12 toggle @Override
222    public URL createSkinURL(String filename, String spaces, String name, XWikiContext context)
223    {
224  12 return createSkinURL(filename, spaces, name, null, context, false);
225    }
226   
 
227  0 toggle public URL createSkinURL(String filename, String spaces, String name, XWikiContext context, boolean skipSkinDirectory)
228    {
229  0 return createSkinURL(filename, spaces, name, null, context, skipSkinDirectory);
230    }
231   
 
232  12 toggle @Override
233    public URL createSkinURL(String fileName, String spaces, String name, String wikiId, XWikiContext context)
234    {
235  12 return createSkinURL(fileName, spaces, name, wikiId, context, false);
236    }
237   
 
238  24 toggle public URL createSkinURL(String fileName, String spaces, String name, String wikiId, XWikiContext context,
239    boolean skipSkinDirectory)
240    {
241  24 URL skinURL;
242  24 if (wikiId == null) {
243  12 skinURL = super.createSkinURL(fileName, spaces, name, context);
244    } else {
245  12 skinURL = super.createSkinURL(fileName, spaces, name, wikiId, context);
246    }
247   
248  24 if (!"skins".equals(spaces)) {
249  0 return skinURL;
250    }
251   
252  24 try {
253  24 getFilesystemExportContext().addNeededSkin(name);
254   
255  24 StringBuffer filePathBuffer = new StringBuffer();
256  24 if (!skipSkinDirectory) {
257  24 filePathBuffer.append("skins/");
258  24 filePathBuffer.append(name);
259  24 filePathBuffer.append("/");
260    }
261  24 filePathBuffer.append(fileName);
262   
263  24 String filePath = filePathBuffer.toString();
264   
265  24 if (!getFilesystemExportContext().hasExportedSkinFile(filePath)) {
266  8 getFilesystemExportContext().addExportedSkinFile(filePath);
267   
268  8 File file = new File(getFilesystemExportContext().getExportDir(), filePath);
269  8 if (!file.exists()) {
270    // Make sure the folder exists
271  8 File folder = file.getParentFile();
272  8 if (!folder.exists()) {
273  2 folder.mkdirs();
274    }
275  8 renderSkinFile(skinURL.getPath(), spaces, name, wikiId, file,
276    StringUtils.countMatches(filePath, "/"), context);
277    }
278   
279  8 followCssImports(file, spaces, name, wikiId, context);
280    }
281   
282  24 StringBuilder newPath = new StringBuilder("file://");
283   
284    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
285  24 adjustCSSPath(newPath);
286   
287  24 newPath.append(filePath);
288   
289  24 skinURL = new URL(newPath.toString());
290    } catch (Exception e) {
291  0 LOGGER.error("Failed to create skin URL", e);
292    }
293   
294  24 return skinURL;
295    }
296   
297    /**
298    * Results go in the passed {@code outputFile}.
299    */
 
300  48 toggle private void renderSkinFile(String path, String spaces, String name, String wikiId, File outputFile,
301    int cssPathAdjustmentValue, XWikiContext context) throws IOException, XWikiException
302    {
303  48 FileOutputStream fos = new FileOutputStream(outputFile);
304  48 String database = context.getWikiId();
305   
306  48 try {
307  48 XWikiServletResponseStub response = new XWikiServletResponseStub();
308  48 response.setOutpuStream(fos);
309  48 context.setResponse(response);
310  48 if (wikiId != null) {
311  48 context.setWikiId(wikiId);
312    }
313   
314    // Adjust path for links inside CSS files.
315  48 getFilesystemExportContext().pushCSSParentLevels(cssPathAdjustmentValue);
316  48 try {
317  48 renderWithSkinAction(spaces, name, wikiId, path, context);
318    } finally {
319  48 getFilesystemExportContext().popCSSParentLevels();
320    }
321    } finally {
322  48 fos.close();
323  48 if (wikiId != null) {
324  48 context.setWikiId(database);
325    }
326    }
327    }
328   
 
329  48 toggle private void renderWithSkinAction(String spaces, String name, String wikiId, String path, XWikiContext context)
330    throws IOException, XWikiException
331    {
332    // We're simulating a Skin Action below. However we need to ensure that we set the right doc
333    // in the XWiki Context since this is what XWikiAction does and if we don't do this it generates
334    // issues since the current doc is put in the context instead of the skin. Specifically we'll
335    // get for example "Main.WebHome" as the current doc instead of "Main.flamingo".
336    // See https://jira.xwiki.org/browse/XWIKI-10922 for details.
337   
338  48 DocumentReference dummyDocumentReference =
339    new DocumentReference(wikiId, this.legacySpaceResolver.resolve(spaces), name);
340  48 XWikiDocument dummyDocument = context.getWiki().getDocument(dummyDocumentReference, context);
341   
342  48 Map<String, Object> backup = new HashMap<>();
343  48 XWikiDocument.backupContext(backup, context);
344  48 try {
345  48 dummyDocument.setAsContextDoc(context);
346  48 SKINACTION.render(path, context);
347    } finally {
348  48 XWikiDocument.restoreContext(backup, context);
349    }
350    }
351   
352    /**
353    * Resolve CSS <code>@import</code> targets.
354    */
 
355  8 toggle private void followCssImports(File file, String spaces, String name, String wikiId, XWikiContext context)
356    throws IOException
357    {
358    // TODO: find better way to know it's css file (not sure it's possible, we could also try to find @import
359    // whatever the content)
360  8 if (file.getName().endsWith(".css")) {
361  6 String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
362   
363    // TODO: use real css parser
364  6 Matcher matcher = CSSIMPORT.matcher(content);
365   
366  6 while (matcher.find()) {
367  0 String fileName = matcher.group(1);
368   
369    // Adjust path for links inside CSS files.
370  0 while (fileName.startsWith("../")) {
371  0 fileName = StringUtils.removeStart(fileName, "../");
372    }
373   
374  0 if (wikiId == null) {
375  0 createSkinURL(fileName, spaces, name, context, true);
376    } else {
377  0 createSkinURL(fileName, spaces, name, wikiId, context, true);
378    }
379    }
380    }
381    }
382   
 
383  231 toggle @Override
384    public URL createResourceURL(String filename, boolean forceSkinAction, XWikiContext context)
385    {
386  231 try {
387  231 File targetFile = new File(getFilesystemExportContext().getExportDir(), "resources/" + filename);
388  231 if (!targetFile.exists()) {
389  128 if (!targetFile.getParentFile().exists()) {
390  26 targetFile.getParentFile().mkdirs();
391    }
392   
393    // Step 1: Copy the resource
394    // If forceSkinAction is false then there's no velocity in the resource and we can just copy it simply.
395    // Otherwise we need to go through the Skin Action to perform the rendering.
396  128 if (forceSkinAction) {
397    // Extract the first path as the wiki page
398  40 int pos = filename.indexOf('/', 0);
399  40 String page = filename.substring(0, pos);
400  40 renderSkinFile("resource/" + filename, "resources", page, context.getWikiId(), targetFile,
401    StringUtils.countMatches(filename, "/") + 1, context);
402    } else {
403  88 try (
404  88 InputStream source = context.getEngineContext().getResourceAsStream("/resources/" + filename)) {
405  88 FileUtils.copyInputStreamToFile(source, targetFile);
406    }
407    }
408    }
409   
410  231 StringBuilder newPath = new StringBuilder("file://");
411   
412    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
413  231 adjustCSSPath(newPath);
414   
415  231 newPath.append("resources");
416   
417  231 addFileName(newPath, filename, false, context);
418   
419  231 return new URL(newPath.toString());
420    } catch (Exception e) {
421  0 LOGGER.error("Failed to create skin URL", e);
422    }
423   
424  0 return super.createResourceURL(filename, forceSkinAction, context);
425    }
426   
 
427  153 toggle @Override
428    public URL createURL(String spaces, String name, String action, String querystring, String anchor, String xwikidb,
429    XWikiContext context)
430    {
431  153 try {
432    // Look for a special handler for the passed action
433  153 try {
434  153 ExportURLFactoryActionHandler handler = Utils.getComponent(ExportURLFactoryActionHandler.class, action);
435  6 return handler.createURL(spaces, name, querystring, anchor, xwikidb, context,
436    getFilesystemExportContext());
437    } catch (Exception e) {
438    // Failed to find such a component or it doesn't work, simply ignore it and continue with the default
439    // behavior!
440    }
441   
442  147 String wikiname = xwikidb == null ? context.getWikiId().toLowerCase() : xwikidb.toLowerCase();
443   
444  147 String serializedReference = this.fsPathEntityReferenceSerializer.serialize(
445    new DocumentReference(wikiname, this.legacySpaceResolver.resolve(spaces), name));
446  147 if (getFilesystemExportContext().hasExportedPage(serializedReference) && "view".equals(action)
447    && context.getLinksAction() == null)
448    {
449  87 StringBuffer newpath = new StringBuffer();
450   
451  87 newpath.append("file://");
452   
453    // Adjust depending on the exported location of the current doc.
454  87 newpath.append(StringUtils.repeat("../", getFilesystemExportContext().getDocParentLevel()));
455   
456  87 newpath.append("pages/");
457   
458    // Compute a valid relative URL from a FS path.
459  87 String relativeURLPath =
460    new File("").toURI().relativize(new File(serializedReference).toURI()).toString();
461   
462  87 newpath.append(relativeURLPath);
463  87 newpath.append(".html");
464   
465  87 if (!StringUtils.isEmpty(anchor)) {
466  0 newpath.append("#");
467  0 newpath.append(anchor);
468    }
469   
470  87 return new URL(newpath.toString());
471    }
472    } catch (Exception e) {
473  0 LOGGER.error("Failed to create page URL", e);
474    }
475   
476  60 return super.createURL(spaces, name, action, querystring, anchor, xwikidb, context);
477    }
478   
479    /**
480    * Generate an url targeting attachment in provided wiki page.
481    *
482    * @param filename the name of the attachment.
483    * @param spaces a serialized space reference which can contain one or several spaces (e.g. "space1.space2"). If
484    * a space name contains a dot (".") it must be passed escaped as in "space1\.with\.dot.space2"
485    * @param name the name of the page containing the attachment.
486    * @param xwikidb the wiki of the page containing the attachment.
487    * @param context the XWiki context.
488    * @return the generated url.
489    * @throws XWikiException error when retrieving document attachment.
490    * @throws IOException error when retrieving document attachment.
491    * @throws URISyntaxException when retrieving document attachment.
492    */
 
493  1 toggle private URL createAttachmentURL(String filename, String spaces, String name, String xwikidb, XWikiContext context)
494    throws XWikiException, IOException, URISyntaxException
495    {
496  1 String db = (xwikidb == null ? context.getWikiId() : xwikidb);
497  1 DocumentReference documentReference =
498    new DocumentReference(db, this.legacySpaceResolver.resolve(spaces), name);
499  1 String serializedReference = this.fsPathEntityReferenceSerializer.serialize(
500    new AttachmentReference(filename, documentReference));
501  1 String path = "attachment/" + serializedReference;
502   
503  1 File file = new File(getFilesystemExportContext().getExportDir(), path);
504  1 if (!file.exists()) {
505  1 XWikiDocument doc = context.getWiki().getDocument(documentReference, context);
506  1 XWikiAttachment attachment = doc.getAttachment(filename);
507  1 file.getParentFile().mkdirs();
508  1 try (InputStream stream = attachment.getContentInputStream(context)) {
509  1 FileUtils.copyInputStreamToFile(stream, file);
510    }
511    }
512   
513  1 StringBuilder newPath = new StringBuilder("file://");
514   
515    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
516  1 adjustCSSPath(newPath);
517   
518    // Compute a valid relative URL from a FS path.
519  1 String relativeURLPath = new File("").toURI().relativize(new File(path).toURI()).toString();
520  1 newPath.append(relativeURLPath);
521   
522  1 return new URL(newPath.toString());
523    }
524   
 
525  1 toggle @Override
526    public URL createAttachmentURL(String filename, String spaces, String name, String action, String querystring,
527    String xwikidb, XWikiContext context)
528    {
529  1 try {
530  1 return createAttachmentURL(filename, spaces, name, xwikidb, context);
531    } catch (Exception e) {
532  0 LOGGER.error("Failed to create attachment URL", e);
533   
534  0 return super.createAttachmentURL(filename, spaces, name, action, null, xwikidb, context);
535    }
536    }
537   
 
538  0 toggle @Override
539    public URL createAttachmentRevisionURL(String filename, String spaces, String name, String revision, String xwikidb,
540    XWikiContext context)
541    {
542  0 try {
543  0 return createAttachmentURL(filename, spaces, name, xwikidb, context);
544    } catch (Exception e) {
545  0 LOGGER.error("Failed to create attachment URL", e);
546   
547  0 return super.createAttachmentRevisionURL(filename, spaces, name, revision, xwikidb, context);
548    }
549    }
550   
 
551  406 toggle @Override
552    public String getURL(URL url, XWikiContext context)
553    {
554  406 if (url == null) {
555  0 return "";
556    }
557   
558  406 String path = url.toString();
559   
560  406 if (url.getProtocol().equals("file")) {
561  338 path = path.substring("file://".length());
562    }
563   
564  406 return path;
565    }
566   
 
567  264 toggle private void adjustCSSPath(StringBuilder path)
568    {
569  264 path.append(StringUtils.repeat("../", getFilesystemExportContext().getCSSParentLevel()
570    + getFilesystemExportContext().getDocParentLevel()));
571    }
572    }