overhauled cookie blocking and p3p policy formatting, support for Firefox 10
authorDave Raggett <dsr@w3.org>
Sat, 03 Dec 2011 22:21:56 +0000
changeset 24 056ef2be1351
parent 23 8dda9be04c23
child 25 2d9d04524c09
overhauled cookie blocking and p3p policy formatting, support for Firefox 10
dashboard/build.sh
dashboard/chrome/content/assess.js
dashboard/chrome/content/dashboard.js
dashboard/chrome/content/dashboard.xul
dashboard/chrome/content/database.js
dashboard/chrome/content/misc.js
dashboard/chrome/content/observer.js
dashboard/chrome/content/overlay.js
dashboard/chrome/content/p3p.js
dashboard/chrome/locale/en-US/dashboard.dtd
dashboard/dashboard.xpi
dashboard/install.rdf
--- a/dashboard/build.sh	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/build.sh	Sat Dec 03 22:21:56 2011 +0000
@@ -7,5 +7,5 @@
 # to include the readme and build script etc.
 touch $TARGET.xpi
 rm $TARGET.xpi
-zip -r ../$TARGET.xpi install.rdf chrome.manifest chrome defaults readme.txt
+zip -r ../$TARGET.xpi install.rdf chrome.manifest chrome defaults readme.txt build.sh
 mv ../$TARGET.xpi .
--- a/dashboard/chrome/content/assess.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/assess.js	Sat Dec 03 22:21:56 2011 +0000
@@ -53,24 +53,6 @@
       assess.sharing_change();
     }, false);
 
-    let norton_button = document.getElementById("exec_norton_button");
-    norton_button.addEventListener("command", function ()
-    {
-      assess.check_norton();
-    }, false);
-
-    let freetrust_button = document.getElementById("exec_freetrust_button");
-    freetrust_button.addEventListener("command", function ()
-    {
-      assess.check_freetrust();
-    }, false);
-
-    let truste_button = document.getElementById("exec_truste_button");
-    freetrust_button.addEventListener("command", function ()
-    {
-      assess.check_truste();
-    }, false);
-
     let simplify = document.getElementById("simplify_choices");
     simplify.addEventListener("command", function ()
     {
@@ -212,24 +194,177 @@
     }
   },
 
-  check_norton: function ()
+  record_prefs: function (evt)
   {
-    window.open("http://safeweb.norton.com/report/show?url="+this.host);
+    // special effort to clear cookies
+    // when setting preferences
+    let id = this.getAttribute("id");
+    let checked = this.getAttribute("checked");
+    let host = assess.host;
+
+    if (id == "prefs_all_lasting_cookies")
+    {
+      if (checked == "true")
+         assess.query_3rd_parties(host,
+           function (host, parties, offsite)
+           {
+             assess.purge_cookies(host, 1<<5, parties, offsite);
+           });
+    }
+    else if (id == "prefs_ext_3rd_cookies" || id == "thoughtful")
+    {
+      if (checked == "true")
+         assess.query_3rd_parties(host,
+           function (host, parties, offsite)
+           {
+             assess.purge_cookies(host, 1<<4, parties, offsite);
+           });
+    }
+    else if (id == "paranoid")
+    {
+      if (checked == "true")
+         assess.query_3rd_parties(host,
+           function (host, parties, offsite)
+           {
+             assess.purge_cookies(host, (1<<4 | 1<<5), parties, offsite);
+           });
+    }
+
+    assess.save_prefs(host);
   },
 
-  check_freetrust: function ()
+  // query for list of 3rd parties for this site, assumes that
+  // database.log_3rd_parties(status); has already taken effect
+  query_3rd_parties: function (host, action)
   {
-    window.open("http://freetrustseal.com/trust.php?site="+this.host);
+    let database = window.database;
+    let query = "SELECT DISTINCT third_party, offsite FROM  parties " +
+                "WHERE page_host = " + database.quote(host);
+
+    let handler = 
+    {
+      parties: [],
+      offsite: [],
+
+      process_row: function (row)
+      {
+        this.parties.push(row.getResultByName("third_party"));
+        this.offsite.push(row.getResultByName("offsite"));        
+      },
+
+      finalize: function () 
+      {
+        action(host, this.parties, this.offsite);
+      }
+    };
+
+    database.select(query, database.db, handler);
   },
 
-  check_truste: function ()
+  // code copied from misc.rate_cookies()
+  purge_cookies: function (host, prefs, parties, offsite)
   {
-    window.open("http://clicktoverify.truste.com/pvr.php?page=validate&url="+this.host);
-  },
+    let cm = Components.classes['@mozilla.org/cookiemanager;1'].
+      getService(Components.interfaces.nsICookieManager2);
 
