You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@roller.apache.org by sn...@apache.org on 2006/02/23 21:32:37 UTC

svn commit: r380218 - in /incubator/roller/branches/roller_2.1/src/org/roller: presentation/servlets/ResourceServlet.java util/Utilities.java

Author: snoopdave
Date: Thu Feb 23 12:32:34 2006
New Revision: 380218

URL: http://svn.apache.org/viewcvs?rev=380218&view=rev
Log:
Fixed ROL-1051 and security issue in ResourceServlet

Modified:
    incubator/roller/branches/roller_2.1/src/org/roller/presentation/servlets/ResourceServlet.java
    incubator/roller/branches/roller_2.1/src/org/roller/util/Utilities.java

Modified: incubator/roller/branches/roller_2.1/src/org/roller/presentation/servlets/ResourceServlet.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.1/src/org/roller/presentation/servlets/ResourceServlet.java?rev=380218&r1=380217&r2=380218&view=diff
==============================================================================
--- incubator/roller/branches/roller_2.1/src/org/roller/presentation/servlets/ResourceServlet.java (original)
+++ incubator/roller/branches/roller_2.1/src/org/roller/presentation/servlets/ResourceServlet.java Thu Feb 23 12:32:34 2006
@@ -1,13 +1,20 @@
 package org.roller.presentation.servlets;
 
-import java.io.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLDecoder;
 import java.util.Date;
-
-import javax.servlet.*;
-import javax.servlet.http.*;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-
 import org.roller.model.RollerFactory;
 
 
@@ -15,7 +22,7 @@
  * Resources servlet.  Acts as a gateway to files uploaded by users.
  *
  * Since we keep uploaded resources in a location outside of the webapp
- * context we need a way to serve them up.  This servlet assumes that 
+ * context we need a way to serve them up.  This servlet assumes that
  * resources are stored on a filesystem in the "uploads.dir" directory.
  *
  * @author Allen Gilliland
@@ -23,46 +30,41 @@
  * @web.servlet name="ResourcesServlet"
  * @web.servlet-mapping url-pattern="/resources/*"
  */
