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