-  record_prefs: function (evt)
-  {
-    assess.save_prefs(assess.host);
+    let cookies = cm.enumerator;
+    let lasting_cookies = [];
+    let session_cookies = [];
+    let int_3rd_lasting_cookies = [];
+    let int_3rd_session_cookies = [];
+    let ext_3rd_lasting_cookies = [];
+    let ext_3rd_session_cookies = [];
+
+    // iterate through cookiemanager's list of current cookies
+    // and check against first and all third party sites, this
+    // would require *many* database queries using SQLite
+    while (cookies.hasMoreElements())
+    {
+      let cookie = cookies.getNext();
+      cookie.QueryInterface(Components.interfaces.nsICookie2);
+
+      // cookie matching web page host
+      if (host.indexOf(cookie.host) >= 0)
+      {
+        if (cookie.isSession)
+          session_cookies.push(cookie);
+        else
+          lasting_cookies.push(cookie);
+      }
+      else  // check for 3rd party cookies
+      {
+        // pump up the cost!
+        for (let i in parties)
+        {
+          if (parties[i].indexOf(cookie.host) >= 0)
+          {
+            if (offsite[i] == 2)
+            {
+              if (cookie.isSession)
+                ext_3rd_session_cookies.push(cookie);
+              else
+                ext_3rd_lasting_cookies.push(cookie);
+            }
+            else
+            {
+              if (cookie.isSession)
+                int_3rd_session_cookies.push(cookie);
+              else
+                int_3rd_lasting_cookies.push(cookie);
+            }
+          }
+        }
+      }
+    }
+
+    let int_3rd_parties = 0;
+    let ext_3rd_parties = 0;
+
+    for (let i = 0; i < offsite.length; ++i)
+    {
+      if (offsite[i] == 2)
+        ++ext_3rd_parties;
+      else
+        ++int_3rd_parties;
+    }
+
+    if (prefs & (1<<5))  // purge all lasting cookies
+    {
+      for (let i = 0; i < lasting_cookies.length; ++i)
+      {
+        cookie = lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+
+      for (let i = 0; i < int_3rd_lasting_cookies.length; ++i)
+      {
+        cookie = int_3rd_lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+
+      for (let i = 0; i < ext_3rd_lasting_cookies.length; ++i)
+      {
+        cookie = ext_3rd_lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+    }
+    else
+    {
+      if (prefs & (1<<4))  // purge all 3rd party cookies
+      {
+        for (let i = 0; i < ext_3rd_session_cookies.length; ++i)
+        {
+          cookie = ext_3rd_session_cookies[i];
+          cm.remove(cookie.host, cookie.name, cookie.path, false);
+        }
+
+        for (let i = 0; i < ext_3rd_lasting_cookies.length; ++i)
+        {
+          cookie = ext_3rd_lasting_cookies[i];
+          cm.remove(cookie.host, cookie.name, cookie.path, false);
+        }
+      }
+    }
   },
 
   refresh_prefs: function (host)
--- a/dashboard/chrome/content/dashboard.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/dashboard.js	Sat Dec 03 22:21:56 2011 +0000
@@ -334,7 +334,8 @@
   // note that the doc uri may differ from the uri passed here
   // which is obtained from the onLocationChange event
   update_current_site: function (status) {
-    this.assess.update_current_site(status);
+    console.log("dashboard.update_current_site: " + status.page_host);
+    assess.update_current_site(status);
   },
 
   show_status: function ()
--- a/dashboard/chrome/content/dashboard.xul	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/dashboard.xul	Sat Dec 03 22:21:56 2011 +0000
@@ -152,8 +152,8 @@
       <tabpanel id="dashboard_current" class="normal">
         <vbox flex="1" style="overflow:auto; padding:10px">
         <html:div id="p3p_policy"/>
+        <vbox  id="site_info">
         <html:h3>&dashboard.dialog.current.heading;</html:h3>
-        <vbox  id="site_info">
         <description>
         <html:p>&dashboard.dialog.cursite.descr;</html:p>
         <html:h2 id="curhost">&dashboard.dialog.cursite.unavailable;</html:h2>
@@ -245,13 +245,6 @@
           <html:p id="context_help_label"/>
         </panel>
         </description>
-        <html:hr/>
-        <html:p>&dashboard.dialog.cursite.test;</html:p>
-        <hbox>
-        <button id="exec_norton_button" label="&dashboard.dialog.cursite.test.norton.label;"/>
-        <button id="exec_freetrust_button" label="&dashboard.dialog.cursite.test.freetrust.label;"/>
-        <button id="exec_truste_button" label="&dashboard.dialog.cursite.test.truste.label;"/>
-        </hbox>
         </vbox>
         </vbox>
       </tabpanel>
--- a/dashboard/chrome/content/database.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/database.js	Sat Dec 03 22:21:56 2011 +0000
@@ -823,10 +823,10 @@
   {
     var prefs;
 
-    var sql = "SELECT prefs FROM site_info WHERE host = '" +
+    let sql = "SELECT prefs FROM site_info WHERE host = '" +
               host + "'";
 
-    var dataset = this.sync_get(sql, db);
+    let dataset = this.sync_get(sql, db);
 
     if (dataset && dataset.length > 0)
       prefs = dataset[0]["prefs"];
--- a/dashboard/chrome/content/misc.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/misc.js	Sat Dec 03 22:21:56 2011 +0000
@@ -400,53 +400,59 @@
   // cookies covers current site and 3rd parties
   rate_site: function (browser)
   {
-    var uri = browser.currentURI;
-    var status = browser.dashboard_status;
+    let uri = browser.currentURI;
+    let status = browser.dashboard_status;
 
     // status is null for about:* pages
-    var host = (status ? status.page_host : null);
+    let host = (status ? status.page_host : null);
+    console.log("misc.rate_site: " + host);
     
     if (host)
     {
-      this.query_3rd_parties(browser, host);
-      this.query_p3p(browser, host);
-      this.query_geo(browser, host);
+      // synchronous functions
       this.html5_ping(browser);
-      this.query_dom_storage(browser);
+      this.spot_web_bugs(browser);
 
-      // allow page scripts to do their evil work before check
-      setTimeout( function ()
-      {
-        dashboard_overlay.misc.spot_web_bugs(browser);
-      }, 10 );
+      // prepare continuation, count is number of processes 
+      // before calling misc.update_dashboard(browser);
+      let counter = { "count": 4 };
+
+      // asynchronous involving db access
+      this.query_3rd_parties(browser, host, counter);
+      this.query_p3p(browser, host, counter);
+      this.query_geo(browser, host, counter);
+      this.query_dom_storage(browser, counter);
     }
     else // treat non-http pages as cool
     {
       let primelife_button = document.getElementById("dashboard-toolbar-button");
       primelife_button.style.listStyleImage = 
              'url("chrome://dashboard-common/skin/glasses-cool.png")';
-    }
-
-    let context = dashboard_overlay.context;
-    let dialog = context.dashboard_dialog;
-
-    // check if this is the current browser to avoid changing dashboard if it's not
-    if (dialog && dialog.dashboard && browser == dashboard_overlay.current_browser())
-    {
-      dialog.dashboard_overlay = dashboard_overlay;
-      var update = dialog.dashboard.update_current_site;
 
-      // some of the above database operations are asynchronous
-      // and need time to complete before we display the results
-      // query p3p has its own means to refresh dashboard display
-      // since it involves external network traffic for policies
-      // 200mS should be plenty for SQLite database operations.
-      setTimeout(function ()
+      this.update_dashboard(browser);
+    }
+  },
+
+  // update dashboard current site user interface
+  // introduce delay to allow change in browser to take effect
+  update_dashboard: function (browser)
+  {
+    setTimeout(function ()
+    {
+      dashboard_overlay.misc.check_rating(browser);
+
+      let status = browser.dashboard_status;
+      let context = dashboard_overlay.context;
+      let dialog = context.dashboard_dialog;
+      console.log("update_dashboard: " + status.page_host);
+
+      // check if this is the current browser to avoid changing dashboard if it's not
+      if (dialog && dialog.dashboard && browser == dashboard_overlay.current_browser())
       {
-        if (status)
-          update(status);
-      }, 200);
-    }
+        dialog.dashboard_overlay = dashboard_overlay;
+        dialog.dashboard.update_current_site(status);
+      }
+    }, 10);
   },
 
   check_rating: function (browser)
@@ -488,8 +494,9 @@
     singleton_window.dashboard_icon = primelife_button.style.listStyleImage;
   },
 
-  // query for list of 3rd parties for this site
-  query_3rd_parties: function (browser, host)
+  // query for list of 3rd parties for this site, assumes that
+  // database.log_3rd_parties(status); has already taken effect
+  query_3rd_parties: function (browser, host, counter)
   {
     let database = dashboard_overlay.database;
     let query = "SELECT DISTINCT third_party, offsite FROM  parties " +
@@ -497,7 +504,6 @@
 
     let handler = 
     {
-      browser: browser,
       parties: [],
       offsite: [],
 
@@ -510,13 +516,16 @@
       finalize: function () 
       {
         dashboard_overlay.misc.rate_cookies(browser, this.parties, this.offsite);
+
+        if (--counter.count <= 0)
+          dashboard_overlay.misc.update_dashboard(browser);
       }
     };
 
     database.select(query, database.db, handler);
   },
 
-  query_dom_storage: function (browser)
+  query_dom_storage: function (browser, counter)
   {
     let database = dashboard_overlay.database;
     let status = browser.dashboard_status;
@@ -549,6 +558,9 @@
       finalize: function ()
       {
         status.dom_storage = this.keys.length;
+
+        if (--counter.count <= 0)
+          dashboard_overlay.misc.update_dashboard(browser);
       }
     };
 
@@ -559,6 +571,8 @@
   // kinds for first party and third parties
   // then do same check for flash supercookies
   // linear in *total* number of cookies for all hosts
+  // it may be faster(?) to scan separately for cookies
+  // for the host and for each 3rd party
   rate_cookies: function (browser, parties, offsite) 
   {
     let cm = Components.classes['@mozilla.org/cookiemanager;1'].
@@ -582,6 +596,7 @@
       let cookie = cookies.getNext();
       cookie.QueryInterface(Components.interfaces.nsICookie2);
 
+      // cookie matching web page host
       if (host.indexOf(cookie.host) >= 0)
       {
         if (cookie.isSession)
@@ -589,7 +604,7 @@
         else
           lasting_cookies.push(cookie);
       }
-      else
+      else  // check for 3rd party cookies
       {
         // pump up the cost!
         for (let i in parties)
@@ -660,10 +675,61 @@
     status.int_3rd_party_flash_cookies = int_3rd_fso.length;
     status.ext_3rd_party_flash_cookies = ext_3rd_fso.length;
 
-    this.check_rating(browser);
+    // get prefs and purge unwanted cookies
+    let database = dashboard_overlay.database;
+    let prefs = database.get_prefs(host, database.db);
+    let cookie = null;
+    console.log("rate_cookies: prefs = " + prefs);
+
+    if (prefs & (1<<5))  // purge all lasting cookies
+    {
+      for (let i = 0; i < lasting_cookies.length; ++i)
+      {
+        cookie = lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+
+      for (let i = 0; i < int_3rd_lasting_cookies.length; ++i)
+      {
+        cookie = int_3rd_lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+
+      for (let i = 0; i < ext_3rd_lasting_cookies.length; ++i)
+      {
+        cookie = ext_3rd_lasting_cookies[i];
+        cm.remove(cookie.host, cookie.name, cookie.path, false);
+      }
+
+      if (prefs & (1<<4))  // purge all 3rd party cookies
+      {
+        for (let i = 0; i < ext_3rd_session_cookies.length; ++i)
+        {
+          cookie = ext_3rd_session_cookies[i];
+          cm.remove(cookie.host, cookie.name, cookie.path, false);
+        }
+      }
+    }
+    else
+    {
+      if (prefs & (1<<4))  // purge all 3rd party cookies
+      {
+        for (let i = 0; i < ext_3rd_session_cookies.length; ++i)
+        {
+          cookie = ext_3rd_session_cookies[i];
+          cm.remove(cookie.host, cookie.name, cookie.path, false);
+        }
+
+        for (let i = 0; i < ext_3rd_lasting_cookies.length; ++i)
+        {
+          cookie = ext_3rd_lasting_cookies[i];
+          cm.remove(cookie.host, cookie.name, cookie.path, false);
+        }
+      }
+    }
   },
 
-  query_p3p: function (browser, host)
+  query_p3p: function (browser, host, counter)
   {
     let database = dashboard_overlay.database;
     let query = "SELECT p3p FROM site_info WHERE host = " + database.quote(host);
@@ -679,6 +745,9 @@
       finalize: function ()
       {
         dashboard_overlay.misc.rate_p3p(browser, this.data);
+
+        if (--counter.count <= 0)
+          dashboard_overlay.misc.update_dashboard(browser);
       }
     };
 
@@ -691,7 +760,7 @@
     this.check_rating(browser);
   },
 
-  query_geo: function (browser, host)
+  query_geo: function (browser, host, counter)
   {
     let database = dashboard_overlay.database;
     let query = "SELECT permission FROM moz_hosts WHERE type = 'geo' AND host = "
@@ -709,6 +778,9 @@
       finalize: function ()
       {
         dashboard_overlay.misc.rate_geo(this.browser, this.data);
+
+        if (--counter.count <= 0)
+          dashboard_overlay.misc.update_dashboard(browser);
       }
     };
 
--- a/dashboard/chrome/content/observer.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/observer.js	Sat Dec 03 22:21:56 2011 +0000
@@ -59,6 +59,7 @@
                     .getService(Components.interfaces.nsIObserverService);
       observerService.addObserver(this, "http-on-modify-request", false);
       observerService.addObserver(this, "http-on-examine-response", false);
+      observerService.addObserver(this, "cookie-changed", false);
 
       // register content policy
 
@@ -95,6 +96,7 @@
 
       observerService.removeObserver(this, "http-on-examine-response");
       observerService.removeObserver(this, "http-on-modify-request");
+      observerService.addObserver(this, "cookie-changed", false);
 
       // is it sensible to delete the category entry here?
       let category_manager = Cc["@mozilla.org/categorymanager;1"]
@@ -218,18 +220,16 @@
 
       let offsite = 0;
       let block_3rd_party_cookie = false;
-      let block_lasting_cookies = false;
       let block_3rd_parties = false;
 
-      // the experimental "do not track" headers, as explained in
-      // http://paranoia.dubfire.net/2011/01/history-of-do-not-track-header.html
+      // the "do not track" header, as defined in
+      // http://www.w3.org/Submission/web-tracking-protection/
       if (prefs & (1 << 15))
       {
-        httpChannel.setRequestHeader("X-Behavioral-Ad-Opt-Out", "1", false);
-        httpChannel.setRequestHeader("X-Do-Not-Track", "1", false);
+        httpChannel.setRequestHeader("DNT", "1", false);
       }
 
-      block_lasting_cookies = prefs & (1<<5);
+      // unwanted cookies are purged after page has loaded
 
       // 3 from bit position in assess:save_prefs   
       block_3rd_parties = prefs & (1<<3);
@@ -400,12 +400,12 @@
     {
       let httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
 
-    let type = "";
-    try {
-      type = httpChannel.getResponseHeader("Content-Type");
-    } catch (e) {
-      type = "undefined";
-    } 
+      let type = "";
+      try {
+        type = httpChannel.getResponseHeader("Content-Type");
+      } catch (e) {
+        type = "undefined";
+      } 
 
       console.log("loading (" + httpChannel.responseStatus +  ") " + type + " from " + httpChannel.URI.spec);
 
@@ -423,10 +423,15 @@
           let rp = dashboard_overlay.host_prefs(target.host);
           let never = rp ? rp & (1 <<2) : false ; // never block content from this site
 
+          let block_lasting_cookies = status.prefs & (1<<5);
           let block_3rd_parties = status.prefs & (1<<3);
           let block_flash = status.prefs & (1<<12);
           let block_java = status.prefs & (1<<13);
 
+          // always block cookies set on P3P policy!
+          if (target.path == "/w3c/p3p.xml")
+            observer.block_p3p_cookies(httpChannel);
+
           if (block_3rd_parties & !never)
             observer.block_3rd_party_content(httpChannel);
           else
@@ -469,6 +474,33 @@
         }
       }
     }
+    else if (topic == "cookie-changed")  // only used for logging purposes
+    {
+      // subject is nsICookie2 or nsIArray of nsICookie2 objects
+      // data  is "deleted", "added", "changed", "batch-deleted", "cleared or "reload"
+      let cookies = null;
+
+      try {
+        cookies = subject.QueryInterface(Components.interfaces.nsIArray);
+      } catch (e) {
+        cookies = null;
+      }
+
+      if (!cookies)
+      {
+        let cookie = subject ? subject.QueryInterface(Components.interfaces.nsICookie2) : null;
+
+        if (cookie)
+          cookies = [ cookie ];
+      }
+
+      let i = 0;
+      while (i < cookies.length)
+      {
+        let cookie = cookies[i++];
+        console.log("cookie " + cookie.name + " for " + cookie.host + " " + data);
+      }
+    }
   },
 
   parties: {},             // set of third parties for page
