/* 
 * Copyright 2000-2004 The Apache Software Foundation. 
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0 
 *  
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
/* 
 * 
 * 
 *  COMPATIBILITY 
 *   
 *      [28.01.2001, RammerI] Tested on W2K, with J2SE, JDK 1.3 
 *      [29.01.2001, RammerI] Tested on W2K, with JDK 1.2.2 
 * 
 * 
 * 
 *  FEATURES 
 *      = Rewriting of <A HREFs, <IMG SRCes, <FORM ACTIONs, <TD BACKGROUNDs, 
 *          <INPUT SRCs, <APPLET CODEBASEs 
 *      = Removal of <SCRIPT>, <STYLE>, <HEAD>, <EMBED>, <OBJECT>, <APPLET>, 
 *          <NOSCRIPT> 
 *  
 **** 
 * Please include the following section in the WebPagePortlet documentation      
 **** 
 * <CODE> 
 * 
 * The following describes how HTML tags are rewritten 
 * 
 * <!-- --> (HTML Comments) 
 *   o Unless otherwise mentioned, comments are striped. 
 *  
 * <A> 
 *   o HREF attribute   - URL merged with base URL (See Note 1) 
 *   o TARGET attribute - Set to "_BLANK" if it does not exist  
 *                        and openInNewWindow = TRUE 
 * <AREA> 
 *   o HREF attribute   - URL merged with base URL (See Note 1) 
 *   o TARGET attribute - Set to "_BLANK" if it does not exist  
 *                        and openInNewWindow = TRUE 
 * <APPLET> 
 *   o Optionally included 
 *   o CODEBASE attribute - Set to the current path if it does 
 *                          not exist. 
 *  
 * <BASE> 
 *   o <HEAD> does NOT have to be included. 
 *   o HREF attribute  - Set the Base URL of the page, but the tag 
 *                       not set in resulting HTML. URL merged with 
 *                       base URL (See Note 1) 
 *  
 * <BODY> 
 *   o Background attribute - Always striped. 
 *  
 * <EMBED> 
 *   o May not work.  Not supported by JDK 1.3/ 
 *  
 * <FORM> 
 *   o ACTION attribute - Set to the current URL if it does 
 *                        not exist. URL merged with base 
 *                        URL (See Note 1) 
 *  
 * <IMG> 
 *   o SRC attribute - URL merged with base URL (See Note 1) 
 *  
 * <INPUT> 
 *   o SRC attribute - URL merged with base URL (See Note 1) 
 *  
 * <LINK> 
 *   o HREF attribute - URL merged with base URL (See Note 1) 
 * 
 * <OBJECT> 
 *   o Optionally included 
 *   o CODEBASE attribute - Set to the current path if it does 
 *                          not exist. URL merged with base 
 *                          URL (See Note 1) 
 *  
 * <SCRIPT> 
 *   o Optionally included 
 *   o Contents may be striped if this tag appears in the <HEAD> 
 *     and the contents are NOT in a comment 
 *   o SRC attribute - URL merged with base URL (See Note 1) 
 *   o Script code that is NOT enclosed in a comment (<!-- -->) 
 *     and in the <HEAD> may NOT be in the resulting HTML.  This 
 *     is related to the HTML parser in included in the JDK  
 *  
 * <TD> 
 *   o BACKGROUND attribute - URL merged with base URL (See Note 1) 
 *  
 * Note 1: URL Merging. 
 *   This is done because the source of the page sent to the 
 *   user's browser is different then source the current page. 
 *   Example: 
 *     Base URL........ http://jakarta.apache.org/jetspeed 
 *     URL............. logo.gif 
 *     Resulting URL... http://jakarta.apache.org/jetspeed/logo.gif 
 *  
 * </CODE> 
 *  KNOWN PROBLEMS 
 * 
 * 
 *  == Seems to have problems with international characters, when the web-pages 
 *     are not downloaded from the original URL but taken from the cache. 
 *     (To reproduce do the following 
 *      1. create a new portlet from the url http://www.sycom.at/default.htm 
 *      2. stop tomcat & restart tomcat 
 *      3. login and customize your page to include this portlet 
 *      4. everything should appear fine, the webpage will show some german  
 *         umlauts 
 *      5. shutdown tomcat and restart it 
 *      6. jetspeed is now taking the HTML not from www.sycom.at, but from the 
 *         cache. Instead of the umlauts, you will see weird characters.  
 * 
 * 
 *  == Does not yet work with XHTML-Pages but only plain-old HTMLs. I.e. Closed 
 *     single tags like <BR /> screw the output up. 
 *       
 * 
 * 
 */ 
