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