@@ -495,10 +527,12 @@
         return gBrowser.getBrowserIndexForDocument(doc);
       }
 
+      console.log("get_tab_index_from_channel: notificationCallbacks is null");
       return -2;
     }
     catch (e)
     {
+      console.log("get_tab_index_from_channel: " + e);
       if (aChannel.loadGroup)
       {
         if (aChannel.loadGroup.notificationCallbacks)
@@ -517,14 +551,35 @@
           }
           catch (e)
           {
+            console.log("get_tab_index_from_channel: " + e);
             return -4;
           }
         }
       }
+
+      console.log("get_tab_index_from_channel: notificationCallbacks is null or loadGroup is null");
       return -3;
     }
   },
 
+  block_p3p_cookies: function (httpChannel)
+  {
+    console.log("observer.block_p3p_cookies on " + httpChannel.URI.host);
+
+    let visitor = {
+      "channel": httpChannel,
+      "host": httpChannel.URI.host,
+
+      "visitHeader": function (header, value)
+      {
+        if (header == "Set-Cookie")
+          this.channel.setResponseHeader(header, null, false);
+      }
+    };
+
+    httpChannel.visitResponseHeaders(visitor);
+  },
+
   // assumes that existence of referrer has already been checked
   block_3rd_party_content: function (httpChannel)
   {
--- a/dashboard/chrome/content/overlay.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/overlay.js	Sat Dec 03 22:21:56 2011 +0000
@@ -301,7 +301,7 @@
     }
     catch (e)
     {
-      alert("couldn't get localized string for " + key);
+      alert("couldn't get localized string for " + key + "\n " + e);
       return key;
     }
   },
