blob: e6d47b7eb18c3944cd8fea834a7d56bfb0cbef52 [file] [log] [blame]
/*
JSPWiki - a JSP-based WikiWiki clone.
Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.ecyrd.jspwiki.attachment;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.ecyrd.jspwiki.*;
import com.ecyrd.jspwiki.parser.MarkupParser;
import com.ecyrd.jspwiki.providers.ProviderException;
import com.ecyrd.jspwiki.providers.WikiAttachmentProvider;
import com.ecyrd.jspwiki.util.ClassUtil;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.NeedsRefreshException;
/**
* Provides facilities for handling attachments. All attachment
* handling goes through this class.
* <p>
* The AttachmentManager provides a facade towards the current WikiAttachmentProvider
* that is in use. It is created by the WikiEngine as a singleton object, and
* can be requested through the WikiEngine.
*
* @author Janne Jalkanen
* @since 1.9.28
*/
public class AttachmentManager
{
/**
* The property name for defining the attachment provider class name.
*/
public static final String PROP_PROVIDER = "jspwiki.attachmentProvider";
/**
* The maximum size of attachments that can be uploaded.
*/
public static final String PROP_MAXSIZE = "jspwiki.attachment.maxsize";
/**
* A space-separated list of attachment types which can be uploaded
*/
public static final String PROP_ALLOWEDEXTENSIONS = "jspwiki.attachment.allowed";
/**
* A space-separated list of attachment types which cannot be uploaded
*/
public static final String PROP_FORDBIDDENEXTENSIONS = "jspwiki.attachment.forbidden";
static Logger log = Logger.getLogger( AttachmentManager.class );
private WikiAttachmentProvider m_provider;
private WikiEngine m_engine;
/**
* Creates a new AttachmentManager. Note that creation will never fail,
* but it's quite likely that attachments do not function.
* <p>
* <b>DO NOT CREATE</b> an AttachmentManager on your own, unless you really
* know what you're doing. Just use WikiEngine.getAttachmentManager() if
* you're making a module for JSPWiki.
*
* @param engine The wikiengine that owns this attachment manager.
* @param props A list of properties from which the AttachmentManager will seek
* its configuration. Typically this is the "jspwiki.properties".
*/
// FIXME: Perhaps this should fail somehow.
public AttachmentManager( WikiEngine engine, Properties props )
{
String classname;
m_engine = engine;
//
// If user wants to use a cache, then we'll use the CachingProvider.
//
boolean useCache = "true".equals(props.getProperty( PageManager.PROP_USECACHE ));
if( useCache )
{
classname = "com.ecyrd.jspwiki.providers.CachingAttachmentProvider";
}
else
{
classname = props.getProperty( PROP_PROVIDER );
}
//
// If no class defined, then will just simply fail.
//
if( classname == null )
{
log.info( "No attachment provider defined - disabling attachment support." );
return;
}
//
// Create and initialize the provider.
//
try
{
Class providerclass = ClassUtil.findClass( "com.ecyrd.jspwiki.providers",
classname );
m_provider = (WikiAttachmentProvider)providerclass.newInstance();
m_provider.initialize( m_engine, props );
}
catch( ClassNotFoundException e )
{
log.error( "Attachment provider class not found",e);
}
catch( InstantiationException e )
{
log.error( "Attachment provider could not be created", e );
}
catch( IllegalAccessException e )
{
log.error( "You may not access the attachment provider class", e );
}
catch( NoRequiredPropertyException e )
{
log.error( "Attachment provider did not find a property that it needed: "+e.getMessage(), e );
m_provider = null; // No, it did not work.
}
catch( IOException e )
{
log.error( "Attachment provider reports IO error", e );
m_provider = null;
}
}
/**
* Returns true, if attachments are enabled and running.
*
* @return A boolean value indicating whether attachment functionality is enabled.
*/
public boolean attachmentsEnabled()
{
return m_provider != null;
}
/**
* Gets info on a particular attachment, latest version.
*
* @param name A full attachment name.
* @return Attachment, or null, if no such attachment exists.
* @throws ProviderException If something goes wrong.
*/
public Attachment getAttachmentInfo( String name )
throws ProviderException
{
return getAttachmentInfo( name, WikiProvider.LATEST_VERSION );
}
/**
* Gets info on a particular attachment with the given version.
*
* @param name A full attachment name.
* @param version A version number.
* @return Attachment, or null, if no such attachment or version exists.
* @throws ProviderException If something goes wrong.
*/
public Attachment getAttachmentInfo( String name, int version )
throws ProviderException
{
if( name == null )
{
return null;
}
return getAttachmentInfo( null, name, version );
}
/**
* Figures out the full attachment name from the context and
* attachment name.
*
* @param context The current WikiContext
* @param attachmentname The file name of the attachment.
* @return Attachment, or null, if no such attachment exists.
* @throws ProviderException If something goes wrong.
*/
public Attachment getAttachmentInfo( WikiContext context,
String attachmentname )
throws ProviderException
{
return getAttachmentInfo( context, attachmentname, WikiProvider.LATEST_VERSION );
}
/**
* Figures out the full attachment name from the context and
* attachment name.
*
* @param context The current WikiContext
* @param attachmentname The file name of the attachment.
* @param version A particular version.
* @return Attachment, or null, if no such attachment or version exists.
* @throws ProviderException If something goes wrong.
*/
public Attachment getAttachmentInfo( WikiContext context,
String attachmentname,
int version )
throws ProviderException
{
if( m_provider == null )
{
return null;
}
WikiPage currentPage = null;
if( context != null )
{
currentPage = context.getPage();
}
//
// Figure out the parent page of this attachment. If we can't find it,
// we'll assume this refers directly to the attachment.
//
int cutpt = attachmentname.lastIndexOf('/');
if( cutpt != -1 )
{
String parentPage = attachmentname.substring(0,cutpt);
parentPage = MarkupParser.cleanLink( parentPage );
attachmentname = attachmentname.substring(cutpt+1);
// If we for some reason have an empty parent page name;
// this can't be an attachment
if(parentPage.length() == 0) return null;
currentPage = m_engine.getPage( parentPage );
//
// Go check for legacy name
//
// FIXME: This should be resolved using CommandResolver,
// not this adhoc way. This also assumes that the
// legacy charset is a subset of the full allowed set.
if( currentPage == null )
{
currentPage = m_engine.getPage( MarkupParser.wikifyLink( parentPage ) );
}
}
//
// If the page cannot be determined, we cannot possibly find the
// attachments.
//
if( currentPage == null || currentPage.getName().length() == 0 )
{
return null;
}
// System.out.println("Seeking info on "+currentPage+"::"+attachmentname);
//
// Finally, figure out whether this is a real attachment or a generated
// attachment.
//
Attachment att;
att = getDynamicAttachment( currentPage.getName()+"/"+attachmentname );
if( att == null )
{
att = m_provider.getAttachmentInfo( currentPage, attachmentname, version );
}
return att;
}
/**
* Returns the list of attachments associated with a given wiki page.
* If there are no attachments, returns an empty Collection.
*
* @param wikipage The wiki page from which you are seeking attachments for.
* @return a valid collection of attachments.
* @throws ProviderException If there was something wrong in the backend.
*/
// FIXME: This API should be changed to return a List.
public Collection listAttachments( WikiPage wikipage )
throws ProviderException
{
if( m_provider == null )
{
return new ArrayList();
}
Collection atts = m_provider.listAttachments( wikipage );
//
// This is just a sanity check; all of our providers return a Collection.
//
if( atts instanceof List )
{
Collections.sort( (List) atts );
}
return atts;
}
/**
* Returns true, if the page has any attachments at all. This is
* a convinience method.
*
*
* @param wikipage The wiki page from which you are seeking attachments for.
* @return True, if the page has attachments, else false.
*/
public boolean hasAttachments( WikiPage wikipage )
{
try
{
return listAttachments( wikipage ).size() > 0;
}
catch( Exception e ) {}
return false;
}
/**
* Finds a (real) attachment from the repository as a stream.
*
* @param att Attachment
* @return An InputStream to read from. May return null, if
* attachments are disabled.
* @throws IOException If the stream cannot be opened
* @throws ProviderException If the backend fails due to some other reason.
*/
public InputStream getAttachmentStream( Attachment att )
throws IOException,
ProviderException
{
return getAttachmentStream( null, att );
}
/**
* Returns an attachment stream using the particular WikiContext. This method
* should be used instead of getAttachmentStream(Attachment), since it also allows
* the DynamicAttachments to function.
*
* @param ctx The Wiki Context
* @param att The Attachment to find
* @return An InputStream. May return null, if attachments are disabled. You must
* take care of closing it.
* @throws ProviderException If the backend fails due to some reason
* @throws IOException If the stream cannot be opened
*/
public InputStream getAttachmentStream( WikiContext ctx, Attachment att )
throws ProviderException, IOException
{
if( m_provider == null )
{
return null;
}
if( att instanceof DynamicAttachment )
{
return ((DynamicAttachment)att).getProvider().getAttachmentData( ctx, att );
}
return m_provider.getAttachmentData( att );
}
private Cache m_dynamicAttachments = new Cache( true, false, false );
/**
* Stores a dynamic attachment. Unlike storeAttachment(), this just stores
* the attachment in the memory.
*
* @param ctx A WikiContext
* @param att An attachment to store
*/
public void storeDynamicAttachment( WikiContext ctx, DynamicAttachment att )
{
m_dynamicAttachments.putInCache( att.getName(), att );
}
/**
* Finds a DynamicAttachment. Normally, you should just use getAttachmentInfo(),
* since that will find also DynamicAttachments.
*
* @param name The name of the attachment to look for
* @return An Attachment, or null.
* @see #getAttachmentInfo(String)
*/
public DynamicAttachment getDynamicAttachment( String name )
{
try
{
return (DynamicAttachment) m_dynamicAttachments.getFromCache( name );
}
catch( NeedsRefreshException e )
{
//
// Remove from cache, it has expired.
//
m_dynamicAttachments.putInCache( name, null );
return null;
}
}
/**
* Stores an attachment that lives in the given file.
* If the attachment did not exist previously, this method
* will create it. If it did exist, it stores a new version.
*
* @param att Attachment to store this under.
* @param source A file to read from.
*
* @throws IOException If writing the attachment failed.
* @throws ProviderException If something else went wrong.
*/
public void storeAttachment( Attachment att, File source )
throws IOException,
ProviderException
{
FileInputStream in = null;
try
{
in = new FileInputStream( source );
storeAttachment( att, in );
}
finally
{
if( in != null ) in.close();
}
}
/**
* Stores an attachment directly from a stream.
* If the attachment did not exist previously, this method
* will create it. If it did exist, it stores a new version.
*
* @param att Attachment to store this under.
* @param in InputStream from which the attachment contents will be read.
*
* @throws IOException If writing the attachment failed.
* @throws ProviderException If something else went wrong.
*/
public void storeAttachment( Attachment att, InputStream in )
throws IOException,
ProviderException
{
if( m_provider == null )
{
return;
}
//
// Checks if the actual, real page exists without any modifications
// or aliases. We cannot store an attachment to a non-existant page.
//
if( !m_engine.getPageManager().pageExists( att.getParentName() ) )
{
throw new ProviderException("Parent page does not exist");
}
m_provider.putAttachmentData( att, in );
m_engine.getReferenceManager().updateReferences( att.getName(),
new java.util.Vector() );
WikiPage parent = new WikiPage( m_engine, att.getParentName() );
m_engine.updateReferences( parent );
m_engine.getSearchManager().reindexPage( att );
}
/**
* Returns a list of versions of the attachment.
*
* @param attachmentName A fully qualified name of the attachment.
*
* @return A list of Attachments. May return null, if attachments are
* disabled.
* @throws ProviderException If the provider fails for some reason.
*/
public List getVersionHistory( String attachmentName )
throws ProviderException
{
if( m_provider == null )
{
return null;
}
Attachment att = getAttachmentInfo( (WikiContext)null, attachmentName );
if( att != null )
{
return m_provider.getVersionHistory( att );
}
return null;
}
/**
* Returns a collection of Attachments, containing each and every attachment
* that is in this Wiki.
*
* @return A collection of attachments. If attachments are disabled, will
* return an empty collection.
* @throws ProviderException If something went wrong with the backend
*/
public Collection getAllAttachments()
throws ProviderException
{
if( attachmentsEnabled() )
{
return m_provider.listAllChanged( new Date(0L) );
}
return new ArrayList();
}
/**
* Returns the current attachment provider.
*
* @return The current provider. May be null, if attachments are disabled.
*/
public WikiAttachmentProvider getCurrentProvider()
{
return m_provider;
}
/**
* Deletes the given attachment version.
*
* @param att The attachment to delete
* @throws ProviderException If something goes wrong with the backend.
*/
public void deleteVersion( Attachment att )
throws ProviderException
{
if( m_provider == null ) return;
m_provider.deleteVersion( att );
}
/**
* Deletes all versions of the given attachment.
* @param att The Attachment to delete.
* @throws ProviderException if something goes wrong with the backend.
*/
// FIXME: Should also use events!
public void deleteAttachment( Attachment att )
throws ProviderException
{
if( m_provider == null ) return;
m_provider.deleteAttachment( att );
m_engine.getSearchManager().pageRemoved( att );
m_engine.getReferenceManager().clearPageEntries( att.getName() );
}
/**
* Validates the filename and makes sure it is legal. It trims and splits
* and replaces bad characters.
*
* @param filename
* @return A validated name with annoying characters replaced.
* @throws WikiException If the filename is not legal (e.g. empty)
*/
static String validateFileName( String filename )
throws WikiException
{
if( filename == null || filename.trim().length() == 0 )
{
AttachmentServlet.log.error("Empty file name given.");
throw new WikiException("Empty file name given.");
}
//
// Should help with IE 5.22 on OSX
//
filename = filename.trim();
// If file name ends with .jsp or .jspf, the user is being naughty!
if( filename.toLowerCase().endsWith( ".jsp" ) || filename.toLowerCase().endsWith(".jspf") )
{
log.info( "Attempt to upload a file with a .jsp/.jspf extension. In certain cases this" +
" can trigger unwanted security side effects, so we're preventing it." );
throw new WikiException( "Unwanted file name." );
}
//
// Some browser send the full path info with the filename, so we need
// to remove it here by simply splitting along slashes and then taking the path.
//
String[] splitpath = filename.split( "[/\\\\]" );
filename = splitpath[splitpath.length-1];
//
// Remove any characters that might be a problem. Most
// importantly - characters that might stop processing
// of the URL.
//
filename = StringUtils.replaceChars( filename, "#?\"'", "____" );
return filename;
}
}