//package org.apache.jetspeed.util; 
 
import java.io.Reader; 
import java.io.StringWriter; 
import java.net.MalformedURLException; 
import java.net.URL; 
import java.util.Enumeration; 
import javax.swing.text.html.HTML; 
import javax.swing.text.html.HTMLEditorKit; 
import javax.swing.text.MutableAttributeSet; 
 
 
/** 
 * 
 * @author  Ingo Rammer ([email protected]) 
 * @author <a href="mailto:[email protected]">Santiago Gala</a> 
 * @author <a href="mailto:[email protected]">Paul Spencer</a> 
 * @version 0.2 
 */ 
 
public class HTMLRewriter  
{ 
    /** 
     * Static initialization of the logger for this class 
     */     
     
    private HTMLRewriter.Callback cb = new HTMLRewriter.Callback(); 
     
/** Sets the parameters for the HTMLRewriter 
 * @param removeScript Shall SCRIPT-Tags and their content be removed 
 * @param removeStyle Shall STYLE-Tags and their content be removed 
 * @param removeNoScript Shall NOSCRIPT-Tags and their content be removed 
 * @param removeMeta Shall META-Tags be removed 
 * @param removeApplet Shall APPLET-Tags and their content be removed 
 * @param removeObject Shall OBJECT-Tags and their content be removed 
 * @param removeHead Shall HEAD-Tags and their content be removed 
 * @param removeOnSomething Shall onClick, onBlur, etc. -Attributes be removed 
 */     
    public HTMLRewriter(boolean removeScript, 
                        boolean removeStyle, 
                        boolean removeNoScript, 
                        boolean removeMeta, 
                        boolean removeApplet, 
                        boolean removeObject, 
                        boolean removeHead, 
                        boolean removeOnSomething) { 
        init ( removeScript, 
        removeStyle, 
        removeNoScript, 
        removeMeta, 
        removeApplet, 
        removeObject, 
        removeHead, 
        removeOnSomething, 
        false); 
    } 
         
    /** 
     * Sets the parameters for the HTMLRewriter 
     * @param removeScript Shall SCRIPT-Tags and their content be removed 
     * @param removeStyle Shall STYLE-Tags and their content be removed 
     * @param removeNoScript Shall NOSCRIPT-Tags and their content be removed 
     * @param removeMeta Shall META-Tags be removed 
     * @param removeApplet Shall APPLET-Tags and their content be removed 
     * @param removeObject Shall OBJECT-Tags and their content be removed 
     * @param removeHead Shall HEAD-Tags and their content be removed 
     * @param removeOnSomething Shall onClick, onBlur, etc. -Attributes be removed 
     */ 
    public HTMLRewriter(boolean removeScript, 
                        boolean removeStyle, 
                        boolean removeNoScript, 
                        boolean removeMeta, 
                        boolean removeApplet, 
                        boolean removeObject, 
                        boolean removeHead, 
                        boolean removeOnSomething, 
                        boolean openInNewWindow ) { 
        init ( removeScript, 
        removeStyle, 
        removeNoScript, 
        removeMeta, 
        removeApplet, 
        removeObject, 
        removeHead, 
        removeOnSomething, 
        openInNewWindow );  
    } 
 