@@ -704,13 +704,26 @@
     }
   },
   
+  // clear tab's dashboard status
   on_page_unload: function(aEvent)
   {  
     var doc = aEvent.originalTarget;
-    // doc is document that triggered "onunload" event  
-    // do something with the unloaded page.
 
-    //gBrowser.removeProgressListener(this.progressListener);
+    if (doc instanceof HTMLDocument)
+    {
+      let win = doc.defaultView;
+
+      if (win.frameElement)
+        return;
+
+      console.log("on page unload event - " + doc.location.href);
+
+      // find browser for this document
+      var browser = this.find_browser_from_doc(doc);
+
+      if (browser)
+        browser.dashboard_status = null;
+    } 
   },
 
   // base domain of nsIURI is used for distinguishing
--- a/dashboard/chrome/content/p3p.js	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/content/p3p.js	Sat Dec 03 22:21:56 2011 +0000
@@ -43,6 +43,8 @@
     if (depth > 4)  // endless loop detector
       return alert("couldn't find correct policy for path: " + path);
 
+    console.log("render_policy: uri = " + uri + " path = " + path);
+
     var req = new XMLHttpRequest();
 
     req.open("GET", uri, true);
@@ -53,7 +55,7 @@
       {
         if (req.status == 200)
         {
-          if (req.responseXML)
+          if (req.responseXML && req.responseXML.documentElement.nodeName != "parsererror")
             dashboard_p3p.find_applicable_policy(path, req.responseXML, depth);
           else
           {
@@ -65,7 +67,21 @@
               browser.selectedTab =  browser.addTab(uri);
             }
 */
-            alert("malformed P3P policy for url: " + uri +
+            console.log("trimming leading space before re-parse");
+            // trim leading whitespace before XML declaration
+            // so that we can view the policy for a common error
+            let s = req.responseText;
+            let i = 0;
+            for (i = 0; i < s.length && s.charCodeAt(i) < 33; ++i);
+            s = s.substr(i);
+
+            let parser = new DOMParser();
+            let dom = parser.parseFromString(s, "text/xml");
+
+            if (dom.documentElement.nodeName != "parsererror")
+              dashboard_p3p.find_applicable_policy(path, dom, depth);
+            else
+              alert("malformed P3P policy for url: " + uri +
                   "\nmedia type is " + req.channel.contentType);
           }
         }
@@ -102,8 +118,13 @@
 
   find_applicable_policy: function (path, doc, depth)
   {
+    let serializer = new XMLSerializer();
+    let xml = serializer.serializeToString(doc);
+    //alert(xml);
+
     let uri = null, el, pattern;
     let root = doc.documentElement;
+    console.log("p3p root element is " + root.nodeName);
     let includes = root.getElementsByTagName("INCLUDE");
 
     for (let i = 0; includes && i < includes.length; ++i)
@@ -120,10 +141,15 @@
 
     if (!uri)
     {
+      console.log("p3p: checking POLICY-REF element");
       let pr = root.getElementsByTagName("POLICY-REF");
 
       if (pr && pr.length > 0)
         uri = pr[0].getAttribute("about");
+      else
+        console.log("p3p: can't find any POLICY-REF elements");
+
+      console.log("p3p policy-ref: " + uri);
     }
 
     if (uri)
@@ -167,8 +193,10 @@
     }
   },
 
