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