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