-  show_policy: function (path, root) {
+  show_policy: function (path, root)
+  {
     let p3p = dashboard_p3p;
+    console.log("show_policy: path = " + path);
 
     if (!p3p.description)
       p3p.description = document.getElementById("p3p_policy");
@@ -180,7 +208,7 @@
     el = document.createElementNS("http://www.w3.org/1999/xhtml", "em");
     p3p.description.appendChild(el);
 
-    el.innerHTML = dashboard.localize("p3p_render.comment");
+    el.innerHTML = dashboard.localize("p3p_render.comment") + "<html:br />\n";
 
     let policies = root.getElementsByTagName("POLICY");
     let policy = policies ? policies[0] : null;
@@ -245,6 +273,7 @@
   dump_contact: function (contact, discuri)
   {
     let obj = {};
+    console.log("p3p dump_contact: " + discuri);
 
     for (let data = contact.firstChild; data; data = data.nextSibling)
     {
@@ -262,56 +291,70 @@
 
     s += "<html:h2>"+obj.business.name+" P3P Privacy Policy</html:h2>\n";
 
-    s += "<html:p><html:em>See also site's <html:a id='shrp'>" +
-         "human readable policy</html:a></html:em></html:p>\n";
+    if (discuri)
+    {
+      s += "<html:p><html:em>See also site's <html:a id='shrp' title='" + discuri + "'>" +
+           "human readable policy</html:a></html:em></html:p>\n";
+    }
 
     s += "<html:p>\n";
 
-    s += obj.business.contact_info.postal.street + "<html:br />\n";
-    s += obj.business.contact_info.postal.city + ", ";
-
-    if (obj.business.contact_info.postal.stateprov)
-      s += obj.business.contact_info.postal.stateprov + " ";
-  
-    if (obj.business.contact_info.postal.postalcode)
-      s += obj.business.contact_info.postal.postalcode + "<html:br />\n";
-
-    s += obj.business.contact_info.postal.country + "<html:br />\n";
-
-    if (obj.business.contact_info.online.email)
-    {
-      s += "<html:br />email: \n";
-      s += obj.business.contact_info.online.email;
-    }
-
-    if (obj.business.contact_info.telecom &&
-           obj.business.contact_info.telecom.telephone)
+    if (typeof(obj.business.contact_info.postal) != "undefined")
     {
-      s += "<html:br />phone: \n";
-      if (obj.business.contact_info.telecom.telephone.intcode)
-      {
-        s += "+"  + obj.business.contact_info.telecom.telephone.intcode;
-        s += "."  + obj.business.contact_info.telecom.telephone.loccode;
-        s += "."  + obj.business.contact_info.telecom.telephone.number;
-      }
-      else if (obj.business.contact_info.telecom.telephone.number)
+      try
       {
-         if (obj.business.contact_info.telecom.telephone.number.substr(0,2) == "00")
-           s += "+" + obj.business.contact_info.telecom.telephone.substr(2);
-         else
-           s += obj.business.contact_info.telecom.telephone.number;
+        let info = obj.business.contact_info;
+
+        s += info.postal.street + "<html:br />\n";
+        s += info.postal.city + ", ";
+
+        if (info.postal.stateprov)
+        s += info.postal.stateprov + " ";
+  
+        if (info.postal.postalcode)
+          s += info.postal.postalcode + "<html:br />\n";
+
+        s += info.postal.country + "<html:br />\n";
+
+        if (info.online.email)
+        {
+          s += "<html:br />email: \n";
+          s += info.online.email;
+        }
+
+        if (info.telecom && info.telecom.telephone)
+        {
+          s += "<html:br />phone: \n";
+
+          if (info.telecom.telephone.intcode)
+          {
+            s += "+"  + info.telecom.telephone.intcode;
+            s += "."  + info.telecom.telephone.loccode;
+            s += "."  + info.telecom.telephone.number;
+          }
+          else if (info.telecom.telephone.number)
+          {
+            if (info.telecom.telephone.number.substr(0,2) == "00")
+              s += "+" + info.telecom.telephone.substr(2);
+            else
+              s += info.telecom.telephone.number;
+          }
+          else if (typeof (info.telecom.telephone) == "string")
+          {
+            if (info.telecom.telephone.substr(0,2) == "00")
+              s += "+" + info.telecom.telephone.substr(2);
+            else
+            s += info.telecom.telephone;
+          }
+        }
       }
-      else if (typeof (obj.business.contact_info.telecom.telephone) == "string")
+      catch (e)
       {
-         if (obj.business.contact_info.telecom.telephone.substr(0,2) == "00")
-           s += "+" + obj.business.contact_info.telecom.telephone.substr(2);
-         else
-           s += obj.business.contact_info.telecom.telephone;
+        console.log("p3p: couldn't format address for "  + obj.business.name);
       }
     }
 
     s += "\n</html:p>\n";
-
     this.description.appendChild(div);
     div.innerHTML += s;
 
@@ -320,14 +363,18 @@
     // on the parent browser window, and to then select that tab
     var parent_window = window.arguments ? window.arguments[1] : null;
     let link = document.getElementById("shrp");
-    link.addEventListener("click", function (e) {
-       let browser = parent_window.gBrowser;
-       browser.selectedTab =  browser.addTab(discuri);
-       e.cancel = true;
-       e.stopPropagation();
-       e.preventDefault();
-       return false;
-    }, false);
+
+    if (discuri)
+    {
+      link.addEventListener("click", function (e) {
+         let browser = parent_window.gBrowser;
+         browser.selectedTab =  browser.addTab(discuri);
+         e.cancel = true;
+         e.stopPropagation();
+         e.preventDefault();
+         return false;
+      }, false);
+    }
   },
 
   // map ref string into object tree
@@ -350,7 +397,9 @@
     }
 
     name = ref[ref.length - 1];
-    part[name] = value;
+
+    if (!part[name])
+      part[name] = value;
   },
 
   dump_statement: function (statement)
@@ -382,6 +431,11 @@
         let ref = data[i].getAttribute("ref").substr(1);
         let desc = this.get_description(ref);
         if (!desc) desc = ref;
+
+        // special case as details are given separately
+        if (ref == "dynamic.miscdata")
+          continue;
+
         li = document.createElementNS("http://www.w3.org/1999/xhtml", "li");
         ul.appendChild(li);
         li.innerHTML = desc;
@@ -406,7 +460,8 @@
           continue;
 
         let purpose = this.purposes[el.nodeName];
-        if (!purpose) purpose = el.nodeName;
+        if (!purpose)
+          break;
 
         let li = document.createElementNS("http://www.w3.org/1999/xhtml", "li");
         ul.appendChild(li);
@@ -466,7 +521,12 @@
           continue;
 
         let retention = this.retention[el.nodeName];
-        if (!retention) purpose = el.nodeName;
+
+        if (!retention)
+        {
+          purpose = el.nodeName;
+          retention = this.retention["undefined"];
+        }
 
         let li = document.createElementNS("http://www.w3.org/1999/xhtml", "li");
         ul.appendChild(li);
@@ -685,7 +745,9 @@
       "Indefinitely<html:span>: Information is retained for an indeterminate " +
       "period of time. The absence of a retention policy would be " +
       "reflected under this option. Where the recipient is a public " +
-      "fora, this is the appropriate retention policy.</html:span>"
+      "fora, this is the appropriate retention policy.</html:span>",
+    "undefined" :
+      "<html:span>A site specific retention policy.</html:span>"
   },
 
   get_description: function (name)
