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