    /** 
     * Sets the parameters for the HTMLRewriter 
     * 
     * @param removeScript Shall SCRIPT-Tags and their content be removed 
     * @param removeStyle Shall STYLE-Tags and their content be removed 
     * @param removeNoScript Shall NOSCRIPT-Tags and their content be removed 
     * @param removeMeta Shall META-Tags be removed 
     * @param removeApplet Shall APPLET-Tags and their content be removed 
     * @param removeObject Shall OBJECT-Tags and their content be removed 
     * @param removeHead Shall HEAD-Tags and their content be removed 
     * @param removeOnSomething Shall onClick, onBlur, etc. -Attributes be removed 
     * @param openInNewWindow Shall links set Target="_blank" 
     */ 
    private void init (boolean removeScript, 
                       boolean removeStyle, 
                       boolean removeNoScript, 
                       boolean removeMeta, 
                       boolean removeApplet, 
                       boolean removeObject, 
                       boolean removeHead, 
                       boolean removeOnSomething, 
                       boolean openInNewWindow )  
    { 
        cb.removeScript = removeScript; 
        cb.removeStyle = removeStyle;  
        cb.removeNoScript = removeNoScript; 
        cb.removeMeta = removeMeta; 
        cb.removeApplet = removeApplet; 
        cb.removeObject = removeObject; 
        cb.removeHead = removeHead; 
        cb.removeOnSomething = removeOnSomething;     
        cb.openInNewWindow = openInNewWindow;     
    } 
     
    /** 
     * Does the conversion of the HTML 
     * @param HTMLrdr Reader for HTML to be converted 
     * @param BaseUrl URL from which this HTML was taken. We be the base-Url 
     * for all URL-rewritings. 
     * @throws MalformedURLException If the BaseUrl is not a valid URL or if an URL inside 
     * the document could not be converted. Should not happen 
     * normally, even in badly formatted HTML. 
     * @return HTML-String with rewritten URLs and removed (according 
     * to constructor-settings) tags 
     */ 
    public synchronized String convertURLs(Reader HTMLrdr, String BaseUrl) throws MalformedURLException 
    { 
        HTMLEditorKit.Parser parse = new HTMLRewriter.ParserGetter().getParser();         
        String res =""; 
        try { 
            if (cb.result != null) { 
              cb.result = null; 
              cb.result = new StringWriter(); 
            } 
            cb.baseUrl = new URL(BaseUrl); 
            parse.parse(HTMLrdr,cb,true); 
            res = cb.getResult();  
        } catch (Exception e) 
        { 
            //logger.error( "Unable to convertURLS", e ); 
            throw new MalformedURLException(e.toString()); 
        } 
        return res; 
    } 
 
     
    /** That Class is needed, because getParser is protected and therefore  
     *  only accessibly by a subclass 
     */ 
    class ParserGetter extends HTMLEditorKit { 
    /** This is needed, because getParser is protected 
     * @return Html Parser 
     */         
      public HTMLEditorKit.Parser getParser(){ 
        return super.getParser(); 
      } 
    }  
 
     
    class Callback extends HTMLEditorKit.ParserCallback { 
 
        // the base-url of which the given html comes from. 
        private URL baseUrl; 
 
        // either handling of <FORM> is buggy, or I made some weird mistake ...  
        // ... JDK 1.3 sends double "</form>"-tags on closing <form> 
        private boolean inForm = false;  
 
         
        // when in multi-part ignored tags (like <script> foobar </script>,  
        // <style> foobar </style>, a counter for the nesting-level will be 
        // kept here 
        private int ignoreLevel = 0; 
         
        private boolean removeScript = true; 
        private boolean removeStyle = true;  
        private boolean removeNoScript = true; 
        private boolean removeMeta = true; 
        private boolean removeApplet = true; 
        private boolean removeObject = true; 
        private boolean removeHead = true; 
        private boolean openInNewWindow = false; 
         
        // remove the onClick=, onBlur=, etc. - Attributes 
        private boolean removeOnSomething = true; 
         
        private boolean inScript = false; 
        private boolean inStyle = false; 
         
        private StringWriter result = new StringWriter(); 
         
        private Callback () { 
        } 
         
         
        private Callback addToResult(Object txt) 
        { 
            // to allow for implementation using Stringbuffer or StringWriter 
            // I don't know yet, which one is better in this case 
            if (ignoreLevel > 0) return this; 
 
            try { 
                result.write(txt.toString()); 
            } catch (Exception e) { /* ignore */ } 
            return this; 
        } 
 
