    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                LOG.debug(e.getMessage(), e);
    213            } // It's okay to skip these.
    214        }
    215
    216        return version;
    217    }
    218
    219    /**
    220     *  Returns the file extension.  For example "test.png" returns "png".
    221     *  <p>
    222     *  If file has no extension, will return "bin"
    223     *
    224     *  @param filename The file name to check
    225     *  @return The extension.  If no extension is found, returns "bin".
    226     */
    227    protected static String getFileExtension( final String filename ) {
    228        String fileExt = "bin";
    229
    230        final int dot = filename.lastIndexOf('.');
    231        if( dot >= 0 && dot < filename.length()-1 ) {
    232            fileExt = mangleName( filename.substring( dot+1 ) );
    233        }
    234
    235        return fileExt;
    236    }
    237
    238    /**
    239     *  Writes the page properties back to the file system.
    240     *  Note that it WILL overwrite any previous properties.
    241     */
    242    private void putPageProperties( final Attachment att, final Properties properties ) throws IOException, ProviderException {
    243        final File attDir = findAttachmentDir( att );
    244        final File propertyFile = new File( attDir, PROPERTY_FILE );
    245        try( final OutputStream out = Files.newOutputStream( propertyFile.toPath() ) ) {
    246            properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" );
    247        }
    248    }
    249
    250    /**
    251     *  Reads page properties from the file system.
    252     */
    253    private Properties getPageProperties( final Attachment att ) throws IOException, ProviderException {
    254        final Properties props = new Properties();
    255        final File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE );
    256        if( propertyFile.exists() ) {
    257            try( final InputStream in = Files.newInputStream( propertyFile.toPath() ) ) {
    258                props.load( in );
    259            } catch( final IOException ioe ) {
    260                LOG.error( ioe.getMessage() );
    261            }
    262        }
    263
    264        return props;
    265    }
    266
    267    /**
    268     *  {@inheritDoc}
    269     */
    270    @Override
    271    public void putAttachmentData( final Attachment att, final InputStream data ) throws ProviderException, IOException {
    272        final File attDir = findAttachmentDir( att );
    273
    274        if( !attDir.exists() ) {
    275            attDir.mkdirs();
    276        }
    277        final int latestVersion = findLatestVersion( att );
    278        final int versionNumber = latestVersion + 1;
    279
    280        final File newfile = new File( attDir, versionNumber + "." + getFileExtension( att.getFileName() ) );
    281        try( final OutputStream out = Files.newOutputStream( newfile.toPath() ) ) {
    282            LOG.info( "Uploading attachment " + att.getFileName() + " to page " + att.getParentName() );
    283            LOG.info( "Saving attachment contents to " + newfile.getAbsolutePath() );
    284            FileUtil.copyContents( data, out );
    285
    286            final Properties props = getPageProperties( att );
    287
    288            String author = att.getAuthor();
    289            if( author == null ) {
    290                author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext
    291            }
    292            props.setProperty( versionNumber + ".author", author );
    293
    294            final String changeNote = att.getAttribute( Page.CHANGENOTE );
    295            if( changeNote != null ) {
    296                props.setProperty( versionNumber + ".changenote", changeNote );
    297            }
    298
    299            putPageProperties( att, props );
    300        } catch( final IOException e ) {
    301            LOG.error( "Could not save attachment data: ", e );
    302            throw (IOException) e.fillInStackTrace();
    303        }
    304    }
    305
    306    /**
    307     *  {@inheritDoc}
    308     */
    309    @Override
    310    public String getProviderInfo() {
    311        return "";
    312    }
    313
    314    private File findFile( final File dir, final Attachment att ) throws FileNotFoundException, ProviderException {
    315        int version = att.getVersion();
    316        if( version == WikiProvider.LATEST_VERSION ) {
    317            version = findLatestVersion( att );
    318        }
    319
    320        final String ext = getFileExtension( att.getFileName() );
    321        File f = new File( dir, version + "." + ext );
    322
    323        if( !f.exists() ) {
    324            if( "bin".equals( ext ) ) {
    325                final File fOld = new File( dir, version + "." );
    326                if( fOld.exists() ) {
    327                    f = fOld;
    328                }
    329            }
    330            if( !f.exists() ) {
    331                throw new FileNotFoundException( "No such file: " + f.getAbsolutePath() + " exists." );
    332            }
    333        }
    334
    335        return f;
    336    }
    337
    338    /**
    339     *  {@inheritDoc}
    340     */
    341    @Override
    342    public InputStream getAttachmentData( final Attachment att ) throws IOException, ProviderException {
    343        final File attDir = findAttachmentDir( att );
    344        try {
    345            final File f = findFile( attDir, att );
    346            return Files.newInputStream( f.toPath() );
    347        } catch( final FileNotFoundException e ) {
    348            LOG.error( "File not found: " + e.getMessage() );
    349            throw new ProviderException( "No such page was found." );
    350        }
    351    }
    352
    353    /**
    354     *  {@inheritDoc}
    355     */
    356    @Override
    357    public List< Attachment > listAttachments( final Page page ) throws ProviderException {
    358        final List< Attachment > result = new ArrayList<>();
    359        final File dir = findPageDir( page.getName() );
    360        final String[] attachments = dir.list();
    361        if( attachments != null ) {
    362            //  We now have a list of all potential attachments in the directory.
    363            for( final String attachment : attachments ) {
    364                final File f = new File( dir, attachment );
    365                if( f.isDirectory() ) {
    366                    String attachmentName = unmangleName( attachment );
    367
    368                    //  Is it a new-stylea attachment directory?  If yes, we'll just deduce the name.  If not, however,
    369                    //  we'll check if there's a suitable property file in the directory.
    370                    if( attachmentName.endsWith( ATTDIR_EXTENSION ) ) {
    371                        attachmentName = attachmentName.substring( 0, attachmentName.length() - ATTDIR_EXTENSION.length() );
    372                    } else {
    373                        final File propFile = new File( f, PROPERTY_FILE );
    374                        if( !propFile.exists() ) {
    375                            //  This is not obviously a JSPWiki attachment, so let's just skip it.
    376                            continue;
    377                        }
    378                    }
    379
    380                    final Attachment att = getAttachmentInfo( page, attachmentName, WikiProvider.LATEST_VERSION );
    381                    //  Sanity check - shouldn't really be happening, unless you mess with the repository directly.
    382                    if( att == null ) {
    383                        LOG.error( "Attachment disappeared while reading information:"
    384                                + " if you did not touch the repository, there is a serious bug somewhere or perhaps it"
    385                                + " was deleted by antivirus software, etc. " + "Attachment = " + attachment
    386                                + ", decoded = " + attachmentName );
    387                    } else {
    388                        result.add( att );
    389                    }
    390                }
    391            }
    392        }
    393
    394        return result;
    395    }
    396
    397    /**
    398     *  {@inheritDoc}
    399     */
    400    @Override
    401    public Collection< Attachment > findAttachments( final QueryItem[] query ) {
    402        return new ArrayList<>();
    403    }
    404
    405    /**
    406     *  {@inheritDoc}
    407     */
    408    // FIXME: Very unoptimized.
    409    @Override
    410    public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException {
    411        final File attDir = new File( m_storageDir );
    412        if( !attDir.exists() ) {
    413            if (!attDir.mkdirs()) {
    414                throw new ProviderException( "Specified attachment directory " + m_storageDir + " does not exist!" );
    415            }
    416        }
    417
    418        final ArrayList< Attachment > list = new ArrayList<>();
    419        final String[] pagesWithAttachments = attDir.list( new AttachmentFilter() );
    420
    421        if( pagesWithAttachments != null ) {
    422            for( final String pagesWithAttachment : pagesWithAttachments ) {
    423                String pageId = unmangleName( pagesWithAttachment );
    424                pageId = pageId.substring( 0, pageId.length() - DIR_EXTENSION.length() );
    425
    426                final Collection< Attachment > c = listAttachments( Wiki.contents().page( m_engine, pageId ) );
    427                for( final Attachment att : c ) {
    428                    if( att.getLastModified().after( timestamp ) ) {
    429                        list.add( att );
    430                    }
    431                }
    432            }
    433        }
    434
    435        list.sort( new PageTimeComparator() );
    436
    437        return list;
    438    }
    439
    440    /**
    441     *  {@inheritDoc}
    442     */
    443    @Override
    444    public Attachment getAttachmentInfo( final Page page, final String name, int version ) throws ProviderException {
    445        final Attachment att = new org.apache.wiki.attachment.Attachment( m_engine, page.getName(), name );
    446        final File dir = findAttachmentDir( att );
    447        if( !dir.exists() ) {
    448            // LOG.debug("Attachment dir not found - thus no attachment can exist.");
    449            return null;
    450        }
    451
    452        if( version == WikiProvider.LATEST_VERSION ) {
    453            version = findLatestVersion(att);
    454        }
    455
    456        att.setVersion( version );
    457
    458        // Should attachment be cachable by the client (browser)?
    459        if( m_disableCache != null ) {
    460            final Matcher matcher = m_disableCache.matcher( name );
    461            if( matcher.matches() ) {
    462                att.setCacheable( false );
    463            }
    464        }
    465
    466        // System.out.println("Fetching info on version "+version);
    467        try {
    468            final Properties props = getPageProperties( att );
    469            att.setAuthor( props.getProperty( version+".author" ) );
    470            final String changeNote = props.getProperty( version+".changenote" );
    471            if( changeNote != null ) {
    472                att.setAttribute( Page.CHANGENOTE, changeNote );
    473            }
    474
    475            final File f = findFile( dir, att );
    476            att.setSize( f.length() );
    477            att.setLastModified( new Date( f.lastModified() ) );
    478        } catch( final FileNotFoundException e ) {
    479            LOG.error( "Can't get attachment properties for " + att, e );
    480            return null;
    481        } catch( final IOException e ) {
    482            LOG.error("Can't read page properties", e );
    483            throw new ProviderException("Cannot read page properties: "+e.getMessage());
    484        }
    485        // FIXME: Check for existence of this particular version.
    486
    487        return att;
    488    }
    489
    490    /**
    491     *  {@inheritDoc}
    492     */
    493    @Override
    494    public List< Attachment > getVersionHistory( final Attachment att ) {
    495        final ArrayList< Attachment > list = new ArrayList<>();
    496        try {
    497            final int latest = findLatestVersion( att );
    498            for( int i = latest; i >= 1; i-- ) {
    499                final Attachment a = getAttachmentInfo( Wiki.contents().page( m_engine, att.getParentName() ), att.getFileName(), i );
    500                if( a != null ) {
    501                    list.add( a );
    502                }
    503            }
    504        } catch( final ProviderException e ) {
    505            LOG.error( "Getting version history failed for page: " + att, e );
    506            // FIXME: Should this fail?
    507        }
    508
    509        return list;
    510    }
    511
    512    /**
    513     *  {@inheritDoc}
    514     */
    515    @Override
    516    public void deleteVersion( final Attachment att ) throws ProviderException {
    517        // FIXME: Does nothing yet.
    518    }
    519
    520    /**
    521     *  {@inheritDoc}
    522     */
    523    @Override
    524    public void deleteAttachment( final Attachment att ) throws ProviderException {
    525        final File dir = findAttachmentDir( att );
    526        final String[] files = dir.list();
    527        for( final String s : files ) {
    528            final File file = new File( dir.getAbsolutePath() + "/" + s );
    529            file.delete();
    530        }
    531        dir.delete();
    532    }
    533
    534    /**
    535     *  Returns only those directories that contain attachments.
    536     */
    537    public static class AttachmentFilter implements FilenameFilter {
    538        /**
    539         *  {@inheritDoc}
    540         */
    541        @Override
    542        public boolean accept( final File dir, final String name )
    543        {
    544            return name.endsWith( DIR_EXTENSION );
    545        }
    546    }
    547
    548    /**
    549     *  Accepts only files that are actual versions, no control files.
    550     */
    551    public static class AttachmentVersionFilter implements FilenameFilter {
    552        /**
    553         *  {@inheritDoc}
    554         */
    555        @Override
    556        public boolean accept( final File dir, final String name )
    557        {
    558            return !name.equals( PROPERTY_FILE );
    559        }
    560    }
    561
    562    /**
    563     *  {@inheritDoc}
    564     */
    565    @Override
    566    public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException {
    567        final File srcDir = findPageDir( oldParent );
    568        final File destDir = findPageDir( newParent );
    569
    570        LOG.debug( "Trying to move all attachments from " + srcDir + " to " + destDir );
    571
    572        // If it exists, we're overwriting an old page (this has already been confirmed at a higher level), so delete any existing attachments.
    573        if( destDir.exists() ) {
    574            LOG.error( "Page rename failed because target directory " + destDir + " exists" );
    575        } else {
    576            // destDir.getParentFile().mkdir();
    577            srcDir.renameTo( destDir );
    578        }
    579    }
    580
    581}
    582