@@ -698,6 +760,10 @@
   data_description:
   {
     "user.name" : "user's name",
+    "user.home-info" : "user's home contact information",
+    "user.home-info.postal" : "user's address",
+    "user.home-info.telecom" : "user's phone number",
+    "user.home-info.online.email" : "user's email",
     "user.employer" : "user's employer",
     "dynamic.clickstream" : "information typically found in Web " +
       "server access logs, e.g.  the IP address or hostname of the user's " +
--- a/dashboard/chrome/locale/en-US/dashboard.dtd	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/chrome/locale/en-US/dashboard.dtd	Sat Dec 03 22:21:56 2011 +0000
@@ -163,16 +163,6 @@
 <!ENTITY dashboard.dialog.cursite.default.tooltip
  "sets the default preferences for sites for which you haven't set overrides">
 
-<!ENTITY dashboard.dialog.cursite.test
- "You can use the following buttons to check the current website in various ways. These use external websites and open in a new window.">
-
-<!ENTITY dashboard.dialog.cursite.test.norton.label
- "Check site with Norton SafeWeb...">
-<!ENTITY dashboard.dialog.cursite.test.freetrust.label
- "Check site with Free Trust Seal...">
-<!ENTITY dashboard.dialog.cursite.test.truste.label
- "Check site with TRUSTe...">
-
 <!-- share table info -->
 
 <!ENTITY dashboard.dialog.share.heading
Binary file dashboard/dashboard.xpi has changed
--- a/dashboard/install.rdf	Wed Jun 22 10:10:42 2011 +0100
+++ b/dashboard/install.rdf	Sat Dec 03 22:21:56 2011 +0000
@@ -3,7 +3,7 @@
  xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>dashboard@dave.raggett</em:id>
-    <em:version>0.9.6</em:version>
+    <em:version>0.9.8</em:version>
     <em:localized>
       <Description> <!-- example localization via google translate -->
         <em:locale>fr-FR</em:locale>
@@ -25,7 +25,7 @@
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- firefox -->
         <em:minVersion>3.0.9</em:minVersion>
-        <em:maxVersion>6.1</em:maxVersion>
+        <em:maxVersion>10.0</em:maxVersion>
       </Description>
     </em:targetApplication>
   </Description>