You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [10/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/AttachmentServlet.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/AttachmentServlet.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/AttachmentServlet.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/AttachmentServlet.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,837 @@
+/*
+    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.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketException;
+import java.security.Permission;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Properties;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.iamvegan.multipartrequest.HttpServletMultipartRequest;
+import net.iamvegan.multipartrequest.MultipartFile;
+import net.iamvegan.multipartrequest.ProgressListener;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.AttachActionBean;
+import com.ecyrd.jspwiki.action.UploadActionBean;
+import com.ecyrd.jspwiki.auth.AuthorizationManager;
+import com.ecyrd.jspwiki.auth.permissions.PermissionFactory;
+import com.ecyrd.jspwiki.dav.AttachmentDavProvider;
+import com.ecyrd.jspwiki.dav.DavPath;
+import com.ecyrd.jspwiki.dav.DavProvider;
+import com.ecyrd.jspwiki.dav.WebdavServlet;
+import com.ecyrd.jspwiki.dav.methods.DavMethod;
+import com.ecyrd.jspwiki.dav.methods.PropFindMethod;
+import com.ecyrd.jspwiki.filters.RedirectException;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.ui.progress.ProgressItem;
+import com.ecyrd.jspwiki.util.HttpUtil;
+
+
+/**
+ *  This is the chief JSPWiki attachment management servlet.  It is used for
+ *  both uploading new content and downloading old content.  It can handle
+ *  most common cases, e.g. check for modifications and return 304's as necessary.
+ *  <p>
+ *  Authentication is done using JSPWiki's normal AAA framework.
+ *  <p>
+ *  This servlet is also capable of managing dynamically created attachments.
+ *
+ *  @author Erik Bunn
+ *  @author Janne Jalkanen
+ *
+ *  @since 1.9.45.
+ */
+public class AttachmentServlet
+    extends WebdavServlet
+{
+    private static final int BUFFER_SIZE = 8192;
+
+    private static final long serialVersionUID = 3257282552187531320L;
+
+    private WikiEngine m_engine;
+    static Logger log = Logger.getLogger(AttachmentServlet.class.getName());
+
+    private static final String HDR_VERSION     = "version";
+    // private static final String HDR_NAME        = "page";
+
+    /** Default expiry period is 1 day */
+    protected static final long DEFAULT_EXPIRY = 1 * 24 * 60 * 60 * 1000;
+
+    private String m_tmpDir;
+
+    private DavProvider m_attachmentProvider;
+
+    /**
+     *  The maximum size that an attachment can be.
+     */
+    private int   m_maxSize = Integer.MAX_VALUE;
+
+    /**
+     *  List of attachment types which are allowed
+     */
+
+    private String[] m_allowedPatterns;
+
+    private String[] m_forbiddenPatterns;
+
+    //
+    // Not static as DateFormat objects are not thread safe.
+    // Used to handle the RFC date format = Sat, 13 Apr 2002 13:23:01 GMT
+    //
+    //private final DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
+
+    /**
+     *  Initializes the servlet from WikiEngine properties.
+     *   
+     *  {@inheritDoc}
+     */
+    public void init( ServletConfig config )
+        throws ServletException
+    {
+        super.init( config );
+
+        m_engine         = WikiEngine.getInstance( config );
+        Properties props = m_engine.getWikiProperties();
+
+        m_attachmentProvider = new AttachmentDavProvider( m_engine );
+        m_tmpDir         = m_engine.getWorkDir()+File.separator+"attach-tmp";
+
+        m_maxSize        = TextUtil.getIntegerProperty( props,
+                                                        AttachmentManager.PROP_MAXSIZE,
+                                                        Integer.MAX_VALUE );
+
+        String allowed = TextUtil.getStringProperty( props,
+                                                     AttachmentManager.PROP_ALLOWEDEXTENSIONS,
+                                                     null );
+
+        if( allowed != null && allowed.length() > 0 )
+            m_allowedPatterns = allowed.toLowerCase().split("\\s");
+        else
+            m_allowedPatterns = new String[0];
+
+        String forbidden = TextUtil.getStringProperty( props,
+                                                       AttachmentManager.PROP_FORDBIDDENEXTENSIONS,
+                                                       null );
+
+        if( forbidden != null && forbidden.length() > 0 )
+            m_forbiddenPatterns = forbidden.toLowerCase().split("\\s");
+        else
+            m_forbiddenPatterns = new String[0];
+
+        File f = new File( m_tmpDir );
+        if( !f.exists() )
+        {
+            f.mkdirs();
+        }
+        else if( !f.isDirectory() )
+        {
+            log.fatal("A file already exists where the temporary dir is supposed to be: "+m_tmpDir+".  Please remove it.");
+        }
+
+        log.debug( "UploadServlet initialized. Using " +
+                   m_tmpDir + " for temporary storage." );
+    }
+
+    private boolean isTypeAllowed( String name )
+    {
+        if( name == null || name.length() == 0 ) return false;
+
+        name = name.toLowerCase();
+
+        for( int i = 0; i < m_forbiddenPatterns.length; i++ )
+        {
+            if( name.endsWith(m_forbiddenPatterns[i]) && m_forbiddenPatterns[i].length() > 0 )
+                return false;
+        }
+
+        for( int i = 0; i < m_allowedPatterns.length; i++ )
+        {
+            if( name.endsWith(m_allowedPatterns[i]) && m_allowedPatterns[i].length() > 0 )
+                return true;
+        }
+
+        return m_allowedPatterns.length == 0;
+    }
+
+    /**
+     *  Implements the PROPFIND method.
+     *  
+     *  @param req The servlet request
+     *  @param res The servlet response
+     *  @throws IOException If input/output fails
+     *  @throws ServletException If the servlet has issues
+     */
+    public void doPropFind( HttpServletRequest req, HttpServletResponse res )
+        throws IOException, ServletException
+    {
+        DavMethod dm = new PropFindMethod( m_attachmentProvider );
+
+        String p = new String(req.getPathInfo().getBytes("ISO-8859-1"), "UTF-8");
+
+        DavPath path = new DavPath( p );
+
+        dm.execute( req, res, path );
+    }
+    /**
+     *  Implements the OPTIONS method.
+     *  
+     *  @param req The servlet request
+     *  @param res The servlet response
+     */
+
+    protected void doOptions( HttpServletRequest req, HttpServletResponse res )
+    {
+        res.setHeader( "DAV", "1" ); // We support only Class 1
+        res.setHeader( "Allow", "GET, PUT, POST, OPTIONS, PROPFIND, PROPPATCH, MOVE, COPY, DELETE");
+        res.setStatus( HttpServletResponse.SC_OK );
+    }
+
+    /**
+     *  Serves a GET with two parameters: 'wikiname' specifying the wikiname
+     *  of the attachment, 'version' specifying the version indicator.
+     *  
+     *  {@inheritDoc}
+     */
+
+    // FIXME: Messages would need to be localized somehow.
+    public void doGet( HttpServletRequest  req, HttpServletResponse res )
+        throws IOException, ServletException
+    {
+        WikiContext context;
+        try 
+        {
+            context = (WikiContext)m_engine.getWikiActionBeanFactory().newActionBean( req, res, AttachActionBean.class );
+        }
+        catch ( WikiException e )
+        {
+            throw new ServletException( e.getMessage() );
+        }
+
+        String version  = req.getParameter( HDR_VERSION );
+        String nextPage = req.getParameter( "nextpage" );
+
+        String msg      = "An error occurred. Ouch.";
+        int    ver      = WikiProvider.LATEST_VERSION;
+
+        AttachmentManager mgr = m_engine.getAttachmentManager();
+        AuthorizationManager authmgr = m_engine.getAuthorizationManager();
+
+
+        String page = context.getPage().getName();
+
+        if( page == null )
+        {
+            log.info("Invalid attachment name.");
+            res.sendError( HttpServletResponse.SC_BAD_REQUEST );
+            return;
+        }
+
+        OutputStream out = null;
+        InputStream  in  = null;
+
+        try
+        {
+            log.debug("Attempting to download att "+page+", version "+version);
+            if( version != null )
+            {
+                ver = Integer.parseInt( version );
+            }
+
+            Attachment att = mgr.getAttachmentInfo( page, ver );
+
+            if( att != null )
+            {
+                //
+                //  Check if the user has permission for this attachment
+                //
+
+                Permission permission = PermissionFactory.getPagePermission( att, "view" );
+                if( !authmgr.checkPermission( context.getWikiSession(), permission ) )
+                {
+                    log.debug("User does not have permission for this");
+                    res.sendError( HttpServletResponse.SC_FORBIDDEN );
+                    return;
+                }
+
+
+                //
+                //  Check if the client already has a version of this attachment.
+                //
+                if( HttpUtil.checkFor304( req, att ) )
+                {
+                    log.debug("Client has latest version already, sending 304...");
+                    res.sendError( HttpServletResponse.SC_NOT_MODIFIED );
+                    return;
+                }
+
+                String mimetype = getMimeType( context, att.getFileName() );
+
+                res.setContentType( mimetype );
+
+                //
+                //  We use 'inline' instead of 'attachment' so that user agents
+                //  can try to automatically open the file.
+                //
+
+                res.addHeader( "Content-Disposition",
+                               "inline; filename=\"" + att.getFileName() + "\";" );
+
+                res.addDateHeader("Last-Modified",att.getLastModified().getTime());
+
+                if( !att.isCacheable() )
+                {
+                    res.addHeader( "Pragma", "no-cache" );
+                    res.addHeader( "Cache-control", "no-cache" );
+                }
+
+                // If a size is provided by the provider, report it.
+                if( att.getSize() >= 0 )
+                {
+                    // log.info("size:"+att.getSize());
+                    res.setContentLength( (int)att.getSize() );
+                }
+
+                out = res.getOutputStream();
+                in  = mgr.getAttachmentStream( context, att );
+
+                int read = 0;
+                byte[] buffer = new byte[BUFFER_SIZE];
+
+                while( (read = in.read( buffer )) > -1 )
+                {
+                    out.write( buffer, 0, read );
+                }
+
+                if(log.isDebugEnabled())
+                {
+                    msg = "Attachment "+att.getFileName()+" sent to "+req.getRemoteUser()+" on "+req.getRemoteAddr();
+                    log.debug( msg );
+                }
+                if( nextPage != null ) res.sendRedirect( nextPage );
+
+                return;
+            }
+
+            msg = "Attachment '" + page + "', version " + ver +
+                  " does not exist.";
+
+            log.info( msg );
+            res.sendError( HttpServletResponse.SC_NOT_FOUND,
+                           msg );
+            return;
+        }
+        catch( ProviderException pe )
+        {
+            msg = "Provider error: "+pe.getMessage();
+            
+            log.debug("Provider failed while reading", pe);
+            //
+            //  This might fail, if the response is already committed.  So in that
+            //  case we just log it.
+            //
+            try
+            {
+                res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                               msg );
+            }
+            catch( IllegalStateException e ) {}
+            return;
+        }
+        catch( NumberFormatException nfe )
+        {
+            msg = "Invalid version number (" + version + ")";
+            res.sendError( HttpServletResponse.SC_BAD_REQUEST,
+                           msg );
+            return;
+        }
+        catch( SocketException se )
+        {
+            //
+            //  These are very common in download situations due to aggressive
+            //  clients.  No need to try and send an error.
+            //
+            log.debug("I/O exception during download",se);
+            return;
+        }
+        catch( IOException ioe )
+        {
+            //
+            //  Client dropped the connection or something else happened.
+            //  We don't know where the error came from, so we'll at least
+            //  try to send an error and catch it quietly if it doesn't quite work.
+            //
+            msg = "Error: " + ioe.getMessage();
+            log.debug("I/O exception during download",ioe);
+            
+            try
+            {
+                res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                               msg );
+            }
+            catch( IllegalStateException e ) {}
+            return;
+        }
+        finally
+        {
+            if( in != null )
+            {
+                try
+                {
+                    in.close();
+                }
+                catch( IOException e ) {}
+            }
+
+            //
+            //  Quite often, aggressive clients close the connection when they have
+            //  received the last bits.  Therefore, we close the output, but ignore
+            //  any exception that might come out of it.
+            //
+
+            if( out != null )
+            {
+                try
+                {
+                    out.close();
+                }
+                catch( IOException e ) {}
+            }
+        }
+    }
+
+    /**
+     *  Returns the mime type for this particular file.  Case does not matter.
+     *
+     * @param ctx WikiContext; required to access the ServletContext of the request.
+     * @param fileName The name to check for.
+     * @return A valid mime type, or application/binary, if not recognized
+     */
+    private static String getMimeType(WikiContext ctx, String fileName )
+    {
+        String mimetype = null;
+
+        HttpServletRequest req = ctx.getHttpRequest();
+        if( req != null )
+        {
+            ServletContext s = req.getSession().getServletContext();
+
+            if( s != null )
+            {
+                mimetype = s.getMimeType( fileName.toLowerCase() );
+            }
+        }
+
+        if( mimetype == null )
+        {
+            mimetype = "application/binary";
+        }
+
+        return mimetype;
+    }
+
+
+    /**
+     * Grabs mime/multipart data and stores it into the temporary area.
+     * Uses other parameters to determine which name to store as.
+     *
+     * <p>The input to this servlet is generated by an HTML FORM with
+     * two parts. The first, named 'page', is the WikiName identifier
+     * for the parent file. The second, named 'content', is the binary
+     * content of the file.
+     * 
+     * {@inheritDoc}
+     */
+    public void doPost( HttpServletRequest  req, HttpServletResponse res )
+        throws IOException, ServletException
+    {
+        try
+        {
+            String nextPage = upload( req, res );
+            req.getSession().removeAttribute("msg");
+            res.sendRedirect( nextPage );
+        }
+        catch( RedirectException e )
+        {
+            WikiSession session = WikiSession.getWikiSession( m_engine, req );
+            session.addMessage( e.getMessage() );
+
+            req.getSession().setAttribute("msg", e.getMessage());
+            res.sendRedirect( e.getRedirect() );
+        }
+    }
+
+    /**
+     *  {@inheritDoc}
+     */
+    public void doPut( HttpServletRequest req, HttpServletResponse res )
+        throws IOException, ServletException
+    {
+        String p = new String(req.getPathInfo().getBytes("ISO-8859-1"), "UTF-8");
+        DavPath path = new DavPath( p );
+
+        try
+        {
+            InputStream data = req.getInputStream();
+
+            WikiContext context;
+            String errorPage; // If something bad happened, Upload should be able to take care of most stuff
+
+            try 
+            {
+                context = (WikiContext)m_engine.getWikiActionBeanFactory().newActionBean( req, res, UploadActionBean .class );
+            }
+            catch ( WikiException e )
+            {
+                throw new ServletException( e.getMessage() );
+            }
+
+            String wikipage = path.get( 0 );
+
+            errorPage = context.getContext().getURL( UploadActionBean.class,
+                                        wikipage );
+
+            String changeNote = null; // FIXME: Does not quite work
+
+            boolean created = executeUpload( context, data,
+                                             path.getName(),
+                                             errorPage, wikipage,
+                                             changeNote,
+                                             req.getContentLength() );
+
+            if( created )
+                res.sendError( HttpServletResponse.SC_CREATED );
+            else
+                res.sendError( HttpServletResponse.SC_OK );
+        }
+        catch( ProviderException e )
+        {
+            res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                           e.getMessage() );
+        }
+        catch( RedirectException e )
+        {
+            res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                           e.getMessage() );
+        }
+    }
+
+    /**
+     *  Validates the next page to be on the same server as this webapp.
+     *  Fixes [JSPWIKI-46].
+     */
+    
+    private String validateNextPage( String nextPage, String errorPage )
+    {
+         if( nextPage.indexOf("://") != -1 )
+         {
+             // It's an absolute link, so unless it starts with our address, we'll
+             // log an error.
+             
+             if( !nextPage.startsWith( m_engine.getBaseURL() ) )
+             {
+                 log.warn("Detected phishing attempt by redirecting to an unsecure location: "+nextPage);
+                 nextPage = errorPage;
+             }
+         }
+         
+         return nextPage;
+    }
+    
+    /**
+     *  Uploads a specific mime multipart input set, intercepts exceptions.
+     *
+     *  @param req The servlet request
+     *  @return The page to which we should go next.
+     *  @throws RedirectException If there's an error and a redirection is needed
+     *  @throws IOException If upload fails
+     */
+    protected String upload( HttpServletRequest req, HttpServletResponse res )
+        throws RedirectException,
+               IOException
+    {
+        String msg     = "";
+        String attName = "(unknown)";
+        String nextPage;
+
+        String progressId = req.getParameter( "progressid" );
+
+        try
+        {
+            HttpServletMultipartRequest multi;
+
+            // Create the context _before_ Multipart operations, otherwise
+            // strict servlet containers may fail when setting encoding.
+            WikiContext context;
+            try 
+            {
+                context = (WikiContext)m_engine.getWikiActionBeanFactory().newActionBean( req, res, AttachActionBean.class );
+            }
+            catch ( WikiException e )
+            {
+                throw new IOException( e.getMessage() );
+            }
+
+            UploadListener pl = new UploadListener();
+
+            m_engine.getProgressManager().startProgress( pl, progressId );
+
+            multi = new HttpServletMultipartRequest( req,
+                                                     Long.MAX_VALUE,
+                                                     HttpServletMultipartRequest.SAVE_TO_TMPDIR,
+                                                     HttpServletMultipartRequest.ABORT_ON_MAX_LENGTH,
+                                                     "UTF-8",
+                                                     pl );
+
+            String errorPage = context.getContext().getURL( UploadActionBean.class, context.getPage().getName() );
+            nextPage        = validateNextPage( multi.getParameter( "nextpage" ), errorPage );
+            String wikipage = multi.getParameter( "page" );
+            String changeNote = multi.getParameter( "changenote" );
+
+            //
+            // FIXME: Kludge alert.  We must end up with the parent page name,
+            //        if this is an upload of a new revision
+            //
+
+            int x = wikipage.indexOf("/");
+
+            if( x != -1 ) wikipage = wikipage.substring(0,x);
+
+            //
+            //  Go through all files being uploaded.
+            //
+            Enumeration files = multi.getFileParameterNames();
+            long fileSize = 0L;
+            while( files.hasMoreElements() )
+            {
+                String part = (String) files.nextElement();
+                MultipartFile multiFile = multi.getFileParameter(part);
+                fileSize += multiFile.getSize();
+                InputStream in = multiFile.getInputStream();
+
+                String filename = multiFile.getName();
+
+                executeUpload( context, in, filename, nextPage, wikipage, changeNote, fileSize );
+            }
+
+            // Inform the JSP page of which file we are handling:
+            // req.setAttribute( ATTR_ATTACHMENT, wikiname );
+        }
+        catch( ProviderException e )
+        {
+            msg = "Upload failed because the provider failed: "+e.getMessage();
+            log.warn( msg + " (attachment: " + attName + ")", e );
+
+            throw new IOException(msg);
+        }
+        catch( IOException e )
+        {
+            // Show the submit page again, but with a bit more
+            // intimidating output.
+            msg = "Upload failure: " + e.getMessage();
+            log.warn( msg + " (attachment: " + attName + ")", e );
+
+            throw e;
+        }
+        finally
+        {
+            m_engine.getProgressManager().stopProgress( progressId );
+            // FIXME: In case of exceptions should absolutely
+            //        remove the uploaded file.
+        }
+
+        return nextPage;
+    }
+
+
+    /**
+     *
+     * @param context the wiki context
+     * @param data the input stream data
+     * @param filename the name of the file to upload
+     * @param errorPage the place to which you want to get a redirection
+     * @param parentPage the page to which the file should be attached
+     * @param changenote The change note
+     * @param contentLength The content length
+     * @return <code>true</code> if upload results in the creation of a new page;
+     * <code>false</code> otherwise
+     * @throws RedirectException If the content needs to be redirected
+     * @throws IOException       If there is a problem in the upload.
+     * @throws ProviderException If there is a problem in the backend.
+     */
+    protected boolean executeUpload( WikiContext context, InputStream data,
+                                     String filename, String errorPage,
+                                     String parentPage, String changenote,
+                                     long contentLength )
+        throws RedirectException,
+               IOException, ProviderException
+    {
+        boolean created = false;
+
+        //
+        //  FIXME: This has the unfortunate side effect that it will receive the
+        //  contents.  But we can't figure out the page to redirect to
+        //  before we receive the file, due to the stupid constructor of MultipartRequest.
+        //
+
+        if( !context.hasAdminPermissions() )
+        {
+            if( contentLength > m_maxSize )
+            {
+                // FIXME: Does not delete the received files.
+                throw new RedirectException( "File exceeds maximum size ("+m_maxSize+" bytes)",
+                                             errorPage );
+            }
+
+            if( !isTypeAllowed(filename) )
+            {
+                throw new RedirectException( "Files of this type may not be uploaded to this wiki",
+                                             errorPage );
+            }
+        }
+
+        Principal user    = context.getCurrentUser();
+
+        AttachmentManager mgr = m_engine.getAttachmentManager();
+
+        if( filename == null || filename.trim().length() == 0 )
+        {
+            log.error("Empty file name given.");
+
+            throw new RedirectException("Empty file name given.",
+                                        errorPage);
+        }
+
+        //
+        //  Should help with IE 5.22 on OSX
+        //
+        filename = filename.trim();
+
+        //
+        //  Remove any characters that might be a problem. Most
+        //  importantly - characters that might stop processing
+        //  of the URL.
+        //
+        filename = StringUtils.replaceChars( filename, "#?\"'", "____" );
+
+        log.debug("file="+filename);
+
+        if( data == null )
+        {
+            log.error("File could not be opened.");
+
+            throw new RedirectException("File could not be opened.",
+                                        errorPage);
+        }
+
+        //
+        //  Check whether we already have this kind of a page.
+        //  If the "page" parameter already defines an attachment
+        //  name for an update, then we just use that file.
+        //  Otherwise we create a new attachment, and use the
+        //  filename given.  Incidentally, this will also mean
+        //  that if the user uploads a file with the exact
+        //  same name than some other previous attachment,
+        //  then that attachment gains a new version.
+        //
+
+        Attachment att = mgr.getAttachmentInfo( context.getPage().getName() );
+
+        if( att == null )
+        {
+            att = new Attachment( m_engine, parentPage, filename );
+            created = true;
+        }
+        att.setSize( contentLength );
+
+        //
+        //  Check if we're allowed to do this?
+        //
+
+        Permission permission = PermissionFactory.getPagePermission( att, "upload" );
+        if( m_engine.getAuthorizationManager().checkPermission( context.getWikiSession(),
+                                                                permission ) )
+        {
+            if( user != null )
+            {
+                att.setAuthor( user.getName() );
+            }
+
+            if( changenote != null && changenote.length() > 0 )
+            {
+                att.setAttribute( WikiPage.CHANGENOTE, changenote );
+            }
+
+            m_engine.getAttachmentManager().storeAttachment( att, data );
+
+            log.info( "User " + user + " uploaded attachment to " + parentPage +
+                      " called "+filename+", size " + att.getSize() );
+        }
+        else
+        {
+            throw new RedirectException("No permission to upload a file",
+                                        errorPage);
+        }
+
+        return created;
+    }
+
+    /**
+     *  Provides tracking for upload progress.
+     *  
+     *  @author Janne Jalkanen
+     */
+    private class UploadListener
+       extends    ProgressItem
+       implements ProgressListener
+    {
+        public long m_currentBytes;
+        public long m_totalBytes;
+        public String m_uid;
+
+        public void update(long recvdBytes, long totalBytes, int item)
+        {
+            m_currentBytes = recvdBytes;
+            m_totalBytes   = totalBytes;
+        }
+
+        public int getProgress()
+        {
+            return (int) (((float)m_currentBytes / m_totalBytes) * 100 + 0.5);
+        }
+    }
+
+}
+
+

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachment.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachment.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachment.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachment.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,104 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 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 com.ecyrd.jspwiki.WikiEngine;
+
+/**
+ *  A DynamicAttachment is an attachment which does not really exist, but is
+ *  created dynamically by a JSPWiki component.
+ *  <p>
+ *  Note that a DynamicAttachment might not be available before it is actually
+ *  created by a component (e.g. plugin), and therefore trying to access it
+ *  before that component has been invoked, might result in a surprising 404.
+ *  <p>
+ *  DynamicAttachments are not listed among regular attachments in the current
+ *  version.
+ *  <p>
+ *  Usage:
+ *
+ *  <pre>
+ *
+ *  class MyDynamicComponent implements DynamicAttachmentProvider
+ *  {
+ *  ...
+ *
+ *     DynamicAttachment destatt = mgr.getDynamicAttachment( destattname );
+ *
+ *     if( destatt == null )
+ *     {
+ *         destatt = new DynamicAttachment( context.getEngine(),
+ *                                          context.getPage().getName(),
+ *                                          destfilename,
+ *                                          this );
+ *         destatt.setCacheable( false );
+ *     }
+ *
+ *     // This is used to check whether the attachment is modified or not
+ *     // so don't forget to update this if your attachment source changes!
+ *     // Else JSPWiki will be serving 304s to anyone who asks...
+ *
+ *     destatt.setLastModified( context.getPage().getLastModified() );
+ *     mgr.storeDynamicAttachment( context,  destatt );
+ *  ...
+ *
+ *      public InputStream getAttachmentData( WikiContext context, Attachment att )
+ *          throws IOException
+ *      {
+ *          byte[] bytes = "This is a test".getBytes();
+ *
+ *          return new ByteArrayInputStream( bytes );
+ *      }
+ *  </pre>
+ *
+ *  @author Janne Jalkanen
+ *  @since 2.5.34
+ */
+public class DynamicAttachment extends Attachment
+{
+    private DynamicAttachmentProvider m_provider  = null;
+
+    /**
+     *  Creates a DynamicAttachment.
+     *
+     *  @param engine  The engine which owns this attachment
+     *  @param parentPage The page which owns this attachment
+     *  @param fileName The filename of the attachment
+     *  @param provider The provider which will be used to generate the attachment.
+     */
+    public DynamicAttachment(WikiEngine engine,
+                             String parentPage,
+                             String fileName,
+                             DynamicAttachmentProvider provider)
+    {
+        super(engine, parentPage, fileName);
+        m_provider = provider;
+    }
+
+    /**
+     *  Returns the provider which is used to generate this attachment.
+     *
+     *  @return A Provider component for this attachment.
+     */
+    public DynamicAttachmentProvider getProvider()
+    {
+        return m_provider;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachmentProvider.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachmentProvider.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachmentProvider.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/DynamicAttachmentProvider.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,53 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 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.IOException;
+import java.io.InputStream;
+
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.providers.ProviderException;
+
+/**
+ *  Provides the data for an attachment.  Please note that there will
+ *  be a strong reference retained for the provider for each Attachment
+ *  it provides, so do try to keep the object light.  Also, reuse objects
+ *  if possible.
+ *  <p>
+ *  The Provider needs to be thread-safe.
+ *
+ *  @author Janne Jalkanen
+ *  @since  2.5.34
+ */
+public interface DynamicAttachmentProvider
+{
+    /**
+     *  Returns a stream of data for this attachment.  The stream will be
+     *  closed by AttachmentServlet.
+     *
+     *  @param context A Wiki Context
+     *  @param att The Attachment for which the data should be received.
+     *  @return InputStream for the data.
+     *  @throws ProviderException If something goes wrong internally
+     *  @throws IOException If something goes wrong when reading the data
+     */
+    public InputStream getAttachmentData( WikiContext context, Attachment att )
+        throws ProviderException, IOException;
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/package.html
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/package.html?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/package.html (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/attachment/package.html Tue Feb 12 21:53:55 2008
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Insert title here</title>
+</head>
+<body>
+
+<p>Attachment management, uploading and downloading.</p>
+
+<h3>Package Specification</h3>
+
+<p>This package manages the JSPWiki attachment subsystem.  It consists of three basic components:</p>
+
+<ul>
+<li>AttachmentServlet - the servlet which allows both uploading and downloading servlets.</li>
+<li>AttachmentManager - The JSPWiki Manager component which manages the storage of attachments.</li>
+<li>Attachment - A special kind of a {@link com.ecyrd.jspwiki.WikiPage} which stores a handle
+   to the attachment data.</li>
+</ul>
+
+<p>Attachments can either be static (i.e. real data, stored somewhere on a filesystem), or <i>dynamic</i>,
+which means that they're generated on the fly by a {@link com.ecyrd.jspwiki.attachment.DynamicAttachmentProvider}.</p>
+
+
+<h3>Related Documentation</h3>
+
+</body>
+</html>
\ No newline at end of file

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthenticationManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthenticationManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthenticationManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthenticationManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,614 @@
+/*
+ * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 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.auth;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.AccessController;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.util.Properties;
+
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.AccountExpiredException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.CredentialExpiredException;
+import javax.security.auth.login.FailedLoginException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.TextUtil;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiException;
+import com.ecyrd.jspwiki.WikiSession;
+import com.ecyrd.jspwiki.auth.authorize.Role;
+import com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer;
+import com.ecyrd.jspwiki.auth.login.CookieAssertionLoginModule;
+import com.ecyrd.jspwiki.auth.login.CookieAuthenticationLoginModule;
+import com.ecyrd.jspwiki.auth.login.WebContainerCallbackHandler;
+import com.ecyrd.jspwiki.auth.login.WikiCallbackHandler;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventManager;
+import com.ecyrd.jspwiki.event.WikiSecurityEvent;
+
+/**
+ * Manages authentication activities for a WikiEngine: user login, logout, and
+ * credential refreshes. This class uses JAAS to determine how users log in.
+ * @author Andrew Jaquith
+ * @author Janne Jalkanen
+ * @author Erik Bunn
+ * @since 2.3
+ */
+public final class AuthenticationManager
+{
+
+    /** The name of the built-in cookie assertion module */
+    public static final String                 COOKIE_MODULE       =  CookieAssertionLoginModule.class.getName();
+
+    /** The name of the built-in cookie authentication module */
+    public static final String                 COOKIE_AUTHENTICATION_MODULE =  CookieAuthenticationLoginModule.class.getName();
+
+    /** The JAAS application name for the web container authentication stack. */
+    public static final String                 LOGIN_CONTAINER     = "JSPWiki-container";
+
+    /** The JAAS application name for the JSPWiki custom authentication stack. */
+    public static final String                 LOGIN_CUSTOM        = "JSPWiki-custom";
+
+    /** If this jspwiki.properties property is <code>true</code>, logs the IP address of the editor on saving. */
+    public static final String                 PROP_STOREIPADDRESS = "jspwiki.storeIPAddress";
+
+    protected static final Logger              log                 = Logger.getLogger( AuthenticationManager.class );
+
+    /** Was JAAS login config already set before we startd up? */
+    protected boolean m_isJaasConfiguredAtStartup = false;
+
+    /** Static Boolean for lazily-initializing the "allows assertions" flag */
+    private static Boolean                     c_allowsAssertions  = null;
+
+    /** Static Boolean for lazily-initializing the "allows cookie authentication" flag */
+    private static Boolean                     c_allowsAuthentication = null;
+
+    private WikiEngine                         m_engine            = null;
+
+    /** If true, logs the IP address of the editor */
+    private boolean                            m_storeIPAddress    = true;
+
+    /** Value specifying that the user wants to use the container-managed security, just like
+     *  in JSPWiki 2.2.
+     */
+    public static final String                SECURITY_OFF      = "off";
+
+    /** Just to provide compatibility with the old versions.  The same
+     *  as SECURITY_OFF.
+     *
+     *  @deprecated
+     */
+    protected static final String             SECURITY_CONTAINER = "container";
+
+    /** Value specifying that the user wants to use the built-in JAAS-based system */
+    public static final String                SECURITY_JAAS     = "jaas";
+
+    /**
+     *  This property determines whether we use JSPWiki authentication or not.
+     *  Possible values are AUTH_JAAS or AUTH_CONTAINER.
+     *
+     */
+
+    public  static final String                PROP_SECURITY       = "jspwiki.security";
+    private static final String                PROP_JAAS_CONFIG    = "java.security.auth.login.config";
+    private static final String                DEFAULT_JAAS_CONFIG = "jspwiki.jaas";
+
+    private static       boolean               c_useJAAS = true;
+
+    /**
+     * Creates an AuthenticationManager instance for the given WikiEngine and
+     * the specified set of properties. All initialization for the modules is
+     * done here.
+     * @param engine the wiki engine
+     * @param props the properties used to initialize the wiki engine
+     * @throws WikiException if the AuthenticationManager cannot be initialized
+     */
+    public final void initialize( WikiEngine engine, Properties props ) throws WikiException
+    {
+        m_engine = engine;
+        m_storeIPAddress = TextUtil.getBooleanProperty( props, PROP_STOREIPADDRESS, m_storeIPAddress );
+        m_isJaasConfiguredAtStartup = PolicyLoader.isJaasConfigured();
+
+        // Yes, writing to a static field is done here on purpose.
+        c_useJAAS = SECURITY_JAAS.equals(props.getProperty( PROP_SECURITY, SECURITY_JAAS ));
+
+        if( !c_useJAAS ) return;
+
+        //
+        //  The rest is JAAS implementation
+        //
+
+        log.info( "Checking JAAS configuration..." );
+
+        if (! m_isJaasConfiguredAtStartup )
+        {
+            URL config = findConfigFile( engine, DEFAULT_JAAS_CONFIG );
+            log.info("JAAS not configured. Installing default configuration: " + config
+                + ". You can set the "+PROP_JAAS_CONFIG+" system property to point to your "
+                + "jspwiki.jaas file, or add the entries from jspwiki.jaas to your own "
+                + "JAAS configuration file.");
+            try
+            {
+                PolicyLoader.setJaasConfiguration( config );
+            }
+            catch ( SecurityException e)
+            {
+                log.error("Could not configure JAAS: " + e.getMessage());
+            }
+        }
+        else
+        {
+            log.info("JAAS already configured by some other application (leaving it alone...)");
+        }
+    }
+
+    /**
+     * Returns true if this WikiEngine uses container-managed authentication.
+     * This method is used primarily for cosmetic purposes in the JSP tier, and
+     * performs no meaningful security function per se. Delegates to
+     * {@link com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer#isContainerAuthorized()},
+     * if used as the external authorizer; otherwise, returns <code>false</code>.
+     * @return <code>true</code> if the wiki's authentication is managed by
+     *         the container, <code>false</code> otherwise
+     */
+    public final boolean isContainerAuthenticated()
+    {
+        if( !c_useJAAS ) return true;
+
+        try
+        {
+            Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
+            if ( authorizer instanceof WebContainerAuthorizer )
+            {
+                 return ( ( WebContainerAuthorizer )authorizer ).isContainerAuthorized();
+            }
+        }
+        catch ( WikiException e )
+        {
+            // It's probably ok to fail silently...
+        }
+        return false;
+    }
+
+    /**
+     * <p>Logs in the user by attempting to populate a WikiSession Subject from
+     * a web servlet request. This method leverages container-managed authentication.
+     * This method logs in the user if the user's status is "unknown" to the
+     * WikiSession, or if the Http servlet container's authentication status has
+     * changed. This method assumes that the HttpServletRequest is not null; otherwise,
+     * an IllegalStateException is thrown. This method is a <em>privileged</em> action;
+     * the caller must posess the (name here) permission.</p>
+     * <p>If <code>request</code> is <code>null</code>, or the WikiSession
+     * cannot be located for this request, this method throws an {@link IllegalStateException}.</p>
+     *             methods return null
+     * @param request servlet request for this user
+     * @return the result of the login operation: <code>true</code> if the user logged in
+     * successfully; <code>false</code> otherwise
+     * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
+     * @since 2.3
+     */
+    public final boolean login( HttpServletRequest request ) throws WikiSecurityException
+    {
+        if ( request == null )
+        {
+            throw new IllegalStateException( "Wiki context's HttpRequest may not be null" );
+        }
+
+        WikiSession wikiSession = WikiSession.getWikiSession( m_engine, request );
+        if ( wikiSession == null )
+        {
+            throw new IllegalStateException( "Wiki context's WikiSession may not be null" );
+        }
+
+        // If using JAAS, try to log in; otherwise logins "always" succeed
+        boolean login = true;
+        if( c_useJAAS )
+        {
+            AuthorizationManager authMgr = m_engine.getAuthorizationManager();
+            CallbackHandler handler = new WebContainerCallbackHandler(
+                    m_engine,
+                    request,
+                    authMgr.getAuthorizer() );
+            login = doLogin( wikiSession, handler, LOGIN_CONTAINER );
+        }
+        return login;
+    }
+
+    /**
+     * Attempts to perform a WikiSession login for the given username/password
+     * combination. This is custom authentication.
+     * @param session the current wiki session; may not be null.
+     * @param username The user name. This is a login name, not a WikiName. In
+     *            most cases they are the same, but in some cases, they might
+     *            not be.
+     * @param password The password
+     * @return true, if the username/password is valid
+     * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
+     */
+    public final boolean login( WikiSession session, String username, String password ) throws WikiSecurityException
+    {
+        if ( session == null )
+        {
+            log.error( "No wiki session provided, cannot log in." );
+            return false;
+        }
+
+        UserManager userMgr = m_engine.getUserManager();
+        CallbackHandler handler = new WikiCallbackHandler(
+                userMgr.getUserDatabase(),
+                username,
+                password );
+        return doLogin( session, handler, LOGIN_CUSTOM );
+    }
+
+    /**
+     * Logs the user out by retrieving the WikiSession associated with the
+     * HttpServletRequest and unbinding all of the Subject's Principals,
+     * except for {@link Role#ALL}, {@link Role#ANONYMOUS}.
+     * is a cheap-and-cheerful way to do it without invoking JAAS LoginModules.
+     * The logout operation will also flush the JSESSIONID cookie from
+     * the user's browser session, if it was set.
+     * @param request the current HTTP request
+     */
+    public final void logout( HttpServletRequest request )
+    {
+        if( request == null )
+        {
+            log.error( "No HTTP reqest provided; cannot log out." );
+            return;
+        }
+
+        HttpSession session = request.getSession();
+        String sid = ( session == null ) ? "(null)" : session.getId();
+        if( log.isDebugEnabled() )
+        {
+            log.debug( "Invalidating WikiSession for session ID=" + sid );
+        }
+        // Retrieve the associated WikiSession and clear the Principal set
+        WikiSession wikiSession = WikiSession.getWikiSession( m_engine, request );
+        Principal originalPrincipal = wikiSession.getLoginPrincipal();
+        wikiSession.invalidate();
+
+        // Remove the wikiSession from the WikiSession cache
+        WikiSession.removeWikiSession( m_engine, request );
+
+        // We need to flush the HTTP session too
+        if ( session != null )
+        {
+            session.invalidate();
+        }
+
+        // Log the event
+        fireEvent( WikiSecurityEvent.LOGOUT, originalPrincipal, null );
+    }
+
+    /**
+     * Determines whether this WikiEngine allows users to assert identities using
+     * cookies instead of passwords. This is determined by inspecting
+     * the LoginConfiguration for application <code>JSPWiki-container</code>.
+     * @return <code>true</code> if cookies are allowed
+     */
+    public static final boolean allowsCookieAssertions()
+    {
+        if( !c_useJAAS ) return true;
+
+        // Lazily initialize
+        if( c_allowsAssertions == null )
+        {
+            c_allowsAssertions = Boolean.FALSE;
+
+            // Figure out whether cookie assertions are allowed
+            Configuration loginConfig = (Configuration)AccessController.doPrivileged(new PrivilegedAction()
+              {
+                  public Object run()
+                  {
+                      return Configuration.getConfiguration();
+                  }
+              });
+
+            if (loginConfig != null)
+            {
+                AppConfigurationEntry[] configs = loginConfig.getAppConfigurationEntry( LOGIN_CONTAINER );
+                if( configs != null )
+                {
+                    for ( int i = 0; i < configs.length; i++ )
+                    {
+                        AppConfigurationEntry config = configs[i];
+                        if ( COOKIE_MODULE.equals( config.getLoginModuleName() ) )
+                        {
+                            c_allowsAssertions = Boolean.TRUE;
+                        }
+                    }
+                }
+            }
+        }
+
+        return c_allowsAssertions.booleanValue();
+    }
+
+    /**
+     *  Determines whether this WikiEngine allows users to authenticate using
+     *  cookies instead of passwords. This is determined by inspecting
+     *  the LoginConfiguration for application <code>JSPWiki-container</code>.
+     *  @return <code>true</code> if cookies are allowed for authentication
+     *  @since 2.5.62
+     */
+    public static final boolean allowsCookieAuthentication()
+    {
+        if( !c_useJAAS ) return true;
+
+        // Lazily initialize
+        if( c_allowsAuthentication == null )
+        {
+            c_allowsAuthentication = Boolean.FALSE;
+
+            // Figure out whether cookie assertions are allowed
+            Configuration loginConfig = (Configuration)AccessController.doPrivileged(new PrivilegedAction()
+              {
+                  public Object run()
+                  {
+                      return Configuration.getConfiguration();
+                  }
+              });
+
+            if (loginConfig != null)
+            {
+                AppConfigurationEntry[] configs = loginConfig.getAppConfigurationEntry( LOGIN_CONTAINER );
+
+                if( configs != null )
+                {
+                    for ( int i = 0; i < configs.length; i++ )
+                    {
+                        AppConfigurationEntry config = configs[i];
+                        if ( COOKIE_AUTHENTICATION_MODULE.equals( config.getLoginModuleName() ) )
+                        {
+                            c_allowsAuthentication = Boolean.TRUE;
+                        }
+                    }
+                }
+            }
+        }
+
+        return c_allowsAuthentication.booleanValue();
+    }
+    /**
+     * Determines whether the supplied Principal is a "role principal".
+     * @param principal the principal to test
+     * @return <code>true</code> if the Principal is of type
+     *         {@link GroupPrincipal} or
+     *         {@link com.ecyrd.jspwiki.auth.authorize.Role},
+     *         <code>false</code> otherwise
+     */
+    public static final boolean isRolePrincipal( Principal principal )
+    {
+        return principal instanceof Role || principal instanceof GroupPrincipal;
+    }
+
+    /**
+     * Determines whether the supplied Principal is a "user principal".
+     * @param principal the principal to test
+     * @return <code>false</code> if the Principal is of type
+     *         {@link GroupPrincipal} or
+     *         {@link com.ecyrd.jspwiki.auth.authorize.Role},
+     *         <code>true</code> otherwise
+     */
+    public static final boolean isUserPrincipal( Principal principal )
+    {
+        return !isRolePrincipal( principal );
+    }
+
+    /**
+     * Log in to the application using a given JAAS LoginConfiguration. Any
+     * configuration error
+     * @param wikiSession the current wiki session, to which the Subject will be associated
+     * @param handler handles callbacks sent by the LoginModules in the configuration
+     * @param application the name of the application whose LoginConfiguration should be used
+     * @return the result of the login
+     * @throws WikiSecurityException
+     */
+    private final boolean doLogin( final WikiSession wikiSession, final CallbackHandler handler, final String application ) throws WikiSecurityException
+    {
+        try
+        {
+            LoginContext loginContext  = (LoginContext)AccessController.doPrivileged(new PrivilegedAction()
+            {
+                public Object run()
+                {
+                    try
+                    {
+                        return wikiSession.getLoginContext( application, handler );
+                    }
+                    catch( LoginException e )
+                    {
+                        log.error( "Couldn't retrieve login configuration.\nMessage="
+                                   + e.getLocalizedMessage() );
+                        return null;
+                    }
+                }
+            });
+
+            if( loginContext != null )
+            {
+                loginContext.login();
+                fireEvent( WikiSecurityEvent.LOGIN_INITIATED, null, wikiSession );
+            }
+            else
+            {
+                log.error("No login context.  Please double-check that JSPWiki found your 'jspwiki.jaas' file or the contents have been appended to your regular JAAS file.");
+                return false;
+            }
+
+            // Fire event for the correct authentication event
+            if ( wikiSession.isAnonymous() )
+            {
+                fireEvent( WikiSecurityEvent.LOGIN_ANONYMOUS, wikiSession.getLoginPrincipal(), wikiSession );
+            }
+            else if ( wikiSession.isAsserted() )
+            {
+                fireEvent( WikiSecurityEvent.LOGIN_ASSERTED, wikiSession.getLoginPrincipal(), wikiSession );
+            }
+            else if ( wikiSession.isAuthenticated() )
+            {
+                fireEvent( WikiSecurityEvent.LOGIN_AUTHENTICATED, wikiSession.getLoginPrincipal(), wikiSession );
+            }
+
+            return true;
+        }
+        catch( FailedLoginException e )
+        {
+            //
+            //  Just a mistyped password or a cracking attempt.  No need to worry
+            //  and alert the admin
+            //
+            log.info("Failed login: "+e.getLocalizedMessage());
+            fireEvent( WikiSecurityEvent.LOGIN_FAILED, wikiSession.getLoginPrincipal(), wikiSession );
+            return false;
+        }
+        catch( AccountExpiredException e )
+        {
+            log.info("Expired account: "+e.getLocalizedMessage());
+            fireEvent( WikiSecurityEvent.LOGIN_ACCOUNT_EXPIRED, wikiSession.getLoginPrincipal(), wikiSession );
+            return false;
+        }
+        catch( CredentialExpiredException e )
+        {
+            log.info("Credentials expired: "+e.getLocalizedMessage());
+            fireEvent( WikiSecurityEvent.LOGIN_CREDENTIAL_EXPIRED, wikiSession.getLoginPrincipal(), wikiSession );
+            return false;
+        }
+        catch( LoginException e )
+        {
+            //
+            //  This should only be caught if something unforeseen happens,
+            //  so therefore we can log it as an error.
+            //
+            log.error( "Couldn't log in.\nMessage="
+                       + e.getLocalizedMessage() );
+            return false;
+        }
+        catch( SecurityException e )
+        {
+            log.error( "Could not log in.  Please check that your jaas.config file is found.", e );
+            return false;
+        }
+    }
+
+    /**
+     * Looks up and obtains a configuration file inside the WEB-INF folder of a
+     * wiki webapp.
+     * @param engine the wiki engine
+     * @param name the file to obtain, <em>e.g.</em>, <code>jspwiki.policy</code>
+     * @return the URL to the file
+     */
+    protected static final URL findConfigFile( WikiEngine engine, String name )
+    {
+        // Try creating an absolute path first
+        File defaultFile = null;
+        if( engine.getRootPath() != null )
+        {
+            defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + name );
+        }
+        if ( defaultFile != null && defaultFile.exists() )
+        {
+            try
+            {
+                return defaultFile.toURL();
+            }
+            catch ( MalformedURLException e)
+            {
+                // Shouldn't happen, but log it if it does
+                log.warn( "Malformed URL: " + e.getMessage() );
+            }
+
+        }
+
+        // Ok, the absolute path didn't work; try other methods
+        ClassLoader cl = AuthenticationManager.class.getClassLoader();
+
+        URL path = cl.getResource("/WEB-INF/"+name);
+
+        if( path == null )
+            path = cl.getResource("/"+name);
+
+        if( path == null )
+            path = cl.getResource(name);
+
+        if( path == null && engine.getServletContext() != null )
+        {
+            try
+            {
+                path = engine.getServletContext().getResource("/WEB-INF/"+name);
+            }
+            catch( MalformedURLException e )
+            {
+                // This should never happen unless I screw up
+                log.fatal("Your code is b0rked.  You are a bad person.");
+            }
+        }
+
+        return path;
+    }
+
+
+    // events processing .......................................................
+
+    /**
+     * Registers a WikiEventListener with this instance.
+     * This is a convenience method.
+     * @param listener the event listener
+     */
+    public final synchronized void addWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.addWikiEventListener( this, listener );
+    }
+
+    /**
+     * Un-registers a WikiEventListener with this instance.
+     * This is a convenience method.
+     * @param listener the event listener
+     */
+    public final synchronized void removeWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.removeWikiEventListener( this, listener );
+    }
+
+    /**
+     *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
+     *  to all registered listeners.
+     *
+     * @see com.ecyrd.jspwiki.event.WikiSecurityEvent
+     * @param type       the event type to be fired
+     * @param principal  the subject of the event, which may be <code>null</code>
+     * @param target     the changed Object, which may be <code>null</code>
+     */
+    protected final void fireEvent( int type, Principal principal, Object target )
+    {
+        if ( WikiEventManager.isListening(this) )
+        {
+            WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,principal,target));
+        }
+    }
+
+}