    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.attachment;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.api.core.Attachment;
    025import org.apache.wiki.api.core.Context;
    026import org.apache.wiki.api.core.Engine;
    027import org.apache.wiki.api.core.Page;
    028import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    029import org.apache.wiki.api.exceptions.ProviderException;
    030import org.apache.wiki.api.providers.AttachmentProvider;
    031import org.apache.wiki.api.spi.Wiki;
    032import org.apache.wiki.cache.CachingManager;
    033import org.apache.wiki.pages.PageManager;
    034import org.apache.wiki.parser.MarkupParser;
    035import org.apache.wiki.references.ReferenceManager;
    036import org.apache.wiki.search.SearchManager;
    037import org.apache.wiki.util.ClassUtil;
    038import org.apache.wiki.util.TextUtil;
    039
    040import java.io.IOException;
    041import java.io.InputStream;
    042import java.util.ArrayList;
    043import java.util.Collection;
    044import java.util.Comparator;
    045import java.util.Date;
    046import java.util.List;
    047import java.util.Properties;
    048
    049
    050/**
    051 *  Default implementation for {@link AttachmentManager}.
    052 *
    053 * {@inheritDoc}
    054 *
    055 *  @since 1.9.28
    056 */
    057public class DefaultAttachmentManager implements AttachmentManager {
    058
    059    /** List of attachment types which are forced to be downloaded */
    060    private String[] m_forceDownloadPatterns;
    061
    062    private static final Logger LOG = LogManager.getLogger( DefaultAttachmentManager.class );
    063    private AttachmentProvider m_provider;
    064    private final Engine m_engine;
    065    private final CachingManager cachingManager;
    066
    067    /**
    068     *  Creates a new AttachmentManager.  Note that creation will never fail, but it's quite likely that attachments do not function.
    069     *  <p><strong>DO NOT CREATE</strong> an AttachmentManager on your own, unless you really know what you're doing. Just use
    070     *  Wikiengine.getManager( AttachmentManager.class ) if you're making a module for JSPWiki.
    071     *
    072     *  @param engine The wikiengine that owns this attachment manager.
    073     *  @param props A list of properties from which the AttachmentManager will seek its configuration. Typically, this is the "jspwiki.properties".
    074     */
    075    public DefaultAttachmentManager( final Engine engine, final Properties props ) {
    076        m_engine = engine;
    077        cachingManager = m_engine.getManager( CachingManager.class );
    078        final String classname;
    079        if( cachingManager.enabled( CachingManager.CACHE_ATTACHMENTS_DYNAMIC ) ) {
    080            classname = "org.apache.wiki.providers.CachingAttachmentProvider";
    081        } else {
    082            classname = TextUtil.getRequiredProperty( props, PROP_PROVIDER, PROP_PROVIDER_DEPRECATED );
    083        }
    084
    085        //  If no class defined, then will just simply fail.
    086        if( classname == null ) {
    087            LOG.info( "No attachment provider defined - disabling attachment support." );
    088            return;
    089        }
    090
    091        //  Create and initialize the provider.
    092        try {
    093            m_provider = ClassUtil.buildInstance( "org.apache.wiki.providers", classname );
    094            m_provider.initialize( m_engine, props );
    095        } catch( final ReflectiveOperationException e ) {
    096            LOG.error( "Attachment provider class could not be instantiated", e );
    097        } catch( final NoRequiredPropertyException e ) {
    098            LOG.error( "Attachment provider did not find a property that it needed: {}", e.getMessage(), e );
    099            m_provider = null; // No, it did not work.
    100        } catch( final IOException e ) {
    101            LOG.error( "Attachment provider reports IO error", e );
    102            m_provider = null;
    103        }
    104
    105        final String forceDownload = TextUtil.getStringProperty( props, PROP_FORCEDOWNLOAD, null );
    106        if( StringUtils.isNotEmpty( forceDownload ) ) {
    107            m_forceDownloadPatterns = forceDownload.toLowerCase().split( "\\s" );
    108        } else {
    109            m_forceDownloadPatterns = new String[ 0 ];
    110        }
    111    }
    112
    113    /** {@inheritDoc} */
    114    @Override
    115    public boolean attachmentsEnabled() {
    116        return m_provider != null;
    117    }
    118
    119    /** {@inheritDoc} */
    120    @Override
    121    public String getAttachmentInfoName( final Context context, final String attachmentname ) {
    122        final Attachment att;
    123        try {
    124            att = getAttachmentInfo( context, attachmentname );
    125        } catch( final ProviderException e ) {
    126            LOG.warn( "Finding attachments failed: ", e );
    127            return null;
    128        }
    129
    130        if( att != null ) {
    131            return att.getName();
    132        } else if( attachmentname.indexOf( '/' ) != -1 ) {
    133            return attachmentname;
    134        }
    135
    136        return null;
    137    }
    138
    139    /** {@inheritDoc} */
    140    @Override
    141    public Attachment getAttachmentInfo( final Context context, String attachmentname, final int version ) throws ProviderException {
    142        if( m_provider == null ) {
    143            return null;
    144        }
    145
    146        Page currentPage = null;
    147
    148        if( context != null ) {
    149            currentPage = context.getPage();
    150        }
    151
    152        //  Figure out the parent page of this attachment.  If we can't find it, we'll assume this refers directly to the attachment.
    153        final int cutpt = attachmentname.lastIndexOf( '/' );
    154        if( cutpt != -1 ) {
    155            String parentPage = attachmentname.substring( 0, cutpt );
    156            parentPage = MarkupParser.cleanLink( parentPage );
    157            attachmentname = attachmentname.substring( cutpt + 1 );
    158
    159            // If we for some reason have an empty parent page name; this can't be an attachment
    160            if( parentPage.isEmpty() ) {
    161                return null;
    162            }
    163
    164            currentPage = m_engine.getManager( PageManager.class ).getPage( parentPage );
    165
    166            // Go check for legacy name
    167            // FIXME: This should be resolved using CommandResolver, not this adhoc way.  This also assumes that the
    168            //        legacy charset is a subset of the full allowed set.
    169            if( currentPage == null ) {
    170                currentPage = m_engine.getManager( PageManager.class ).getPage( MarkupParser.wikifyLink( parentPage ) );
    171            }
    172        }
    173
    174        //  If the page cannot be determined, we cannot possibly find the attachments.
    175        if( currentPage == null || currentPage.getName().isEmpty() ) {
    176            return null;
    177        }
    178
    179        //  Finally, figure out whether this is a real attachment or a generated attachment.
    180        Attachment att = getDynamicAttachment( currentPage.getName() + "/" + attachmentname );
    181        if( att == null ) {
    182            att = m_provider.getAttachmentInfo( currentPage, attachmentname, version );
    183        }
    184
    185        return att;
    186    }
    187
    188    /** {@inheritDoc} */
    189    @Override
    190    public List< Attachment > listAttachments( final Page wikipage ) throws ProviderException {
    191        if( m_provider == null ) {
    192            return new ArrayList<>();
    193        }
    194
    195        final List< Attachment > atts = new ArrayList<>( m_provider.listAttachments( wikipage ) );
    196        atts.sort( Comparator.comparing( Attachment::getName, m_engine.getManager( PageManager.class ).getPageSorter() ) );
    197
    198        return atts;
    199    }
    200
    201    /** {@inheritDoc} */
    202    @Override
    203    public boolean forceDownload( String name ) {
    204        if( name == null || name.isEmpty() ) {
    205            return false;
    206        }
    207
    208        name = name.toLowerCase();
    209        if( name.indexOf( '.' ) == -1 ) {
    210            return true;  // force download on attachments without extension or type indication
    211        }
    212
    213        for( final String forceDownloadPattern : m_forceDownloadPatterns ) {
    214            if( ( name.endsWith( forceDownloadPattern ) && !forceDownloadPattern.isEmpty() ) || "*".equals( forceDownloadPattern ) ) {
    215                return true;
    216            }
    217        }
    218
    219        return false;
    220    }
    221
    222    /** {@inheritDoc} */
    223    @Override
    224    public InputStream getAttachmentStream( final Context ctx, final Attachment att ) throws ProviderException, IOException {
    225        if( m_provider == null ) {
    226            return null;
    227        }
    228
    229        if( att instanceof DynamicAttachment ) {
    230            return ( ( DynamicAttachment )att ).getProvider().getAttachmentData( ctx, att );
    231        }
    232
    233        return m_provider.getAttachmentData( att );
    234    }
    235
    236    /** {@inheritDoc} */
    237    @Override
    238    public void storeDynamicAttachment( final Context ctx, final DynamicAttachment att ) {
    239        cachingManager.put( CachingManager.CACHE_ATTACHMENTS_DYNAMIC, att.getName(), att );
    240    }
    241
    242    /** {@inheritDoc} */
    243    @Override
    244    public DynamicAttachment getDynamicAttachment( final String name ) {
    245        return cachingManager.get( CachingManager.CACHE_ATTACHMENTS_DYNAMIC, name, () -> null );
    246    }
    247
    248    /** {@inheritDoc} */
    249    @Override
    250    public void storeAttachment( final Attachment att, final InputStream in ) throws IOException, ProviderException {
    251        if( m_provider == null ) {
    252            return;
    253        }
    254
    255        // Checks if the actual, real page exists without any modifications or aliases. We cannot store an attachment to a non-existent page.
    256        if( !m_engine.getManager( PageManager.class ).pageExists( att.getParentName() ) ) {
    257            // the caller should catch the exception and use the exception text as an i18n key
    258            throw new ProviderException( "attach.parent.not.exist" );
    259        }
    260
    261        m_provider.putAttachmentData( att, in );
    262        m_engine.getManager( ReferenceManager.class ).updateReferences( att.getName(), new ArrayList<>() );
    263
    264        final Page parent = Wiki.contents().page( m_engine, att.getParentName() );
    265        m_engine.getManager( ReferenceManager.class ).updateReferences( parent );
    266        m_engine.getManager( SearchManager.class ).reindexPage( att );
    267    }
    268
    269    /** {@inheritDoc} */
    270    @Override
    271    public List< Attachment > getVersionHistory( final String attachmentName ) throws ProviderException {
    272        if( m_provider == null ) {
    273            return null;
    274        }
    275
    276        final Attachment att = getAttachmentInfo( null, attachmentName );
    277        if( att != null ) {
    278            return m_provider.getVersionHistory( att );
    279        }
    280
    281        return null;
    282    }
    283
    284    /** {@inheritDoc} */
    285    @Override
    286    public Collection< Attachment > getAllAttachments() throws ProviderException {
    287        if( attachmentsEnabled() ) {
    288            return m_provider.listAllChanged( new Date( 0L ) );
    289        }
    290
    291        return new ArrayList<>();
    292    }
    293
    294    /** {@inheritDoc} */
    295    @Override
    296    public AttachmentProvider getCurrentProvider() {
    297        return m_provider;
    298    }
    299
    300    /** {@inheritDoc} */
    301    @Override
    302    public void deleteVersion( final Attachment att ) throws ProviderException {
    303        if( m_provider == null ) {
    304            return;
    305        }
    306
    307        m_provider.deleteVersion( att );
    308    }
    309
    310    /** {@inheritDoc} */
    311    @Override
    312    // FIXME: Should also use events!
    313    public void deleteAttachment( final Attachment att ) throws ProviderException {
    314        if( m_provider == null ) {
    315            return;
    316        }
    317
    318        m_provider.deleteAttachment( att );
    319        m_engine.getManager( SearchManager.class ).pageRemoved( att );
    320        m_engine.getManager( ReferenceManager.class ).clearPageEntries( att.getName() );
    321    }
    322
    323}