        private Callback addToResult(char[] txt) 
        { 
            if (ignoreLevel > 0) return this; 
 
            try { 
                result.write(txt); 
            } catch (Exception e) { /* ignore */ } 
            return this; 
        } 
         
        /** Accessor to the Callback's content-String 
         * @return Cleaned and rewritten HTML-Content 
         */         
        public String getResult() { 
            try { 
                result.flush(); 
            } catch (Exception e) { /* ignore */ } 
             
            // WARNING: doesn't work, if you remove " " + ... but don't know why 
            String res = " " + result.toString();  
 
            return res; 
        } 
         
        
        public void flush() throws javax.swing.text.BadLocationException { 
            // nothing to do here ... 
        } 
 
        /**  
         * Because Scripts and Stlyle sometimes are defined in comments, thoese 
         * will be written. Otherwise comments are removed 
         */ 
        public void handleComment(char[] values,int param) { 
            if ( !( inStyle || inScript)) 
                return; 
 
            try { 
                result.write("<!--"); 
                result.write(values); 
                result.write("-->"); 
            } catch (Exception e) { /* ignore */ } 
          // we ignore them  
        } 
 
        public void handleEndOfLineString(java.lang.String str) { 
            addToResult("\n"); 
        } 
 
        public void handleError(java.lang.String str,int param) { 
            // ignored 
        } 
 
        public void handleSimpleTag(HTML.Tag tag,MutableAttributeSet attrs,int param) { 
            if (removeMeta && (tag == HTML.Tag.META)) { 
                return; 
            }             
            appendTagToResult(tag,attrs);         
        } 
 
        public void handleStartTag(HTML.Tag tag,  MutableAttributeSet attrs, int position) { 
            appendTagToResult(tag,attrs); 
        } 
 
        public void handleEndTag(HTML.Tag tag, int position) { 
            if ((tag ==HTML.Tag.FORM) && (inForm)) {  
                // form handling seems to be buggy 
                addToResult("</").addToResult(tag).addToResult(">"); 
                inForm = false; 
            } else if (tag == HTML.Tag.FORM) { 
                // do nothing! ... i.e. we are now outside of any <FORM>, so a 
                // closing </form> is not really needed ...  
            } else { 
                addToResult("</").addToResult(tag).addToResult(">"); 
            } 
             
             
            if ( (removeScript == false) && (tag == HTML.Tag.SCRIPT)) { 
                inScript = false; 
            } else if ( (removeStyle == false) && (tag == HTML.Tag.STYLE)) { 
                inStyle = false; 
            } 
 
            if ( removeScript && (tag == HTML.Tag.SCRIPT)) { 
                ignoreLevel --; 
            } else if ( removeStyle && (tag == HTML.Tag.STYLE)) { 
                ignoreLevel --; 
            } else if ( removeHead && (tag == HTML.Tag.HEAD)) { 
                ignoreLevel --; 
            } else if ( removeApplet && (tag == HTML.Tag.APPLET)) { 
                ignoreLevel --; 
            } else if ( removeObject && (tag == HTML.Tag.OBJECT)) { 
                ignoreLevel --; 
            } else if ( removeNoScript && (tag.toString().equalsIgnoreCase("NOSCRIPT"))) { 
                ignoreLevel --; 
            } 
        } 
   
