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