Bug 806037 - use nsIPrincipal for origin checks. r=gavin
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 27 Dec 2012 18:28:49 +1100
changeset 126242 e078df6b131677485230ec1e900be30fa8de79c3
parent 126241 cd2211a458e5ac4da6be8c7821e5597232d2fb3b
child 126243 5840a8b5cc2175fdcfb0689dea6058d85ac1d238
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin
bugs806037
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 806037 - use nsIPrincipal for origin checks. r=gavin
toolkit/components/social/MozSocialAPI.jsm
toolkit/components/social/SocialService.jsm
toolkit/components/social/WorkerAPI.jsm
toolkit/components/social/test/browser/browser_notifications.js
toolkit/components/social/test/browser/browser_workerAPI.js
toolkit/components/social/test/browser/head.js
toolkit/components/social/test/browser/worker_social.js
toolkit/components/social/test/xpcshell/test_SocialService.js
--- a/toolkit/components/social/MozSocialAPI.jsm
+++ b/toolkit/components/social/MozSocialAPI.jsm
@@ -65,22 +65,21 @@ function injectController(doc, topic, da
     });
   } catch(e) {
     Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e);
   }
 }
 
 // Loads mozSocial support functions associated with provider into targetWindow
 function attachToWindow(provider, targetWindow) {
-  // If the loaded document isn't from the provider's origin, don't attach
-  // the mozSocial API.
-  let origin = provider.origin;
+  // If the loaded document isn't from the provider's origin (or a protocol
+  // that inherits the principal), don't attach the mozSocial API.
   let targetDocURI = targetWindow.document.documentURIObject;
-  if (provider.origin != targetDocURI.prePath) {
-    let msg = "MozSocialAPI: not attaching mozSocial API for " + origin +
+  if (!provider.isSameOrigin(targetDocURI)) {
+    let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin +
               " to " + targetDocURI.spec + " since origins differ."
     Services.console.logStringMessage(msg);
     return;
   }
 
   var port = provider.getWorkerPort(targetWindow);
 
   let mozSocialObj = {
@@ -120,20 +119,19 @@ function attachToWindow(provider, target
       enumerable: true,
       configurable: true,
       writable: true,
       value: function(toURL, offset, callback) {
         let chromeWindow = getChromeWindow(targetWindow);
         if (!chromeWindow.SocialFlyout)
           return;
         let url = targetWindow.document.documentURIObject.resolve(toURL);
-        let fullURL = ensureProviderOrigin(provider, url);
-        if (!fullURL)
+        if (!provider.isSameOrigin(url))
           return;
-        chromeWindow.SocialFlyout.open(fullURL, offset, callback);
+        chromeWindow.SocialFlyout.open(url, offset, callback);
       }
     },
     closePanel: {
       enumerable: true,
       configurable: true,
       writable: true,
       value: function(toURL, offset, callback) {
         let chromeWindow = getChromeWindow(targetWindow);
@@ -224,36 +222,16 @@ function getChromeWindow(contentWin) {
   return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIWebNavigation)
                    .QueryInterface(Ci.nsIDocShellTreeItem)
                    .rootTreeItem
                    .QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindow);
 }
 
-function ensureProviderOrigin(provider, url) {
-  // resolve partial URLs and check prePath matches
-  let uri;
-  let fullURL;
-  try {
-    fullURL = Services.io.newURI(provider.origin, null, null).resolve(url);
-    uri = Services.io.newURI(fullURL, null, null);
-  } catch (ex) {
-    Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex);
-    return null;
-  }
-
-  if (provider.origin != uri.prePath) {
-    Cu.reportError("mozSocial: unable to load new location, " +
-                   provider.origin + " != " + uri.prePath);
-    return null;
-  }
-  return fullURL;
-}
-
 function isWindowGoodForChats(win) {
   return win.SocialChatBar && win.SocialChatBar.isAvailable;
 }
 
 function findChromeWindowForChats(preferredWindow) {
   if (preferredWindow && isWindowGoodForChats(preferredWindow))
     return preferredWindow;
   // no good - so let's go hunting.  We are now looking for a navigator:browser
@@ -284,16 +262,16 @@ function findChromeWindowForChats(prefer
   return best || first;
 }
 
 this.openChatWindow =
  function openChatWindow(chromeWindow, provider, url, callback, mode) {
   chromeWindow = findChromeWindowForChats(chromeWindow);
   if (!chromeWindow)
     return;
-  let fullURL = ensureProviderOrigin(provider, url);
-  if (!fullURL)
+  let fullURI = provider.resolveUri(url);
+  if (!provider.isSameOrigin(fullURI))
     return;
-  chromeWindow.SocialChatBar.openChat(provider, fullURL, callback, mode);
+  chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode);
   // getAttention is ignored if the target window is already foreground, so
   // we can call it unconditionally.
   chromeWindow.getAttention();
 }
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -241,16 +241,18 @@ function SocialProvider(input) {
   if (!input.origin)
     throw new Error("SocialProvider must be passed an origin");
 
   this.name = input.name;
   this.iconURL = input.iconURL;
   this.workerURL = input.workerURL;
   this.sidebarURL = input.sidebarURL;
   this.origin = input.origin;
+  let originUri = Services.io.newURI(input.origin, null, null);
+  this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);
   this.ambientNotificationIcons = {};
   this.errorState = null;
   this._active = ActiveProviders.has(this.origin);
 }
 
 SocialProvider.prototype = {
   // Provider enabled/disabled state. Disabled providers do not have active
   // connections to their FrameWorkers.
@@ -331,24 +333,26 @@ SocialProvider.prototype = {
       return;
     }
     for (let sub of ["share", "unshare"]) {
       let url = data.images[sub];
       if (!url || typeof url != "string" || url.length == 0) {
         reportError('images["' + sub + '"] is missing or not a non-empty string');
         return;
       }
-      // resolve potentially relative URLs then check the scheme is acceptable.
-      url = Services.io.newURI(this.origin, null, null).resolve(url);
-      let uri = Services.io.newURI(url, null, null);
-      if (!uri.schemeIs("http") && !uri.schemeIs("https") && !uri.schemeIs("data")) {
-        reportError('images["' + sub + '"] does not have a valid scheme');
+      // resolve potentially relative URLs but there is no same-origin check
+      // for images to help providers utilize content delivery networks...
+      // Also note no scheme checks are necessary - even a javascript: URL
+      // is safe as gecko evaluates them in a sandbox.
+      let imgUri = this.resolveUri(url);
+      if (!imgUri) {
+        reportError('images["' + sub + '"] is an invalid URL');
         return;
       }
-      promptImages[sub] = url;
+      promptImages[sub] = imgUri.spec;
     }
     for (let sub of ["shareTooltip", "unshareTooltip",
                      "sharedLabel", "unsharedLabel", "unshareLabel",
                      "portraitLabel",
                      "unshareConfirmLabel", "unshareConfirmAccessKey",
                      "unshareCancelLabel", "unshareCancelAccessKey"]) {
       if (typeof data.messages[sub] != "string" || data.messages[sub].length == 0) {
         reportError('messages["' + sub + '"] is not a valid string');
@@ -446,10 +450,57 @@ SocialProvider.prototype = {
    *
    * @param {DOMWindow} window (optional)
    */
   getWorkerPort: function getWorkerPort(window) {
     if (!this.workerURL || !this.enabled)
       return null;
     return getFrameWorkerHandle(this.workerURL, window,
                                 "SocialProvider:" + this.origin, this.origin).port;
+  },
+
+  /**
+   * Checks if a given URI is of the same origin as the provider.
+   *
+   * Returns true or false.
+   *
+   * @param {URI or string} uri
+   */
+  isSameOrigin: function isSameOrigin(uri, allowIfInheritsPrincipal) {
+    if (!uri)
+      return false;
+    if (typeof uri == "string") {
+      try {
+        uri = Services.io.newURI(uri, null, null);
+      } catch (ex) {
+        // an invalid URL can't be loaded!
+        return false;
+      }
+    }
+    try {
+      this.principal.checkMayLoad(
+        uri, // the thing to check.
+        false, // reportError - we do our own reporting when necessary.
+        allowIfInheritsPrincipal
+      );
+      return true;
+    } catch (ex) {
+      return false;
+    }
+  },
+
+  /**
+   * Resolve partial URLs for a provider.
+   *
+   * Returns nsIURI object or null on failure
+   *
+   * @param {string} url
+   */
+  resolveUri: function resolveUri(url) {
+    try {
+      let fullURL = this.principal.URI.resolve(url);
+      return Services.io.newURI(fullURL, null, null);
+    } catch (ex) {
+      Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex);
+      return null;
+    }
   }
 }
--- a/toolkit/components/social/WorkerAPI.jsm
+++ b/toolkit/components/social/WorkerAPI.jsm
@@ -97,29 +97,28 @@ WorkerAPI.prototype = {
           port.postMessage({topic: "social.notification-action",
                             data: {id: id,
                                    action: action,
                                    actionArgs: actionArgs}});
           switch (action) {
             case "link":
               // if there is a url, make it open a tab
               if (actionArgs.toURL) {
-                try {
-                  let pUri = Services.io.newURI(provider.origin, null, null);
-                  let nUri = Services.io.newURI(pUri.resolve(actionArgs.toURL),
-                                                null, null);
-                  // fixup
-                  if (nUri.scheme != pUri.scheme)
-                    nUri.scheme = pUri.scheme;
-                  if (nUri.prePath == provider.origin) {
-                    let xulWindow = Services.wm.getMostRecentWindow("navigator:browser");
-                    xulWindow.openUILinkIn(nUri.spec, "tab");
-                  }
-                } catch(e) {
-                  Cu.reportError("social.notification-create error: "+e);
+                let uriToOpen = provider.resolveUri(actionArgs.toURL);
+                // Bug 815970 - facebook gives us http:// links even though
+                // the origin is https:// - so we perform a fixup here.
+                let pUri = Services.io.newURI(provider.origin, null, null);
+                if (uriToOpen.scheme != pUri.scheme)
+                  uriToOpen.scheme = pUri.scheme;
+                if (provider.isSameOrigin(uriToOpen)) {
+                  let xulWindow = Services.wm.getMostRecentWindow("navigator:browser");
+                  xulWindow.openUILinkIn(uriToOpen.spec, "tab");
+                } else {
+                  Cu.reportError("Not opening notification link " + actionArgs.toURL
+                                 + " as not in provider origin");
                 }
               }
               break;
             default:
               break;
           }
         }
       }
--- a/toolkit/components/social/test/browser/browser_notifications.js
+++ b/toolkit/components/social/test/browser/browser_notifications.js
@@ -1,80 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const TEST_PROVIDER_ORIGIN = 'http://example.com';
 
 Cu.import("resource://gre/modules/Services.jsm");
 
-// A mock notifications server.  Based on:
-// dom/tests/mochitest/notification/notification_common.js
-const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
-    getService(Ci.nsIUUIDGenerator).generateUUID();
-
-const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
-const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
-
-function MockAlertsService() {}
-
-MockAlertsService.prototype = {
-
-    showAlertNotification: function(imageUrl, title, text, textClickable,
-                                    cookie, alertListener, name) {
-        let obData = JSON.stringify({
-          imageUrl: imageUrl,
-          title: title,
-          text:text,
-          textClickable: textClickable,
-          cookie: cookie,
-          name: name
-        });
-        Services.obs.notifyObservers(null, "social-test:notification-alert", obData);
-
-        if (textClickable) {
-          // probably should do this async....
-          alertListener.observe(null, "alertclickcallback", cookie);
-        }
-
-        alertListener.observe(null, "alertfinished", cookie);
-    },
-
-    QueryInterface: function(aIID) {
-        if (aIID.equals(Ci.nsISupports) ||
-            aIID.equals(Ci.nsIAlertsService))
-            return this;
-        throw Cr.NS_ERROR_NO_INTERFACE;
-    }
-};
-
-var factory = {
-    createInstance: function(aOuter, aIID) {
-        if (aOuter != null)
-            throw Cr.NS_ERROR_NO_AGGREGATION;
-        return new MockAlertsService().QueryInterface(aIID);
-    }
-};
-
-function replacePromptService() {
-  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
-            .registerFactory(FAKE_CID, "",
-                             ALERTS_SERVICE_CONTRACT_ID,
-                             factory)
-}
-
-function restorePromptService() {
-  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
-            .registerFactory(ALERTS_SERVICE_CID, "",
-                             ALERTS_SERVICE_CONTRACT_ID,
-                             null);
-}
-// end of alerts service mock.
-
-
 function ensureProvider(workerFunction, cb) {
   let manifest = {
     origin: TEST_PROVIDER_ORIGIN,
     name: "Example Provider",
     workerURL: "data:application/javascript;charset=utf-8," + encodeURI("let run=" + workerFunction.toSource()) + ";run();"
   };
 
   ensureSocialEnabled();
@@ -85,18 +21,18 @@ function ensureProvider(workerFunction, 
 }
 
 function test() {
   waitForExplicitFinish();
 
   let cbPostTest = function(cb) {
     SocialService.removeProvider(TEST_PROVIDER_ORIGIN, function() {cb()});
   };
-  replacePromptService();
-  registerCleanupFunction(restorePromptService);
+  replaceAlertsService();
+  registerCleanupFunction(restoreAlertsService);
   runTests(tests, undefined, cbPostTest);
 }
 
 let tests = {
   testNotificationCallback: function(cbnext) {
     let run = function() {
       let testPort, apiPort;
       onconnect = function(e) {
--- a/toolkit/components/social/test/browser/browser_workerAPI.js
+++ b/toolkit/components/social/test/browser/browser_workerAPI.js
@@ -2,16 +2,19 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 let provider;
 
 function test() {
   waitForExplicitFinish();
 
+  replaceAlertsService();
+  registerCleanupFunction(restoreAlertsService);
+
   let manifest = {
     origin: 'http://example.com',
     name: "Example Provider",
     workerURL: "http://example.com/browser/toolkit/components/social/test/browser/worker_social.js"
   };
 
   ensureSocialEnabled();
 
@@ -139,10 +142,67 @@ let tests = {
           let newWorker = fw.getFrameWorkerHandle(provider.workerURL, undefined, "testWorkerReload");
           is(worker._worker, newWorker._worker, "worker is the same after reload");
           ok(true, "worker reloaded and testPort was reconnected");
           next();
           break;
       }
     }
     port.postMessage({topic: "test-initialization"});
-  }
+  },
+
+  testNotificationLinks: function(next) {
+    let port = provider.getWorkerPort();
+    let data = {
+      id: 'an id',
+      body: 'the text',
+      action: 'link',
+      actionArgs: {} // will get a toURL elt during the tests...
+    }
+    let testArgs = [
+      // toURL,                 expectedLocation,     expectedWhere]
+      ["http://example.com",      "http://example.com/", "tab"],
+      // bug 815970 - test that a mis-matched scheme gets patched up.
+      ["https://example.com",     "http://example.com/", "tab"],
+      // check an off-origin link is not opened.
+      ["https://mochitest:8888/", null,                 null]
+    ];
+
+    // we monkey-patch openUILinkIn
+    let oldopenUILinkIn = window.openUILinkIn;
+    registerCleanupFunction(function () {
+      // restore the monkey-patch
+      window.openUILinkIn = oldopenUILinkIn;
+    });
+    let openLocation;
+    let openWhere;
+    window.openUILinkIn = function(location, where) {
+      openLocation = location;
+      openWhere = where;
+    }
+
+    // the testing framework.
+    let toURL, expectedLocation, expectedWhere;
+    function nextTest() {
+      if (testArgs.length == 0) {
+        port.close();
+        next(); // all out of tests!
+        return;
+      }
+      openLocation = openWhere = null;
+      [toURL, expectedLocation, expectedWhere] = testArgs.shift();
+      data.actionArgs.toURL = toURL;
+      port.postMessage({topic: 'test-notification-create', data: data});
+    };
+
+    port.onmessage = function(evt) {
+      if (evt.data.topic == "did-notification-create") {
+        is(openLocation, expectedLocation, "url actually opened was " + openLocation);
+        is(openWhere, expectedWhere, "the url was opened in a " + expectedWhere);
+        nextTest();
+      }
+    }
+    // and kick off the tests.
+    port.postMessage({topic: "test-initialization"});
+    nextTest();
+  },
+
 };
--- a/toolkit/components/social/test/browser/head.js
+++ b/toolkit/components/social/test/browser/head.js
@@ -54,8 +54,71 @@ function runTests(tests, cbPreTest, cbPo
           ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
           cleanupAndRunNextTest();
         }
       })
     });
   }
   runNextTest();
 }
