com.ecyrd.jspwiki.attachment.SilverpeasAttachmentServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.ecyrd.jspwiki.attachment.SilverpeasAttachmentServlet.java

Source

/**
 * Copyright (C) 2000 - 2013 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have recieved a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.ecyrd.jspwiki.attachment;

import java.io.File;
import java.io.FileOutputStream;
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.List;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.log4j.Logger;

import com.ecyrd.jspwiki.TextUtil;
import com.ecyrd.jspwiki.WikiContext;
import com.ecyrd.jspwiki.WikiEngine;
import com.ecyrd.jspwiki.WikiException;
import com.ecyrd.jspwiki.WikiPage;
import com.ecyrd.jspwiki.WikiProvider;
import com.ecyrd.jspwiki.WikiSession;
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.i18n.InternationalizationManager;
import com.ecyrd.jspwiki.providers.ProviderException;
import com.ecyrd.jspwiki.ui.progress.ProgressItem;
import com.ecyrd.jspwiki.util.HttpUtil;
import com.silverpeas.util.FileUtil;
import com.silverpeas.wiki.control.WikiMultiInstanceManager;
import com.stratelia.silverpeas.peasCore.ComponentSessionController;

/**
 * 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
 * @since 1.9.45.
 */
public class SilverpeasAttachmentServlet extends WebdavServlet {

    private static final int BUFFER_SIZE = 8192;
    private static final long serialVersionUID = 3257282552187531320L;
    private WikiEngine m_engine;
    static Logger log = Logger.getLogger(SilverpeasAttachmentServlet.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 = m_engine.createContext(req, WikiContext.ATTACH);

        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) {
                    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);
                }
                System.out.println("Attachment file is c:/tmp/result/" + att.getFileName());
                System.out.println("Attachment " + att.getFileName() + " sent to " + req.getRemoteUser() + " on "
                        + req.getRemoteAddr());
                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 = FileUtil.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);
            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 errorPage = m_engine.getURL(WikiContext.ERROR, "", null, false); // If something bad
        // happened, Upload
        // should be able to
        // take care of most
        // stuff

        String p = new String(req.getPathInfo().getBytes("ISO-8859-1"), "UTF-8");
        DavPath path = new DavPath(p);

        try {
            InputStream data = req.getInputStream();

            WikiContext context = m_engine.createContext(req, WikiContext.UPLOAD);

            String wikipage = path.get(0);

            errorPage = context.getURL(WikiContext.UPLOAD, 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
     * @throws FileUploadException
     */
    @SuppressWarnings("unchecked")
    protected String upload(HttpServletRequest req) throws RedirectException, IOException {
        String msg = "";
        String attName = "(unknown)";
        String errorPage = m_engine.getURL(WikiContext.ERROR, "", null, false); // If something bad
        // happened, Upload
        // should be able to
        // take care of most
        // stuff
        String nextPage = errorPage;

        String progressId = req.getParameter("progressid");

        // Check that we have a file upload request
        if (!ServletFileUpload.isMultipartContent(req)) {
            throw new RedirectException("Not a file upload", errorPage);
        }

        try {
            FileItemFactory factory = new DiskFileItemFactory();

            // Create the context _before_ Multipart operations, otherwise
            // strict servlet containers may fail when setting encoding.
            WikiContext context = m_engine.createContext(req, WikiContext.ATTACH);

            UploadListener pl = new UploadListener();

            m_engine.getProgressManager().startProgress(pl, progressId);

            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setHeaderEncoding("UTF-8");
            upload.setFileSizeMax(m_maxSize);
            upload.setProgressListener(pl);
            List<FileItem> items = upload.parseRequest(req);

            String wikipage = null;
            String changeNote = null;
            FileItem actualFile = null;

            for (FileItem item : items) {
                if (item.isFormField()) {
                    if (item.getFieldName().equals("page")) {
                        //
                        // FIXME: Kludge alert. We must end up with the parent page name,
                        // if this is an upload of a new revision
                        //

                        wikipage = item.getString("UTF-8");
                        int x = wikipage.indexOf("/");

                        if (x != -1) {
                            wikipage = wikipage.substring(0, x);
                        }
                    } else if (item.getFieldName().equals("changenote")) {
                        changeNote = item.getString("UTF-8");
                    } else if (item.getFieldName().equals("nextpage")) {
                        nextPage = validateNextPage(item.getString("UTF-8"), errorPage);
                    }
                } else {
                    actualFile = item;
                }
            }

            if (actualFile == null) {
                throw new RedirectException("Broken file upload", errorPage);
            }

            //
            // FIXME: Unfortunately, with Apache fileupload we will get the form fields in
            // order. This means that we have to gather all the metadata from the
            // request prior to actually touching the uploaded file itself. This
            // is because the changenote appears after the file upload box, and we
            // would not have this information when uploading. This also means
            // that with current structure we can only support a single file upload
            // at a time.
            //
            String filename = actualFile.getName();
            long fileSize = actualFile.getSize();
            InputStream in = actualFile.getInputStream();

            try {
                executeUpload(context, in, filename, nextPage, wikipage, changeNote, fileSize);
            } finally {
                in.close();
            }

        } 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;
        } catch (FileUploadException 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 new IOException(msg);
        } 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;

        try {
            filename = validateFileName(filename);
        } catch (WikiException e) {
            // this is a kludge, the exception that is caught here contains the i18n key
            // here we have the context available, so we can internationalize it properly :
            throw new RedirectException(
                    context.getBundle(InternationalizationManager.CORE_BUNDLE).getString(e.getMessage()),
                    errorPage);
        }

        //
        // 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();

        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);
        att.setAttribute("userId", ((ComponentSessionController) context.getHttpRequest().getSession()
                .getAttribute("Silverpeas_Wiki_" + WikiMultiInstanceManager.getComponentId())).getUserId());
        //
        // 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);
            }

            try {
                m_engine.getAttachmentManager().storeAttachment(att, data);
            } catch (ProviderException pe) {
                // this is a kludge, the exception that is caught here contains the i18n key
                // here we have the context available, so we can internationalize it properly :
                throw new ProviderException(
                        context.getBundle(InternationalizationManager.CORE_BUNDLE).getString(pe.getMessage()));
            }

            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 static 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);
        }
    }

    /**
     * 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) {
            log.error("Empty file name given.");

            // the caller should catch the exception and use the exception text as an i18n key
            throw new WikiException("attach.empty.file");
        }

        //
        // 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.");
            //
            // the caller should catch the exception and use the exception text as an i18n key
            throw new WikiException("attach.unwanted.file");
        }

        //
        // 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 = org.apache.commons.lang.StringUtils.replaceChars(filename, "#?\"'", "____");

        return filename;
    }
}