1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.web

File ExportURLFactory.java

 

Coverage histogram

../../../../img/srcFileCovDistChart2.png
81% of files have more coverage

Code metrics

50
168
20
1
553
351
54
0.32
8.4
20
2.7

Classes

Class Line # Actions
ExportURLFactory 62 168 0% 54 201
0.1554621915.5%
 

Contributing tests

This file is covered by 1 test. .

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.FileInputStream;
24    import java.io.FileOutputStream;
25    import java.io.IOException;
26    import java.io.InputStream;
27    import java.net.URISyntaxException;
28    import java.net.URL;
29    import java.util.Collection;
30    import java.util.HashMap;
31    import java.util.HashSet;
32    import java.util.Map;
33    import java.util.Set;
34    import java.util.regex.Matcher;
35    import java.util.regex.Pattern;
36   
37    import javax.inject.Provider;
38   
39    import org.apache.commons.io.IOUtils;
40    import org.apache.commons.lang3.StringUtils;
41    import org.slf4j.Logger;
42    import org.slf4j.LoggerFactory;
43    import org.xwiki.component.util.DefaultParameterizedType;
44    import org.xwiki.model.reference.AttachmentReference;
45    import org.xwiki.model.reference.DocumentReference;
46    import org.xwiki.model.reference.EntityReferenceSerializer;
47    import org.xwiki.url.filesystem.FilesystemExportContext;
48   
49    import com.xpn.xwiki.XWikiContext;
50    import com.xpn.xwiki.XWikiException;
51    import com.xpn.xwiki.doc.XWikiAttachment;
52    import com.xpn.xwiki.doc.XWikiDocument;
53    import com.xpn.xwiki.internal.model.LegacySpaceResolver;
54   
55    /**
56    * Handle URL generation in rendered wiki pages. This implementation makes sure that generated URLs will be file URLs
57    * pointing to the local filesystem, for exported content (like skin, attachment and pages). This is needed for example
58    * for the HTML export.
59    *
60    * @version $Id: 1863c7e2cca469e9cf7d11d81a7c8f5c7738e0f3 $
61    */
 
62    public class ExportURLFactory extends XWikiServletURLFactory
63    {
64    /**
65    * Logging tool.
66    */
67    protected static final Logger LOGGER = LoggerFactory.getLogger(ExportURLFactory.class);
68   
69    /** The encoding to use when reading text resources from the filesystem and when sending css/javascript responses. */
70    private static final String ENCODING = "UTF-8";
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> pathEntityReferenceSerializer =
80    Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "path");
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  1 toggle public ExportURLFactory()
104    {
105    }
106   
107    /**
108    * @since 7.2M1
109    */
 