        private void appendTagToResult(HTML.Tag tag, MutableAttributeSet attrs) { 
 
            if (tag.toString().equalsIgnoreCase("__ENDOFLINETAG__")) { 
                // jdk 1.2.2 places a tag <__ENDOFLINETAG__> in the result ... 
                // we don't want this one 
                return; 
            } 
             
            if (tag.toString().equalsIgnoreCase("__IMPLIED__")) { 
                // jdk 1.3 places a tag <__IMPLIED__> in the result ... 
                // we don't want this one 
                return; 
            } 
             
            convertURLS(tag,attrs); 
            Enumeration e = attrs.getAttributeNames(); 
            if (tag == HTML.Tag.BASE) 
                return; 
             
            addToResult("<").addToResult(tag); 
            while (e.hasMoreElements()) { 
                Object attr = e.nextElement(); 
                String attrName = attr.toString(); 
                String value = attrs.getAttribute(attr).toString(); 
 
                // include attribute only when Not(RemoveOnSomething = True and starts with "on") 
                if (!(removeOnSomething 
                && attrName.toLowerCase().startsWith("on") 
                && (attrName.length() > 2))) { 
                    // Attribute included 
                    addToResult(" ").addToResult(attr).addToResult("=\"") 
                    .addToResult(value).addToResult("\""); 
                } 
            } 
            addToResult(">"); 
        } 
                    
        /** Here the magic happens. 
         * 
         * If someone wants new types of URLs to be rewritten, add them here 
         * @param tag TAG from the Callback-Interface 
         * @param attrs Attribute-Set from the Callback-Interface 
         */ 
         
