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}