001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.attachment;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.Date;
030import java.util.List;
031import java.util.Properties;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.log4j.Logger;
035import org.apache.wiki.WikiContext;
036import org.apache.wiki.WikiEngine;
037import org.apache.wiki.WikiPage;
038import org.apache.wiki.WikiProvider;
039import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
040import org.apache.wiki.api.exceptions.ProviderException;
041import org.apache.wiki.api.exceptions.WikiException;
042import org.apache.wiki.pages.PageManager;
043import org.apache.wiki.parser.MarkupParser;
044import org.apache.wiki.providers.WikiAttachmentProvider;
045import org.apache.wiki.util.ClassUtil;
046import org.apache.wiki.util.TextUtil;
047
048import net.sf.ehcache.Cache;
049import net.sf.ehcache.CacheManager;
050import net.sf.ehcache.Element;
051
052
053/**
054 *  Provides facilities for handling attachments.  All attachment handling goes through this class.
055 *  <p>
056 *  The AttachmentManager provides a facade towards the current WikiAttachmentProvider that is in use.
057 *  It is created by the WikiEngine as a singleton object, and can be requested through the WikiEngine.
058 *
059 *  @since 1.9.28
060 */
061public class AttachmentManager
062{
063    /**
064     *  The property name for defining the attachment provider class name.
065     */
066    public static final String  PROP_PROVIDER = "jspwiki.attachmentProvider";
067
068    /**
069     *  The maximum size of attachments that can be uploaded.
070     */
071    public static final String  PROP_MAXSIZE  = "jspwiki.attachment.maxsize";
072
073    /**
074     *  A space-separated list of attachment types which can be uploaded
075     */
076    public static final String PROP_ALLOWEDEXTENSIONS    = "jspwiki.attachment.allowed";
077
078    /**
079     *  A space-separated list of attachment types which cannot be uploaded
080     */
081    public static final String PROP_FORBIDDENEXTENSIONS = "jspwiki.attachment.forbidden";
082
083    /**
084     *  A space-separated list of attachment types which never will open in the browser.
085     */
086    public static final String PROP_FORCEDOWNLOAD = "jspwiki.attachment.forceDownload";
087
088    /** List of attachment types which are forced to be downloaded */
089    private String[] m_forceDownloadPatterns;
090
091    static Logger log = Logger.getLogger( AttachmentManager.class );
092    private WikiAttachmentProvider m_provider;
093    private WikiEngine             m_engine;
094    private CacheManager m_cacheManager = CacheManager.getInstance();
095
096    private Cache m_dynamicAttachments;
097    /** Name of the page cache. */
098    public static final String CACHE_NAME = "jspwiki.dynamicAttachmentCache";
099
100    /** The capacity of the cache, if you want something else, tweak ehcache.xml. */
101    public static final int   DEFAULT_CACHECAPACITY   = 1000;
102
103    /**
104     *  Creates a new AttachmentManager.  Note that creation will never fail,
105     *  but it's quite likely that attachments do not function.
106     *  <p>
107     *  <b>DO NOT CREATE</b> an AttachmentManager on your own, unless you really
108     *  know what you're doing.  Just use WikiEngine.getAttachmentManager() if
109     *  you're making a module for JSPWiki.
110     *
111     *  @param engine The wikiengine that owns this attachment manager.
112     *  @param props  A list of properties from which the AttachmentManager will seek
113     *  its configuration.  Typically this is the "jspwiki.properties".
114     */
115
116    // FIXME: Perhaps this should fail somehow.
117    public AttachmentManager( WikiEngine engine, Properties props )
118    {
119        String classname;
120
121        m_engine = engine;
122
123
124        //
125        //  If user wants to use a cache, then we'll use the CachingProvider.
126        //
127        boolean useCache = "true".equals(props.getProperty( PageManager.PROP_USECACHE ));
128
129        if( useCache )
130        {
131            classname = "org.apache.wiki.providers.CachingAttachmentProvider";
132        }
133        else
134        {
135            classname = props.getProperty( PROP_PROVIDER );
136        }
137
138        //
139        //  If no class defined, then will just simply fail.
140        //
141        if( classname == null )
142        {
143            log.info( "No attachment provider defined - disabling attachment support." );
144            return;
145        }
146
147        //
148        //  Create and initialize the provider.
149        //
150        String cacheName = engine.getApplicationName() + "." + CACHE_NAME;
151        try {
152            if (m_cacheManager.cacheExists(cacheName)) {
153                m_dynamicAttachments = m_cacheManager.getCache(cacheName);
154            } else {
155                log.info("cache with name " + cacheName + " not found in ehcache.xml, creating it with defaults.");
156                m_dynamicAttachments = new Cache(cacheName, DEFAULT_CACHECAPACITY, false, false, 0, 0);
157                m_cacheManager.addCache(m_dynamicAttachments);
158            }
159
160            Class<?> providerclass = ClassUtil.findClass("org.apache.wiki.providers", classname);
161
162            m_provider = (WikiAttachmentProvider) providerclass.newInstance();
163
164            m_provider.initialize(m_engine, props);
165        } catch( ClassNotFoundException e )
166        {
167            log.error( "Attachment provider class not found",e);
168        }
169        catch( InstantiationException e )
170        {
171            log.error( "Attachment provider could not be created", e );
172        }
173        catch( IllegalAccessException e )
174        {
175            log.error( "You may not access the attachment provider class", e );
176        }
177        catch( NoRequiredPropertyException e )
178        {
179            log.error( "Attachment provider did not find a property that it needed: "+e.getMessage(), e );
180            m_provider = null; // No, it did not work.
181        }
182        catch( IOException e )
183        {
184            log.error( "Attachment provider reports IO error", e );
185            m_provider = null;
186        }
187
188        String forceDownload = TextUtil.getStringProperty( props, PROP_FORCEDOWNLOAD, null );
189
190        if( forceDownload != null && forceDownload.length() > 0 )
191            m_forceDownloadPatterns = forceDownload.toLowerCase().split("\\s");
192        else
193            m_forceDownloadPatterns = new String[0];
194
195
196    }
197
198    /**
199     *  Returns true, if attachments are enabled and running.
200     *
201     *  @return A boolean value indicating whether attachment functionality is enabled.
202     */
203    public boolean attachmentsEnabled()
204    {
205        return m_provider != null;
206    }
207
208    /**
209     *  Gets info on a particular attachment, latest version.
210     *
211     *  @param name A full attachment name.
212     *  @return Attachment, or null, if no such attachment exists.
213     *  @throws ProviderException If something goes wrong.
214     */
215    public Attachment getAttachmentInfo( String name )
216        throws ProviderException
217    {
218        return getAttachmentInfo( name, WikiProvider.LATEST_VERSION );
219    }
220
221    /**
222     *  Gets info on a particular attachment with the given version.
223     *
224     *  @param name A full attachment name.
225     *  @param version A version number.
226     *  @return Attachment, or null, if no such attachment or version exists.
227     *  @throws ProviderException If something goes wrong.
228     */
229
230    public Attachment getAttachmentInfo( String name, int version )
231        throws ProviderException
232    {
233        if( name == null )
234        {
235            return null;
236        }
237
238        return getAttachmentInfo( null, name, version );
239    }
240
241    /**
242     *  Figures out the full attachment name from the context and
243     *  attachment name.
244     *
245     *  @param context The current WikiContext
246     *  @param attachmentname The file name of the attachment.
247     *  @return Attachment, or null, if no such attachment exists.
248     *  @throws ProviderException If something goes wrong.
249     */
250    public Attachment getAttachmentInfo( WikiContext context,
251                                         String attachmentname )
252        throws ProviderException
253    {
254        return getAttachmentInfo( context, attachmentname, WikiProvider.LATEST_VERSION );
255    }
256
257    /**
258     *  Figures out the full attachment name from the context and attachment name.
259     *
260     *  @param context The current WikiContext
261     *  @param attachmentname The file name of the attachment.
262     *  @return Attachment, or null, if no such attachment exists.
263     *  @throws ProviderException If something goes wrong.
264     */
265    public String getAttachmentInfoName( WikiContext context,
266                                         String attachmentname )
267    {
268        Attachment att = null;
269
270        try
271        {
272            att = getAttachmentInfo( context, attachmentname );
273        }
274        catch( ProviderException e )
275        {
276            log.warn("Finding attachments failed: ",e);
277            return null;
278        }
279
280        if( att != null )
281        {
282            return att.getName();
283        }
284        else if( attachmentname.indexOf('/') != -1 )
285        {
286            return attachmentname;
287        }
288
289        return null;
290    }
291
292    /**
293     *  Figures out the full attachment name from the context and
294     *  attachment name.
295     *
296     *  @param context The current WikiContext
297     *  @param attachmentname The file name of the attachment.
298     *  @param version A particular version.
299     *  @return Attachment, or null, if no such attachment or version exists.
300     *  @throws ProviderException If something goes wrong.
301     */
302
303    public Attachment getAttachmentInfo( WikiContext context,
304                                         String attachmentname,
305                                         int version )
306        throws ProviderException
307    {
308        if( m_provider == null )
309        {
310            return null;
311        }
312
313        WikiPage currentPage = null;
314
315        if( context != null )
316        {
317            currentPage = context.getPage();
318        }
319
320        //
321        //  Figure out the parent page of this attachment.  If we can't find it,
322        //  we'll assume this refers directly to the attachment.
323        //
324        int cutpt = attachmentname.lastIndexOf('/');
325
326        if( cutpt != -1 )
327        {
328            String parentPage = attachmentname.substring(0,cutpt);
329            parentPage = MarkupParser.cleanLink( parentPage );
330            attachmentname = attachmentname.substring(cutpt+1);
331
332            // If we for some reason have an empty parent page name;
333            // this can't be an attachment
334            if(parentPage.length() == 0) return null;
335
336            currentPage = m_engine.getPage( parentPage );
337
338            //
339            // Go check for legacy name
340            //
341            // FIXME: This should be resolved using CommandResolver,
342            //        not this adhoc way.  This also assumes that the
343            //        legacy charset is a subset of the full allowed set.
344            if( currentPage == null )
345            {
346                currentPage = m_engine.getPage( MarkupParser.wikifyLink( parentPage ) );
347            }
348        }
349
350        //
351        //  If the page cannot be determined, we cannot possibly find the
352        //  attachments.
353        //
354        if( currentPage == null || currentPage.getName().length() == 0 )
355        {
356            return null;
357        }
358
359        // System.out.println("Seeking info on "+currentPage+"::"+attachmentname);
360
361        //
362        //  Finally, figure out whether this is a real attachment or a generated attachment.
363        //
364        Attachment att = getDynamicAttachment( currentPage.getName()+"/"+attachmentname );
365
366        if( att == null )
367        {
368            att = m_provider.getAttachmentInfo( currentPage, attachmentname, version );
369        }
370
371        return att;
372    }
373
374    /**
375     *  Returns the list of attachments associated with a given wiki page.
376     *  If there are no attachments, returns an empty Collection.
377     *
378     *  @param wikipage The wiki page from which you are seeking attachments for.
379     *  @return a valid collection of attachments.
380     *  @throws ProviderException If there was something wrong in the backend.
381     */
382    public List< Attachment > listAttachments( WikiPage wikipage ) throws ProviderException {
383        if( m_provider == null ) {
384            return new ArrayList<>();
385        }
386
387        List< Attachment >atts = new ArrayList<>( m_provider.listAttachments( wikipage ) );
388        Collections.< Attachment >sort( atts, Comparator.comparing( Attachment::getName, m_engine.getPageManager().getPageSorter() ) );
389
390        return atts;
391    }
392
393    /**
394     *  Returns true, if the page has any attachments at all.  This is
395     *  a convinience method.
396     *
397     *
398     *  @param wikipage The wiki page from which you are seeking attachments for.
399     *  @return True, if the page has attachments, else false.
400     */
401    public boolean hasAttachments( WikiPage wikipage )
402    {
403        try
404        {
405            return listAttachments( wikipage ).size() > 0;
406        }
407        catch( Exception e ) {}
408
409        return false;
410    }
411
412    /**
413     *  Check if attachement link should force a download iso opening the attachment in the browser.
414     *
415     *  @param name  Name of attachment to be checked
416     *  @return true, if the attachment should be downloaded when clicking the link
417     *  @since 2.11.0 M4
418    */
419    public boolean forceDownload( String name )
420    {
421        if( name == null || name.length() == 0 ) return false;
422
423        name = name.toLowerCase();
424
425        if( name.indexOf('.') == -1) return true;  //force download on attachments without extension or type indication
426
427        for( int i = 0; i < m_forceDownloadPatterns.length; i++ )
428        {
429            if( name.endsWith(m_forceDownloadPatterns[i]) && m_forceDownloadPatterns[i].length() > 0 )
430                return true;
431        }
432
433        return false;
434    }
435
436    /**
437     *  Finds a (real) attachment from the repository as a stream.
438     *
439     *  @param att Attachment
440     *  @return An InputStream to read from.  May return null, if
441     *          attachments are disabled.
442     *  @throws IOException If the stream cannot be opened
443     *  @throws ProviderException If the backend fails due to some other reason.
444     */
445    public InputStream getAttachmentStream( Attachment att )
446        throws IOException,
447               ProviderException
448    {
449        return getAttachmentStream( null, att );
450    }
451
452    /**
453     *  Returns an attachment stream using the particular WikiContext.  This method
454     *  should be used instead of getAttachmentStream(Attachment), since it also allows
455     *  the DynamicAttachments to function.
456     *
457     *  @param ctx The Wiki Context
458     *  @param att The Attachment to find
459     *  @return An InputStream.  May return null, if attachments are disabled.  You must
460     *          take care of closing it.
461     *  @throws ProviderException If the backend fails due to some reason
462     *  @throws IOException If the stream cannot be opened
463     */
464    public InputStream getAttachmentStream( WikiContext ctx, Attachment att )
465        throws ProviderException, IOException
466    {
467        if( m_provider == null )
468        {
469            return null;
470        }
471
472        if( att instanceof DynamicAttachment )
473        {
474            return ((DynamicAttachment)att).getProvider().getAttachmentData( ctx, att );
475        }
476
477        return m_provider.getAttachmentData( att );
478    }
479
480
481
482    /**
483     *  Stores a dynamic attachment.  Unlike storeAttachment(), this just stores
484     *  the attachment in the memory.
485     *
486     *  @param ctx A WikiContext
487     *  @param att An attachment to store
488     */
489    public void storeDynamicAttachment( WikiContext ctx, DynamicAttachment att )
490    {
491        m_dynamicAttachments.put(new Element(att.getName(), att));
492    }
493
494    /**
495     *  Finds a DynamicAttachment.  Normally, you should just use getAttachmentInfo(),
496     *  since that will find also DynamicAttachments.
497     *
498     *  @param name The name of the attachment to look for
499     *  @return An Attachment, or null.
500     *  @see #getAttachmentInfo(String)
501     */
502
503    public DynamicAttachment getDynamicAttachment(String name) {
504        Element element = m_dynamicAttachments.get(name);
505        if (element != null) {
506            return (DynamicAttachment) element.getObjectValue();
507        } else {
508            //
509            //  Remove from cache, it has expired.
510            //
511            m_dynamicAttachments.put(new Element(name, null));
512
513            return null;
514        }
515    }
516
517    /**
518     *  Stores an attachment that lives in the given file.
519     *  If the attachment did not exist previously, this method
520     *  will create it.  If it did exist, it stores a new version.
521     *
522     *  @param att Attachment to store this under.
523     *  @param source A file to read from.
524     *
525     *  @throws IOException If writing the attachment failed.
526     *  @throws ProviderException If something else went wrong.
527     */
528    public void storeAttachment( Attachment att, File source )
529        throws IOException,
530               ProviderException
531    {
532        FileInputStream in = null;
533
534        try
535        {
536            in = new FileInputStream( source );
537            storeAttachment( att, in );
538        }
539        finally
540        {
541            if( in != null ) in.close();
542        }
543    }
544
545    /**
546     *  Stores an attachment directly from a stream.
547     *  If the attachment did not exist previously, this method
548     *  will create it.  If it did exist, it stores a new version.
549     *
550     *  @param att Attachment to store this under.
551     *  @param in  InputStream from which the attachment contents will be read.
552     *
553     *  @throws IOException If writing the attachment failed.
554     *  @throws ProviderException If something else went wrong.
555     */
556    public void storeAttachment( Attachment att, InputStream in )
557        throws IOException,
558               ProviderException
559    {
560        if( m_provider == null )
561        {
562            return;
563        }
564
565        //
566        //  Checks if the actual, real page exists without any modifications
567        //  or aliases.  We cannot store an attachment to a non-existent page.
568        //
569        if( !m_engine.getPageManager().pageExists( att.getParentName() ) )
570        {
571            // the caller should catch the exception and use the exception text as an i18n key
572            throw new ProviderException(  "attach.parent.not.exist"  );
573        }
574
575        m_provider.putAttachmentData( att, in );
576
577        m_engine.getReferenceManager().updateReferences( att.getName(), new ArrayList< String >() );
578
579        WikiPage parent = new WikiPage( m_engine, att.getParentName() );
580        m_engine.updateReferences( parent );
581
582        m_engine.getSearchManager().reindexPage( att );
583    }
584
585    /**
586     *  Returns a list of versions of the attachment.
587     *
588     *  @param attachmentName A fully qualified name of the attachment.
589     *
590     *  @return A list of Attachments.  May return null, if attachments are
591     *          disabled.
592     *  @throws ProviderException If the provider fails for some reason.
593     */
594    public List<Attachment> getVersionHistory( String attachmentName )
595        throws ProviderException
596    {
597        if( m_provider == null )
598        {
599            return null;
600        }
601
602        Attachment att = getAttachmentInfo( (WikiContext)null, attachmentName );
603
604        if( att != null )
605        {
606            return m_provider.getVersionHistory( att );
607        }
608
609        return null;
610    }
611
612    /**
613     *  Returns a collection of Attachments, containing each and every attachment
614     *  that is in this Wiki.
615     *
616     *  @return A collection of attachments.  If attachments are disabled, will
617     *          return an empty collection.
618     *  @throws ProviderException If something went wrong with the backend
619     */
620    public Collection<Attachment> getAllAttachments() throws ProviderException {
621        if( attachmentsEnabled() ) {
622            return m_provider.listAllChanged( new Date(0L) );
623        }
624
625        return new ArrayList<>();
626    }
627
628    /**
629     *  Returns the current attachment provider.
630     *
631     *  @return The current provider.  May be null, if attachments are disabled.
632     */
633    public WikiAttachmentProvider getCurrentProvider()
634    {
635        return m_provider;
636    }
637
638    /**
639     *  Deletes the given attachment version.
640     *
641     *  @param att The attachment to delete
642     *  @throws ProviderException If something goes wrong with the backend.
643     */
644    public void deleteVersion( Attachment att )
645        throws ProviderException
646    {
647        if( m_provider == null ) return;
648
649        m_provider.deleteVersion( att );
650    }
651
652    /**
653     *  Deletes all versions of the given attachment.
654     *  @param att The Attachment to delete.
655     *  @throws ProviderException if something goes wrong with the backend.
656     */
657    // FIXME: Should also use events!
658    public void deleteAttachment( Attachment att )
659        throws ProviderException
660    {
661        if( m_provider == null ) return;
662
663        m_provider.deleteAttachment( att );
664
665        m_engine.getSearchManager().pageRemoved( att );
666
667        m_engine.getReferenceManager().clearPageEntries( att.getName() );
668
669    }
670
671    /**
672     *  Validates the filename and makes sure it is legal.  It trims and splits
673     *  and replaces bad characters.
674     *
675     *  @param filename
676     *  @return A validated name with annoying characters replaced.
677     *  @throws WikiException If the filename is not legal (e.g. empty)
678     */
679    static String validateFileName( String filename )
680        throws WikiException
681    {
682        if( filename == null || filename.trim().length() == 0 )
683        {
684            log.error("Empty file name given.");
685
686            // the caller should catch the exception and use the exception text as an i18n key
687            throw new WikiException(  "attach.empty.file" );
688        }
689
690        //
691        //  Should help with IE 5.22 on OSX
692        //
693        filename = filename.trim();
694
695        // If file name ends with .jsp or .jspf, the user is being naughty!
696        if( filename.toLowerCase().endsWith( ".jsp" ) || filename.toLowerCase().endsWith(".jspf") )
697        {
698            log.info( "Attempt to upload a file with a .jsp/.jspf extension.  In certain cases this " +
699                      "can trigger unwanted security side effects, so we're preventing it." );
700            //
701            // the caller should catch the exception and use the exception text as an i18n key
702            throw new WikiException(  "attach.unwanted.file"  );
703        }
704
705        //
706        //  Some browser send the full path info with the filename, so we need
707        //  to remove it here by simply splitting along slashes and then taking the path.
708        //
709
710        String[] splitpath = filename.split( "[/\\\\]" );
711        filename = splitpath[splitpath.length-1];
712
713        //
714        //  Remove any characters that might be a problem. Most
715        //  importantly - characters that might stop processing
716        //  of the URL.
717        //
718        filename = StringUtils.replaceChars( filename, "#?\"'", "____" );
719
720        return filename;
721    }
722}