+
+// A mock notifications server.  Based on:
+// dom/tests/mochitest/notification/notification_common.js
+const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
+    getService(Ci.nsIUUIDGenerator).generateUUID();
+
+const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
+
+function MockAlertsService() {}
+
+MockAlertsService.prototype = {
+
+    showAlertNotification: function(imageUrl, title, text, textClickable,
+                                    cookie, alertListener, name) {
+        let obData = JSON.stringify({
+          imageUrl: imageUrl,
+          title: title,
+          text:text,
+          textClickable: textClickable,
+          cookie: cookie,
+          name: name
+        });
+        Services.obs.notifyObservers(null, "social-test:notification-alert", obData);
+
+        if (textClickable) {
+          // probably should do this async....
+          alertListener.observe(null, "alertclickcallback", cookie);
+        }
+
+        alertListener.observe(null, "alertfinished", cookie);
+    },
+
+    QueryInterface: function(aIID) {
+        if (aIID.equals(Ci.nsISupports) ||
+            aIID.equals(Ci.nsIAlertsService))
+            return this;
+        throw Cr.NS_ERROR_NO_INTERFACE;
+    }
+};
+
+var factory = {
+    createInstance: function(aOuter, aIID) {
+        if (aOuter != null)
+            throw Cr.NS_ERROR_NO_AGGREGATION;
+        return new MockAlertsService().QueryInterface(aIID);
+    }
+};
+
+function replaceAlertsService() {
+  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+            .registerFactory(FAKE_CID, "",
+                             ALERTS_SERVICE_CONTRACT_ID,
+                             factory)
+}
+
+function restoreAlertsService() {
+  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+            .registerFactory(ALERTS_SERVICE_CID, "",
+                             ALERTS_SERVICE_CONTRACT_ID,
+                             null);
+}
+// end of alerts service mock.
--- a/toolkit/components/social/test/browser/worker_social.js
+++ b/toolkit/components/social/test/browser/worker_social.js
@@ -37,16 +37,21 @@ onconnect = function(e) {
       case "social.cookies-get-response":
         testerPort.postMessage({topic: "test.cookies-get-response", data: data});
         break;
       case "test-reload-init":
         // browser_social_sidebar.js started test, tell the sidebar to
         // start
         apiPort.postMessage({topic: 'social.reload-worker'});
         break;
+      case "test-notification-create":
+        apiPort.postMessage({topic: 'social.notification-create',
+                             data: data});
+        testerPort.postMessage({topic: 'did-notification-create'});
+        break;
     }
   }
   // used for "test-reload-worker"
   if (apiPort && apiPort != port) {
     port.postMessage({topic: "worker.connected"})
   }
 
 }
