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     */
019    package org.apache.wiki.attachment;
020    
021    import java.io.File;
022    import java.io.FileInputStream;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.Date;
028    import java.util.List;
029    import java.util.Properties;
030    
031    import net.sf.ehcache.Cache;
032    import net.sf.ehcache.CacheManager;
033    import net.sf.ehcache.Element;
034    
035    import org.apache.commons.lang.StringUtils;
036    import org.apache.log4j.Logger;
037    import org.apache.wiki.PageManager;
038    import org.apache.wiki.WikiContext;
039    import org.apache.wiki.WikiEngine;
040    import org.apache.wiki.WikiPage;
041    import org.apache.wiki.WikiProvider;
042    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
043    import org.apache.wiki.api.exceptions.ProviderException;
044    import org.apache.wiki.api.exceptions.WikiException;
045    import org.apache.wiki.parser.MarkupParser;
046    import org.apache.wiki.providers.WikiAttachmentProvider;
047    import 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     */
059    public 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    }