110  4 toggle public FilesystemExportContext getFilesystemExportContext()
111    {
112  4 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 context the XWiki context.
141    */
 
142  1 toggle public void init(Collection<String> exportedPages, File exportDir, XWikiContext context)
143    {
144  1 super.init(context);
145   
146  1 Provider<FilesystemExportContext> exportContextProvider = Utils.getComponent(
147    new DefaultParameterizedType(null, Provider.class, FilesystemExportContext.class));
148  1 this.exportContext = exportContextProvider.get();
149   
150  1 if (exportDir != null) {
151  1 getFilesystemExportContext().setExportDir(exportDir);
152   
153    // Backward-compatibility, also set the exportDir deprecated variable.
154  1 this.exportDir = getFilesystemExportContext().getExportDir();
155    }
156   
157  1 if (exportedPages != null) {
158  0 for (String pageName : exportedPages) {
159  0 XWikiDocument doc = new XWikiDocument();
160   
161  0 doc.setFullName(pageName);
162   
163  0 String absolutePageName = "";
164   
165  0 if (doc.getDatabase() != null) {
166  0 absolutePageName += doc.getDatabase().toLowerCase();
167    } else {
168  0 absolutePageName += context.getWikiId().toLowerCase();
169    }
170   
171  0 absolutePageName += XWikiDocument.DB_SPACE_SEP;
172   
173  0 absolutePageName += doc.getFullName();
174   
175  0 getFilesystemExportContext().addExportedPage(absolutePageName);
176   
177    // Backward-compatibility, also set the exportedPages deprecated variable.
178  0 this.exportedPages.addAll(getFilesystemExportContext().getExportedPages());
179    }
180    }
181    }
182   
 
183  0 toggle @Override
184    public URL createSkinURL(String filename, String skin, XWikiContext context)
185    {
186  0 try {
187  0 getFilesystemExportContext().addNeededSkin(skin);
188   
189  0 StringBuilder newPath = new StringBuilder("file://");
190   
191    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
192  0 adjustCSSPath(newPath);
193   
194  0 newPath.append("skins/");
195  0 newPath.append(skin);
196   
197  0 addFileName(newPath, filename, false, context);
198   
199  0 return new URL(newPath.toString());
200    } catch (Exception e) {
201  0 LOGGER.error("Failed to create skin URL", e);
202    }
203   
204  0 return super.createSkinURL(filename, skin, context);
205    }
206   
 
207  0 toggle @Override
208    public URL createSkinURL(String filename, String spaces, String name, XWikiContext context)
209    {
210  0 return createSkinURL(filename, spaces, name, null, context, false);
211    }
212   
 
213  0 toggle public URL createSkinURL(String filename, String spaces, String name, XWikiContext context, boolean skipSkinDirectory)
214    {
215  0 return createSkinURL(filename, spaces, name, null, context, skipSkinDirectory);
216    }
217   
 
218  0 toggle @Override
219    public URL createSkinURL(String fileName, String spaces, String name, String wikiId, XWikiContext context)
220    {
221  0 return createSkinURL(fileName, spaces, name, wikiId, context, false);
222    }
223   
 
224  0 toggle public URL createSkinURL(String fileName, String spaces, String name, String wikiId, XWikiContext context,
225    boolean skipSkinDirectory)
226    {
227  0 URL skinURL;
228  0 if (wikiId == null) {
229  0 skinURL = super.createSkinURL(fileName, spaces, name, context);
230    } else {
231  0 skinURL = super.createSkinURL(fileName, spaces, name, wikiId, context);
232    }
233   
234  0 if (!"skins".equals(spaces)) {
235  0 return skinURL;
236    }
237   
238  0 try {
239  0 getFilesystemExportContext().addNeededSkin(name);
240   
241  0 StringBuffer filePathBuffer = new StringBuffer();
242  0 if (!skipSkinDirectory) {
243  0 filePathBuffer.append("skins/");
244  0 filePathBuffer.append(name);
245  0 filePathBuffer.append("/");
246    }
247  0 filePathBuffer.append(fileName);
248   
249  0 String filePath = filePathBuffer.toString();
250   
251  0 if (!getFilesystemExportContext().hasExportedSkinFile(filePath)) {
252  0 getFilesystemExportContext().addExportedSkinFile(filePath);
253   
254  0 File file = new File(getFilesystemExportContext().getExportDir(), filePath);
255  0 if (!file.exists()) {
256    // Make sure the folder exists
257  0 File folder = file.getParentFile();
258  0 if (!folder.exists()) {
259  0 folder.mkdirs();
260    }
261  0 renderSkinFile(skinURL.getPath(), spaces, name, wikiId, file, StringUtils.countMatches(filePath, "/"),
262    context);
263    }
264   
265  0 followCssImports(file, spaces, name, wikiId, context);
266    }
267   
268  0 StringBuilder newPath = new StringBuilder("file://");
269   
270    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
271  0 adjustCSSPath(newPath);
272   
273  0 newPath.append(filePath);
274   
275  0 skinURL = new URL(newPath.toString());
276    } catch (Exception e) {
277  0 LOGGER.error("Failed to create skin URL", e);
278    }
279   
280  0 return skinURL;
281    }
282   
283    /**
284    * Results go in the passed {@code outputFile}.
285    */
 
286  0 toggle private void renderSkinFile(String path, String spaces, String name, String wikiId, File outputFile,
287    int cssPathAdjustmentValue, XWikiContext context) throws IOException, XWikiException
288    {
289  0 FileOutputStream fos = new FileOutputStream(outputFile);
290  0 String database = context.getWikiId();
291   
292  0 try {
293  0 XWikiServletResponseStub response = new XWikiServletResponseStub();
294  0 response.setOutpuStream(fos);
295  0 context.setResponse(response);
296  0 if (wikiId != null) {
297  0 context.setWikiId(wikiId);
298    }
299   
300    // Adjust path for links inside CSS files.
301  0 getFilesystemExportContext().pushCSSParentLevels(cssPathAdjustmentValue);
302  0 try {
303  0 renderWithSkinAction(spaces, name, wikiId, path, context);
304    } finally {
305  0 getFilesystemExportContext().popCSSParentLevels();
306    }
307    } finally {
308  0 fos.close();
309  0 if (wikiId != null) {
310  0 context.setWikiId(database);
311    }
312    }
313    }
314   
 
315  0 toggle private void renderWithSkinAction(String spaces, String name, String wikiId, String path, XWikiContext context)
316    throws IOException, XWikiException
317    {
318    // We're simulating a Skin Action below. However we need to ensure that we set the right doc
319    // in the XWiki Context since this is what XWikiAction does and if we don't do this it generates
320    // issues since the current doc is put in the context instead of the skin. Specifically we'll
321    // get for example "Main.WebHome" as the current doc instead of "Main.flamingo".
322    // See http://jira.xwiki.org/browse/XWIKI-10922 for details.
323   
324  0 DocumentReference dummyDocumentReference =
325    new DocumentReference(wikiId, this.legacySpaceResolver.resolve(spaces), name);
326  0 XWikiDocument dummyDocument = context.getWiki().getDocument(dummyDocumentReference, context);
327   
328  0 Map<String, Object> backup = new HashMap<>();
329  0 XWikiDocument.backupContext(backup, context);
330  0 try {
331  0 dummyDocument.setAsContextDoc(context);
332  0 SKINACTION.render(path, context);
333    } finally {
334  0 XWikiDocument.restoreContext(backup, context);
335    }
336    }
337   
338    /**
339    * Resolve CSS <code>@import</code> targets.
340    */
 
341  0 toggle private void followCssImports(File file, String spaces, String name, String wikiId, XWikiContext context)
342    throws IOException
343    {
344    // TODO: find better way to know it's css file (not sure it's possible, we could also try to find @import
345    // whatever the content)
346  0 if (file.getName().endsWith(".css")) {
347  0 FileInputStream fis = new FileInputStream(file);
348   
349  0 try {
350  0 String content = IOUtils.toString(fis, ENCODING);
351   
352    // TODO: use real css parser
353  0 Matcher matcher = CSSIMPORT.matcher(content);
354   
355  0 while (matcher.find()) {
356  0 String fileName = matcher.group(1);
357   
358    // Adjust path for links inside CSS files.
359  0 while (fileName.startsWith("../")) {
360  0 fileName = StringUtils.removeStart(fileName, "../");
361    }
362   
363  0 if (wikiId == null) {
364  0 createSkinURL(fileName, spaces, name, context, true);
365    } else {
366  0 createSkinURL(fileName, spaces, name, wikiId, context, true);
367    }
368    }
369    } finally {
370  0 fis.close();
371    }
372    }
373    }
374   
 
375  0 toggle @Override
376    public URL createResourceURL(String filename, boolean forceSkinAction, XWikiContext context)
377    {
378  0 try {
379  0 File targetFile = new File(getFilesystemExportContext().getExportDir(), "resources/" + filename);
380  0 if (!targetFile.exists()) {
381  0 if (!targetFile.getParentFile().exists()) {
382  0 targetFile.getParentFile().mkdirs();
383    }
384   
385    // Step 1: Copy the resource
386    // If forceSkinAction is false then there's no velocity in the resource and we can just copy it simply.
387    // Otherwise we need to go through the Skin Action to perform the rendering.
388  0 if (forceSkinAction) {
389    // Extract the first path as the wiki page
390  0 int pos = filename.indexOf('/', 0);
391  0 String page = filename.substring(0, pos);
392  0 renderSkinFile("resource/" + filename, "resources", page, context.getDatabase(), targetFile,
393    StringUtils.countMatches(filename, "/") + 1, context);
394    } else {
395  0 FileOutputStream fos = new FileOutputStream(targetFile);
396  0 InputStream source = context.getEngineContext().getResourceAsStream("/resources/" + filename);
397  0 IOUtils.copy(source, fos);
398  0 fos.close();
399    }
400    }
401   
402  0 StringBuilder newPath = new StringBuilder("file://");
403   
404    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
405  0 adjustCSSPath(newPath);
406   
407  0 newPath.append("resources");
408   
409  0 addFileName(newPath, filename, false, context);
410   
411  0 return new URL(newPath.toString());
412    } catch (Exception e) {
413  0 LOGGER.error("Failed to create skin URL", e);
414    }
415   
416  0 return super.createResourceURL(filename, forceSkinAction, context);
417    }
418   
 
419  0 toggle @Override
420    public URL createURL(String spaces, String name, String action, String querystring, String anchor, String xwikidb,
421    XWikiContext context)
422    {
423  0 try {
424    // Look for a special handler for the passed action
425  0 try {
426  0 ExportURLFactoryActionHandler handler = Utils.getComponent(ExportURLFactoryActionHandler.class, action);
427  0 return handler.createURL(spaces, name, querystring, anchor, xwikidb, context,
428    getFilesystemExportContext());
429    } catch (Exception e) {
430    // Failed to find such a component or it doesn't work, simply ignore it and continue with the default
431    // behavior!
432    }
433   
434  0 String wikiname = xwikidb == null ? context.getWikiId().toLowerCase() : xwikidb.toLowerCase();
435   
436  0 String serializedReference = this.pathEntityReferenceSerializer.serialize(
437    new DocumentReference(wikiname, this.legacySpaceResolver.resolve(spaces), name));
438  0 if (getFilesystemExportContext().hasExportedPage(serializedReference) && "view".equals(action)
439    && context.getLinksAction() == null)
440    {
441  0 StringBuffer newpath = new StringBuffer();
442   
443  0 newpath.append("file://");
444  0 newpath.append(serializedReference);
445  0 newpath.append(".html");
446   
447  0 if (!StringUtils.isEmpty(anchor)) {
448  0 newpath.append("#");
449  0 newpath.append(anchor);
450    }
451   
452  0 return new URL(newpath.toString());
453    }
454    } catch (Exception e) {
455  0 LOGGER.error("Failed to create page URL", e);
456    }
457   
458  0 return super.createURL(spaces, name, action, querystring, anchor, xwikidb, context);
459    }
460   
461    /**
462    * Generate an url targeting attachment in provided wiki page.
463    *
464    * @param filename the name of the attachment.
465    * @param spaces a serialized space reference which can contain one or several spaces (e.g. "space1.space2"). If
466    * a space name contains a dot (".") it must be passed escaped as in "space1\.with\.dot.space2"
467    * @param name the name of the page containing the attachment.
468    * @param xwikidb the wiki of the page containing the attachment.
469    * @param context the XWiki context.
470    * @return the generated url.
471    * @throws XWikiException error when retrieving document attachment.
472    * @throws IOException error when retrieving document attachment.
473    * @throws URISyntaxException when retrieving document attachment.
474    */
 
475  1 toggle private URL createAttachmentURL(String filename, String spaces, String name, String xwikidb, XWikiContext context)
476    throws XWikiException, IOException, URISyntaxException
477    {
478  1 String db = (xwikidb == null ? context.getWikiId() : xwikidb);
479  1 DocumentReference documentReference =
480    new DocumentReference(db, this.legacySpaceResolver.resolve(spaces), name);
481  1 String serializedReference = this.pathEntityReferenceSerializer.serialize(
482    new AttachmentReference(filename, documentReference));
483  1 String path = "attachment/" + serializedReference;
484   
485  1 File file = new File(getFilesystemExportContext().getExportDir(), path);
486  1 if (!file.exists()) {
487  1 XWikiDocument doc = context.getWiki().getDocument(documentReference, context);
488  1 XWikiAttachment attachment = doc.getAttachment(filename);
489  1 file.getParentFile().mkdirs();
490  1 FileOutputStream fos = new FileOutputStream(file);
491  1 IOUtils.copy(attachment.getContentInputStream(context), fos);
492  1 fos.close();
493    }
494   
495  1 StringBuilder newPath = new StringBuilder("file://");
496   
497    // Adjust path for links inside CSS files (since they need to be relative to the CSS file they're in).
498  1 adjustCSSPath(newPath);
499   
500  1 newPath.append(path);
501   
502    // Since the returned URL is used in HTML links, we need to escape "%" characters so that browsers don't decode
503    // for example %2E as "." by default which would lead to the browser not finding the file on the filesystem.
504  1 return new URL(newPath.toString().replaceAll("%", "%25"));
505    }
506   
 
507  1 toggle @Override
508    public URL createAttachmentURL(String filename, String spaces, String name, String action, String querystring,
509    String xwikidb, XWikiContext context)
510    {
511  1 try {
512  1 return createAttachmentURL(filename, spaces, name, xwikidb, context);
513    } catch (Exception e) {
514  0 LOGGER.error("Failed to create attachment URL", e);
515   
516  0 return super.createAttachmentURL(filename, spaces, name, action, null, xwikidb, context);
517    }
518    }
519   
 
520  0 toggle @Override
521    public URL createAttachmentRevisionURL(String filename, String spaces, String name, String revision, String xwikidb,
522    XWikiContext context)
523    {
524  0 try {
525  0 return createAttachmentURL(filename, spaces, name, xwikidb, context);
526    } catch (Exception e) {
527  0 LOGGER.error("Failed to create attachment URL", e);
528   
529  0 return super.createAttachmentRevisionURL(filename, spaces, name, revision, xwikidb, context);
530    }
531    }
532   
 
533  0 toggle @Override
534    public String getURL(URL url, XWikiContext context)
535    {
536  0 if (url == null) {
537  0 return "";
538    }
539   
540  0 String path = url.toString();
541   
542  0 if (url.getProtocol().equals("file")) {
543  0 path = path.substring("file://".length());
544    }
545   
546  0 return path;
547    }
548   
 
549  1 toggle private void adjustCSSPath(StringBuilder path)
550    {
551  1 path.append(StringUtils.repeat("../", getFilesystemExportContext().getCSSParentLevel()));
552    }
553    }