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;
030import java.util.Vector;
031
032import net.sf.ehcache.Cache;
033import net.sf.ehcache.CacheManager;
034import net.sf.ehcache.Element;
035
036import org.apache.commons.lang.StringUtils;
037import org.apache.log4j.Logger;
038import org.apache.wiki.PageManager;
039import org.apache.wiki.WikiContext;
040import org.apache.wiki.WikiEngine;
041import org.apache.wiki.WikiPage;
042import org.apache.wiki.WikiProvider;
043import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
044import org.apache.wiki.api.exceptions.ProviderException;
045import org.apache.wiki.api.exceptions.WikiException;
046import org.apache.wiki.parser.MarkupParser;
047import org.apache.wiki.providers.WikiAttachmentProvider;
048import org.apache.wiki.util.ClassUtil;
049
050
051/**
052 *  Provides facilities for handling attachments.  All attachment handling goes through this class.
053 *  <p>
054 *  The AttachmentManager provides a facade towards the current WikiAttachmentProvider that is in use.
055 *  It is created by the WikiEngine as a singleton object, and 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    public Attachment getAttachmentInfo( WikiContext context,
232                                         String attachmentname )
233        throws ProviderException
234    {
235        return getAttachmentInfo( context, attachmentname, WikiProvider.LATEST_VERSION );
236    }
237
238    /**
239     *  Figures out the full attachment name from the context and attachment name.
240     *
241     *  @param context The current WikiContext
242     *  @param attachmentname The file name of the attachment.
243     *  @return Attachment, or null, if no such attachment exists.
244     *  @throws ProviderException If something goes wrong.
245     */
246    public String getAttachmentInfoName( WikiContext context,
247                                         String attachmentname )
248    {
249        Attachment att = null;
250
251        try
252        {
253            att = getAttachmentInfo( context, attachmentname );
254        }
255        catch( ProviderException e )
256        {
257            log.warn("Finding attachments failed: ",e);
258            return null;
259        }
260
261        if( att != null )
262        {
263            return att.getName();
264        }
265        else if( attachmentname.indexOf('/') != -1 )
266        {
267            return attachmentname;
268        }
269
270        return null;
271    }
272
273    /**
274     *  Figures out the full attachment name from the context and
275     *  attachment name.
276     *
277     *  @param context The current WikiContext
278     *  @param attachmentname The file name of the attachment.
279     *  @param version A particular version.
280     *  @return Attachment, or null, if no such attachment or version exists.
281     *  @throws ProviderException If something goes wrong.
282     */
283
284    public Attachment getAttachmentInfo( WikiContext context,
285                                         String attachmentname,
286                                         int version )
287        throws ProviderException
288    {
289        if( m_provider == null )
290        {
291            return null;
292        }
293
294        WikiPage currentPage = null;
295
296        if( context != null )
297        {
298            currentPage = context.getPage();
299        }
300
301        //
302        //  Figure out the parent page of this attachment.  If we can't find it,
303        //  we'll assume this refers directly to the attachment.
304        //
305        int cutpt = attachmentname.lastIndexOf('/');
306
307        if( cutpt != -1 )
308        {
309            String parentPage = attachmentname.substring(0,cutpt);
310            parentPage = MarkupParser.cleanLink( parentPage );
311            attachmentname = attachmentname.substring(cutpt+1);
312
313            // If we for some reason have an empty parent page name;
314            // this can't be an attachment
315            if(parentPage.length() == 0) return null;
316
317            currentPage = m_engine.getPage( parentPage );
318
319            //
320            // Go check for legacy name
321            //
322            // FIXME: This should be resolved using CommandResolver,
323            //        not this adhoc way.  This also assumes that the
324            //        legacy charset is a subset of the full allowed set.
325            if( currentPage == null )
326            {
327                currentPage = m_engine.getPage( MarkupParser.wikifyLink( parentPage ) );
328            }
329        }
330
331        //
332        //  If the page cannot be determined, we cannot possibly find the
333        //  attachments.
334        //
335        if( currentPage == null || currentPage.getName().length() == 0 )
336        {
337            return null;
338        }
339
340        // System.out.println("Seeking info on "+currentPage+"::"+attachmentname);
341
342        //
343        //  Finally, figure out whether this is a real attachment or a generated attachment.
344        //
345        Attachment att = getDynamicAttachment( currentPage.getName()+"/"+attachmentname );
346
347        if( att == null )
348        {
349            att = m_provider.getAttachmentInfo( currentPage, attachmentname, version );
350        }
351
352        return att;
353    }
354
355    /**
356     *  Returns the list of attachments associated with a given wiki page.
357     *  If there are no attachments, returns an empty Collection.
358     *
359     *  @param wikipage The wiki page from which you are seeking attachments for.
360     *  @return a valid collection of attachments.
361     *  @throws ProviderException If there was something wrong in the backend.
362     */
363
364    // FIXME: This API should be changed to return a List.
365    @SuppressWarnings("unchecked")
366    public Collection listAttachments( WikiPage wikipage )
367        throws ProviderException
368    {
369        if( m_provider == null )
370        {
371            return new ArrayList();
372        }
373
374        Collection atts = m_provider.listAttachments( wikipage );
375
376        //
377        //  This is just a sanity check; all of our providers return a Collection.
378        //
379        if( atts instanceof List )
380        {
381            m_engine.getPageSorter().sortPages( (List) atts );
382        }
383
384        return atts;
385    }
386
387    /**
388     *  Returns true, if the page has any attachments at all.  This is
389     *  a convinience method.
390     *
391     *
392     *  @param wikipage The wiki page from which you are seeking attachments for.
393     *  @return True, if the page has attachments, else false.
394     */
395    public boolean hasAttachments( WikiPage wikipage )
396    {
397        try
398        {
399            return listAttachments( wikipage ).size() > 0;
400        }
401        catch( Exception e ) {}
402
403        return false;
404    }
405
406    /**
407     *  Finds a (real) attachment from the repository as a stream.
408     *
409     *  @param att Attachment
410     *  @return An InputStream to read from.  May return null, if
411     *          attachments are disabled.
412     *  @throws IOException If the stream cannot be opened
413     *  @throws ProviderException If the backend fails due to some other reason.
414     */
415    public InputStream getAttachmentStream( Attachment att )
416        throws IOException,
417               ProviderException
418    {
419        return getAttachmentStream( null, att );
420    }
421
422    /**
423     *  Returns an attachment stream using the particular WikiContext.  This method
424     *  should be used instead of getAttachmentStream(Attachment), since it also allows
425     *  the DynamicAttachments to function.
426     *
427     *  @param ctx The Wiki Context
428     *  @param att The Attachment to find
429     *  @return An InputStream.  May return null, if attachments are disabled.  You must
430     *          take care of closing it.
431     *  @throws ProviderException If the backend fails due to some reason
432     *  @throws IOException If the stream cannot be opened
433     */
434    public InputStream getAttachmentStream( WikiContext ctx, Attachment att )
435        throws ProviderException, IOException
436    {
437        if( m_provider == null )
438        {
439            return null;
440        }
441
442        if( att instanceof DynamicAttachment )
443        {
444            return ((DynamicAttachment)att).getProvider().getAttachmentData( ctx, att );
445        }
446
447        return m_provider.getAttachmentData( att );
448    }
449
450
451
452    /**
453     *  Stores a dynamic attachment.  Unlike storeAttachment(), this just stores
454     *  the attachment in the memory.
455     *
456     *  @param ctx A WikiContext
457     *  @param att An attachment to store
458     */
459    public void storeDynamicAttachment( WikiContext ctx, DynamicAttachment att )
460    {
461        m_dynamicAttachments.put(new Element(att.getName(), att));
462    }
463
464    /**
465     *  Finds a DynamicAttachment.  Normally, you should just use getAttachmentInfo(),
466     *  since that will find also DynamicAttachments.
467     *
468     *  @param name The name of the attachment to look for
469     *  @return An Attachment, or null.
470     *  @see #getAttachmentInfo(String)
471     */
472
473    public DynamicAttachment getDynamicAttachment(String name) {
474        Element element = m_dynamicAttachments.get(name);
475        if (element != null) {
476            return (DynamicAttachment) element.getObjectValue();
477        } else {
478            //
479            //  Remove from cache, it has expired.
480            //
481            m_dynamicAttachments.put(new Element(name, null));
482
483            return null;
484        }
485    }
486
487    /**
488     *  Stores an attachment that lives in the given file.
489     *  If the attachment did not exist previously, this method
490     *  will create it.  If it did exist, it stores a new version.
491     *
492     *  @param att Attachment to store this under.
493     *  @param source A file to read from.
494     *
495     *  @throws IOException If writing the attachment failed.
496     *  @throws ProviderException If something else went wrong.
497     */
498    public void storeAttachment( Attachment att, File source )
499        throws IOException,
500               ProviderException
501    {
502        FileInputStream in = null;
503
504        try
505        {
506            in = new FileInputStream( source );
507            storeAttachment( att, in );
508        }
509        finally
510        {
511            if( in != null ) in.close();
512        }
513    }
514
515    /**
516     *  Stores an attachment directly from a stream.
517     *  If the attachment did not exist previously, this method
518     *  will create it.  If it did exist, it stores a new version.
519     *
520     *  @param att Attachment to store this under.
521     *  @param in  InputStream from which the attachment contents will be read.
522     *
523     *  @throws IOException If writing the attachment failed.
524     *  @throws ProviderException If something else went wrong.
525     */
526    public void storeAttachment( Attachment att, InputStream in )
527        throws IOException,
528               ProviderException
529    {
530        if( m_provider == null )
531        {
532            return;
533        }
534
535        //
536        //  Checks if the actual, real page exists without any modifications
537        //  or aliases.  We cannot store an attachment to a non-existent page.
538        //
539        if( !m_engine.getPageManager().pageExists( att.getParentName() ) )
540        {
541            // the caller should catch the exception and use the exception text as an i18n key
542            throw new ProviderException(  "attach.parent.not.exist"  );
543        }
544
545        m_provider.putAttachmentData( att, in );
546
547        m_engine.getReferenceManager().updateReferences( att.getName(), new Vector< String >() );
548
549        WikiPage parent = new WikiPage( m_engine, att.getParentName() );
550        m_engine.updateReferences( parent );
551
552        m_engine.getSearchManager().reindexPage( att );
553    }
554
555    /**
556     *  Returns a list of versions of the attachment.
557     *
558     *  @param attachmentName A fully qualified name of the attachment.
559     *
560     *  @return A list of Attachments.  May return null, if attachments are
561     *          disabled.
562     *  @throws ProviderException If the provider fails for some reason.
563     */
564    public List getVersionHistory( String attachmentName )
565        throws ProviderException
566    {
567        if( m_provider == null )
568        {
569            return null;
570        }
571
572        Attachment att = getAttachmentInfo( (WikiContext)null, attachmentName );
573
574        if( att != null )
575        {
576            return m_provider.getVersionHistory( att );
577        }
578
579        return null;
580    }
581
582    /**
583     *  Returns a collection of Attachments, containing each and every attachment
584     *  that is in this Wiki.
585     *
586     *  @return A collection of attachments.  If attachments are disabled, will
587     *          return an empty collection.
588     *  @throws ProviderException If something went wrong with the backend
589     */
590    public Collection getAllAttachments()
591        throws ProviderException
592    {
593        if( attachmentsEnabled() )
594        {
595            return m_provider.listAllChanged( new Date(0L) );
596        }
597
598        return new ArrayList<Attachment>();
599    }
600
601    /**
602     *  Returns the current attachment provider.
603     *
604     *  @return The current provider.  May be null, if attachments are disabled.
605     */
606    public WikiAttachmentProvider getCurrentProvider()
607    {
608        return m_provider;
609    }
610
611    /**
612     *  Deletes the given attachment version.
613     *
614     *  @param att The attachment to delete
615     *  @throws ProviderException If something goes wrong with the backend.
616     */
617    public void deleteVersion( Attachment att )
618        throws ProviderException
619    {
620        if( m_provider == null ) return;
621
622        m_provider.deleteVersion( att );
623    }
624
625    /**
626     *  Deletes all versions of the given attachment.
627     *  @param att The Attachment to delete.
628     *  @throws ProviderException if something goes wrong with the backend.
629     */
630    // FIXME: Should also use events!
631    public void deleteAttachment( Attachment att )
632        throws ProviderException
633    {
634        if( m_provider == null ) return;
635
636        m_provider.deleteAttachment( att );
637
638        m_engine.getSearchManager().pageRemoved( att );
639
640        m_engine.getReferenceManager().clearPageEntries( att.getName() );
641
642    }
643
644    /**
645     *  Validates the filename and makes sure it is legal.  It trims and splits
646     *  and replaces bad characters.
647     *
648     *  @param filename
649     *  @return A validated name with annoying characters replaced.
650     *  @throws WikiException If the filename is not legal (e.g. empty)
651     */
652    static String validateFileName( String filename )
653        throws WikiException
654    {
655        if( filename == null || filename.trim().length() == 0 )
656        {
657            log.error("Empty file name given.");
658
659            // the caller should catch the exception and use the exception text as an i18n key
660            throw new WikiException(  "attach.empty.file" );
661        }
662
663        //
664        //  Should help with IE 5.22 on OSX
665        //
666        filename = filename.trim();
667
668        // If file name ends with .jsp or .jspf, the user is being naughty!
669        if( filename.toLowerCase().endsWith( ".jsp" ) || filename.toLowerCase().endsWith(".jspf") )
670        {
671            log.info( "Attempt to upload a file with a .jsp/.jspf extension.  In certain cases this " +
672                      "can trigger unwanted security side effects, so we're preventing it." );
673            //
674            // the caller should catch the exception and use the exception text as an i18n key
675            throw new WikiException(  "attach.unwanted.file"  );
676        }
677
678        //
679        //  Some browser send the full path info with the filename, so we need
680        //  to remove it here by simply splitting along slashes and then taking the path.
681        //
682
683        String[] splitpath = filename.split( "[/\\\\]" );
684        filename = splitpath[splitpath.length-1];
685
686        //
687        //  Remove any characters that might be a problem. Most
688        //  importantly - characters that might stop processing
689        //  of the URL.
690        //
691        filename = StringUtils.replaceChars( filename, "#?\"'", "____" );
692
693        return filename;
694    }
695}