        private void convertURLS( HTML.Tag tag, MutableAttributeSet attrs ) { 
 
           // first we do an URL-rewrite on different tags 
             
            if (tag == HTML.Tag.A) { 
                if (attrs.getAttribute(HTML.Attribute.HREF) != null) { 
                    // ---- CHECKING <A HREF 
                    addConvertedAttribute( HTML.Attribute.HREF, 
                    attrs ); 
                } 
                if ((attrs.getAttribute(HTML.Attribute.TARGET) == null) && cb.openInNewWindow) { 
                    attrs.addAttribute(HTML.Attribute.TARGET, "_BLANK"); 
                } 
            } else if (tag == HTML.Tag.AREA) { 
                if (attrs.getAttribute(HTML.Attribute.HREF) != null) { 
                    // ---- CHECKING <A HREF 
                    addConvertedAttribute( HTML.Attribute.HREF, 
                    attrs ); 
                } 
                if ((attrs.getAttribute(HTML.Attribute.TARGET) == null) && cb.openInNewWindow) { 
                    attrs.addAttribute(HTML.Attribute.TARGET, "_BLANK"); 
                } 
            } else if (((tag == HTML.Tag.IMG) || (tag == HTML.Tag.INPUT) || (tag == HTML.Tag.SCRIPT)) 
                         && (attrs.getAttribute(HTML.Attribute.SRC) != null)) { 
                // ---- CHECKING <IMG SRC & <INPUT SRC 
                addConvertedAttribute( HTML.Attribute.SRC, 
                                       attrs ); 
            } else if (tag == HTML.Tag.LINK) { 
                if (attrs.getAttribute(HTML.Attribute.HREF) != null) { 
                    // ---- CHECKING <LINK HREF 
                    addConvertedAttribute( HTML.Attribute.HREF, 
                    attrs ); 
                } 
            } else if ( tag == HTML.Tag.APPLET ) { 
                // ---- CHECKING <APPLET CODEBASE= 
                if (attrs.getAttribute(HTML.Attribute.CODEBASE) == null) { 
                    int endOfPath = baseUrl.toString().lastIndexOf("/"); 
                    attrs.addAttribute(HTML.Attribute.CODEBASE,  
                                       baseUrl.toString().substring(0,endOfPath +1)); 
                } else { 
                    addConvertedAttribute( HTML.Attribute.CODEBASE, attrs ); 
                } 
            } else if (tag == HTML.Tag.OBJECT) { 
                // ---- CHECKING <OBJECT CODEBASE= 
                if (attrs.getAttribute(HTML.Attribute.CODEBASE) == null) { 
                    int endOfPath = baseUrl.toString().lastIndexOf("/"); 
                    attrs.addAttribute(HTML.Attribute.CODEBASE,  
                                       baseUrl.toString().substring(0,endOfPath +1)); 
                } else { 
                    addConvertedAttribute( HTML.Attribute.CODEBASE, attrs ); 
                } 
            } else if (tag == HTML.Tag.BODY) { 
                if (attrs.getAttribute(HTML.Attribute.BACKGROUND) != null) { 
                    // background images are applied to the ENTIRE page, this remove them! 
                    attrs.removeAttribute( HTML.Attribute.BACKGROUND); 
                } 
            } else if (tag == HTML.Tag.BASE) { 
                if (attrs.getAttribute(HTML.Attribute.HREF) != null) { 
                    try { 
                        baseUrl = new URL(attrs.getAttribute(HTML.Attribute.HREF).toString()); 
                    } catch (Throwable t) { 
                       // logger.error( "HTMLRewriter: Setting BASE="  
                       // + attrs.getAttribute(HTML.Attribute.HREF).toString() 
                       // + t.getMessage()); 
                    } 
                    attrs.removeAttribute(HTML.Attribute.HREF); 
                } 
            } else if (tag == HTML.Tag.FORM) { 
                // ---- CHECKING <FORM ACTION= 
                  inForm = true; // buggy <form> handling in jdk 1.3  
                  if (attrs.getAttribute(HTML.Attribute.ACTION) == null) { 
                      //self referencing <FORM> 
                       attrs.addAttribute(HTML.Attribute.ACTION, 
                                          baseUrl.toString()); 
                  } else { 
                        addConvertedAttribute( HTML.Attribute.ACTION, 
                                               attrs ); 
                  } 
            } else if (tag == HTML.Tag.TD) { 
                // ---- CHECKING <TD BACKGROUND= 
                  if (! (attrs.getAttribute(HTML.Attribute.BACKGROUND) == null)) { 
                      addConvertedAttribute( HTML.Attribute.BACKGROUND, 
                                             attrs ); 
                  } 
            } 
 
             
            // then we check for ignored tags ... 
            // btw. I know, that this code could be written in a shorter way, but 
            // I think it's more readable like this ... 
 
            // don't forget to add changes to  handleEndTag() as well, else  
            // things will get screwed up! 
             
            if ( (removeScript == false) && (tag == HTML.Tag.SCRIPT)) { 
                inScript = true; 
            } else if ( (removeStyle == false) && (tag == HTML.Tag.STYLE)) { 
                inStyle = true; 
            } 
 
            if ( removeScript && (tag == HTML.Tag.SCRIPT)) { 
                  ignoreLevel ++; 
            } else if ( removeStyle && (tag == HTML.Tag.STYLE)) { 
                  ignoreLevel ++; 
            } else if ( removeHead && (tag == HTML.Tag.HEAD)) { 
                  ignoreLevel ++; 
            } else if ( removeApplet && (tag == HTML.Tag.APPLET)) { 
                  ignoreLevel ++; 
            } else if ( removeObject && (tag == HTML.Tag.OBJECT)) { 
                  ignoreLevel ++; 
            } else if (removeNoScript && (tag.toString().equalsIgnoreCase("NOSCRIPT"))) { 
                  ignoreLevel ++; 
            } 
        } 
 
        /** 
         * 
         * Converts the given attribute to base URL, if not null 
         * 
         */ 
        private void addConvertedAttribute( HTML.Attribute attr, 
                                            MutableAttributeSet attrs ) { 
            if( attrs.getAttribute( attr ) != null ) { 
                String attrSource =  attrs.getAttribute( attr ).toString(); 
                attrs.addAttribute( attr, 
                                    generateNewUrl( attrSource ) ); 
            } 
        } 
               
               
        private String generateNewUrl(String oldURL) { 
            try { 
                URL x = new URL(baseUrl,oldURL); 
                return x.toString(); 
            } catch (Throwable t) { 
                if (oldURL.toLowerCase().startsWith("javascript:")) { 
                    return oldURL; 
                } 
                //logger.error( "HTMLRewriter: Setting BASE=" 
                //+ baseUrl 
                //+ " Old = " 
                //+ oldURL 
                //+ t.getMessage()); 
                return oldURL; // default behaviour ... 
            } 
        } 
 
        public void handleText(char[] values,int param) { 
            addToResult(values); 
        } 
    } 
} 
 
    
     
     
   |     
 
 |