Friday Apr 24, 2009

Cross-domain IFrame Resizing

IFrames are a great way to do a low-tech mashup. The first thing you will want to do after getting an IFrame on your page is to resize it to get rid of the scroll bars that are probably present. A little googling turns up solutions like this,

function resizeFrame(f) {
    f.style.height = f.contentWindow.document.body.scrollHeight+'px';
} 
 ...
<iframe onload="resizeFrame(this)" src="..." ... 

This works great as long as the IFrame source is in the same domain as your client page. If it's not, your page is prevented from getting or setting any attributes of the IFrame. To get around this, you can make a  local proxy to fool your page into thinking that the IFrame is in your domain. For example,

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<%
    public static String scrape(String url) {
        try {
            URL u = new URL(url);
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(
                    u.openStream()));

            String inputLine;
            StringBuffer b = new StringBuffer();
            while ((inputLine = in.readLine()) != null) {
                b.append(inputLine);
            }

            in.close();
            return b.toString();
        } catch (IOException ioe) {
            return "Could not scrape URL: " + ioe;
        } 
    }
%>
<%= scrape(request.getParameter("url"))%> 

Now your IFrame looks like,

<iframe 
  onload="resizeFrame(this)" 
  src="proxy.jsp?url=..."></iframe>

where ... is the page outside your domain. As soon as this page loads into your IFrame you will notice the next problem. Since our proxy is dumb and doesn't rewrite URLs, any relative URLs in the scraped page are broken, including images. To fix this, use the BASE tag in the head of the scraped page,

<html>
  <head>
    <base href="http://opensso.dev.java.net/console"> 
...

This obviously relies on you being able to modify the IFrame source, which is not possible in most situations.

A thought I had was to dynamically modify the base URI of the IFrame. Since the IFrame source is now local I expected that would be possible. Apparently, the base URI (an attribute of the document) is set when the IFrame loads and is read only from then on out. That left me with mucking with the markup before it was processed by the  browser. A bit of code to insert the BASE tag is all that was needed,

public class Scraper {

    private String url;

    public Scraper(String url) {
        this.url = url;
    }

    public String scrape() throws IOException {
        URL u = new URL(url);
        BufferedReader in = new BufferedReader(
                new InputStreamReader(
                u.openStream()));

        String inputLine;
        StringBuffer b = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            b.append(inputLine);
        }

        in.close();

        String base = getBase(url);
        String result;

        if (base != null) {
            result = setBase(b.toString(), base);
        } else {
            result = b.toString();
        }

        return result;
    }

    private static String getBase(String url) {
        try {
            URL u = new URL(url);
            StringBuffer b = new StringBuffer();
            b.append(u.getProtocol());
            b.append("://");
            b.append(u.getHost());
            if (u.getPort() != -1) {
                b.append(":");
                b.append(u.getPort());
            }

            return b.toString();
        } catch (MalformedURLException mfue) {
            return null;
        }
    }

    private static String setBase(String content, String base) {
        // remove base tag if it exists
        Pattern basePattern = Pattern.compile("<base.\*?>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher baseMatcher = basePattern.matcher(content);
        if (baseMatcher.find()) {
            // base is already set
            return content;
        }

        // add new base tag
        Pattern headPattern = Pattern.compile("<head>(.\*?)</head>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher headMatcher = headPattern.matcher(content);

        if (headMatcher.find()) {
            StringBuffer newHead = new StringBuffer();
            newHead.append("<head>\\n");
            newHead.append("<base href=\\"");
            newHead.append(base);
            newHead.append("\\" target=\\"_blank\\"/>\\n");
            newHead.append(headMatcher.group(1));
            newHead.append("\\n");
            newHead.append("</head>\\n");

            content = headMatcher.replaceFirst(newHead.toString());
        }

        return content;
    }
}

This solution is obviously limited in that it relied on you being able to put code on your server. Another solution I found relies only on modifying the source of the IFrame, but I found the hackiness offensive.

About

jtb

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today