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.providers;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.FilenameFilter;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Date;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Properties;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039import org.apache.commons.io.IOUtils;
040import org.apache.log4j.Logger;
041import org.apache.wiki.WikiEngine;
042import org.apache.wiki.WikiPage;
043import org.apache.wiki.WikiProvider;
044import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
045import org.apache.wiki.api.exceptions.ProviderException;
046import org.apache.wiki.attachment.Attachment;
047import org.apache.wiki.pages.PageTimeComparator;
048import org.apache.wiki.search.QueryItem;
049import org.apache.wiki.util.FileUtil;
050import org.apache.wiki.util.TextUtil;
051
052/**
053 *  Provides basic, versioning attachments.
054 *
055 *  <PRE>
056 *   Structure is as follows:
057 *      attachment_dir/
058 *         ThisPage/
059 *            attachment.doc/
060 *               attachment.properties
061 *               1.doc
062 *               2.doc
063 *               3.doc
064 *            picture.png/
065 *               attachment.properties
066 *               1.png
067 *               2.png
068 *         ThatPage/
069 *            picture.png/
070 *               attachment.properties
071 *               1.png
072 *             
073 *  </PRE>
074 *
075 *  The names of the directories will be URLencoded.
076 *  <p>
077 *  "attachment.properties" consists of the following items:
078 *  <UL>
079 *   <LI>1.author = author name for version 1 (etc)
080 *  </UL>
081 */
082public class BasicAttachmentProvider
083    implements WikiAttachmentProvider
084{
085    private WikiEngine         m_engine;
086    private String             m_storageDir;
087    
088    /** The property name for where the attachments should be stored.  Value is <tt>{@value}</tt>. */
089    public static final String PROP_STORAGEDIR = "jspwiki.basicAttachmentProvider.storageDir";
090    
091    /*
092     * Disable client cache for files with patterns
093     * since 2.5.96
094     */
095    private Pattern            m_disableCache = null;
096    
097    /** The property name for specifying which attachments are not cached.  Value is <tt>{@value}</tt>. */
098    public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache";
099
100    /** The name of the property file. */
101    public static final String PROPERTY_FILE   = "attachment.properties";
102
103    /** The default extension for the page attachment directory name. */
104    public static final String DIR_EXTENSION   = "-att";
105    
106    /** The default extension for the attachment directory. */
107    public static final String ATTDIR_EXTENSION = "-dir";
108    
109    static final Logger log = Logger.getLogger( BasicAttachmentProvider.class );
110
111    /**
112     *  {@inheritDoc}
113     */
114    @Override
115    public void initialize( WikiEngine engine, Properties properties ) 
116        throws NoRequiredPropertyException,
117               IOException
118    {
119        m_engine = engine;
120        m_storageDir = TextUtil.getCanonicalFilePathProperty(properties, PROP_STORAGEDIR,
121                System.getProperty("user.home") + File.separator + "jspwiki-files");
122
123        String patternString = engine.getWikiProperties().getProperty( PROP_DISABLECACHE );
124        if ( patternString != null )
125        {
126            m_disableCache = Pattern.compile(patternString);
127        }
128
129        //
130        //  Check if the directory exists - if it doesn't, create it.
131        //
132        File f = new File( m_storageDir );
133
134        if( !f.exists() )
135        {
136            f.mkdirs();
137        }
138
139        //
140        // Some sanity checks
141        //
142        if( !f.exists() ) 
143            throw new IOException("Could not find or create attachment storage directory '"+m_storageDir+"'");
144
145        if( !f.canWrite() ) 
146            throw new IOException("Cannot write to the attachment storage directory '"+m_storageDir+"'");
147        
148        if( !f.isDirectory() )
149            throw new IOException("Your attachment storage points to a file, not a directory: '"+m_storageDir+"'");
150    }
151
152    /**
153     *  Finds storage dir, and if it exists, makes sure that it is valid.
154     *
155     *  @param wikipage Page to which this attachment is attached.
156     */
157    private File findPageDir( String wikipage )
158        throws ProviderException
159    {
160        wikipage = mangleName( wikipage );
161
162        File f = new File( m_storageDir, wikipage+DIR_EXTENSION );
163
164        if( f.exists() && !f.isDirectory() )
165        {
166            throw new ProviderException("Storage dir '"+f.getAbsolutePath()+"' is not a directory!");
167        }
168
169        return f;
170    }
171
172    private static String mangleName( String wikiname )
173    {
174        String res = TextUtil.urlEncodeUTF8( wikiname );
175
176        return res;
177    }
178
179    private static String unmangleName( String filename )
180    {
181        return TextUtil.urlDecodeUTF8( filename );
182    }
183    
184    /**
185     *  Finds the dir in which the attachment lives.
186     */
187    private File findAttachmentDir( Attachment att )
188        throws ProviderException
189    {
190        File f = new File( findPageDir(att.getParentName()), 
191                           mangleName(att.getFileName()+ATTDIR_EXTENSION) );
192
193        //
194        //  Migration code for earlier versions of JSPWiki.
195        //  Originally, we used plain filename.  Then we realized we need
196        //  to urlencode it.  Then we realized that we have to use a
197        //  postfix to make sure illegal file names are never formed.
198        //
199        if( !f.exists() )
200        {
201            File oldf = new File( findPageDir( att.getParentName() ),
202                                  mangleName( att.getFileName() ) );
203            if( oldf.exists() )
204            {
205                f = oldf;
206            }
207            else
208            {
209                oldf = new File( findPageDir( att.getParentName() ),
210                                 att.getFileName() );
211
212                if( oldf.exists() )
213                {
214                    f = oldf;
215                }
216            }
217        }
218
219        return f;
220    }
221
222    /**
223     *  Goes through the repository and decides which version is
224     *  the newest one in that directory.
225     *
226     *  @return Latest version number in the repository, or 0, if
227     *          there is no page in the repository.
228     */
229    private int findLatestVersion( Attachment att )
230        throws ProviderException
231    {
232        // File pageDir = findPageDir( att.getName() );
233        File attDir  = findAttachmentDir( att );
234
235        // log.debug("Finding pages in "+attDir.getAbsolutePath());
236        String[] pages = attDir.list( new AttachmentVersionFilter() );
237
238        if( pages == null )
239        {
240            return 0; // No such thing found.
241        }
242
243        int version = 0;
244
245        for( int i = 0; i < pages.length; i++ )
246        {
247            // log.debug("Checking: "+pages[i]);
248            int cutpoint = pages[i].indexOf( '.' );
249                String pageNum = ( cutpoint > 0 ) ? pages[i].substring( 0, cutpoint ) : pages[i] ;
250
251                try
252                {
253                    int res = Integer.parseInt( pageNum );
254
255                    if( res > version )
256                    {
257                        version = res;
258                    }
259                }
260                catch( NumberFormatException e ) {} // It's okay to skip these.
261            }
262
263        return version;
264    }
265
266    /**
267     *  Returns the file extension.  For example "test.png" returns "png".
268     *  <p>
269     *  If file has no extension, will return "bin"
270     *  
271     *  @param filename The file name to check
272     *  @return The extension.  If no extension is found, returns "bin".
273     */
274    protected static String getFileExtension( String filename )
275    {
276        String fileExt = "bin";
277
278        int dot = filename.lastIndexOf('.');
279        if( dot >= 0 && dot < filename.length()-1 )
280        {
281            fileExt = mangleName( filename.substring( dot+1 ) );
282        }
283
284        return fileExt;
285    }
286
287    /**
288     *  Writes the page properties back to the file system.
289     *  Note that it WILL overwrite any previous properties.
290     */
291    private void putPageProperties( Attachment att, Properties properties ) throws IOException, ProviderException {
292        File attDir = findAttachmentDir( att );
293        File propertyFile = new File( attDir, PROPERTY_FILE );
294
295        OutputStream out = null;
296        
297        try {
298            out = new FileOutputStream( propertyFile );
299            properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" );
300        } catch ( IOException ioe ) {
301            IOUtils.closeQuietly( out );
302            throw ioe;
303        } finally {
304            IOUtils.closeQuietly( out );
305        }
306    }
307
308    /**
309     *  Reads page properties from the file system.
310     */
311    private Properties getPageProperties( Attachment att ) throws IOException, ProviderException {
312        Properties props = new Properties();
313
314        File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE );
315
316        if( propertyFile.exists() ) {
317            InputStream in = null;
318            try {
319                in = new FileInputStream( propertyFile );
320                props.load( in );
321            } catch ( IOException ioe ) {
322                IOUtils.closeQuietly( in );
323                throw ioe;
324            } finally {
325                IOUtils.closeQuietly( in );
326            }
327        }
328        
329        return props;
330    }
331
332    /**
333     *  {@inheritDoc}
334     */
335    @Override
336    public void putAttachmentData( Attachment att, InputStream data ) throws ProviderException, IOException {
337        OutputStream out = null;
338        File attDir = findAttachmentDir( att );
339
340        if(!attDir.exists())
341        {
342            attDir.mkdirs();
343        }
344
345        int latestVersion = findLatestVersion( att );
346
347        // System.out.println("Latest version is "+latestVersion);
348
349        try
350        {
351            int versionNumber = latestVersion+1;
352
353            File newfile = new File( attDir, versionNumber+"."+
354                                     getFileExtension(att.getFileName()) );
355
356            log.info("Uploading attachment "+att.getFileName()+" to page "+att.getParentName());
357            log.info("Saving attachment contents to "+newfile.getAbsolutePath());
358            out = new FileOutputStream(newfile);
359
360            FileUtil.copyContents( data, out );
361
362            Properties props = getPageProperties( att );
363
364            String author = att.getAuthor();
365
366            if( author == null )
367            {
368                author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext
369            }
370
371            props.setProperty( versionNumber+".author", author );
372            
373            String changeNote = (String)att.getAttribute(WikiPage.CHANGENOTE);
374            if( changeNote != null )
375            {
376                props.setProperty( versionNumber+".changenote", changeNote );
377            }
378            
379            putPageProperties( att, props );
380        }
381        catch( IOException e )
382        {
383            log.error( "Could not save attachment data: ", e );
384            IOUtils.closeQuietly( out );
385            throw (IOException) e.fillInStackTrace();
386        }
387        finally
388        {
389            IOUtils.closeQuietly( out );
390        }
391    }
392
393    /**
394     *  {@inheritDoc}
395     */
396    @Override
397    public String getProviderInfo()
398    {
399        return "";
400    }
401
402    private File findFile( File dir, Attachment att )
403        throws FileNotFoundException,
404               ProviderException
405    {
406        int version = att.getVersion();
407
408        if( version == WikiProvider.LATEST_VERSION )
409        {
410            version = findLatestVersion( att );
411        }
412
413        String ext = getFileExtension( att.getFileName() );
414        File f = new File( dir, version+"."+ext );
415
416        if( !f.exists() )
417        {
418            if ("bin".equals(ext))
419            {
420                File fOld = new File( dir, version+"." );
421                if (fOld.exists())
422                    f = fOld;
423            }
424            if( !f.exists() )
425            {
426                throw new FileNotFoundException("No such file: "+f.getAbsolutePath()+" exists.");
427            }
428        }
429
430        return f;
431    }
432
433    /**
434     *  {@inheritDoc}
435     */
436    @Override
437    public InputStream getAttachmentData( Attachment att )
438        throws IOException,
439               ProviderException
440    {
441        File attDir = findAttachmentDir( att );
442
443        try
444        {
445            File f = findFile( attDir, att );
446
447            return new FileInputStream( f );
448        }
449        catch( FileNotFoundException e )
450        {
451            log.error("File not found: "+e.getMessage());
452            throw new ProviderException("No such page was found.");
453        }
454    }
455
456    /**
457     *  {@inheritDoc}
458     */
459    @Override
460    public List< Attachment > listAttachments( WikiPage page )
461        throws ProviderException
462    {
463        List<Attachment> result = new ArrayList<>();
464
465        File dir = findPageDir( page.getName() );
466
467        if( dir != null )
468        {
469            String[] attachments = dir.list();
470
471            if( attachments != null )
472            {
473                //
474                //  We now have a list of all potential attachments in 
475                //  the directory.
476                //
477                for( int i = 0; i < attachments.length; i++ )
478                {
479                    File f = new File( dir, attachments[i] );
480
481                    if( f.isDirectory() )
482                    {
483                        String attachmentName = unmangleName( attachments[i] );
484
485                        //
486                        //  Is it a new-stylea attachment directory?  If yes,
487                        //  we'll just deduce the name.  If not, however,
488                        //  we'll check if there's a suitable property file
489                        //  in the directory.
490                        //
491                        if( attachmentName.endsWith( ATTDIR_EXTENSION ) )
492                        {
493                            attachmentName = attachmentName.substring( 0, attachmentName.length()-ATTDIR_EXTENSION.length() );
494                        }
495                        else
496                        {
497                            File propFile = new File( f, PROPERTY_FILE );
498
499                            if( !propFile.exists() )
500                            {
501                                //
502                                //  This is not obviously a JSPWiki attachment,
503                                //  so let's just skip it.
504                                //
505                                continue;
506                            }
507                        }
508
509                        Attachment att = getAttachmentInfo( page, attachmentName,
510                                                            WikiProvider.LATEST_VERSION );
511
512                        //
513                        //  Sanity check - shouldn't really be happening, unless
514                        //  you mess with the repository directly.
515                        //
516                        if( att == null )
517                        {
518                            throw new ProviderException("Attachment disappeared while reading information:"+
519                                                        " if you did not touch the repository, there is a serious bug somewhere. "+
520                                                        "Attachment = "+attachments[i]+
521                                                        ", decoded = "+attachmentName );
522                        }
523
524                        result.add( att );
525                    }
526                }
527            }
528        }
529
530        return result;
531    }
532
533    /**
534     *  {@inheritDoc}
535     */
536    @Override
537    public Collection< Attachment > findAttachments( QueryItem[] query )
538    {
539        return new ArrayList<>();
540    }
541
542    /**
543     *  {@inheritDoc}
544     */
545    // FIXME: Very unoptimized.
546    @Override
547    public List<Attachment> listAllChanged( Date timestamp )
548        throws ProviderException
549    {
550        File attDir = new File( m_storageDir );
551
552        if( !attDir.exists() )
553        {
554            throw new ProviderException("Specified attachment directory "+m_storageDir+" does not exist!");
555        }
556
557        ArrayList<Attachment> list = new ArrayList<>();
558
559        String[] pagesWithAttachments = attDir.list( new AttachmentFilter() );
560
561        for( int i = 0; i < pagesWithAttachments.length; i++ )
562        {
563            String pageId = unmangleName( pagesWithAttachments[i] );
564            pageId = pageId.substring( 0, pageId.length()-DIR_EXTENSION.length() );
565            
566            Collection<Attachment> c = listAttachments( new WikiPage( m_engine, pageId ) );
567
568            for( Iterator<Attachment> it = c.iterator(); it.hasNext(); )
569            {
570                Attachment att = it.next();
571
572                if( att.getLastModified().after( timestamp ) )
573                {
574                    list.add( att );
575                }
576            }
577        }
578
579        Collections.sort( list, new PageTimeComparator() );
580
581        return list;
582    }
583
584    /**
585     *  {@inheritDoc}
586     */
587    @Override
588    public Attachment getAttachmentInfo( WikiPage page, String name, int version )
589        throws ProviderException
590    {
591        Attachment att = new Attachment( m_engine, page.getName(), name );
592        File dir = findAttachmentDir( att );
593
594        if( !dir.exists() )
595        {
596            // log.debug("Attachment dir not found - thus no attachment can exist.");
597            return null;
598        }
599        
600        if( version == WikiProvider.LATEST_VERSION )
601        {
602            version = findLatestVersion(att);
603        }
604
605        att.setVersion( version );
606        
607        // Should attachment be cachable by the client (browser)?
608        if (m_disableCache != null) 
609        {
610            Matcher matcher = m_disableCache.matcher(name);
611            if (matcher.matches()) 
612            {
613                att.setCacheable(false);
614            }
615        }
616        
617
618        // System.out.println("Fetching info on version "+version);
619        try
620        {
621            Properties props = getPageProperties(att);
622
623            att.setAuthor( props.getProperty( version+".author" ) );
624
625            String changeNote = props.getProperty( version+".changenote" );
626            if( changeNote != null )
627            {
628                att.setAttribute(WikiPage.CHANGENOTE, changeNote);
629            }
630            
631            File f = findFile( dir, att );
632
633            att.setSize( f.length() );
634            att.setLastModified( new Date(f.lastModified()) );
635        }
636        catch( FileNotFoundException e )
637        {
638            log.error( "Can't get attachment properties for " + att, e );
639            return null;
640        }
641        catch( IOException e )
642        {
643            log.error("Can't read page properties", e );
644            throw new ProviderException("Cannot read page properties: "+e.getMessage());
645        }
646        // FIXME: Check for existence of this particular version.
647
648        return att;
649    }
650
651    /**
652     *  {@inheritDoc}
653     */
654    @Override
655    public List<Attachment> getVersionHistory( Attachment att )
656    {
657        ArrayList<Attachment> list = new ArrayList<>();
658
659        try
660        {
661            int latest = findLatestVersion( att );
662
663            for( int i = latest; i >= 1; i-- )
664            {
665                Attachment a = getAttachmentInfo( new WikiPage( m_engine, att.getParentName() ), 
666                                                  att.getFileName(), i );
667
668                if( a != null )
669                {
670                    list.add( a );
671                }
672            }
673        }
674        catch( ProviderException e )
675        {
676            log.error("Getting version history failed for page: "+att,e);
677            // FIXME: SHould this fail?
678        }
679
680        return list;
681    }
682
683    /**
684     *  {@inheritDoc}
685     */
686    @Override
687    public void deleteVersion( Attachment att )
688        throws ProviderException
689    {
690        // FIXME: Does nothing yet.
691    }
692
693    /**
694     *  {@inheritDoc}
695     */
696    @Override
697    public void deleteAttachment( Attachment att )
698        throws ProviderException
699    {
700        File dir = findAttachmentDir( att );
701        String[] files = dir.list();
702
703        for( int i = 0; i < files.length; i++ )
704        {
705            File file = new File( dir.getAbsolutePath() + "/" + files[i] );
706            file.delete();
707        }
708        dir.delete();
709    }
710
711
712    /**
713     *  Returns only those directories that contain attachments.
714     */
715    public static class AttachmentFilter
716        implements FilenameFilter
717    {
718        /**
719         *  {@inheritDoc}
720         */
721        @Override
722        public boolean accept( File dir, String name )
723        {
724            return name.endsWith( DIR_EXTENSION );
725        }
726    }
727
728    /**
729     *  Accepts only files that are actual versions, no control files.
730     */
731    public static class AttachmentVersionFilter
732        implements FilenameFilter
733    {
734        /**
735         *  {@inheritDoc}
736         */
737        @Override
738        public boolean accept( File dir, String name )
739        {
740            return !name.equals( PROPERTY_FILE );
741        }
742    }
743
744    /**
745     *  {@inheritDoc}
746     */
747
748    @Override
749    public void moveAttachmentsForPage( String oldParent, String newParent )
750        throws ProviderException
751    {
752        File srcDir = findPageDir( oldParent );
753        File destDir = findPageDir( newParent );
754
755        log.debug("Trying to move all attachments from "+srcDir+" to "+destDir);
756
757        // If it exists, we're overwriting an old page (this has already been
758        // confirmed at a higher level), so delete any existing attachments.
759        if (destDir.exists())
760        {
761            log.error("Page rename failed because target dirctory "+destDir+" exists");
762        }
763        else
764        {
765            //destDir.getParentFile().mkdir();
766            srcDir.renameTo(destDir);
767        }
768    }
769}
770