-public class ResourceServlet extends HttpServlet
-{   
-    private static Log mLogger =
-            LogFactory.getFactory().getInstance(ResourceServlet.class);
+public class ResourceServlet extends HttpServlet {
+    
+    private static Log mLogger = LogFactory.getLog(ResourceServlet.class);
     
     private String upload_dir = null;
     private ServletContext context = null;
     
     
-    /** Initializes the servlet.*/
     public void init(ServletConfig config) throws ServletException {
+        
         super.init(config);
         
         this.context = config.getServletContext();
-
+        
         try {
             this.upload_dir = RollerFactory.getRoller().getFileManager().getUploadDir();
             mLogger.debug("upload dir is ["+this.upload_dir+"]");
         } catch(Exception e) { mLogger.warn(e); }
-
-    }
-    
-    /** Destroys the servlet.
-     */
-    public void destroy() {
         
     }
     
     
-    /** Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
-     * @param request servlet request
-     * @param response servlet response
+    /** 
+     * Handles requests for user uploaded resources.
      */
-    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
-    throws ServletException, IOException {
+    public void doGet(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
         
         String context = request.getContextPath();
         String servlet = request.getServletPath();
         String reqURI = request.getRequestURI();
         
+        // url decoding
+        reqURI = URLDecoder.decode(reqURI, "UTF-8");
+        
         // calculate the path of the requested resource
         // we expect ... /<context>/<servlet>/path/to/resource
         String reqResource = reqURI.substring(servlet.length() + context.length());
@@ -70,12 +72,19 @@
         // now we can formulate the *real* path to the resource on the filesystem
         String resource_path = this.upload_dir + reqResource;
         File resource = new File(resource_path);
-
+        
         mLogger.debug("Resource requested ["+reqURI+"]");
         mLogger.debug("Real path is ["+resource.getAbsolutePath()+"]");
         
         // do a quick check to make sure the resource exits, otherwise 404
-        if(!resource.exists() || !resource.canRead()) {
+        if(!resource.exists() || !resource.canRead() || resource.isDirectory()) {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        // make sure someone isn't trying to sneek outside the uploads dir
+        File uploadDir = new File(this.upload_dir);
+        if(!resource.getCanonicalPath().startsWith(uploadDir.getCanonicalPath())) {
             response.sendError(HttpServletResponse.SC_NOT_FOUND);
             return;
         }
@@ -109,28 +118,10 @@
     }
     
     
-    /** Handles the HTTP <code>GET</code> method.
-     * @param request servlet request
-     * @param response servlet response
-     */
-    protected void doGet(HttpServletRequest request, HttpServletResponse response)
-    throws ServletException, IOException {
-        processRequest(request, response);
-    }
-    
-    /** Handles the HTTP <code>POST</code> method.
-     * @param request servlet request
-     * @param response servlet response
-     */
-    protected void doPost(HttpServletRequest request, HttpServletResponse response)
-    throws ServletException, IOException {
-        processRequest(request, response);
-    }
-    
-    /** Returns a short description of the servlet.
-     */
-    public String getServletInfo() {
-        return "ResourceServlet ... serving you since 2005 ;)";
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        doGet(request, response);
     }
     
 }
+

Modified: incubator/roller/branches/roller_2.1/src/org/roller/util/Utilities.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.1/src/org/roller/util/Utilities.java?rev=380218&r1=380217&r2=380218&view=diff
==============================================================================
--- incubator/roller/branches/roller_2.1/src/org/roller/util/Utilities.java (original)
+++ incubator/roller/branches/roller_2.1/src/org/roller/util/Utilities.java Thu Feb 23 12:32:34 2006
@@ -10,6 +10,7 @@
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.net.URLDecoder;
 import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.Date;
@@ -23,81 +24,96 @@
 import org.apache.commons.logging.LogFactory;
 
 /**
- * General purpose utilities. 
- * 
+ * General purpose utilities.
+ *
  * <pre>
- * Includes TextToHTML methods dondated by Erik Thauvin - "Donated to the 
- * Roller Weblogger project for publication under the terms of the Roller 
- * Software License. 
+ * Includes TextToHTML methods donated by Erik Thauvin
  * Copyright (C) 2002-2003 by Erik C. Thauvin (erik@thauvin.net).
  * All rights reserved.
  * </pre>
  * 
+ * <pre>
+ * Also includes addNoFollow() and transformToHTMLSubset() from Simon
+ * Brown's Pebble blog server.
+ * Copyright (c) 2003-2005, Simon Brown
+ * All rights reserved.
+ * </pre>
+ *
  * @author David M Johnson
  * @author Lance Lavandowska
  * @author Matt Raible (added encryption methods)
  */
-public class Utilities
-{
+public class Utilities {
     /** The <code>Log</code> instance for this class. */
     private static Log mLogger = LogFactory.getLog(Utilities.class);
     
     /** Pattern for matching HTML links */
-    private static Pattern mLinkPattern = 
-        Pattern.compile("<a href=.*?>", Pattern.CASE_INSENSITIVE);
-
+    private static Pattern mLinkPattern =
+            Pattern.compile("<a href=.*?>", Pattern.CASE_INSENSITIVE);
+    
+    private static final Pattern OPENING_B_TAG_PATTERN = Pattern.compile("&lt;b&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_B_TAG_PATTERN = Pattern.compile("&lt;/b&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_I_TAG_PATTERN = Pattern.compile("&lt;i&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_I_TAG_PATTERN = Pattern.compile("&lt;/i&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_BLOCKQUOTE_TAG_PATTERN = Pattern.compile("&lt;blockquote&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_BLOCKQUOTE_TAG_PATTERN = Pattern.compile("&lt;/blockquote&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern BR_TAG_PATTERN = Pattern.compile("&lt;br */*&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_P_TAG_PATTERN = Pattern.compile("&lt;p&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_P_TAG_PATTERN = Pattern.compile("&lt;/p&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_PRE_TAG_PATTERN = Pattern.compile("&lt;pre&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_PRE_TAG_PATTERN = Pattern.compile("&lt;/pre&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_UL_TAG_PATTERN = Pattern.compile("&lt;ul&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_UL_TAG_PATTERN = Pattern.compile("&lt;/ul&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_OL_TAG_PATTERN = Pattern.compile("&lt;ol&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_OL_TAG_PATTERN = Pattern.compile("&lt;/ol&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_LI_TAG_PATTERN = Pattern.compile("&lt;li&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_LI_TAG_PATTERN = Pattern.compile("&lt;/li&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern CLOSING_A_TAG_PATTERN = Pattern.compile("&lt;/a&gt;", Pattern.CASE_INSENSITIVE);
+    private static final Pattern OPENING_A_TAG_PATTERN = Pattern.compile("&lt;a href=.*?&gt;", Pattern.CASE_INSENSITIVE);
+  
     /**
-     * Utility methods for calling StringUtils since it cannot be 
+     * Utility methods for calling StringUtils since it cannot be
      * instantiated and Utilties can.
      */
-    public static boolean isNotEmpty(String str)
-    {
+    public static boolean isNotEmpty(String str) {
         return StringUtils.isNotEmpty(str);
     }
     
     //------------------------------------------------------------------------
     /** Strip jsessionid off of a URL */
-    public static String stripJsessionId( String url )
-    {
+    public static String stripJsessionId( String url ) {
         // Strip off jsessionid found in referer URL
         int startPos = url.indexOf(";jsessionid=");
-        if ( startPos != -1 )
-        {
+        if ( startPos != -1 ) {
             int endPos = url.indexOf("?",startPos);
-            if ( endPos == -1 )
-            {
-                url = url.substring(0,startPos);   
-            }
-            else
-            {
+            if ( endPos == -1 ) {
+                url = url.substring(0,startPos);
+            } else {
                 url = url.substring(0,startPos)
-                    + url.substring(endPos,url.length());   
+                + url.substring(endPos,url.length());
             }
         }
         return url;
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Escape, but do not replace HTML.
      * The default behaviour is to escape ampersands.
      */
-    public static String escapeHTML(String s)
-    {
+    public static String escapeHTML(String s) {
         return escapeHTML(s, true);
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Escape, but do not replace HTML.
-     * @param escapseAmpersand Optionally escape
+     * @param escapeAmpersand Optionally escape
      * ampersands (&amp;).
      */
-    public static String escapeHTML(String s, boolean escapeAmpersand)
-    {
+    public static String escapeHTML(String s, boolean escapeAmpersand) {
         // got to do amp's first so we don't double escape
-        if (escapeAmpersand)
-        {
+        if (escapeAmpersand) {
             s = stringReplace(s, "&", "&amp;");
         }
         s = stringReplace(s, "&nbsp;", " ");
@@ -106,29 +122,31 @@
         s = stringReplace(s, ">", "&gt;");
         return s;
     }
-
+     
+    public static String unescapeHTML(String str) {
+        return StringEscapeUtils.unescapeHtml(str);
+    }
+    
     //------------------------------------------------------------------------
     /**
      * Remove occurences of html, defined as any text
      * between the characters "&lt;" and "&gt;".  Replace
      * any HTML tags with a space.
      */
-    public static String removeHTML(String str)
-    {
+    public static String removeHTML(String str) {
         return removeHTML(str, true);
     }
     
     /**
      * Remove occurences of html, defined as any text
-     * between the characters "&lt;" and "&gt;". 
+     * between the characters "&lt;" and "&gt;".
      * Optionally replace HTML tags with a space.
-     * 
+     *
      * @param str
      * @param addSpace
      * @return
      */
-    public static String removeHTML(String str, boolean addSpace)
-    {
+    public static String removeHTML(String str, boolean addSpace) {
         if (str == null) return "";
         StringBuffer ret = new StringBuffer(str.length());
         int start = 0;
@@ -136,11 +154,9 @@
         int endTag = 0;
         if (beginTag == -1)
             return str;
-
-        while (beginTag >= start)
-        {
-            if (beginTag > 0)
-            {
+        
+        while (beginTag >= start) {
+            if (beginTag > 0) {
                 ret.append(str.substring(start, beginTag));
                 
                 // replace each tag with a space (looks better)
@@ -149,43 +165,38 @@
             endTag = str.indexOf(">", beginTag);
             
             // if endTag found move "cursor" forward
-            if (endTag > -1)
-            {
+            if (endTag > -1) {
                 start = endTag + 1;
                 beginTag = str.indexOf("<", start);
             }
             // if no endTag found, get rest of str and break
-            else
-            {
+            else {
                 ret.append(str.substring(beginTag));
                 break;
             }
         }
         // append everything after the last endTag
-        if (endTag > -1 && endTag + 1 < str.length())
-        {
+        if (endTag > -1 && endTag + 1 < str.length()) {
             ret.append(str.substring(endTag + 1));
         }
         return ret.toString().trim();
     }
-
+    
     //------------------------------------------------------------------------
     /** Run both removeHTML and escapeHTML on a string.
      * @param s String to be run through removeHTML and escapeHTML.
      * @return String with HTML removed and HTML special characters escaped.
      */
-    public static String removeAndEscapeHTML( String s )
-    {
+    public static String removeAndEscapeHTML( String s ) {
         if ( s==null ) return "";
         else return Utilities.escapeHTML( Utilities.removeHTML(s) );
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Autoformat.
      */
-    public static String autoformat(String s)
-    {
+    public static String autoformat(String s) {
         String ret = StringUtils.replace(s, "\n", "<br />");
         return ret;
     }
@@ -194,100 +205,86 @@
     /**
      * Format date in ISO-8601 format.
      */
-    public static String formatIso8601Date(Date d)
-    {
+    public static String formatIso8601Date(Date d) {
         return DateUtil.formatIso8601(d);
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Format date in ISO-8601 format.
      */
-    public static String formatIso8601Day(Date d)
-    {
+    public static String formatIso8601Day(Date d) {
         return DateUtil.formatIso8601Day(d);
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Return a date in RFC-822 format.
      */
-    public static String formatRfc822Date(Date date) 
-    {
+    public static String formatRfc822Date(Date date) {
         return DateUtil.formatRfc822(date);
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Return a date in RFC-822 format.
      */
-    public static String format8charsDate(Date date) 
-    {
+    public static String format8charsDate(Date date) {
         return DateUtil.format8chars(date);
     }
-
-	//------------------------------------------------------------------------
-	/**
-	 * Replaces occurences of non-alphanumeric characters with an underscore.
-	 */
-	public static String replaceNonAlphanumeric(String str)
-	{
-		return replaceNonAlphanumeric(str, '_');
-	}
-
-	//------------------------------------------------------------------------
-	/**
-	 * Replaces occurences of non-alphanumeric characters with a
-	 * supplied char.
-	 */
-	public static String replaceNonAlphanumeric(String str, char subst)
-	{
-		StringBuffer ret = new StringBuffer(str.length());
-		char[] testChars = str.toCharArray();
-		for (int i = 0; i < testChars.length; i++)
-		{
-			if (Character.isLetterOrDigit(testChars[i]))
-			{
-				ret.append(testChars[i]);
-			}
-			else
-			{
-				ret.append( subst );
-			}
-		}
-		return ret.toString();
-	}
-
+    
+    //------------------------------------------------------------------------
+    /**
+     * Replaces occurences of non-alphanumeric characters with an underscore.
+     */
+    public static String replaceNonAlphanumeric(String str) {
+        return replaceNonAlphanumeric(str, '_');
+    }
+    
+    //------------------------------------------------------------------------
+    /**
+     * Replaces occurences of non-alphanumeric characters with a
+     * supplied char.
+     */
+    public static String replaceNonAlphanumeric(String str, char subst) {
+        StringBuffer ret = new StringBuffer(str.length());
+        char[] testChars = str.toCharArray();
+        for (int i = 0; i < testChars.length; i++) {
+            if (Character.isLetterOrDigit(testChars[i])) {
+                ret.append(testChars[i]);
+            } else {
+                ret.append( subst );
+            }
+        }
+        return ret.toString();
+    }
+    
     //------------------------------------------------------------------------
     /**
      * Remove occurences of non-alphanumeric characters.
      */
-    public static String removeNonAlphanumeric(String str)
-    {
+    public static String removeNonAlphanumeric(String str) {
         StringBuffer ret = new StringBuffer(str.length());
         char[] testChars = str.toCharArray();
-        for (int i = 0; i < testChars.length; i++)
-        {
+        for (int i = 0; i < testChars.length; i++) {
             // MR: Allow periods in page links
             if (Character.isLetterOrDigit(testChars[i]) ||
-                testChars[i] == '.')
-            {
+                    testChars[i] == '.') {
                 ret.append(testChars[i]);
             }
         }
         return ret.toString();
     }
-
+    
     //------------------------------------------------------------------------
     /**
-     * @param pathArray
+     * @param stringArray
+     * @param delim
      * @return
      */
-    public static String stringArrayToString(String[] stringArray, String delim)
-    {
+    public static String stringArrayToString(String[] stringArray, String delim) {
         String ret = "";
-        for (int i = 0; i < stringArray.length; i++)
-        {
+        for (int i = 0; i < stringArray.length; i++) {
             if (ret.length() > 0)
                 ret = ret + delim + stringArray[i];
             else
@@ -298,73 +295,65 @@
     
     //------------------------------------------------------------------------
     /**
-     * Replace occurrences of str1 in string str with str2 
+     * Replace occurrences of str1 in string str with str2
      */
-    public static String stringReplace(String str, String str1, String str2)
-    {
+    public static String stringReplace(String str, String str1, String str2) {
         String ret = StringUtils.replace(str,str1,str2);
         return ret;
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Replace occurrences of str1 in string str with str2
-     * @param str String to operate on 
+     * @param str String to operate on
      * @param str1 String to be replaced
      * @param str2 String to be used as replacement
      * @param maxCount Number of times to replace, 0 for all
      */
     public static String stringReplace(
-        String str,
-        String str1,
-        String str2,
-        int maxCount)
-    {
+            String str,
+            String str1,
+            String str2,
+            int maxCount) {
         String ret = StringUtils.replace(str,str1,str2,maxCount);
         return ret;
     }
-
+    
     //--------------------------------------------------------------------------
     /** Convert string to string array. */
     public static String[] stringToStringArray(String instr, String delim)
-        throws NoSuchElementException, NumberFormatException
-    {
+    throws NoSuchElementException, NumberFormatException {
         StringTokenizer toker = new StringTokenizer(instr, delim);
         String stringArray[] = new String[toker.countTokens()];
         int i = 0;
-
-        while (toker.hasMoreTokens())
-        {
+        
+        while (toker.hasMoreTokens()) {
             stringArray[i++] = toker.nextToken();
         }
         return stringArray;
     }
-
+    
     //--------------------------------------------------------------------------
     /** Convert string to integer array. */
     public static int[] stringToIntArray(String instr, String delim)
-        throws NoSuchElementException, NumberFormatException
-    {
+    throws NoSuchElementException, NumberFormatException {
         StringTokenizer toker = new StringTokenizer(instr, delim);
         int intArray[] = new int[toker.countTokens()];
         int i = 0;
-
-        while (toker.hasMoreTokens())
-        {
+        
+        while (toker.hasMoreTokens()) {
             String sInt = toker.nextToken();
             int nInt = Integer.parseInt(sInt);
             intArray[i++] = new Integer(nInt).intValue();
         }
         return intArray;
     }
-
+    
     //-------------------------------------------------------------------
     /** Convert integer array to a string. */
-    public static String intArrayToString(int[] intArray)
-    {
+    public static String intArrayToString(int[] intArray) {
         String ret = "";
-        for (int i = 0; i < intArray.length; i++)
-        {
+        for (int i = 0; i < intArray.length; i++) {
             if (ret.length() > 0)
                 ret = ret + "," + Integer.toString(intArray[i]);
             else
@@ -372,149 +361,117 @@
         }
         return ret;
     }
-
+    
     //------------------------------------------------------------------------
-    public static void copyFile(File from, File to) throws IOException
-    {
+    public static void copyFile(File from, File to) throws IOException {
         InputStream in = null;
         OutputStream out = null;
-
-        try
-        {
+        
+        try {
             in = new FileInputStream(from);
-        }
-        catch (IOException ex)
-        {
+        } catch (IOException ex) {
             throw new IOException(
-                "Utilities.copyFile: opening input stream '"
+                    "Utilities.copyFile: opening input stream '"
                     + from.getPath()
                     + "', "
                     + ex.getMessage());
         }
-
-        try
-        {
+        
+        try {
             out = new FileOutputStream(to);
-        }
-        catch (Exception ex)
-        {
-            try
-            {
+        } catch (Exception ex) {
+            try {
                 in.close();
-            }
-            catch (IOException ex1)
-            {
+            } catch (IOException ex1) {
             }
             throw new IOException(
-                "Utilities.copyFile: opening output stream '"
+                    "Utilities.copyFile: opening output stream '"
                     + to.getPath()
                     + "', "
                     + ex.getMessage());
         }
-
+        
         copyInputToOutput(in, out, from.length());
     }
-
+    
     //------------------------------------------------------------------------
     /**
      * Utility method to copy an input stream to an output stream.
      * Wraps both streams in buffers. Ensures right numbers of bytes copied.
      */
     public static void copyInputToOutput(
-        InputStream input,
-        OutputStream output,
-        long byteCount)
-        throws IOException
-    {
+            InputStream input,
+            OutputStream output,
+            long byteCount)
+            throws IOException {
         int bytes;
         long length;
-
+        
         BufferedInputStream in = new BufferedInputStream(input);
         BufferedOutputStream out = new BufferedOutputStream(output);
-
+        
         byte[] buffer;
         buffer = new byte[8192];
-
-        for (length = byteCount; length > 0;)
-        {
+        
+        for (length = byteCount; length > 0;) {
             bytes = (int) (length > 8192 ? 8192 : length);
-
-            try
-            {
+            
+            try {
                 bytes = in.read(buffer, 0, bytes);
-            }
-            catch (IOException ex)
-            {
-                try
-                {
+            } catch (IOException ex) {
+                try {
                     in.close();
                     out.close();
-                }
-                catch (IOException ex1)
-                {
+                } catch (IOException ex1) {
                 }
                 throw new IOException(
-                    "Reading input stream, " + ex.getMessage());
+                        "Reading input stream, " + ex.getMessage());
             }
-
+            
             if (bytes < 0)
                 break;
-
+            
             length -= bytes;
-
-            try
-            {
+            
+            try {
                 out.write(buffer, 0, bytes);
-            }
-            catch (IOException ex)
-            {
-                try
-                {
+            } catch (IOException ex) {
+                try {
                     in.close();
                     out.close();
-                }
-                catch (IOException ex1)
-                {
+                } catch (IOException ex1) {
                 }
                 throw new IOException(
-                    "Writing output stream, " + ex.getMessage());
+                        "Writing output stream, " + ex.getMessage());
             }
         }
-
-        try
-        {
+        
+        try {
             in.close();
             out.close();
-        }
-        catch (IOException ex)
-        {
+        } catch (IOException ex) {
             throw new IOException("Closing file streams, " + ex.getMessage());
         }
     }
-
+    
     //------------------------------------------------------------------------
     public static void copyInputToOutput(
-        InputStream input,
-        OutputStream output)
-        throws IOException
-    {
+            InputStream input,
+            OutputStream output)
+            throws IOException {
         BufferedInputStream in = new BufferedInputStream(input);
         BufferedOutputStream out = new BufferedOutputStream(output);
         byte buffer[] = new byte[8192];
-        for (int count = 0; count != -1;)
-        {
+        for (int count = 0; count != -1;) {
             count = in.read(buffer, 0, 8192);
             if (count != -1)
                 out.write(buffer, 0, count);
         }
-
-        try
-        {
+        
+        try {
             in.close();
             out.close();
-        }
-        catch (IOException ex)
-        {
+        } catch (IOException ex) {
             throw new IOException("Closing file streams, " + ex.getMessage());
         }
     }
@@ -530,47 +487,41 @@
      *
      * @return encypted password based on the algorithm.
      */
-    public static String encodePassword(String password, String algorithm) 
-    {
+    public static String encodePassword(String password, String algorithm) {
         byte[] unencodedPassword = password.getBytes();
-
+        
         MessageDigest md = null;
-
-        try 
-        {
+        
+        try {
             // first create an instance, given the provider
             md = MessageDigest.getInstance(algorithm);
-        } 
-        catch (Exception e) 
-        {
+        } catch (Exception e) {
             mLogger.error("Exception: " + e);
             return password;
         }
-
+        
         md.reset();
-
+        
         // call the update method one or more times
         // (useful when you don't know the size of your data, eg. stream)
         md.update(unencodedPassword);
-
+        
         // now calculate the hash
         byte[] encodedPassword = md.digest();
-
+        
         StringBuffer buf = new StringBuffer();
-
-        for (int i = 0; i < encodedPassword.length; i++) 
-        {
-            if ((encodedPassword[i] & 0xff) < 0x10) 
-            {
+        
+        for (int i = 0; i < encodedPassword.length; i++) {
+            if ((encodedPassword[i] & 0xff) < 0x10) {
                 buf.append("0");
             }
-
+            
             buf.append(Long.toString(encodedPassword[i] & 0xff, 16));
         }
-
+        
         return buf.toString();
     }
-
+    
     /**
      * Encode a string using Base64 encoding. Used when storing passwords
      * as cookies.
@@ -582,14 +533,13 @@
      * @return String
      * @throws IOException
      */
-    public static String encodeString(String str) throws IOException 
-    {
+    public static String encodeString(String str) throws IOException {
         sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
         String encodedStr = encoder.encodeBuffer(str.getBytes());
-
+        
         return (encodedStr.trim());
     }
-
+    
     /**
      * Decode a string using Base64 encoding.
      *
@@ -597,11 +547,10 @@
      * @return String
      * @throws IOException
      */
-    public static String decodeString(String str) throws IOException 
-    {
+    public static String decodeString(String str) throws IOException {
         sun.misc.BASE64Decoder dec = new sun.misc.BASE64Decoder();
         String value = new String(dec.decodeBuffer(str));
-
+        
         return (value);
     }
     
@@ -609,41 +558,35 @@
      * Strips HTML and truncates.
      */
     public static String truncate(
-            String str, int lower, int upper, String appendToEnd)
-    {
+            String str, int lower, int upper, String appendToEnd) {
         // strip markup from the string
         String str2 = removeHTML(str, false);
         
         // quickly adjust the upper if it is set lower than 'lower'
-        if (upper < lower) 
-        {
+        if (upper < lower) {
             upper = lower;
-        }       
+        }
         
         // now determine if the string fits within the upper limit
         // if it does, go straight to return, do not pass 'go' and collect $200
-        if(str2.length() > upper) 
-        {
+        if(str2.length() > upper) {
             // the magic location int
             int loc;
-        
+            
             // first we determine where the next space appears after lower
             loc = str2.lastIndexOf(' ', upper);
             
             // now we'll see if the location is greater than the lower limit
-            if(loc >= lower) 
-            {
+            if(loc >= lower) {
                 // yes it was, so we'll cut it off here
                 str2 = str2.substring(0, loc);
-            } 
-            else 
-            {
+            } else {
                 // no it wasnt, so we'll cut it off at the upper limit
                 str2 = str2.substring(0, upper);
                 loc = upper;
             }
-           
-            // the string was truncated, so we append the appendToEnd String                
+            
+            // the string was truncated, so we append the appendToEnd String
             str2 = str2 + appendToEnd;
         }
         
@@ -655,15 +598,14 @@
      * http://cvs.apache.org/viewcvs/jakarta-taglibs/string/src/org/apache/taglibs/string/util/StringW.java?rev=1.16&content-type=text/vnd.viewcvs-markup
      * Copyright (c) 1999 The Apache Software Foundation.
      * Author: Henri Yandell bayard@generationjava.com
-     * 
+     *
      * @param str
      * @param lower
      * @param upper
      * @param appendToEnd
      * @return
      */
-    public static String truncateNicely(String str, int lower, int upper, String appendToEnd)
-    {
+    public static String truncateNicely(String str, int lower, int upper, String appendToEnd) {
         // strip markup from the string
         String str2 = removeHTML(str, false);
         boolean diff = (str2.length() < str.length());
@@ -671,14 +613,14 @@
         // quickly adjust the upper if it is set lower than 'lower'
         if(upper < lower) {
             upper = lower;
-        }       
+        }
         
         // now determine if the string fits within the upper limit
         // if it does, go straight to return, do not pass 'go' and collect $200
         if(str2.length() > upper) {
             // the magic location int
             int loc;
-        
+            
             // first we determine where the next space appears after lower
             loc = str2.lastIndexOf(' ', upper);
             
@@ -693,8 +635,7 @@
             }
             
             // HTML was removed from original str
-            if (diff)
-            {
+            if (diff) {
                 
                 // location of last space in truncated string
                 loc = str2.lastIndexOf(' ', loc);
@@ -718,20 +659,17 @@
                 // append the appendToEnd String and
                 // add extracted HTML back onto truncated string
                 str = str2 + appendToEnd + str3;
-            }
-            else
-            {
-                // the string was truncated, so we append the appendToEnd String                
+            } else {
+                // the string was truncated, so we append the appendToEnd String
                 str = str2 + appendToEnd;
             }
-    
+            
         }
         
         return str;
     }
     
-    public static String truncateText(String str, int lower, int upper, String appendToEnd)
-    {
+    public static String truncateText(String str, int lower, int upper, String appendToEnd) {
         // strip markup from the string
         String str2 = removeHTML(str, false);
         boolean diff = (str2.length() < str.length());
@@ -739,14 +677,14 @@
         // quickly adjust the upper if it is set lower than 'lower'
         if(upper < lower) {
             upper = lower;
-        }       
+        }
         
         // now determine if the string fits within the upper limit
         // if it does, go straight to return, do not pass 'go' and collect $200
         if(str2.length() > upper) {
             // the magic location int
             int loc;
-        
+            
             // first we determine where the next space appears after lower
             loc = str2.lastIndexOf(' ', upper);
             
@@ -758,49 +696,47 @@
                 // no it wasnt, so we'll cut it off at the upper limit
                 str2 = str2.substring(0, upper);
                 loc = upper;
-            }            
-            // the string was truncated, so we append the appendToEnd String                
+            }
+            // the string was truncated, so we append the appendToEnd String
             str = str2 + appendToEnd;
-        }        
+        }
         return str;
     }
     
     /**
-	 * @param str
-	 * @return
-	 */
-	private static String stripLineBreaks(String str)
-	{
+     * @param str
+     * @return
+     */
+    private static String stripLineBreaks(String str) {
         // TODO: use a string buffer, ignore case !
-		str = str.replaceAll("<br>", "");
+        str = str.replaceAll("<br>", "");
         str = str.replaceAll("<br/>", "");
         str = str.replaceAll("<br />", "");
         str = str.replaceAll("<p></p>", "");
         str = str.replaceAll("<p/>","");
         str = str.replaceAll("<p />","");
         return str;
-	}
-	
+    }
+    
     /**
-     * Need need to get rid of any user-visible HTML tags once all text has been 
-     * removed such as &lt;BR&gt;. This sounds like a better approach than removing 
+     * Need need to get rid of any user-visible HTML tags once all text has been
+     * removed such as &lt;BR&gt;. This sounds like a better approach than removing
      * all HTML tags and taking the chance to leave some tags un-closed.
-     * 
+     *
      * WARNING: this method has serious performance problems a
-     * 
+     *
      * @author Alexis Moussine-Pouchkine <al...@france.sun.com>
      * @author Lance Lavandowska
      * @param str the String object to modify
      * @return the new String object without the HTML "visible" tags
      */
-    private static String removeVisibleHTMLTags(String str) 
-    {
+    private static String removeVisibleHTMLTags(String str) {
         str = stripLineBreaks(str);
         StringBuffer result = new StringBuffer(str);
         StringBuffer lcresult = new StringBuffer(str.toLowerCase());
-
+        
         // <img should take care of smileys
-        String[] visibleTags = {"<img"}; // are there others to add?        
+        String[] visibleTags = {"<img"}; // are there others to add?
         int stringIndex;
         for ( int j = 0 ;  j < visibleTags.length ; j++ ) {
             while ( (stringIndex = lcresult.indexOf(visibleTags[j])) != -1 ) {
@@ -819,17 +755,15 @@
                 }
             }
         }
-
+        
         // TODO:  This code is buggy by nature.  It doesn't deal with nesting of tags properly.
         // remove certain elements with open & close tags
         String[] openCloseTags = {"li", "a", "div", "h1", "h2", "h3", "h4"}; // more ?
-        for (int j = 0; j < openCloseTags.length; j++)
-        {
+        for (int j = 0; j < openCloseTags.length; j++) {
             // could this be better done with a regular expression?
             String closeTag = "</"+openCloseTags[j]+">";
             int lastStringIndex = 0;
-            while ( (stringIndex = lcresult.indexOf( "<"+openCloseTags[j], lastStringIndex)) > -1)
-            {
+            while ( (stringIndex = lcresult.indexOf( "<"+openCloseTags[j], lastStringIndex)) > -1) {
                 lastStringIndex = stringIndex;
                 // Try to find the matching closing tag  (ignores possible nesting!)
                 int endIndex = lcresult.indexOf(closeTag, stringIndex);
@@ -845,7 +779,7 @@
                         // Looks like it, so remove it.
                         result.delete(stringIndex, endIndex + 1);
                         lcresult.delete(stringIndex, endIndex + 1);
-
+                        
                     }
                 }
             }
@@ -853,14 +787,13 @@
         
         return result.toString();
     }
-
-	/**
+    
+    /**
      * Extract (keep) JUST the HTML from the String.
      * @param str
      * @return
      */
-    public static String extractHTML(String str)
-    {
+    public static String extractHTML(String str) {
         if (str == null) return "";
         StringBuffer ret = new StringBuffer(str.length());
         int start = 0;
@@ -868,14 +801,12 @@
         int endTag = 0;
         if (beginTag == -1)
             return str;
-
-        while (beginTag >= start)
-        {
+        
+        while (beginTag >= start) {
             endTag = str.indexOf(">", beginTag);
             
             // if endTag found, keep tag
-            if (endTag > -1)
-            {
+            if (endTag > -1) {
                 ret.append( str.substring(beginTag, endTag+1) );
                 
                 // move start forward and find another tag
@@ -883,27 +814,24 @@
                 beginTag = str.indexOf("<", start);
             }
             // if no endTag found, break
-            else
-            {
+            else {
                 break;
             }
         }
         return ret.toString();
     }
-
     
-    public static String hexEncode(String str)
-    {
+    
+    public static String hexEncode(String str) {
         if (StringUtils.isEmpty(str)) return str;
         
         return RegexUtil.encode(str);
     }
     
-    public static String encodeEmail(String str)
-    {
+    public static String encodeEmail(String str) {
         return str!=null ? RegexUtil.encodeEmail(str) : null;
     }
-
+    
     /**
      * Converts a character to HTML or XML entity.
      *
@@ -913,56 +841,49 @@
      *
      * @return The converted string.
      */
-    public static final String charToHTML(char ch, boolean xml)
-    {
+    public static final String charToHTML(char ch, boolean xml) {
         int c;
-
+        
         // Convert left bracket
-        if (ch == '<')
-        {
+        if (ch == '<') {
             return ("&lt;");
         }
-
+        
         // Convert left bracket
-        else if (ch == '>')
-        {
+        else if (ch == '>') {
             return ("&gt;");
         }
-
+        
         // Convert ampersand
-        else if (ch == '&')
-        {
+        else if (ch == '&') {
             return ("&amp;");
         }
-
+        
         // Commented out to eliminate redundant numeric character codes (ROL-507)
         // High-ASCII character
         //else if (ch >= 128)
         //{
-            //c = ch;
-            //return ("&#" + c + ';');
+        //c = ch;
+        //return ("&#" + c + ';');
         //}
-
+        
         // Convert double quote
-        else if (xml && (ch == '"'))
-        {
+        else if (xml && (ch == '"')) {
             return ("&quot;");
         }
-
+        
         // Convert single quote
-        else if (xml && (ch == '\''))
-        {
+        else if (xml && (ch == '\'')) {
             return ("&#39;");
         }
-
+        
         // No conversion
-        else
-        {
+        else {
             // Return character as string
             return (String.valueOf(ch));
         }
     }
-
+    
     /**
      * Converts a text string to HTML or XML entities.
      *
@@ -972,22 +893,20 @@
      *
      * @return The converted string.
      */
-    public static final String textToHTML(String text, boolean xml)
-    {
+    public static final String textToHTML(String text, boolean xml) {
         if (text == null) return "null";
         final StringBuffer html = new StringBuffer();
-
+        
         // Loop thru each characters of the text
-        for (int i = 0; i < text.length(); i++)
-        {
+        for (int i = 0; i < text.length(); i++) {
             // Convert character to HTML/XML
             html.append(charToHTML(text.charAt(i), xml));
         }
-
+        
         // Return HTML/XML string
         return html.toString();
     }
-
+    
     /**
      * Converts a text string to HTML or XML entities.
      *
@@ -995,11 +914,10 @@
      * @author Erik C. Thauvin
      * @return The converted string.
      */
-    public static final String textToHTML(String text)
-    {
+    public static final String textToHTML(String text) {
         return textToHTML(text, false);
     }
-
+    
     /**
      * Converts a text string to XML entities.
      *
@@ -1007,114 +925,118 @@
      * @author Erik C. Thauvin
      * @return The converted string.
      */
-    public static final String textToXML(String text)
-    {
+    public static final String textToXML(String text) {
         return textToHTML(text, true);
     }
-
+    
     /**
      * Converts a text string to HTML or XML entities.
      * @param text The string to convert.
      * @return The converted string.
      */
-    public static final String textToCDATA(String text)
-    {
+    public static final String textToCDATA(String text) {
         if (text == null) return "null";
         final StringBuffer html = new StringBuffer();
-
+        
         // Loop thru each characters of the text
-        for (int i = 0; i < text.length(); i++)
-        {
+        for (int i = 0; i < text.length(); i++) {
             // Convert character to HTML/XML
             html.append(charToCDATA(text.charAt(i)));
         }
-
+        
         // Return HTML/XML string
         return html.toString();
     }
-
+    
     /**
      * Converts a character to CDATA character.
      * @param ch The character to convert.
      * @return The converted string.
      */
-    public static final String charToCDATA(char ch)
-    {
+    public static final String charToCDATA(char ch) {
         int c;
-
-        if (ch >= 128)
-        {
+        
+        if (ch >= 128) {
             c = ch;
-
+            
             return ("&#" + c + ';');
         }
-
+        
         // No conversion
-        else
-        {
+        else {
             // Return character as string
             return (String.valueOf(ch));
         }
     }
-    
-    public static final String encode(String s)
-    {
-       try
-       {
-           if (s != null)
-               return URLEncoder.encode(s, "utf-8");
-           else
-               return s;
-       }
-       catch (UnsupportedEncodingException e)
-       {
-           return s;
-       }
-   }
+
+    /**
+     * URL encoding.
+     * @param s a string to be URL-encoded
+     * @return URL encoding of s using character encoding UTF-8; null if s is null.
+     */
+    public static final String encode(String s) {
+        try {
+            if (s != null)
+                return URLEncoder.encode(s, "UTF-8");
+            else
+                return s;
+        } catch (UnsupportedEncodingException e) {
+            // Java Spec requires UTF-8 be in all Java environments, so this should not happen
+            return s;
+        }
+    }
+
+    /**
+     * URL decoding.
+     * @param s a URL-encoded string to be URL-decoded
+     * @return URL decoded value of s using character encoding UTF-8; null if s is null.
+     */
+    public static final String decode(String s) {
+        try {
+            if (s != null)
+                return URLDecoder.decode(s, "UTF-8");
+            else
+                return s;
+        } catch (UnsupportedEncodingException e) {
+            // Java Spec requires UTF-8 be in all Java environments, so this should not happen
+            return s;
+        }
+    }
+
 
     /**
      * @param string
      * @return
      */
-    public static int stringToInt(String string)
-    {
-        try
-        {
+    public static int stringToInt(String string) {
+        try {
             return Integer.valueOf(string).intValue();
-        }
-        catch (NumberFormatException e)
-        {
+        } catch (NumberFormatException e) {
             mLogger.debug("Invalid Integer:" + string);
         }
         return 0;
     }
     
-    /** 
+    /**
      * Code (stolen from Pebble) to add rel="nofollow" string to all links in HTML.
      */
-    public static String addNofollow(String html) 
-    {
-        if (html == null || html.length() == 0) 
-        {
+    public static String addNofollow(String html) {
+        if (html == null || html.length() == 0) {
             return html;
         }
         Matcher m = mLinkPattern.matcher(html);
         StringBuffer buf = new StringBuffer();
-        while (m.find()) 
-        {
+        while (m.find()) {
             int start = m.start();
             int end = m.end();
             String link = html.substring(start, end);
             buf.append(html.substring(0, start));
-            if (link.indexOf("rel=\"nofollow\"") == -1) 
-            {
+            if (link.indexOf("rel=\"nofollow\"") == -1) {
                 buf.append(
-                    link.substring(0, link.length() - 1) + " rel=\"nofollow\">");
-            } 
-            else 
-            {
+                        link.substring(0, link.length() - 1) + " rel=\"nofollow\">");
+            } else {
                 buf.append(link);
-            }            
+            }
             html = html.substring(end, html.length());
             m = mLinkPattern.matcher(html);
         }
@@ -1122,19 +1044,70 @@
         return buf.toString();
     }
     
-    public static String unescapeHTML(String str) 
-    {
-        return StringEscapeUtils.unescapeHtml(str);
+    /**
+     * Transforms the given String into a subset of HTML displayable on a web
+     * page. The subset includes &lt;b&gt;, &lt;i&gt;, &lt;p&gt;, &lt;br&gt;,
+     * &lt;pre&gt; and &lt;a href&gt; (and their corresponding end tags).
+     *
+     * @param s   the String to transform
+     * @return    the transformed String
+     */
+    public static String transformToHTMLSubset(String s) {
+        
+        if (s == null) {
+            return null;
+        }
+        
+        s = replace(s, OPENING_B_TAG_PATTERN, "<b>");
+        s = replace(s, CLOSING_B_TAG_PATTERN, "</b>");
+        s = replace(s, OPENING_I_TAG_PATTERN, "<i>");
+        s = replace(s, CLOSING_I_TAG_PATTERN, "</i>");
+        s = replace(s, OPENING_BLOCKQUOTE_TAG_PATTERN, "<blockquote>");
+        s = replace(s, CLOSING_BLOCKQUOTE_TAG_PATTERN, "</blockquote>");
+        s = replace(s, BR_TAG_PATTERN, "<br />");
+        s = replace(s, OPENING_P_TAG_PATTERN, "<p>");
+        s = replace(s, CLOSING_P_TAG_PATTERN, "</p>");
+        s = replace(s, OPENING_PRE_TAG_PATTERN, "<pre>");
+        s = replace(s, CLOSING_PRE_TAG_PATTERN, "</pre>");
+        s = replace(s, OPENING_UL_TAG_PATTERN, "<ul>");
+        s = replace(s, CLOSING_UL_TAG_PATTERN, "</ul>");
+        s = replace(s, OPENING_OL_TAG_PATTERN, "<ol>");
+        s = replace(s, CLOSING_OL_TAG_PATTERN, "</ol>");
+        s = replace(s, OPENING_LI_TAG_PATTERN, "<li>");
+        s = replace(s, CLOSING_LI_TAG_PATTERN, "</li>");
+        
+        // HTTP links
+        s = replace(s, CLOSING_A_TAG_PATTERN, "</a>");
+        Matcher m = OPENING_A_TAG_PATTERN.matcher(s);
+        while (m.find()) {
+            int start = m.start();
+            int end = m.end();
+            String link = s.substring(start, end);
+            link = "<" + link.substring(4, link.length() - 4) + ">";
+            s = s.substring(0, start) + link + s.substring(end, s.length());
+            m = OPENING_A_TAG_PATTERN.matcher(s);
+        }
+        
+        // escaped angle brackets
+        s = s.replaceAll("&amp;lt;", "&lt;");
+        s = s.replaceAll("&amp;gt;", "&gt;");
+        s = s.replaceAll("&amp;#", "&#");
+        
+        return s;
     }
     
-    
+    private static String replace(String string, Pattern pattern, String replacement) {
+        Matcher m = pattern.matcher(string);
+        return m.replaceAll(replacement);
+    }
+        
     /**
      * Convert a byte array into a Base64 string (as used in mime formats)
      */
     public static String toBase64(byte[] aValue) {
         
         final String m_strBase64Chars =
-            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
         
         int byte1;
         int byte2;
@@ -1158,3 +1131,4 @@
         return tt.toString();
     }
 }
+