--- a/toolkit/components/social/test/xpcshell/test_SocialService.js
+++ b/toolkit/components/social/test/xpcshell/test_SocialService.js
@@ -27,16 +27,18 @@ function run_test() {
   Cu.import("resource://gre/modules/SocialService.jsm");
 
   let runner = new AsyncRunner();
   let next = runner.next.bind(runner);
   runner.appendIterator(testGetProvider(manifests, next));
   runner.appendIterator(testGetProviderList(manifests, next));
   runner.appendIterator(testEnabled(manifests, next));
   runner.appendIterator(testAddRemoveProvider(manifests, next));
+  runner.appendIterator(testIsSameOrigin(manifests, next));
+  runner.appendIterator(testResolveUri  (manifests, next));
   runner.next();
 }
 
 function testGetProvider(manifests, next) {
   for (let i = 0; i < manifests.length; i++) {
     let manifest = manifests[i];
     let provider = yield SocialService.getProvider(manifest.origin, next);
     do_check_neq(provider, null);
@@ -131,8 +133,36 @@ function testAddRemoveProvider(manifests
   // Now remove the provider
   yield SocialService.removeProvider(newProvider.origin, next);
   providersAfter = yield SocialService.getProviderList(next);
   do_check_eq(providersAfter.length, originalProviders.length);
   do_check_eq(providersAfter.indexOf(newProvider), -1);
   newProvider = yield SocialService.getProvider(newProvider.origin, next);
   do_check_true(!newProvider);
 }
+
+function testIsSameOrigin(manifests, next) {
+  let providers = yield SocialService.getProviderList(next);
+  let provider = providers[0];
+  // provider.origin is a string.
+  do_check_true(provider.isSameOrigin(provider.origin));
+  do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin, null, null)));
+  do_check_true(provider.isSameOrigin(provider.origin + "/some-sub-page"));
+  do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin + "/some-sub-page", null, null)));
+  do_check_false(provider.isSameOrigin("http://something.com"));
+  do_check_false(provider.isSameOrigin(Services.io.newURI("http://something.com", null, null)));
+  do_check_false(provider.isSameOrigin("data:text/html,<p>hi"));
+  do_check_true(provider.isSameOrigin("data:text/html,<p>hi", true));
+  do_check_false(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null)));
+  do_check_true(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null), true));
+  // we explicitly handle null and return false
+  do_check_false(provider.isSameOrigin(null));
+}
+
+function testResolveUri(manifests, next) {
+  let providers = yield SocialService.getProviderList(next);
+  let provider = providers[0];
+  do_check_eq(provider.resolveUri(provider.origin).spec, provider.origin + "/");
+  do_check_eq(provider.resolveUri("foo.html").spec, provider.origin + "/foo.html");
+  do_check_eq(provider.resolveUri("/foo.html").spec, provider.origin + "/foo.html");
+  do_check_eq(provider.resolveUri("http://somewhereelse.com/foo.html").spec, "http://somewhereelse.com/foo.html");
+  do_check_eq(provider.resolveUri("data:text/html,<p>hi").spec, "data:text/html,<p>hi");
+}