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