Bug 889157 - Uplift Add-on SDK to Firefox r=me
authorWes Kocher <wkocher@mozilla.com>
Tue, 09 Jul 2013 19:15:10 -0700
changeset 137942 e4c650dfee0e75b2e7702446db50e245d0c0ef0e
parent 137941 e54450e7bb5c93ce58e93caad30857f7603def59
child 137943 131d38534c6b8a0570614387449b767e79c36e42
push id24939
push userryanvm@gmail.com
push dateWed, 10 Jul 2013 17:49:39 +0000
treeherdermozilla-central@dde4dcd6fa46 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersme
bugs889157
milestone25.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 889157 - Uplift Add-on SDK to Firefox r=me
addon-sdk/source/README
addon-sdk/source/doc/dev-guide-source/cfx-tool.md
addon-sdk/source/doc/dev-guide-source/console.md
addon-sdk/source/doc/dev-guide-source/guides/content-scripts/cross-domain.md
addon-sdk/source/doc/dev-guide-source/guides/content-scripts/index.md
addon-sdk/source/doc/dev-guide-source/guides/index.md
addon-sdk/source/doc/dev-guide-source/guides/xul-migration.md
addon-sdk/source/doc/dev-guide-source/package-spec.md
addon-sdk/source/doc/dev-guide-source/search.md
addon-sdk/source/doc/dev-guide-source/tutorials/adding-menus.md
addon-sdk/source/doc/dev-guide-source/tutorials/installation.md
addon-sdk/source/doc/dev-guide-source/tutorials/logging.md
addon-sdk/source/doc/dev-guide-source/tutorials/mobile.md
addon-sdk/source/doc/dev-guide-source/tutorials/modifying-web-pages-url.md
addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md
addon-sdk/source/doc/module-source/sdk/context-menu.md
addon-sdk/source/doc/module-source/sdk/page-mod.md
addon-sdk/source/doc/module-source/sdk/panel.md
addon-sdk/source/doc/module-source/sdk/private-browsing.md
addon-sdk/source/doc/module-source/sdk/tabs.md
addon-sdk/source/doc/module-source/sdk/util/match-pattern.md
addon-sdk/source/lib/sdk/addon-page.js
addon-sdk/source/lib/sdk/addon/events.js
addon-sdk/source/lib/sdk/addon/host.js
addon-sdk/source/lib/sdk/content/events.js
addon-sdk/source/lib/sdk/content/utils.js
addon-sdk/source/lib/sdk/context-menu.js
addon-sdk/source/lib/sdk/core/heritage.js
addon-sdk/source/lib/sdk/deprecated/events.js
addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js
addon-sdk/source/lib/sdk/fs/path.js
addon-sdk/source/lib/sdk/io/buffer.js
addon-sdk/source/lib/sdk/io/fs.js
addon-sdk/source/lib/sdk/io/stream.js
addon-sdk/source/lib/sdk/lang/weak-set.js
addon-sdk/source/lib/sdk/page-mod.js
addon-sdk/source/lib/sdk/page-mod/match-pattern.js
addon-sdk/source/lib/sdk/page-worker.js
addon-sdk/source/lib/sdk/panel.js
addon-sdk/source/lib/sdk/panel/events.js
addon-sdk/source/lib/sdk/panel/utils.js
addon-sdk/source/lib/sdk/places/bookmarks.js
addon-sdk/source/lib/sdk/places/contract.js
addon-sdk/source/lib/sdk/places/history.js
addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
addon-sdk/source/lib/sdk/places/host/host-query.js
addon-sdk/source/lib/sdk/places/host/host-tags.js
addon-sdk/source/lib/sdk/places/utils.js
addon-sdk/source/lib/sdk/private-browsing.js
addon-sdk/source/lib/sdk/private-browsing/utils.js
addon-sdk/source/lib/sdk/system.js
addon-sdk/source/lib/sdk/tabs/tab-fennec.js
addon-sdk/source/lib/sdk/test.js
addon-sdk/source/lib/sdk/test/utils.js
addon-sdk/source/lib/sdk/util/array.js
addon-sdk/source/lib/sdk/util/match-pattern.js
addon-sdk/source/lib/sdk/util/object.js
addon-sdk/source/lib/sdk/util/rules.js
addon-sdk/source/lib/sdk/widget.js
addon-sdk/source/lib/sdk/window/utils.js
addon-sdk/source/lib/toolkit/loader.js
addon-sdk/source/mapping.json
addon-sdk/source/python-lib/cuddlefish/docs/renderapi.readme.md
addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
addon-sdk/source/python-lib/jetpack_sdk_env.py
addon-sdk/source/test/addons/layout-change/main.js
addon-sdk/source/test/fixtures/loader/unsupported/fennec.js
addon-sdk/source/test/fixtures/loader/unsupported/firefox.js
addon-sdk/source/test/places-helper.js
addon-sdk/source/test/tabs/test-fennec-tabs.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
addon-sdk/source/test/test-addon-page.js
addon-sdk/source/test/test-content-worker.js
addon-sdk/source/test/test-fs.js
addon-sdk/source/test/test-host-events.js
addon-sdk/source/test/test-match-pattern.js
addon-sdk/source/test/test-object.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-page-worker.js
addon-sdk/source/test/test-panel.js
addon-sdk/source/test/test-path.js
addon-sdk/source/test/test-places-bookmarks.js
addon-sdk/source/test/test-places-favicon.js
addon-sdk/source/test/test-places-history.js
addon-sdk/source/test/test-places-host.js
addon-sdk/source/test/test-places-utils.js
addon-sdk/source/test/test-private-browsing.js
addon-sdk/source/test/test-rules.js
addon-sdk/source/test/test-tabs-common.js
addon-sdk/source/test/test-test-utils-async.js
addon-sdk/source/test/test-test-utils-sync.js
addon-sdk/source/test/test-unsupported-skip.js
addon-sdk/source/test/test-url.js
addon-sdk/source/test/test-weak-set.js
addon-sdk/source/test/test-widget.js
addon-sdk/source/test/test-windows-common.js
addon-sdk/source/test/windows/test-firefox-windows.js
--- a/addon-sdk/source/README
+++ b/addon-sdk/source/README
@@ -17,22 +17,18 @@ is in (the SDK's root directory) using a
 or on Windows with MSYS, you can execute the following command:
 
   source bin/activate
 
 Windows users using cmd.exe should instead run:
 
   bin\activate.bat
 
-Then run:
-
-  cfx docs
-
-This should start a documentation server and open a web browser
-with further instructions.
+Then go to https://addons.mozilla.org/developers/docs/sdk/latest/dev-guide to
+browse the SDK documentation.
 
 If you get an error when running cfx or have any other problems getting
 started, see the "Troubleshooting" guide at:
 https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/tutorials/troubleshooting.html
 
 Bugs
 -------
 
--- a/addon-sdk/source/doc/dev-guide-source/cfx-tool.md
+++ b/addon-sdk/source/doc/dev-guide-source/cfx-tool.md
@@ -17,35 +17,26 @@ commands (for example `--help`). `cfx` s
 
 <pre>
   -h, --help        - show a help message and exit
   -v, --verbose     - enable lots of output
 </pre>
 
 "Command-specific options" are documented alongside the commands.
 
-There are five supported cfx commands:
+There are four supported cfx commands:
 
 <table>
   <colgroup>
     <col width="10%">
     <col width="90%">
   </colgroup>
 
   <tr>
     <td>
-      <a href="dev-guide/cfx-tool.html#cfx-docs"><code>cfx docs</code></a>
-    </td>
-    <td>
-      Display the documentation for the SDK.
-    </td>
-  </tr>
-
-  <tr>
-    <td>
       <a href="dev-guide/cfx-tool.html#cfx-init"><code>cfx init</code></a>
     </td>
     <td>
       Create a skeleton add-on as a starting point for your own add-on.
     </td>
   </tr>
 
   <tr>
@@ -77,37 +68,16 @@ There are five supported cfx commands:
   </tr>
 
 </table>
 
 There are also a number of
 [internal commands](dev-guide/cfx-tool.html#internal-commands),
 which are more likely to be useful to SDK developers than to add-on developers.
 
-## <a name="cfx-docs">cfx docs</a> ##
-
-This command displays the documentation for the SDK. The documentation is
-shipped with the SDK in [Markdown](http://daringfireball.net/projects/markdown/)
-format. The first time this command is executed, and any time after the
-Markdown files on disk have changed, `cfx docs` will generate a set of HTML
-pages from them and launch a web browser to display them. If the Markdown files
-haven't changed, `cfx docs` just launches a browser initialized to the set of
-generated pages.
-
-To regenerate the documentation associated with a single file, you can
-specify the file as an argument. For example:
-
-<pre>
-  cfx docs doc/dev-guide-source/addon-development/cfx-tool.md 
-</pre>
-
-This command will regenerate only the HTML page you're reading.
-This is useful if you're iteratively editing a single file, and don't want to wait for cfx to
-regenerate the complete documentation tree.
-
 ## <a name="cfx-init">cfx init</a> ##
 
 Create a new directory called "my-addon", change into it, and run `cfx init`.
 
 This command will create an skeleton add-on, as a starting point for your
 own add-on development, with the following file structure:
 
 <ul class="tree">
@@ -805,18 +775,17 @@ add-on whenever it is run.
 
 </table>
 
 ## <a name="internal-commands">Internal Commands</a> ##
 
 ### cfx sdocs ###
 
 Executing this command builds a static HTML version of the SDK documentation
-that can be hosted on a web server without the special application support
-required by `cfx docs`.
+that can be hosted on a web server.
 
 #### Options ####
 
 <table>
 <colgroup>
 <col width="50%">
 <col width="50%">
 </colgroup>
--- a/addon-sdk/source/doc/dev-guide-source/console.md
+++ b/addon-sdk/source/doc/dev-guide-source/console.md
@@ -1,46 +1,207 @@
 <!-- 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/. -->
 
 # console #
 
 The `console` object enables your add-on to log messages. If you have started
-the host application for your add-on from the command line (for example, by
-executing `cfx run` or `cfx test`) then these messages appear in the command
-shell you used. If the add-on has been installed in the host application, then
-the messages appear in the host application's
+Firefox for your add-on from the command line with `cfx run` or `cfx test`
+then these messages appear in the command shell you used. If the add-on has
+been installed in Firefox, then the messages appear in the host application's
 [Error Console](https://developer.mozilla.org/en/Error_Console).
 
-The `console` object has the following methods:
-
-<code>console.**log**(*object*[, *object*, ...])</code>
+If you're developing your add-on using the
+[Add-on Builder](https://builder.addons.mozilla.org/) or are using
+the [Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/),
+then the add-on is installed in Firefox, meaning that messages will appear in
+the Error Console. But see the discussion of
+[logging levels](dev-guide/console.html#Logging Levels): by default, messages
+logged using  `log()`, `info()`, `trace()`, or `warn()` won't be logged in
+these situations.
 
-Logs an informational message to the shell.
+## Console Methods ##
+
+All console methods except `exception()` and `trace()` accept one or
+more JavaScript objects as arguments and log them to the console.
 Depending on the console's underlying implementation and user interface,
-you may be able to introspect into the properties of non-primitive objects
+you may be able to examine the properties of non-primitive objects
 that are logged.
 
-<code>console.**info**(*object*[, *object*, ...])</code>
+### <code>console.log(*object*[, *object*, ...])</code> ###
+
+Logs the arguments to the console, preceded by "info:" and the name of your
+add-on:
+
+    console.log("This is an informational message");
+
+<pre>
+info: my-addon: This is an informational message
+</pre>
+
+### <code>console.info(*object*[, *object*, ...])</code> ###
 
 A synonym for `console.log()`.
 
-<code>console.**warn**(*object*[, *object*, ...])</code>
+### <code>console.warn(*object*[, *object*, ...])</code> ###
+
+Logs the arguments to the console, preceded by "warn:" and the name of your
+add-on:
+
+    console.warn("This is a warning message");
 
-Logs a warning message.
+<pre>
+warn: my-addon: This is a warning message
+</pre>
 
-<code>console.**error**(*object*[, *object*, ...])</code>
+### <code>console.error(*object*[, *object*, ...])</code> ###
+
+Logs the arguments to the console, preceded by "error:" and the name of your
+add-on:
 
-Logs an error message.
+    console.error("This is an error message");
 
-<code>console.**debug**(*object*[, *object*, ...])</code>
+<pre>
+error: my-addon: This is an error message
+</pre>
+
+### <code>console.debug(*object*[, *object*, ...])</code> ###
 
-Logs a debug message.
+Logs the arguments to the console, preceded by "debug:" and the name of your
+add-on:
+
+    console.error("This is a debug message");
 
-<code>console.**exception**(*exception*)</code>
+<pre>
+debug: my-addon: This is a debug message
+</pre>
+
+### <code>console.exception(*exception*)</code> ###
 
 Logs the given exception instance as an error, outputting information
 about the exception's stack traceback if one is available.
 
-<code>console.**trace**()</code>
+    try {
+       doThing();
+    } catch (e) {
+       console.exception(e);
+    }
+
+    function UserException(message) {
+       this.message = message;
+       this.name = "UserException";
+    }
+
+    function doThing() {
+      throw new UserException("Thing could not be done!");
+    }
+
+<pre>
+error: my-addon: An exception occurred.
+UserException: Thing could not be done!
+</pre>
+
+### <code>console.trace()</code> ###
+
+Logs a stack trace at the point the function is called.
+
+<h2 id="Logging Levels">Logging Levels</h2>
+
+Logging's useful, of course, especially during development. But the more
+logging there is, the more noise you see in the console output.
+Especially when debug logging shows up in a production environment, the
+noise can make it harder, not easier, to debug issues.
+
+This is the problem that logging levels are designed to fix. The console
+defines a number of logging levels, from "more verbose" to "less verbose",
+and a number of different logging functions that correspond to these levels,
+which are arranged in order of "severity" from informational
+messages, through warnings, to errors.
+
+At a given logging level, only calls to the corresponding functions and
+functions with a higher severity will have any effect.
+
+For example, if the logging level is set to "info", then calls to `info()`,
+`log()`, `warn()`, and `error()` will all result in output being written.
+But if the logging level is "warn" then only calls to `warn()` and `error()`
+have any effect, and calls to `info()` and `log()` are simply discarded.
+
+This means that the same code can be more verbose in a development
+environment than in a production environment - you just need to arrange for
+the appropriate logging level to be set.
+
+The complete set of logging levels is given in the table below, along
+with the set of functions that will result in output at each level:
+
+<table>
+  <colgroup>
+    <col width="10%">
+    <col width="90%">
+  </colgroup>
+
+  <tr>
+    <th>Level</th>
+    <th>Will log calls to:</th>
+  </tr>
 
-Logs a stack trace at the point this function is called.
+  <tr>
+    <td>all</td>
+    <td>Any console method</td>
+  </tr>
+
+  <tr>
+    <td>debug</td>
+    <td><code>debug()</code>, <code>log()</code>, <code>info()</code>, <code>trace()</code>, <code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
+  </tr>
+
+  <tr>
+    <td>info</td>
+    <td><code>log()</code>, <code>info()</code>, <code>trace()</code>, <code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
+  </tr>
+
+  <tr>
+    <td>warn</td>
+    <td><code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
+  </tr>
+
+  <tr>
+    <td>error</td>
+    <td><code>exception()</code>, <code>error()</code></td>
+  </tr>
+
+  <tr>
+    <td>off</td>
+    <td>Nothing</td>
+  </tr>
+
+</table>
+
+### Setting the Logging Level ###
+
+The logging level defaults to "error".
+
+There are two system preferences that can be used to override this default:
+
+* **extensions.sdk.console.logLevel**: if set, this determines the logging
+level for all installed SDK-based add-ons.
+
+* **extensions.[extension-id].sdk.console.logLevel**: if set, this determines
+the logging level for the specified add-on. This overrides the global
+preference if both are set.
+
+Both these preferences can be set programmatically using the
+[`preferences/service`](modules/sdk/preferences/service.html) API, or manually
+using [about:config](http://kb.mozillazine.org/About:config). The value for each
+preference is the desired logging level, given as a string. 
+
+When you run your add-on using `cfx run` or `cfx test`, the global
+**extensions.sdk.console.logLevel** preference is automatically set to "info".
+This means that calls to `console.log()` will appear in the console output.
+
+When you install an add-on into Firefox, the logging level will be "error"
+by default (that is, unless you have set one of the two preferences). This
+means that messages written using `debug()`, `log()`, `info()`, `trace()`,
+and `warn()` will not appear in the console.
+
+This includes add-ons being developed using the
+[Add-on Builder](https://builder.addons.mozilla.org/) or the
+[Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/).
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/doc/dev-guide-source/guides/content-scripts/cross-domain.md
@@ -0,0 +1,177 @@
+<!-- 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/. -->
+
+# Cross-domain Content Scripts #
+
+By default, content scripts don't have any cross-domain privileges.
+In particular, they can't:
+
+* [access content hosted in an `iframe`, if that content is served from a different domain](dev-guide/guides/content-scripts/cross-domain.html#Cross-domain iframes)
+* [make cross-domain XMLHttpRequests](dev-guide/guides/content-scripts/cross-domain.html#Cross-domain XMLHttpRequest)
+
+However, you can enable these features for specific domains
+by adding them to your add-on's [package.json](dev-guide/package-spec.html)
+under the `"cross-domain-content"` key, which itself lives under the
+`"permissions"` key:
+
+<pre>
+"permissions": {
+    "cross-domain-content": ["http://example.org/", "http://example.com/"]
+}
+</pre>
+
+* The domains listed must include the scheme and fully qualified domain name,
+and these must exactly match the domains serving the content - so in the
+example above, the content script will not be allowed to access content
+served from `https://example.com/`.
+* Wildcards are not allowed.
+* This feature is currently only available for content scripts, not for page
+scripts included in HTML files shipped with your add-on.
+
+## Cross-domain iframes ##
+
+The following "main.js" creates a page-worker which loads a local HTML file
+called "page.html", attaches a content script called "page.js" to the
+page, waits for messages from the script, and logs the payload.
+
+    //main.js
+    var data = require("sdk/self").data;
+
+    var pageWorker = require("sdk/page-worker").Page({
+      contentURL: data.url("page.html"),
+      contentScriptFile: data.url("page-script.js")
+    });
+
+    pageWorker.on("message", function(message) {
+      console.log(message);
+    });
+
+The "page.html" file embeds an iframe whose content is
+served from "http://en.m.wikipedia.org/":
+
+<pre class="brush: html">
+    &lt;!doctype html&gt;
+    &lt;!-- page.html --&gt;
+    &lt;html&gt;
+      &lt;head>&lt;/head&gt;
+      &lt;body&gt;
+        &lt;iframe id="wikipedia" src="http://en.m.wikipedia.org/"&gt;&lt;/iframe&gt;
+      &lt;/body&gt;
+    &lt;/html&gt;
+</pre>
+
+The "page-script.js" file locates "Today's Featured Article" and sends its
+content to "main.js":
+
+    // page-script.js
+    var iframe = window.document.getElementById("wikipedia");
+    var todaysFeaturedArticle = iframe.contentWindow.document.getElementById("mp-tfa");
+    self.postMessage(todaysFeaturedArticle.textContent);
+
+For this to work, we need to add the `"cross-domain-content"` key to
+"package.json":
+
+<pre>
+"permissions": {
+  "cross-domain-content": ["http://en.m.wikipedia.org/"]
+}
+</pre>
+
+The add-on should successfully retrieve the iframe's content.
+
+## Cross-domain XMLHttpRequest ##
+
+The following add-on creates a panel whose content is the summary weather
+forecast for [Shetland](https://en.wikipedia.org/wiki/Shetland).
+If you want to try it out, you'll need to
+[register](http://www.metoffice.gov.uk/datapoint/support/API)
+and get an API key.
+
+The "main.js":
+
+* creates a panel whose content is supplied by "panel.html" and
+adds a content script "panel-script.js" to it
+* sends the panel a "show" message when it is shown
+* attaches the panel to a widget
+
+<!-- terminate Markdown list -->
+
+    // main.js
+    var data = require("sdk/self").data;
+
+    var forecast_panel = require("sdk/panel").Panel({
+      height: 50,
+      contentURL: data.url("panel.html"),
+      contentScriptFile: data.url("panel-script.js")
+    });
+
+    forecast_panel.on("show", function(){
+      forecast_panel.port.emit("show");
+    });
+
+    require("sdk/widget").Widget({
+      id: "forecast",
+      label: "Weather Forecast",
+      contentURL: "http://www.metoffice.gov.uk/favicon.ico",
+      panel: forecast_panel
+    });
+
+The "panel.html" just includes a `<div>` block for the forecast:
+
+<pre class="brush: html">
+&lt;!doctype HTML&gt;
+&lt;!-- panel.html --&gt;
+
+&lt;html&gt;
+  &lt;head&gt;&lt;/head&gt;
+  &lt;body&gt;
+    &lt;div id="forecast_summary">&lt;/div&gt;
+  &lt;/body&gt;
+&lt;/html&gt;
+</pre>
+
+The "panel-script.js" uses [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest)
+to fetch the latest forecast:
+
+    // panel-script.js
+
+    var url = "http://datapoint.metoffice.gov.uk/public/data/txt/wxfcs/regionalforecast/json/500?key=YOUR-API-KEY";
+
+    self.port.on("show", function () {
+      var request = new XMLHttpRequest();
+      request.open("GET", url, true);
+      request.onload = function () {
+        var jsonResponse = JSON.parse(request.responseText);
+        var summary = getSummary(jsonResponse);
+        var element = document.getElementById("forecast_summary");
+        element.textContent = summary;
+      };
+      request.send();
+    });
+
+    function getSummary(forecast) {
+      return forecast.RegionalFcst.FcstPeriods.Period[0].Paragraph[0].$;
+    }
+
+
+Finally, we need to add the `"cross-domain-content"` key to "package.json":
+
+<pre>
+"permissions": {
+  "cross-domain-content": ["http://datapoint.metoffice.gov.uk"]
+}
+</pre>
+
+## Content Permissions and unsafeWindow ##
+
+If you use `"cross-domain-content"`, then JavaScript values in content
+scripts will not be available from pages. Suppose your content script includes
+a line like:
+
+    // content-script.js:
+    unsafeWindow.myCustomAPI = function () {};
+
+If you have included the `"cross-domain-content"` key, when the page script
+tries to access `myCustomAPI` this will result in a "permission denied"
+exception.
--- a/addon-sdk/source/doc/dev-guide-source/guides/content-scripts/index.md
+++ b/addon-sdk/source/doc/dev-guide-source/guides/content-scripts/index.md
@@ -87,10 +87,12 @@ detail about the access content scripts 
 detail about how content scripts can communicate with "main.js", with other
 content scripts, and with scripts loaded by the web page itself
 * [Communicating Using <code>port</code>](dev-guide/guides/content-scripts/using-port.html):
 how to communicate between your add-on and its content scripts using the
 <code>port</code> object
 * [Communicating using <code>postMessage()</code>](dev-guide/guides/content-scripts/using-postmessage.html):
 how to communicate between your add-on and its content scripts using the
 <code>postMessage()</code> API
+* [Cross-domain Content Scripts](dev-guide/guides/content-scripts/cross-domain.html):
+how to enable a content script to interact with content served from other domains.
 * [Example](dev-guide/guides/content-scripts/reddit-example.html):
 a simple example add-on using content scripts
--- a/addon-sdk/source/doc/dev-guide-source/guides/index.md
+++ b/addon-sdk/source/doc/dev-guide-source/guides/index.md
@@ -139,16 +139,22 @@ This page lists more theoretical in-dept
       this technique and the <code>port</code> object.
     </td>
 
   </tr>
 
   <tr>
 
     <td>
+      <h4><a href="dev-guide/guides/content-scripts/cross-domain.html">Cross-domain content scripts</a></h4>
+      How to enable content scripts to interact with content served from different domains.
+    </td>
+
+
+    <td>
       <h4><a href="dev-guide/guides/content-scripts/reddit-example.html">Reddit example</a></h4>
       A simple add-on which uses content scripts.
     </td>
 
   </tr>
 
 </table>
 
--- a/addon-sdk/source/doc/dev-guide-source/guides/xul-migration.md
+++ b/addon-sdk/source/doc/dev-guide-source/guides/xul-migration.md
@@ -48,16 +48,87 @@ supported APIs, it's a good candidate fo
 APIs, or XPCOM, then the cost of migrating is high, and may not be worth
 it at this point.
 
 * If your add-on only needs a little help from those techniques, and can
 accomplish most of what it needs using the supported APIs, then it might
 still be worth migrating: we'll add more supported APIs in future releases
 to meet important use cases.
 
+## <a name="user-interface-components">User Interface Components</a>##
+
+XUL-based add-ons typically implement a user interface using a combination
+of two techniques: XUL overlays and XUL windows.
+
+### XUL Overlays ###
+
+XUL overlays are used to modify existing windows such as the main browser
+window. In this way an extension can integrate its user interface into the
+browser: for example, adding menu items, buttons, and toolbars.
+
+Because SDK-based extensions are restartless, they can't use XUL overlays. To
+add user interface components to the browser, there are a few different
+options. In order of complexity, the main options are:
+
+* the SDK includes modules that implement some basic user interface
+components including [buttons](modules/sdk/widget.html),
+[dialogs](modules/sdk/panel.html), and
+[context menu items](modules/sdk/context-menu.html).
+
+* there is a collection of
+[community-developed modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules)
+that includes various user interface components, including
+[toolbar buttons](https://github.com/voldsoftware/toolbarbutton-jplib) and
+[menu items](https://github.com/voldsoftware/menuitems-jplib).
+
+* by using the SDK's
+[low-level APIs](dev-guide/guides/xul-migration.html#Using the Low-level APIs)
+you can directly modify the browser chrome.
+
+### XUL Windows
+
+XUL windows are used to define completely new windows to host user interface
+elements specific to the add-on.
+
+The SDK generally expects you to specify your user interface using HTML, not
+XUL. However, you can include a
+[chrome.manifest file](https://developer.mozilla.org/en-US/docs/Chrome_Registration)
+in your add-on and it will be included in the generated XPI.
+
+<ul class="tree">
+  <li>my-addon
+    <ul>
+    <li class="highlight-tree-node">chrome
+      <ul><li>content</li>
+          <li>locale</li>
+          <li>skin</li></ul>
+    </li>
+    <li class="highlight-tree-node">chrome.manifest</li>
+    <li>data</li>
+    <li>lib</li>
+    <li>package.json</li>
+    </ul>
+  </li>
+</ul>
+
+There are limitations on what you can do in this manifest file: for example,
+you can't register overlays, `resource:` URIs, or components. However, you
+can register a `chrome:` URI, with a skin and locale, and this means you
+can include XUL windows in an SDK-based add-on.
+
+You can keep the "chrome.manifest" file in your add-on's root directory
+and create a directory there called "chrome". In that directory you can keep
+your "content", "locale", and "skin" subdirectories:
+
+This allows you to refer to objects in these directories from "chrome.manifest" using a relative path, like "chrome/content".
+
+This is provided only as a migration aid, and it's still a good idea to port XUL windows to HTML.
+
+<div style="clear:both"></div>
+
 ## <a name="content-scripts">Content Scripts</a> ##
 
 In a XUL-based add-on, code that uses XPCOM objects, code that manipulates
 the browser chrome, and code that interacts with web pages all runs in the
 same context. But the SDK makes a distinction between:
 
 * **add-on scripts**, which can use the SDK APIs, but are not able to interact
 with web pages
@@ -66,37 +137,18 @@ the SDK's APIs
 
 Content scripts and add-on scripts communicate by sending each other JSON
 messages: in fact, the ability to communicate with the add-on scripts is the
 only extra privilege a content script is granted over a normal remote web
 page script.
 
 A XUL-based add-on will need to be reorganized to respect this distinction.
 
-Suppose an add-on wants to make a cross-domain XMLHttpRequest based on some
-data extracted from a web page. In a XUL-based extension you would implement
-all this in a single script. An SDK-based equivalent would need to be
-structured like this:
-
-* the main add-on code (1) attaches a content script to the page, and (2)
-registers a listener function for messages from the content script
-* the content script (3) extracts the data from the page and (4) sends
-it to the main add-on code in a message
-* the main add-on code (5) receives the message and (6) sends the request,
-using the SDK's [`request`](modules/sdk/request.html) API
-
-<img class="image-center" src="static-files/media/xul-migration-cs.png"
-alt="Content script organization">
-
-There are two related reasons for this design. The first is security: it
-reduces the risk that a malicious web page will be able to access privileged
-APIs. The second is the need to be compatible with the multi-process architecture
-planned for Firefox: after this is implemented in Firefox, all add-ons will
-need to use a similar pattern, so it's likely that a XUL-based add-on will
-need to be rewritten anyway.
+The main reason for this design is security: it reduces the risk that a
+malicious web page will be able to access privileged APIs.
 
 There's much more information on content scripts in the
 [Working With Content Scripts](dev-guide/guides/content-scripts/index.html) guide.
 
 ## <a name="supported-apis">Using the Supported APIs</a> ##
 
 The SDK provides a set of high level APIs
 providing some basic user interface components and functionality commonly
--- a/addon-sdk/source/doc/dev-guide-source/package-spec.md
+++ b/addon-sdk/source/doc/dev-guide-source/package-spec.md
@@ -174,19 +174,23 @@ directory the first time you run
 
 <tr>
   <td id="permissions"><code>permissions</code></td>
   <td><p>A set of permissions that the add-on needs.</p>
     <p><strong><code>private-browsing</code></strong>: a boolean
   indicating whether or not the
   add-on supports private browsing. If this value is not <code>true</code>
   or is omitted, then the add-on will not see any private windows or
-objects, such as tabs, that are associated with private windows. See the
-documentation for the
-<a href="modules/sdk/private-browsing.html"><code>private-browsing</code> module</a>.</p>
+  objects, such as tabs, that are associated with private windows. See the
+  documentation for the
+  <a href="modules/sdk/private-browsing.html"><code>private-browsing</code> module</a>.</p>
+    <p><strong><code>cross-domain-content</code></strong>: a list of domains for
+  which content scripts are given cross-domain privileges to access content in
+  iframes or to make XMLHTTPRequests. See the documentation for
+<a href="dev-guide/guides/content-scripts/cross-domain.html">enabling cross-domain content scripts</a>.</p>
   </td>
 </tr>
 
 <tr>
   <td id="preferences"><code>preferences</code></td>
   <td><p>An array of JSON objects that use the following keys:
   <code>name</code>,<code>type</code>, <code>value</code>,
   <code>title</code>, and <code>description</code>.  These JSON objects will be used to
--- a/addon-sdk/source/doc/dev-guide-source/search.md
+++ b/addon-sdk/source/doc/dev-guide-source/search.md
@@ -1,14 +1,14 @@
 <!-- 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/. -->
 
 <div id="cse" style="width: 100%;">Loading</div>
-<script src="http://www.google.com/jsapi" type="text/javascript"></script>
+<script src="https://www.google.com/jsapi" type="text/javascript"></script>
 <script type="text/javascript">
 function parseQueryFromUrl () {
   var queryParamName = "q";
   var search = window.location.search.substr(1);
   var parts = search.split('&');
   for (var i = 0; i < parts.length; i++) {
     var keyvaluepair = parts[i].split('=');
     if (decodeURIComponent(keyvaluepair[0]) == queryParamName) {
@@ -33,17 +33,17 @@ google.setOnLoadCallback(function() {
     searchBox.value = queryFromUrl;
     searchBox.focus();
     searchBox.blur();
     customSearchControl.execute(queryFromUrl);
   }
 }, true);
 </script>
 
-<link rel="stylesheet" href="http://www.google.com/cse/style/look/default.css" type="text/css" />
+<link rel="stylesheet" href="https://www.google.com/cse/style/look/default.css" type="text/css" />
 
 <style type="text/css">
   #cse table, #cse tr, #cse td {
     border: none;
   }
 
   .gsc-above-wrapper-area, .gsc-result-info-container {
 	border: none;
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/adding-menus.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/adding-menus.md
@@ -16,49 +16,60 @@ But it's extensible by design, so anyone
 modules for add-on developers to use. Luckily, Erik Vold has written
 a [`menuitems`](https://github.com/erikvold/menuitems-jplib) module
 that enables us to add menu items.
 
 This tutorial does double-duty. It describes the general method for
 using an external, third-party module in your add-on, and it
 describes how to add a menu item using the `menuitems` module in particular.
 
+First, create a new add-on. Make a directory called "clickme" wherever you
+like, navigate to it and run `cfx init`.
+
+<pre>
+mkdir clickme
+cd clickme
+cfx init
+</pre>
+
+The usual directory structure will be created:
+
+<ul class="tree">
+  <li>clickme
+    <ul>
+    <li>data</li>
+    <li>docs
+      <ul><li>main.md</li></ul>
+    </li>
+    <li>lib
+      <ul><li>main.js</li></ul>
+    </li>
+    <li>package.json</li>
+    <li>README.md</li>
+    <li>tests
+      <ul><li>test-main.js</li></ul>
+    </li>
+    </ul>
+  </li>
+</ul>
+
+<div style="clear:both"></div>
+
 ## Installing `menuitems` ##
 
-First we'll download the `menuitems` package from
-[https://github.com/erikvold/menuitems-jplib](https://github.com/erikvold/menuitems-jplib/zipball/51080383cbb0fe2a05f8992a8aae890f4c014176).
-
-Third-party packages like `menuitems` can be installed in three
-different places:
-
-* in the `packages` directory under the SDK root. If you do this the package
-is available to any other add-ons you're developing using that SDK instance,
-and the package's documentation is visible through `cfx docs`.
-* in a `packages` directory you create under your add-on's root: if you
-do this, the package is only available to that add-on.
-* in a directory indicated using the `packages` key in
-your add-on's [package.json](dev-guide/package-spec.html). If you
-do this, you may not keep any packages in your add-on's `packages`
-directory, or they will not be found.
-
-In this example we will install the package under the SDK root. From
-the SDK root directory, execute something like the following commands:
+Create a directory under "clickme" called "packages".
+Then download the `menuitems` package from
+[https://github.com/erikvold/menuitems-jplib](https://github.com/erikvold/menuitems-jplib/zipball/51080383cbb0fe2a05f8992a8aae890f4c014176) and extract it into the "packages" directory you just created:
 
 <pre>
+mkdir packages
 cd packages
 tar -xf ../erikvold-menuitems-jplib-d80630c.zip
 </pre>
 
-Now if you run `cfx docs` you'll see a new section appear in the sidebar
-labeled "Third-Party APIs", which lists the modules in the `menuitems`
-package: this package contains a single module, also
-called `menuitems`.
-
-Click on the module name and you'll see API documentation for the module.
-
 ## Module Dependencies ##
 
 If third-party modules only depend on SDK modules, you can use them right
 away, but if they depend on other third-party modules, you'll have to install
 those dependencies as well.
 
 In the package's main directory you'll find a file called "package.json".
 Open it up and look for an entry named "dependencies". The entry for the
@@ -70,33 +81,28 @@ Open it up and look for an entry named "
 
 This tells us that we need to install the `vold-utils` package,
 which we can do by downloading it from
 [https://github.com/erikvold/vold-utils-jplib](https://github.com/voldsoftware/vold-utils-jplib/zipball/1b2ad874c2d3b2070a1b0d43301aa3731233e84f)
 and adding it under the `packages` directory alongside `menuitems`.
 
 ## Using `menuitems` ##
 
-We can use the `menuitems` module in exactly the same way we use built-in
-modules.
-
-The documentation for the `menuitems` module tells us to we create a menu
-item using `MenuItem()`. Of the options accepted by `MenuItem()`, we'll use
-this minimal set:
+The [documentation for the `menuitems` module](https://github.com/erikvold/menuitems-jplib/blob/master/docs/menuitems.md)
+tells us to create a menu item using `MenuItem()`. Of the options
+accepted by `MenuItem()`, we'll use this minimal set:
 
 * `id`: identifier for this menu item
 * `label`: text the item displays
 * `command`: function called when the user selects the item
 * `menuid`: identifier for the item's parent element
 * `insertbefore`: identifier for the item before which we want our item to
 appear
 
-Next, create a new add-on. Make a directory called 'clickme' wherever you
-like, navigate to it and run `cfx init`. Open `lib/main.js` and add the
-following code:
+<!--comment to terminate Markdown list -->
 
     var menuitem = require("menuitems").Menuitem({
       id: "clickme",
       menuid: "menu_ToolsPopup",
       label: "Click Me!",
       onCommand: function() {
         console.log("clicked");
       },
@@ -121,22 +127,11 @@ like this:
 </pre>
 
 Now we're done. Run the add-on and you'll see the new item appear in the
 `Tools` menu: select it and you'll see `info: clicked` appear in the
 console.
 
 ## Caveats ##
 
-Eventually we expect the availability of a rich set of third party packages
-will be one of the most valuable aspects of the SDK. Right now they're a great
-way to use features not supported by the supported APIs without the
-complexity of using the low-level APIs, but there are some caveats you should
-be aware of:
-
-* our support for third party packages is still fairly immature. One
-consequence of this is that it's not always obvious where to find third-party
-packages, although the
-[Community Developed Modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules)
-page in the SDK's GitHub Wiki lists a number of packages.
-
-* because third party modules typically use low-level APIs, they may be broken
-by new releases of Firefox.
+Third-party modules are a great way to use features not directly supported by
+the SDK, but because third party modules typically use low-level APIs,
+they may be broken by new releases of Firefox.
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/installation.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/installation.md
@@ -138,21 +138,16 @@ many lines of usage information:
 <pre>
 Usage: cfx [options] [command]
 </pre>
 
 This is the `cfx` command-line program.  It's your primary interface to the
 Add-on SDK.  You use it to launch Firefox and test your add-on, package your
 add-on for distribution, view documentation, and run unit tests.
 
-## cfx docs ##
-
-If you're reading these documents online, try running `cfx docs`. This will
-build the documentation for the SDK and display it in a browser.
-
 ## Problems? ##
 
 Try the [Troubleshooting](dev-guide/tutorials/troubleshooting.html)
 page.
 
 ## Next Steps ##
 
 Next, take a look at the
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/logging.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/logging.md
@@ -56,12 +56,21 @@ script:
 If you are running your add-on from the command line (for example,
 executing `cfx run` or `cfx test`) then the console's messages appear
 in the command shell you used.
 
 If you've installed the add-on in Firefox, or you're running the
 add-on in the Add-on Builder, then the messages appear in Firefox's
 [Error Console](https://developer.mozilla.org/en/Error_Console).
 
+But note that **by default, calls to `console.log()` will not result
+in any output in the Error Console for any installed add-ons**: this
+includes add-ons installed using the Add-on Builder or using tools
+like the
+[Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/).
+
+See ["Logging Levels"](dev-guide/console.html#Logging Levels)
+in the console reference documentation for more information on this.
+
 ## Learning More ##
 
 For the complete `console` API, see its
 [API reference](dev-guide/console.html).
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/mobile.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/mobile.md
@@ -307,17 +307,16 @@ Modules not yet supported in Firefox Mob
  - keyboard/observer
  - keyboard/utils
  - lang/functional
  - lang/type
  - [loader/cuddlefish](modules/sdk/loader/cuddlefish.html)
  - [loader/sandbox](modules/sdk/loader/sandbox.html)
  - [net/url](modules/sdk/net/url.html)
  - [net/xhr](modules/sdk/net/xhr.html)
- - [page-mod/match-pattern](modules/sdk/page-mod/match-pattern.html)
  - [platform/xpcom](modules/sdk/platform/xpcom.html)
  - [preferences/service](modules/sdk/preferences/service.html)
  - [system/environment](modules/sdk/system/environment.html)
  - [system/events](modules/sdk/system/events.html)
  - system/globals
  - [system/runtime](modules/sdk/system/runtime.html)
  - [system/unload](modules/sdk/system/unload.html)
  - [system/xul-app](modules/sdk/system/xul-app.html)
@@ -325,11 +324,12 @@ Modules not yet supported in Firefox Mob
  - [test/harness](modules/sdk/test/harness.html)
  - [test/httpd](modules/sdk/test/httpd.html)
  - [test/runner](modules/sdk/test/runner.html)
  - test/tmp-file
  - util/array
  - [util/collection](modules/sdk/util/collection.html)
  - [util/deprecate](modules/sdk/util/deprecate.html)
  - [util/list](modules/sdk/util/list.html)
+ - [util/match-pattern](modules/sdk/util/match-pattern.html)
  - util/registry
  - [util/uuid](modules/sdk/util/uuid.html)
  - [window/utils](modules/sdk/window/utils.html)
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/modifying-web-pages-url.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/modifying-web-pages-url.md
@@ -47,17 +47,17 @@ Try it out:
 This is what you should see:
 
 <img  class="image-center" src="static-files/media/screenshots/pagemod-ietf.png"
 alt="ietf.org eaten by page-mod" />
 
 ## Specifying the Match Pattern ##
 
 The match pattern uses the
-[`match-pattern`](modules/sdk/page-mod/match-pattern.html)
+[`match-pattern`](modules/sdk/util/match-pattern.html)
 syntax. You can pass a single match-pattern string, or an array.
 
 ## Keeping the Content Script in a Separate File ##
 
 In the example above we've passed in the content script as a string. Unless
 the script is extremely simple, you should instead maintain the script as a
 separate file. This makes the code easier to maintain, debug, and review.
 
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md
@@ -366,37 +366,16 @@ to the add-on:
 ### Repackaging ###
 
 Next we'll repackage the geolocation module.
 
 * create a new directory called "geolocation", and run `cfx init` in it.
 * delete the "main.js" that `cfx` generated, and copy "geolocation.js"
 there instead.
 
-### Documentation ###
-
-If you document your modules, people who install your package and
-execute `cfx docs` will see the documentation
-integrated with the SDK's own documentation.
-
-You can document the geolocation module by creating a file called
-"geolocation.md" in your package's "doc" directory. This file is also
-written in Markdown, although you can optionally use some
-[extended syntax](https://wiki.mozilla.org/Jetpack/SDK/Writing_Documentation#APIDoc_Syntax)
-to document APIs.
-
-Try it:
-
-* add a "geolocation.md" under "doc"
-* copy your geolocation package under the "packages" directory in the SDK root
-* execute `cfx docs`
-
-Once `cfx docs` has finished, you should see a new entry appear in the
-sidebar called "Third-Party APIs", which lists the geolocation module.
-
 ### Editing "package.json" ###
 
 The "package.json" file in your package's root directory contains metadata
 for your package. See the
 [package specification](dev-guide/package-spec.html) for
 full details. If you intend to distribute the package, this is a good place
 to add your name as the author, choose a distribution license, and so on.
 
--- a/addon-sdk/source/doc/module-source/sdk/context-menu.md
+++ b/addon-sdk/source/doc/module-source/sdk/context-menu.md
@@ -116,17 +116,17 @@ exported by the `context-menu` module.
     <td>
       This context occurs when the menu is invoked on pages with particular
       URLs. <code>matchPattern</code> is a match pattern string or an array of
       match pattern strings. When <code>matchPattern</code> is an array, the
       context occurs when the menu is invoked on a page whose URL matches any of
       the patterns. These are the same match pattern strings that you use with
       the <a href="modules/sdk/page-mod.html"><code>page-mod</code></a>
       <code>include</code> property.
-      <a href="modules/sdk/page-mod/match-pattern.html">Read more about patterns</a>.
+      <a href="modules/sdk/util/match-pattern.html">Read more about patterns</a>.
     </td>
   </tr>
   <tr>
     <td>
       array
     </td>
     <td>
       An array of any of the other types. This context occurs when all contexts
@@ -798,12 +798,12 @@ top-level context menu.
 
 <api name="URLContext">
 @class
 <api name="URLContext">
 @constructor
   Creates a context that matches pages with particular URLs. See Specifying
   Contexts above.
 @param matchPattern {string,array}
-  A [match pattern](modules/sdk/page-mod/match-pattern.html) string, regexp or an
+  A [match pattern](modules/sdk/util/match-pattern.html) string, regexp or an
   array of match pattern strings or regexps.
 </api>
 </api>
--- a/addon-sdk/source/doc/module-source/sdk/page-mod.md
+++ b/addon-sdk/source/doc/module-source/sdk/page-mod.md
@@ -298,17 +298,17 @@ Creates a page-mod.
 
         var pageMod = require("sdk/page-mod");
         pageMod.PageMod({
           include: "*.mozilla.org",
           contentScript: 'window.alert("Page matches ruleset");'
         });
 
     You can specify a set of URLs using a
-    [regular expression](modules/sdk/page-mod/match-pattern.html#Regular Expressions).
+    [regular expression](modules/sdk/util/match-pattern.html#Regular Expressions).
     The pattern must match the entire URL, not just a subset, and has
     `global`, `ignoreCase`, and `multiline` disabled.
 
         var pageMod = require("sdk/page-mod");
         pageMod.PageMod({
           include: /.*developer.*/,
           contentScript: 'window.alert("Page matches ruleset");'
         });
@@ -316,17 +316,17 @@ Creates a page-mod.
   To specify multiple patterns, pass an array of match patterns:
 
       var pageMod = require("sdk/page-mod");
       pageMod.PageMod({
         include: ["*.developer.mozilla.org", "*.addons.mozilla.org"],
         contentScript: 'window.alert("Page matches ruleset");'
       });
 
-    See the [match-pattern](modules/sdk/page-mod/match-pattern.html) module for
+    See the [match-pattern](modules/sdk/util/match-pattern.html) module for
     a detailed description of match pattern syntax.
 
   @prop [contentScriptFile] {string,array}
     This option specifies one or more content scripts to attach to targeted
     documents.
 
     Each script is supplied as a separate file under your add-on's "data"
     directory, and is specified by a URL typically constructed using the
--- a/addon-sdk/source/doc/module-source/sdk/panel.md
+++ b/addon-sdk/source/doc/module-source/sdk/panel.md
@@ -28,23 +28,16 @@ and the content remains loaded when a pa
 to keep a panel around in the background, updating its content as appropriate
 in preparation for the next time it is shown.
 
 Your add-on can receive notifications when a panel is shown or hidden by
 listening to its `show` and `hide` events.
 
 Opening a panel will close an already opened panel.
 
-<div class="warning">
-If your add-on has
-<a href="modules/sdk/private-browsing.html#Opting into private browsing">opted into private browsing</a>,
-then you can't use panels in your add-on. This is due to a platform bug which we expect to
-be fixed in Firefox 21.
-</div>
-
 ## Panel Content ##
 
 The panel's content is specified as HTML, which is loaded from the URL
 supplied in the `contentURL` option to the panel's constructor.
 
 You can load remote HTML into the panel:
 
     var panel = require("sdk/panel").Panel({
@@ -389,23 +382,19 @@ alt="OS X panel default style">
 This helps to ensure that the panel's style is consistent with the dialogs
 displayed by Firefox and other applications, but means you need to take care
 when applying your own styles. For example, if you set the panel's
 `background-color` property to `white` and do not set the `color` property,
 then the panel's text will be invisible on OS X although it looks fine on Ubuntu.
 
 ## Private Browsing ##
 
-If your add-on has
+If your add-on has not
 [opted into private browsing](modules/sdk/private-browsing.html#Opting into private browsing),
-then **you can't use panels in your add-on**. This is due to a platform bug which we expect to
-be fixed in Firefox 21.
-
-If your add-on has not opted into private browsing, and it calls `panel.show()`
-when the currently active window is a
+and it calls `panel.show()` when the currently active window is a
 [private window](modules/sdk/private-browsing.html#Per-window private browsing),
 then the panel will not be shown.
 
 <api name="Panel">
 @class
 The Panel object represents a floating modal dialog that can by an add-on to
 present user interface content.
 
--- a/addon-sdk/source/doc/module-source/sdk/private-browsing.md
+++ b/addon-sdk/source/doc/module-source/sdk/private-browsing.md
@@ -46,25 +46,20 @@ context menus that belong to private bro
 content scripts to documents belonging to private browser windows
 
 * any [`panel`](modules/sdk/panel.html) objects will not be shown if the
 active window is a private browser window
 
 * the [`selection`](modules/sdk/selection.html) module will not include
 any selections made in private browser windows
 
-Add-ons that have opted in:
-
-* will see private windows, so they will need to
+Add-ons that have opted in will see private windows, so they will need to
 use the `private-browsing` module to check whether objects are private,
 so as to avoid storing data derived from such objects.
 
-* will not be able to use panels in their code. This is due to a platform
-restriction which will be fixed in Firefox 21.
-
 Additionally, add-ons that use low-level modules such as
 [`window/utils`](modules/sdk/window/utils.html) may see private browser
 windows with certain functions, even if they have not explicitly opted
 into private browsing.
 
 ## Respecting private browsing ##
 
 The `private-browsing` module exports a single function
--- a/addon-sdk/source/doc/module-source/sdk/tabs.md
+++ b/addon-sdk/source/doc/module-source/sdk/tabs.md
@@ -2,62 +2,119 @@
    - 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/. -->
 
 <!-- contributed by Dietrich Ayala [dietrich@mozilla.com]  -->
 <!-- edited by Noelle Murata [fiveinchpixie@gmail.com]  -->
 
 The `tabs` module provides easy access to tabs and tab-related events.
 
-The module itself can be used like a basic list of all opened
-tabs across all windows. In particular, you can enumerate it:
-
-    var tabs = require('sdk/tabs');
-    for each (var tab in tabs)
-      console.log(tab.title);
+## Module-level Operations ##
 
-You can also access individual tabs by index:
-
-    var tabs = require('sdk/tabs');
-
-    tabs.on('ready', function () {
-      console.log('first: ' + tabs[0].title);
-      console.log('last: ' + tabs[tabs.length-1].title);
-    });
+### Open a Tab ###
 
 You can open a new tab, specifying various properties including location:
 
     var tabs = require("sdk/tabs");
     tabs.open("http://www.example.com");
 
+### Track Tabs ###
+
 You can register event listeners to be notified when tabs open, close, finish
 loading DOM content, or are made active or inactive:
 
     var tabs = require("sdk/tabs");
 
     // Listen for tab openings.
     tabs.on('open', function onOpen(tab) {
       myOpenTabs.push(tab);
     });
 
     // Listen for tab content loads.
     tabs.on('ready', function(tab) {
       console.log('tab is loaded', tab.title, tab.url)
     });
 
+### Access Tabs ###
+
+The module itself can be used as a list of all opened
+tabs across all windows. In particular, you can enumerate it:
+
+    var tabs = require('sdk/tabs');
+    for each (var tab in tabs)
+      console.log(tab.title);
+
+You can also access individual tabs by index:
+
+    var tabs = require('sdk/tabs');
+
+    tabs.on('ready', function () {
+      console.log('first: ' + tabs[0].title);
+      console.log('last: ' + tabs[tabs.length-1].title);
+    });
+
+You can access the currently active tab:
+
+    var tabs = require('sdk/tabs');
+
+    tabs.on('activate', function () {
+      console.log('active: ' + tabs.activeTab.url);
+    });
+
+## Tab-level Operations ##
+
+### Track a Tab ###
+
+Given a tab, you can register event listeners to be notified when the
+tab is closed, activated or deactivated, or when the page hosted by the
+tab is loaded or retrieved from the
+["back-forward cache"](https://developer.mozilla.org/en-US/docs/Working_with_BFCache):
+
+    var tabs = require("sdk/tabs");
+
+    function onOpen(tab) {
+      console.log(tab.url + " is open");
+      tab.on("pageshow", logShow);
+      tab.on("activate", logActivate);
+      tab.on("deactivate", logDeactivate);
+      tab.on("close", logClose);
+    }
+
+    function logShow(tab) {
+      console.log(tab.url + " is loaded");
+    }
+
+    function logActivate(tab) {
+      console.log(tab.url + " is activated");
+    }
+
+    function logDeactivate(tab) {
+      console.log(tab.url + " is deactivated");
+    }
+
+    function logClose(tab) {
+      console.log(tab.url + " is closed");
+    }
+
+    tabs.on('open', onOpen);
+
+### Manipulate a Tab ###
+
 You can get and set various properties of tabs (but note that properties
  relating to the tab's content, such as the URL, will not contain valid
 values until after the tab's `ready` event fires). By setting the `url`
 property you can load a new page in the tab:
 
     var tabs = require("sdk/tabs");
     tabs.on('activate', function(tab) {
       tab.url = "http://www.example.com";
     });
 
+### Run Scripts in a Tab ###
+
 You can attach a [content script](dev-guide/guides/content-scripts/index.html)
 to the page hosted in a tab, and use that to access and manipulate the page's
 content (see the
 [Modifying the Page Hosted by a Tab](dev-guide/tutorials/modifying-web-pages-tab.html) tutorial):
 
     var tabs = require("sdk/tabs");
 
     tabs.on('activate', function(tab) {
@@ -157,35 +214,35 @@ Boolean which will determine whether the
 If your add-on does not support private browsing this will have no effect.
 See the [private-browsing](modules/sdk/private-browsing.html) documentation for more information.
 
 @prop [isPinned] {boolean}
 If present and true, then the new tab will be pinned as an
 [app tab](http://support.mozilla.com/en-US/kb/what-are-app-tabs).
 
 @prop [onOpen] {function}
-A callback function that will be registered for 'open' event.
+A callback function that will be registered for the 'open' event.
 This is an optional property.
 @prop [onClose] {function}
-A callback function that will be registered for 'close' event.
+A callback function that will be registered for the 'close' event.
 This is an optional property.
 @prop [onReady] {function}
-A callback function that will be registered for 'ready' event.
+A callback function that will be registered for the 'ready' event.
 This is an optional property.
 @prop [onLoad] {function}
-A callback function that will be registered for 'load' event.
+A callback function that will be registered for the 'load' event.
 This is an optional property.
 @prop [onPageShow] {function}
-A callback function that will be registered for 'pageshow' event.
+A callback function that will be registered for the 'pageshow' event.
 This is an optional property.
 @prop [onActivate] {function}
-A callback function that will be registered for 'activate' event.
+A callback function that will be registered for the 'activate' event.
 This is an optional property.
 @prop [onDeactivate] {function}
-A callback function that will be registered for 'deactivate' event.
+A callback function that will be registered for the 'deactivate' event.
 This is an optional property.
 </api>
 
 <api name="Tab">
 @class
 A `Tab` instance represents a single open tab. It contains various tab
 properties, several methods for manipulation, as well as per-tab event
 registration.
@@ -335,67 +392,80 @@ tab's window is closed.
 @argument {Tab}
 Listeners are passed the tab object.
 </api>
 
 <api name="ready">
 @event
 
 This event is emitted when the DOM for the tab's content is ready. It is
-equivalent to the `DOMContentLoaded` event for the given content page.
+equivalent to the
+[`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/DOMContentLoaded)
+event for the given content page.
 
 A single tab will emit this event every time the DOM is loaded: so it will be
 emitted again if the tab's location changes or the content is reloaded.
-
 After this event has been emitted, all properties relating to the tab's
 content can be used.
 
 @argument {Tab}
 Listeners are passed the tab object.
 </api>
 
 <api name="load">
 @event
 
 This event is emitted when the page for the tab's content is loaded. It is
-equivalent to the `load` event for the given content page.
+equivalent to the
+[`load`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/load)
+event for the given content page.
 
 A single tab will emit this event every time the page is loaded: so it will be
 emitted again if the tab's location changes or the content is reloaded.
+This event is similar to the [`ready`](modules/sdk/tabs.html#ready) event,
+except that it can be used for pages that do not have a `DOMContentLoaded`
+event, like images.
 
 After this event has been emitted, all properties relating to the tab's
-content can be used.
-
-This is fired after the `ready` event on DOM content pages and can be used
-for pages that do not have a `DOMContentLoaded` event, like images.
+content can be used. For pages that have a `DOMContentLoaded` event, `load`
+is fired after `ready`.
 
 @argument {Tab}
 Listeners are passed the tab object.
 </api>
 
 <api name="pageshow">
 @event
 
-This event is emitted when the page for the tab's content is potentially
-from the cache. It is equivilent to the [pageshow](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow) event for the given
-content page.
+The `pageshow` event is emitted when the page for a tab's content is loaded.
+It is equivalent to the
+[`pageshow`](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow)
+event for the given content page.
+
+This event is similar to the [`load`](modules/sdk/tabs.html#load) and
+[`ready`](modules/sdk/tabs.html#ready) events, except unlike
+`load` and `ready`, `pageshow` is triggered if the page was retrieved from the
+[bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache).
+This means that if the user loads a page, loads a new page, then
+moves back to the previous page using the "Back" button,
+the `pageshow` event is emitted when the user moves back to the previous
+page, while the `load` and `ready` events are not.
+
+This event is *not* emitted when the tab is made the active tab: to get
+notified about that, you need to listen to the
+[`activate`](modules/sdk/tabs.html#activate) event.
 
 After this event has been emitted, all properties relating to the tab's
-content can be used.
-
-While the `ready` and `load` events will not be fired when a user uses the back
-or forward buttons to navigate history, the `pageshow` event will be fired.
-If the `persisted` argument is true, then the contents were loaded from the
-bfcache.
+content can be used. It is emitted after `load` and `ready`.
 
 @argument {Tab}
 Listeners are passed the tab object.
 @argument {persisted}
 Listeners are passed a boolean value indicating whether or not the page
-was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache) or not.
+was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache).
 </api>
 
 <api name="activate">
 @event
 
 This event is emitted when the tab is made active.
 
 @argument {Tab}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/doc/module-source/sdk/util/match-pattern.md
@@ -0,0 +1,259 @@
+<!-- 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/. -->
+
+The `match-pattern` module can be used to test strings containing URLs
+against simple patterns.
+
+## Specifying Patterns ##
+
+There are three ways you can specify patterns:
+
+* as an exact match string
+* using a wildcard in a string
+* using a regular expression
+
+### Exact Matches ###
+
+**A URL** matches only that URL. The URL must start with a scheme, end with a
+slash, and contain no wildcards.
+
+<table>
+
+  <colgroup>
+    <col width="20%">
+    <col width="25%">
+    <col width="55%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+    <th>Example non-matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>"http://example.com/"</code></td>
+    <td><code>http://example.com/</code></td>
+    <td><code>http://example.com</code><br>
+        <code>http://example.com/foo</code><br>
+        <code>https://example.com/</code><br>
+        <code>http://foo.example.com/</code></td>
+  </tr>
+
+</table>
+
+### Wildcards ###
+
+**A single asterisk** matches any URL with an `http`, `https`, or `ftp`
+scheme. For other schemes like `file`, `resource`, or `data`, use a scheme
+followed by an asterisk, as below.
+
+<table>
+
+  <colgroup>
+    <col width="20%">
+    <col width="25%">
+    <col width="55%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+    <th>Example non-matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>"*"</code></td>
+    <td><code>http://example.com/</code><br>
+        <code>https://example.com/</code><br>
+        <code>ftp://example.com/</code><br>
+        <code>http://bar.com/foo.js</code><br>
+        <code>http://foo.com/</code></td>
+    <td><code>file://example.js</code><br>
+        <code>resource://me/my-addon/data/file.html</code><br>
+        <code>data:text/html,Hi there</code></td>
+  </tr>
+
+</table>
+
+**A domain name prefixed with an asterisk and dot** matches any URL of that
+domain or a subdomain, using any of `http`, `https`, `ftp`.
+
+<table>
+
+  <colgroup>
+    <col width="20%">
+    <col width="25%">
+    <col width="55%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+    <th>Example non-matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>"*.example.com"</code></td>
+    <td><code>http://example.com/</code><br>
+        <code>http://foo.example.com/</code><br>
+        <code>https://example.com/</code><br>
+        <code>http://example.com/foo</code><br>
+        <code>ftp://foo.example.com/</code></td>
+    <td><code>ldap://example.com</code><br>
+        <code>http://example.foo.com/</code></td>
+  </tr>
+
+</table>
+
+**A URL followed by an asterisk** matches that URL and any URL prefixed with
+the pattern.
+
+<table>
+
+  <colgroup>
+    <col width="20%">
+    <col width="25%">
+    <col width="55%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+    <th>Example non-matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>"https://foo.com/*"</code></td>
+    <td><code>https://foo.com/</code><br>
+        <code>https://foo.com/bar</code></td>
+    <td><code>http://foo.com/</code><br>
+        <code>https://foo.com</code><br>
+        <code>https://bar.foo.com/</code></td>
+  </tr>
+
+</table>
+
+**A scheme followed by an asterisk** matches all URLs with that scheme. To
+match local files, use `file://*`, and to match files loaded from your
+add-on's [data](modules/sdk/self.html#data) directory, use `resource://`.
+
+<table>
+
+  <colgroup>
+    <col width="20%">
+    <col width="80%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>"file://*"</code></td>
+    <td><code>file://C:/file.html</code><br>
+        <code>file:///home/file.png</code></td>
+  </tr>
+
+  <tr>
+    <td><code>"resource://*"</code></td>
+    <td><code>resource://my-addon-at-me-dot-org/my-addon/data/file.html</code></td>
+  </tr>
+
+  <tr>
+    <td><code>"data:*"</code></td>
+    <td><code>data:text/html,Hi there</code></td>
+  </tr>
+
+</table>
+
+### Regular Expressions ###
+
+You can specify patterns using a
+[regular expression](https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions):
+
+    var { MatchPattern } = require("sdk/util/match-pattern");
+    var pattern = new MatchPattern(/.*example.*/);
+
+The regular expression is subject to restrictions based on those applied to the
+[HTML5 pattern attribute](http://dev.w3.org/html5/spec/common-input-element-attributes.html#attr-input-pattern). In particular:
+
+* The pattern must match the entire value, not just any subset. For example, the
+pattern `/moz.*/` will not match the URL `http://mozilla.org`.
+
+* The expression is compiled with the `global`, `ignoreCase`, and `multiline` flags
+  disabled. The `MatchPattern` constructor will throw an exception
+  if you try to set any of these flags.
+
+<table>
+
+  <colgroup>
+    <col width="30%">
+    <col width="35%">
+    <col width="35%">
+  </colgroup>
+
+  <tr>
+    <th>Example pattern</th>
+    <th>Example matching URLs</th>
+    <th>Example non-matching URLs</th>
+  </tr>
+
+  <tr>
+    <td><code>/.*moz.*/</code></td>
+    <td><code>http://foo.mozilla.org/</code><br>
+        <code>http://mozilla.org</code><br>
+        <code>https://mozilla.org</code><br>
+        <code>http://foo.com/mozilla</code><br>
+        <code>http://hemozoon.org</code><br>
+        <code>mozscheme://foo.org</code><br></td>
+    <td><code>http://foo.org</code><br>
+  </tr>
+
+  <tr>
+    <td><code>/http:\/\/moz.*/</code></td>
+    <td><code>http://mozilla.org</code><br>
+        <code>http://mozzarella.com</code></td>
+    <td><code>https://mozilla.org</code><br>
+        <code>http://foo.mozilla.org/</code><br>
+        <code>http://foo.com/moz</code></td>
+  </tr>
+
+  <tr>
+    <td><code>/http.*moz.*/</code><br></td>
+    <td><code>http://foo.mozilla.org/</code><br>
+        <code>http://mozilla.org</code><br>
+        <code>http://hemozoon.org/</code></td>
+        <td><code>ftp://http/mozilla.org</code></td>
+  </tr>
+
+</table>
+
+## Examples ##
+
+    var { MatchPattern } = require("sdk/util/match-pattern");
+    var pattern = new MatchPattern("http://example.com/*");
+    console.log(pattern.test("http://example.com/"));       // true
+    console.log(pattern.test("http://example.com/foo"));    // true
+    console.log(pattern.test("http://foo.com/"));           // false!
+
+<api name="MatchPattern">
+@class
+<api name="MatchPattern">
+@constructor
+  This constructor creates match pattern objects that can be used to test URLs.
+@param pattern {string}
+  The pattern to use.  See Patterns above.
+</api>
+
+<api name="test">
+@method
+  Tests a URL against the match pattern.
+@param url {string}
+  The URL to test.
+@returns {boolean}
+  True if the URL matches the pattern and false otherwise.
+</api>
+</api>
--- a/addon-sdk/source/lib/sdk/addon-page.js
+++ b/addon-sdk/source/lib/sdk/addon-page.js
@@ -32,39 +32,42 @@ WindowTracker({
     let { XULBrowserWindow } = window;
     let { hideChromeForLocation } = XULBrowserWindow;
 
     windows(window).hideChromeForLocation = hideChromeForLocation;
 
     // Augmenting the behavior of `hideChromeForLocation` method, as
     // suggested by https://developer.mozilla.org/en-US/docs/Hiding_browser_chrome
     XULBrowserWindow.hideChromeForLocation = function(url) {
-      if (url.indexOf(addonURL) === 0) {
-        let rest = url.substr(addonURL.length);
-        return rest.length === 0 || ['#','?'].indexOf(rest.charAt(0)) > -1
-      }
-
-      return hideChromeForLocation.call(this, url);
+      return isAddonURL(url) || hideChromeForLocation.call(this, url);
     }
   },
 
   onUntrack: function onUntrack(window) {
     if (isXULBrowser(window))
       getTabs(window).filter(tabFilter).forEach(untrackTab.bind(null, window));
   }
 });
 
+function isAddonURL(url) {
+  if (url.indexOf(addonURL) === 0) {
+    let rest = url.substr(addonURL.length);
+    return ((rest.length === 0) || (['#','?'].indexOf(rest.charAt(0)) > -1));
+  }
+  return false;
+}
+
 function tabFilter(tab) {
-  return getURI(tab) === addonURL;
+  return isAddonURL(getURI(tab));
 }
 
 function untrackTab(window, tab) {
   // Note: `onUntrack` will be called for all windows on add-on unloads,
   // so we want to clean them up from these URLs.
   let { hideChromeForLocation } = windows(window);
 
   if (hideChromeForLocation) {
-    window.XULBrowserWindow.hideChromeForLocation = hideChromeForLocation;
+    window.XULBrowserWindow.hideChromeForLocation = hideChromeForLocation.bind(window.XULBrowserWindow);
     windows(window).hideChromeForLocation = null;
   }
 
   closeTab(tab);
 }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/events.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+'use strict';
+
+module.metadata = {
+  'stability': 'experimental'
+};
+
+let { request: hostReq, response: hostRes } = require('./host');
+let { defer: async } = require('../lang/functional');
+let { defer } = require('../core/promise');
+let { emit: emitSync, on, off } = require('../event/core');
+let { uuid } = require('../util/uuid');
+let emit = async(emitSync);
+
+// Map of IDs to deferreds
+let requests = new Map();
+
+// May not be necessary to wrap this in `async`
+// once promises are async via bug 881047
+let receive = async(function ({data, id, error}) {
+  let request = requests.get(id);
+  if (request) {
+    if (error) request.reject(error);
+    else request.resolve(clone(data));
+    requests.delete(id);
+  }
+});
+on(hostRes, 'data', receive);
+
+/*
+ * Send is a helper to be used in client APIs to send
+ * a request to host
+ */
+function send (eventName, data) {
+  let id = uuid();
+  let deferred = defer();
+  requests.set(id, deferred);
+  emit(hostReq, 'data', {
+    id: id,
+    data: clone(data),
+    event: eventName
+  });
+  return deferred.promise;
+}
+exports.send = send;
+
+/*
+ * Implement internal structured cloning algorithm in the future?
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm
+ */
+function clone (obj) JSON.parse(JSON.stringify(obj || {}))
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/host.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+exports.request = {};
+exports.response = {};
--- a/addon-sdk/source/lib/sdk/content/events.js
+++ b/addon-sdk/source/lib/sdk/content/events.js
@@ -25,33 +25,33 @@ let insert = observe("document-element-i
 let windowCreate = merge([
   observe("content-document-global-created"),
   observe("chrome-document-global-created")
 ]);
 let create = map(windowCreate, function({target, data, type}) {
   return { target: target.document, type: type, data: data }
 });
 
-function readStates({document}) {
+function streamEventsFrom({document}) {
   // Map supported event types to a streams of those events on the given
   // `window` for the inserted document and than merge these streams into
   // single form stream off all window state change events.
   let stateChanges = TYPES.map(function(type) {
     return open(document, type, { capture: true });
   });
 
   // Since load events on document occur for every loded resource
   return filter(merge(stateChanges), function({target}) {
     return target instanceof Ci.nsIDOMDocument
   })
 }
-
+exports.streamEventsFrom = streamEventsFrom;
 
 let opened = windows(null, { includePrivate: true });
-let state = merge(opened.map(readStates));
+let state = merge(opened.map(streamEventsFrom));
 
 
 let futureReady = filter(windowEvents, function({type})
                                         type === "DOMContentLoaded");
 let futureWindows = map(futureReady, function({target}) target);
-let futureState = expand(futureWindows, readStates);
+let futureState = expand(futureWindows, streamEventsFrom);
 
 exports.events = merge([insert, create, state, futureState]);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -0,0 +1,43 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+let assetsURI = require("../self").data.url();
+let isArray = Array.isArray;
+
+function isAddonContent({ contentURL }) {
+  return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
+}
+exports.isAddonContent = isAddonContent;
+
+function hasContentScript({ contentScript, contentScriptFile }) {
+  return (isArray(contentScript) ? contentScript.length > 0 :
+         !!contentScript) ||
+         (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
+         !!contentScriptFile);
+}
+exports.hasContentScript = hasContentScript;
+
+function requiresAddonGlobal(model) {
+  return isAddonContent(model) && !hasContentScript(model);
+}
+exports.requiresAddonGlobal = requiresAddonGlobal;
+
+function getAttachEventType(model) {
+  if (!model) return null;
+  let when = model.contentScriptWhen;
+  return requiresAddonGlobal(model) ? "document-element-inserted" :
+         when === "start" ? "document-element-inserted" :
+         when === "end" ? "load" :
+         when === "ready" ? "DOMContentLoaded" :
+         null;
+}
+exports.getAttachEventType = getAttachEventType;
+
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -10,17 +10,17 @@ module.metadata = {
 const { Class, mix } = require("./core/heritage");
 const { addCollectionProperty } = require("./util/collection");
 const { ns } = require("./core/namespace");
 const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
 const { URL, isValidURI } = require("./url");
 const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
 const { isBrowser, getInnerId } = require("./window/utils");
 const { Ci } = require("chrome");
-const { MatchPattern } = require("./page-mod/match-pattern");
+const { MatchPattern } = require("./util/match-pattern");
 const { Worker } = require("./content/worker");
 const { EventTarget } = require("./event/target");
 const { emit } = require('./event/core');
 const { when } = require('./system/unload');
 
 // All user items we add have this class.
 const ITEM_CLASS = "addon-context-menu-item";
 
--- a/addon-sdk/source/lib/sdk/core/heritage.js
+++ b/addon-sdk/source/lib/sdk/core/heritage.js
@@ -155,18 +155,22 @@ var Class = new function() {
     var initialize = prototype.initialize;
 
     // Combine ancestor attributes with prototype's attributes so that
     // ancestors attributes also become initializeable.
     var attributes = mix(descriptor.extends.constructor.attributes || {},
                          getDataProperties(prototype));
 
     constructor.attributes = attributes;
-    constructor.prototype = prototype;
-    return freeze(constructor);
+    Object.defineProperty(constructor, 'prototype', {
+      configurable: false,
+      writable: false,
+      value: prototype
+    });
+    return constructor;
   };
 }
 Class.prototype = extend(null, obscure({
   constructor: function constructor() {
     this.initialize.apply(this, arguments);
     return this;
   },
   initialize: function initialize() {
--- a/addon-sdk/source/lib/sdk/deprecated/events.js
+++ b/addon-sdk/source/lib/sdk/deprecated/events.js
@@ -87,17 +87,17 @@ const eventEmitter =  {
   /**
    * Returns an array of listeners for the specified event `type`. This array
    * can be manipulated, e.g. to remove listeners.
    * @param {String} type
    *    The type of event.
    */
   _listeners: function listeners(type) {
     let events = this._events || (this._events = {});
-    return events[type] || (events[type] = []);
+    return (events.hasOwnProperty(type) && events[type]) || (events[type] = []);
   },
 
   /**
    * Execute each of the listeners in order with the supplied arguments.
    * Returns `true` if listener for this event was called, `false` if there are
    * no listeners for this event `type`.
    *
    * All the exceptions that are thrown by listeners during the emit
--- a/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js
@@ -50,26 +50,39 @@ TestFinder.prototype = {
       };
     } else
       filter = function() {return true};
 
     suites.forEach(
       function(suite) {
         // Load each test file as a main module in its own loader instance
         // `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
-        var loader = Loader(module);
-        var module = cuddlefish.main(loader, suite);
+        let loader = Loader(module);
+        let suiteModule;
+
+        try {
+          suiteModule = cuddlefish.main(loader, suite);
+        }
+        catch (e) {
+          if (!/^Unsupported Application/.test(e.message))
+            throw e;
+          // If `Unsupported Application` error thrown during test,
+          // skip the test suite
+          suiteModule = {
+            'test suite skipped': assert => assert.pass(e.message)
+          };
+        }
 
         if (self.testInProcess)
-          for each (let name in Object.keys(module).sort()) {
+          for each (let name in Object.keys(suiteModule).sort()) {
             if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) {
               tests.push({
-                           setup: module.setup,
-                           teardown: module.teardown,
-                           testFunction: module[name],
+                           setup: suiteModule.setup,
+                           teardown: suiteModule.teardown,
+                           testFunction: suiteModule[name],
                            name: suite + "." + name
                          });
             }
           }
       });
 
     cb(tests);
   }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/fs/path.js
@@ -0,0 +1,500 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// Adapted version of:
+// https://github.com/joyent/node/blob/v0.11.3/lib/path.js
+
+// Shim process global from node.
+var process = Object.create(require('../system'));
+process.cwd = process.pathFor.bind(process, 'CurProcD');
+
+// Update original check in node `process.platform === 'win32'` since in SDK it's `winnt`.
+var isWindows = process.platform.indexOf('win') === 0;
+
+
+
+// resolves . and .. elements in a path array with directory names there
+// must be no slashes, empty elements, or device names (c:\) in the array
+// (so also no leading and trailing slashes - it does not distinguish
+// relative and absolute paths)
+function normalizeArray(parts, allowAboveRoot) {
+  // if the path tries to go above the root, `up` ends up > 0
+  var up = 0;
+  for (var i = parts.length - 1; i >= 0; i--) {
+    var last = parts[i];
+    if (last === '.') {
+      parts.splice(i, 1);
+    } else if (last === '..') {
+      parts.splice(i, 1);
+      up++;
+    } else if (up) {
+      parts.splice(i, 1);
+      up--;
+    }
+  }
+
+  // if the path is allowed to go above the root, restore leading ..s
+  if (allowAboveRoot) {
+    for (; up--; up) {
+      parts.unshift('..');
+    }
+  }
+
+  return parts;
+}
+
+
+if (isWindows) {
+  // Regex to split a windows path into three parts: [*, device, slash,
+  // tail] windows-only
+  var splitDeviceRe =
+      /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
+
+  // Regex to split the tail part of the above into [*, dir, basename, ext]
+  var splitTailRe =
+      /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
+
+  // Function to split a filename into [root, dir, basename, ext]
+  // windows version
+  var splitPath = function(filename) {
+    // Separate device+slash from tail
+    var result = splitDeviceRe.exec(filename),
+        device = (result[1] || '') + (result[2] || ''),
+        tail = result[3] || '';
+    // Split the tail into dir, basename and extension
+    var result2 = splitTailRe.exec(tail),
+        dir = result2[1],
+        basename = result2[2],
+        ext = result2[3];
+    return [device, dir, basename, ext];
+  };
+
+  var normalizeUNCRoot = function(device) {
+    return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
+  };
+
+  // path.resolve([from ...], to)
+  // windows version
+  exports.resolve = function() {
+    var resolvedDevice = '',
+        resolvedTail = '',
+        resolvedAbsolute = false;
+
+    for (var i = arguments.length - 1; i >= -1; i--) {
+      var path;
+      if (i >= 0) {
+        path = arguments[i];
+      } else if (!resolvedDevice) {
+        path = process.cwd();
+      } else {
+        // Windows has the concept of drive-specific current working
+        // directories. If we've resolved a drive letter but not yet an
+        // absolute path, get cwd for that drive. We're sure the device is not
+        // an unc path at this points, because unc paths are always absolute.
+        path = process.env['=' + resolvedDevice];
+        // Verify that a drive-local cwd was found and that it actually points
+        // to our drive. If not, default to the drive's root.
+        if (!path || path.substr(0, 3).toLowerCase() !==
+            resolvedDevice.toLowerCase() + '\\') {
+          path = resolvedDevice + '\\';
+        }
+      }
+
+      // Skip empty and invalid entries
+      if (typeof path !== 'string') {
+        throw new TypeError('Arguments to path.resolve must be strings');
+      } else if (!path) {
+        continue;
+      }
+
+      var result = splitDeviceRe.exec(path),
+          device = result[1] || '',
+          isUnc = device && device.charAt(1) !== ':',
+          isAbsolute = exports.isAbsolute(path),
+          tail = result[3];
+
+      if (device &&
+          resolvedDevice &&
+          device.toLowerCase() !== resolvedDevice.toLowerCase()) {
+        // This path points to another device so it is not applicable
+        continue;
+      }
+
+      if (!resolvedDevice) {
+        resolvedDevice = device;
+      }
+      if (!resolvedAbsolute) {
+        resolvedTail = tail + '\\' + resolvedTail;
+        resolvedAbsolute = isAbsolute;
+      }
+
+      if (resolvedDevice && resolvedAbsolute) {
+        break;
+      }
+    }
+
+    // Convert slashes to backslashes when `resolvedDevice` points to an UNC
+    // root. Also squash multiple slashes into a single one where appropriate.
+    if (isUnc) {
+      resolvedDevice = normalizeUNCRoot(resolvedDevice);
+    }
+
+    // At this point the path should be resolved to a full absolute path,
+    // but handle relative paths to be safe (might happen when process.cwd()
+    // fails)
+
+    // Normalize the tail path
+
+    function f(p) {
+      return !!p;
+    }
+
+    resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(f),
+                                  !resolvedAbsolute).join('\\');
+
+    return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) ||
+           '.';
+  };
+
+  // windows version
+  exports.normalize = function(path) {
+    var result = splitDeviceRe.exec(path),
+        device = result[1] || '',
+        isUnc = device && device.charAt(1) !== ':',
+        isAbsolute = exports.isAbsolute(path),
+        tail = result[3],
+        trailingSlash = /[\\\/]$/.test(tail);
+
+    // If device is a drive letter, we'll normalize to lower case.
+    if (device && device.charAt(1) === ':') {
+      device = device[0].toLowerCase() + device.substr(1);
+    }
+
+    // Normalize the tail path
+    tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) {
+      return !!p;
+    }), !isAbsolute).join('\\');
+
+    if (!tail && !isAbsolute) {
+      tail = '.';
+    }
+    if (tail && trailingSlash) {
+      tail += '\\';
+    }
+
+    // Convert slashes to backslashes when `device` points to an UNC root.
+    // Also squash multiple slashes into a single one where appropriate.
+    if (isUnc) {
+      device = normalizeUNCRoot(device);
+    }
+
+    return device + (isAbsolute ? '\\' : '') + tail;
+  };
+
+  // windows version
+  exports.isAbsolute = function(path) {
+    var result = splitDeviceRe.exec(path),
+        device = result[1] || '',
+        isUnc = device && device.charAt(1) !== ':';
+    // UNC paths are always absolute
+    return !!result[2] || isUnc;
+  };
+
+  // windows version
+  exports.join = function() {
+    function f(p) {
+      if (typeof p !== 'string') {
+        throw new TypeError('Arguments to path.join must be strings');
+      }
+      return p;
+    }
+
+    var paths = Array.prototype.filter.call(arguments, f);
+    var joined = paths.join('\\');
+
+    // Make sure that the joined path doesn't start with two slashes, because
+    // normalize() will mistake it for an UNC path then.
+    //
+    // This step is skipped when it is very clear that the user actually
+    // intended to point at an UNC path. This is assumed when the first
+    // non-empty string arguments starts with exactly two slashes followed by
+    // at least one more non-slash character.
+    //
+    // Note that for normalize() to treat a path as an UNC path it needs to
+    // have at least 2 components, so we don't filter for that here.
+    // This means that the user can use join to construct UNC paths from
+    // a server name and a share name; for example:
+    //   path.join('//server', 'share') -> '\\\\server\\share\')
+    if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
+      joined = joined.replace(/^[\\\/]{2,}/, '\\');
+    }
+
+    return exports.normalize(joined);
+  };
+
+  // path.relative(from, to)
+  // it will solve the relative path from 'from' to 'to', for instance:
+  // from = 'C:\\orandea\\test\\aaa'
+  // to = 'C:\\orandea\\impl\\bbb'
+  // The output of the function should be: '..\\..\\impl\\bbb'
+  // windows version
+  exports.relative = function(from, to) {
+    from = exports.resolve(from);
+    to = exports.resolve(to);
+
+    // windows is not case sensitive
+    var lowerFrom = from.toLowerCase();
+    var lowerTo = to.toLowerCase();
+
+    function trim(arr) {
+      var start = 0;
+      for (; start < arr.length; start++) {
+        if (arr[start] !== '') break;
+      }
+
+      var end = arr.length - 1;
+      for (; end >= 0; end--) {
+        if (arr[end] !== '') break;
+      }
+
+      if (start > end) return [];
+      return arr.slice(start, end - start + 1);
+    }
+
+    var toParts = trim(to.split('\\'));
+
+    var lowerFromParts = trim(lowerFrom.split('\\'));
+    var lowerToParts = trim(lowerTo.split('\\'));
+
+    var length = Math.min(lowerFromParts.length, lowerToParts.length);
+    var samePartsLength = length;
+    for (var i = 0; i < length; i++) {
+      if (lowerFromParts[i] !== lowerToParts[i]) {
+        samePartsLength = i;
+        break;
+      }
+    }
+
+    if (samePartsLength == 0) {
+      return to;
+    }
+
+    var outputParts = [];
+    for (var i = samePartsLength; i < lowerFromParts.length; i++) {
+      outputParts.push('..');
+    }
+
+    outputParts = outputParts.concat(toParts.slice(samePartsLength));
+
+    return outputParts.join('\\');
+  };
+
+  exports.sep = '\\';
+  exports.delimiter = ';';
+
+} else /* posix */ {
+
+  // Split a filename into [root, dir, basename, ext], unix version
+  // 'root' is just a slash, or nothing.
+  var splitPathRe =
+      /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
+  var splitPath = function(filename) {
+    return splitPathRe.exec(filename).slice(1);
+  };
+
+  // path.resolve([from ...], to)
+  // posix version
+  exports.resolve = function() {
+    var resolvedPath = '',
+        resolvedAbsolute = false;
+
+    for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+      var path = (i >= 0) ? arguments[i] : process.cwd();
+
+      // Skip empty and invalid entries
+      if (typeof path !== 'string') {
+        throw new TypeError('Arguments to path.resolve must be strings');
+      } else if (!path) {
+        continue;
+      }
+
+      resolvedPath = path + '/' + resolvedPath;
+      resolvedAbsolute = path.charAt(0) === '/';
+    }
+
+    // At this point the path should be resolved to a full absolute path, but
+    // handle relative paths to be safe (might happen when process.cwd() fails)
+
+    // Normalize the path
+    resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) {
+      return !!p;
+    }), !resolvedAbsolute).join('/');
+
+    return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
+  };
+
+  // path.normalize(path)
+  // posix version
+  exports.normalize = function(path) {
+    var isAbsolute = exports.isAbsolute(path),
+        trailingSlash = path.substr(-1) === '/';
+
+    // Normalize the path
+    path = normalizeArray(path.split('/').filter(function(p) {
+      return !!p;
+    }), !isAbsolute).join('/');
+
+    if (!path && !isAbsolute) {
+      path = '.';
+    }
+    if (path && trailingSlash) {
+      path += '/';
+    }
+
+    return (isAbsolute ? '/' : '') + path;
+  };
+
+  // posix version
+  exports.isAbsolute = function(path) {
+    return path.charAt(0) === '/';
+  };
+
+  // posix version
+  exports.join = function() {
+    var paths = Array.prototype.slice.call(arguments, 0);
+    return exports.normalize(paths.filter(function(p, index) {
+      if (typeof p !== 'string') {
+        throw new TypeError('Arguments to path.join must be strings');
+      }
+      return p;
+    }).join('/'));
+  };
+
+
+  // path.relative(from, to)
+  // posix version
+  exports.relative = function(from, to) {
+    from = exports.resolve(from).substr(1);
+    to = exports.resolve(to).substr(1);
+
+    function trim(arr) {
+      var start = 0;
+      for (; start < arr.length; start++) {
+        if (arr[start] !== '') break;
+      }
+
+      var end = arr.length - 1;
+      for (; end >= 0; end--) {
+        if (arr[end] !== '') break;
+      }
+
+      if (start > end) return [];
+      return arr.slice(start, end - start + 1);
+    }
+
+    var fromParts = trim(from.split('/'));
+    var toParts = trim(to.split('/'));
+
+    var length = Math.min(fromParts.length, toParts.length);
+    var samePartsLength = length;
+    for (var i = 0; i < length; i++) {
+      if (fromParts[i] !== toParts[i]) {
+        samePartsLength = i;
+        break;
+      }
+    }
+
+    var outputParts = [];
+    for (var i = samePartsLength; i < fromParts.length; i++) {
+      outputParts.push('..');
+    }
+
+    outputParts = outputParts.concat(toParts.slice(samePartsLength));
+
+    return outputParts.join('/');
+  };
+
+  exports.sep = '/';
+  exports.delimiter = ':';
+}
+
+exports.dirname = function(path) {
+  var result = splitPath(path),
+      root = result[0],
+      dir = result[1];
+
+  if (!root && !dir) {
+    // No dirname whatsoever
+    return '.';
+  }
+
+  if (dir) {
+    // It has a dirname, strip trailing slash
+    dir = dir.substr(0, dir.length - 1);
+  }
+
+  return root + dir;
+};
+
+
+exports.basename = function(path, ext) {
+  var f = splitPath(path)[2];
+  // TODO: make this comparison case-insensitive on windows?
+  if (ext && f.substr(-1 * ext.length) === ext) {
+    f = f.substr(0, f.length - ext.length);
+  }
+  return f;
+};
+
+
+exports.extname = function(path) {
+  return splitPath(path)[3];
+};
+
+if (isWindows) {
+  exports._makeLong = function(path) {
+    // Note: this will *probably* throw somewhere.
+    if (typeof path !== 'string')
+      return path;
+
+    if (!path) {
+      return '';
+    }
+
+    var resolvedPath = exports.resolve(path);
+
+    if (/^[a-zA-Z]\:\\/.test(resolvedPath)) {
+      // path is local filesystem path, which needs to be converted
+      // to long UNC path.
+      return '\\\\?\\' + resolvedPath;
+    } else if (/^\\\\[^?.]/.test(resolvedPath)) {
+      // path is network UNC path, which needs to be converted
+      // to long UNC path.
+      return '\\\\?\\UNC\\' + resolvedPath.substring(2);
+    }
+
+    return path;
+  };
+} else {
+  exports._makeLong = function(path) {
+    return path;
+  };
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/buffer.js
@@ -0,0 +1,82 @@
+/* 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/.
+ */
+"use strict";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+
+const { Cc, Ci, CC } = require("chrome");
+const { Class } = require("../core/heritage");
+
+const Transcoder = CC("@mozilla.org/intl/scriptableunicodeconverter",
+                      "nsIScriptableUnicodeConverter");
+
+var Buffer = Class({
+  initialize: function initialize(subject, encoding) {
+    subject = subject ? subject.valueOf() : 0;
+    let length = typeof subject === "number" ? subject : 0;
+    this.encoding = encoding || "utf-8";
+    this.valueOf(Array.isArray(subject) ? subject : new Array(length));
+
+    if (typeof subject === "string") this.write(subject);
+  },
+  get length() {
+    return this.valueOf().length;
+  },
+  get: function get(index) {
+    return this.valueOf()[index];
+  },
+  set: function set(index, value) {
+    return this.valueOf()[index] = value;
+  },
+  valueOf: function valueOf(value) {
+    Object.defineProperty(this, "valueOf", {
+      value: Array.prototype.valueOf.bind(value),
+      configurable: false,
+      writable: false,
+      enumerable: false
+    });
+  },
+  toString: function toString(encoding, start, end) {
+    let bytes = this.valueOf().slice(start || 0, end || this.length);
+    let transcoder = Transcoder();
+    transcoder.charset = String(encoding || this.encoding).toUpperCase();
+    return transcoder.convertFromByteArray(bytes, this.length);
+  },
+  toJSON: function toJSON() {
+    return this.toString()
+  },
+  write: function write(string, offset, encoding) {
+    offset = Math.max(offset || 0, 0);
+    let value = this.valueOf();
+    let transcoder = Transcoder();
+    transcoder.charset = String(encoding || this.encoding).toUpperCase();
+    let bytes = transcoder.convertToByteArray(string, {});
+    value.splice.apply(value, [
+      offset,
+      Math.min(value.length - offset, bytes.length, bytes)
+    ].concat(bytes));
+    return bytes;
+  },
+  slice: function slice(start, end) {
+    return new Buffer(this.valueOf().slice(start, end));
+  },
+  copy: function copy(target, offset, start, end) {
+    offset = Math.max(offset || 0, 0);
+    target = target.valueOf();
+    let bytes = this.valueOf();
+    bytes.slice(Math.max(start || 0, 0), end);
+    target.splice.apply(target, [
+      offset,
+      Math.min(target.length - offset, bytes.length),
+    ].concat(bytes));
+  }
+});
+Buffer.isBuffer = function isBuffer(buffer) {
+  return buffer instanceof Buffer
+};
+exports.Buffer = Buffer;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/fs.js
@@ -0,0 +1,906 @@
+/* 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/.
+ */
+"use strict";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { Cc, Ci, CC } = require("chrome");
+
+const { setTimeout } = require("../timers");
+const { Stream, InputStream, OutputStream } = require("./stream");
+const { Buffer } = require("./buffer");
+const { ns } = require("../core/namespace");
+const { Class } = require("../core/heritage");
+
+const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
+                        "initWithPath");
+const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1",
+                            "nsIFileOutputStream", "init");
+const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
+                           "nsIFileInputStream", "init");
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream", "setInputStream");
+const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+                              "nsIBinaryOutputStream", "setOutputStream");
+const StreamPump = CC("@mozilla.org/network/input-stream-pump;1",
+                      "nsIInputStreamPump", "init");
+
+const { createOutputTransport, createInputTransport } =
+  Cc["@mozilla.org/network/stream-transport-service;1"].
+  getService(Ci.nsIStreamTransportService);
+
+
+const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream;
+const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile;
+const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream;
+
+const FILE_PERMISSION = parseInt("0666", 8);
+const PR_UINT32_MAX = 0xfffffff;
+// Values taken from:
+// http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615
+const PR_RDONLY =       0x01;
+const PR_WRONLY =       0x02;
+const PR_RDWR =         0x04;
+const PR_CREATE_FILE =  0x08;
+const PR_APPEND =       0x10;
+const PR_TRUNCATE =     0x20;
+const PR_SYNC =         0x40;
+const PR_EXCL =         0x80;
+
+const FLAGS = {
+  "r":                  PR_RDONLY,
+  "r+":                 PR_RDWR,
+  "w":                  PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY,
+  "w+":                 PR_CREATE_FILE | PR_TRUNCATE | PR_RDWR,
+  "a":                  PR_APPEND | PR_CREATE_FILE | PR_WRONLY,
+  "a+":                 PR_APPEND | PR_CREATE_FILE | PR_RDWR
+};
+
+function accessor() {
+  let map = new WeakMap();
+  return function(fd, value) {
+    if (value === null) map.delete(fd);
+    if (value !== undefined) map.set(fd, value);
+    return map.get(fd);
+  }
+}
+
+let nsIFile = accessor();
+let nsIFileInputStream = accessor();
+let nsIFileOutputStream = accessor();
+let nsIBinaryInputStream = accessor();
+let nsIBinaryOutputStream = accessor();
+
+// Just a contstant object used to signal that all of the file
+// needs to be read.
+const ALL = new String("Read all of the file");
+
+function isWritable(mode) !!(mode & PR_WRONLY || mode & PR_RDWR)
+function isReadable(mode) !!(mode & PR_RDONLY || mode & PR_RDWR)
+
+function isString(value) typeof(value) === "string"
+function isFunction(value) typeof(value) === "function"
+
+function toArray(enumerator) {
+  let value = [];
+  while(enumerator.hasMoreElements())
+    value.push(enumerator.getNext())
+  return value
+}
+
+function getFileName(file) file.QueryInterface(Ci.nsIFile).leafName
+
+
+function remove(path, recursive) {
+  let fd = new nsILocalFile(path)
+  if (fd.exists()) {
+    fd.remove(recursive || false);
+  }
+  else {
+    throw FSError("remove", "ENOENT", 34, path);
+  }
+}
+
+function Mode(mode, fallback) {
+  return isString(mode) ? parseInt(mode) : mode || fallback;
+}
+function Flags(flag) {
+  return !isString(flag) ? flag :
+         FLAGS[flag] || Error("Unknown file open flag: " + flag);
+}
+
+
+function FSError(op, code, errno, path, file, line) {
+  let error = Error(code + ", " + op + " " + path, file, line);
+  error.code = code;
+  error.path = path;
+  error.errno = errno;
+  return error;
+}
+
+const ReadStream = Class({
+  extends: InputStream,
+  initialize: function initialize(path, options) {
+    this.position = -1;
+    this.length = -1;
+    this.flags = "r";
+    this.mode = FILE_PERMISSION;
+    this.bufferSize = 64 * 1024;
+
+    options = options || {};
+
+    if ("flags" in options && options.flags)
+      this.flags = options.flags;
+    if ("bufferSize" in options && options.bufferSize)
+      this.bufferSize = options.bufferSize;
+    if ("length" in options && options.length)
+      this.length = options.length;
+    if ("position" in options && options.position !== undefined)
+      this.position = options.position;
+
+    let { flags, mode, position, length } = this;
+    let fd = isString(path) ? openSync(path, flags, mode) : path;
+    this.fd = fd;
+
+    let input = nsIFileInputStream(fd);
+    // Setting a stream position, unless it"s `-1` which means current position.
+    if (position >= 0)
+      input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
+    // We use `nsIStreamTransportService` service to transform blocking
+    // file input stream into a fully asynchronous stream that can be written
+    // without blocking the main thread.
+    let transport = createInputTransport(input, position, length, false);
+    // Open an input stream on a transport. We don"t pass flags to guarantee
+    // non-blocking stream semantics. Also we use defaults for segment size &
+    // count.
+    let asyncInputStream = transport.openInputStream(null, 0, 0);
+    let binaryInputStream = BinaryInputStream(asyncInputStream);
+    nsIBinaryInputStream(fd, binaryInputStream);
+    let pump = StreamPump(asyncInputStream, position, length, 0, 0, false);
+
+    InputStream.prototype.initialize.call(this, {
+      input: binaryInputStream, pump: pump
+    });
+    this.read();
+  },
+  destroy: function() {
+    closeSync(this.fd);
+    InputStream.prototype.destroy.call(this);
+  }
+});
+exports.ReadStream = ReadStream;
+exports.createReadStream = function createReadStream(path, options) {
+  return new ReadStream(path, options);
+};
+
+const WriteStream = Class({
+  extends: OutputStream,
+  initialize: function initialize(path, options) {
+    this.drainable = true;
+    this.flags = "w";
+    this.position = -1;
+    this.mode = FILE_PERMISSION;
+
+    options = options || {};
+
+    if ("flags" in options && options.flags)
+      this.flags = options.flags;
+    if ("mode" in options && options.mode)
+      this.mode = options.mode;
+    if ("position" in options && options.position !== undefined)
+      this.position = options.position;
+
+    let { position, flags, mode } = this;
+    // If pass was passed we create a file descriptor out of it. Otherwise
+    // we just use given file descriptor.
+    let fd = isString(path) ? openSync(path, flags, mode) : path;
+    this.fd = fd;
+
+    let output = nsIFileOutputStream(fd);
+    // Setting a stream position, unless it"s `-1` which means current position.
+    if (position >= 0)
+      output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
+    // We use `nsIStreamTransportService` service to transform blocking
+    // file output stream into a fully asynchronous stream that can be written
+    // without blocking the main thread.
+    let transport = createOutputTransport(output, position, -1, false);
+    // Open an output stream on a transport. We don"t pass flags to guarantee
+    // non-blocking stream semantics. Also we use defaults for segment size &
+    // count.
+    let asyncOutputStream = transport.openOutputStream(null, 0, 0);
+    // Finally we create a non-blocking binary output stream. This will allows
+    // us to write buffers as byte arrays without any further transcoding.
+    let binaryOutputStream = BinaryOutputStream(asyncOutputStream);
+    nsIBinaryOutputStream(fd, binaryOutputStream);
+
+    // Storing output stream so that it can beaccessed later.
+    OutputStream.prototype.initialize.call(this, {
+      output: binaryOutputStream,
+      asyncOutputStream: asyncOutputStream
+    });
+  },
+  destroy: function() {
+    closeSync(this.fd);
+    OutputStream.prototype.destroy.call(this);
+  }
+});
+exports.WriteStream = WriteStream;
+exports.createWriteStream = function createWriteStream(path, options) {
+  return new WriteStream(path, options);
+};
+
+const Stats = Class({
+  initialize: function initialize(path) {
+    let file = new nsILocalFile(path);
+    if (!file.exists()) throw FSError("stat", "ENOENT", 34, path);
+    nsIFile(this, file);
+  },
+  isDirectory: function() nsIFile(this).isDirectory(),
+  isFile: function() nsIFile(this).isFile(),
+  isSymbolicLink: function() nsIFile(this).isSymlink(),
+  get mode() nsIFile(this).permissions,
+  get size() nsIFile(this).fileSize,
+  get mtime() nsIFile(this).lastModifiedTime,
+  isBlockDevice: function() nsIFile(this).isSpecial(),
+  isCharacterDevice: function() nsIFile(this).isSpecial(),
+  isFIFO: function() nsIFile(this).isSpecial(),
+  isSocket: function() nsIFile(this).isSpecial(),
+  // non standard
+  get exists() nsIFile(this).exists(),
+  get hidden() nsIFile(this).isHidden(),
+  get writable() nsIFile(this).isWritable(),
+  get readable() nsIFile(this).isReadable()
+});
+exports.Stats = Stats;
+
+const LStats = Class({
+  extends: Stats,
+  get size() this.isSymbolicLink() ? nsIFile(this).fileSizeOfLink :
+                                     nsIFile(this).fileSize,
+  get mtime() this.isSymbolicLink() ? nsIFile(this).lastModifiedTimeOfLink :
+                                      nsIFile(this).lastModifiedTime,
+  // non standard
+  get permissions() this.isSymbolicLink() ? nsIFile(this).permissionsOfLink :
+                                            nsIFile(this).permissions
+});
+
+const FStat = Class({
+  extends: Stats,
+  initialize: function initialize(fd) {
+    nsIFile(this, nsIFile(fd));
+  }
+});
+
+function noop() {}
+function Async(wrapped) {
+  return function (path, callback) {
+    let args = Array.slice(arguments);
+    callback = args.pop();
+    // If node is not given a callback argument
+    // it just does not calls it.
+    if (typeof(callback) !== "function") {
+      args.push(callback);
+      callback = noop;
+    }
+    setTimeout(function() {
+      try {
+        var result = wrapped.apply(this, args);
+        if (result === undefined) callback(null);
+        else callback(null, result);
+      } catch (error) {
+        callback(error);
+      }
+    }, 0);
+  }
+}
+
+
+/**
+ * Synchronous rename(2)
+ */
+function renameSync(oldPath, newPath) {
+  let source = new nsILocalFile(oldPath);
+  let target = new nsILocalFile(newPath);
+  if (!source.exists()) throw FSError("rename", "ENOENT", 34, oldPath);
+  return source.moveTo(target.parent, target.leafName);
+};
+exports.renameSync = renameSync;
+
+/**
+ * Asynchronous rename(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let rename = Async(renameSync);
+exports.rename = rename;
+
+/**
+ * Test whether or not the given path exists by checking with the file system.
+ */
+function existsSync(path) {
+  return new nsILocalFile(path).exists();
+}
+exports.existsSync = existsSync;
+
+let exists = Async(existsSync);
+exports.exists = exists;
+
+/**
+ * Synchronous ftruncate(2).
+ */
+function truncateSync(path, length) {
+  let fd = openSync(path, "w");
+  ftruncateSync(fd, length);
+  closeSync(fd);
+}
+exports.truncateSync = truncateSync;
+
+/**
+ * Asynchronous ftruncate(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+function truncate(path, length, callback) {
+  open(path, "w", function(error, fd) {
+    if (error) return callback(error);
+    ftruncate(fd, length, function(error) {
+      if (error) {
+        closeSync(fd);
+        callback(error);
+      }
+      else {
+        close(fd, callback);
+      }
+    });
+  });
+}
+exports.truncate = truncate;
+
+function ftruncate(fd, length, callback) {
+  write(fd, new Buffer(length), 0, length, 0, function(error) {
+    callback(error);
+  });
+}
+exports.ftruncate = ftruncate;
+
+function ftruncateSync(fd, length) {
+  writeSync(fd, new Buffer(length), 0, length, 0);
+}
+exports.ftruncateSync = ftruncateSync;
+
+function chownSync(path, uid, gid) {
+  throw Error("Not implemented yet!!");
+}
+exports.chownSync = chownSync;
+
+let chown = Async(chownSync);
+exports.chown = chown;
+
+function lchownSync(path, uid, gid) {
+  throw Error("Not implemented yet!!");
+}
+exports.lchownSync = chownSync;
+
+let lchown = Async(lchown);
+exports.lchown = lchown;
+
+/**
+ * Synchronous chmod(2).
+ */
+function chmodSync (path, mode) {
+  throw Error("Not implemented yet!!");
+};
+exports.chmodSync = chmodSync;
+/**
+ * Asynchronous chmod(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let chmod = Async(chmodSync);
+exports.chmod = chmod;
+
+/**
+ * Synchronous chmod(2).
+ */
+function fchmodSync(fd, mode) {
+  throw Error("Not implemented yet!!");
+};
+exports.fchmodSync = fchmodSync;
+/**
+ * Asynchronous chmod(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let fchmod = Async(fchmodSync);
+exports.chmod = fchmod;
+
+
+/**
+ * Synchronous stat(2). Returns an instance of `fs.Stats`
+ */
+function statSync(path) {
+  return new Stats(path);
+};
+exports.statSync = statSync;
+
+/**
+ * Asynchronous stat(2). The callback gets two arguments (err, stats) where
+ * stats is a `fs.Stats` object. It looks like this:
+ */
+let stat = Async(statSync);
+exports.stat = stat;
+
+/**
+ * Synchronous lstat(2). Returns an instance of `fs.Stats`.
+ */
+function lstatSync(path) {
+  return new LStats(path);
+};
+exports.lstatSync = lstatSync;
+
+/**
+ * Asynchronous lstat(2). The callback gets two arguments (err, stats) where
+ * stats is a fs.Stats object. lstat() is identical to stat(), except that if
+ * path is a symbolic link, then the link itself is stat-ed, not the file that
+ * it refers to.
+ */
+let lstat = Async(lstatSync);
+exports.lstat = lstat;
+
+/**
+ * Synchronous fstat(2). Returns an instance of `fs.Stats`.
+ */
+function fstatSync(fd) {
+  return new FStat(fd);
+};
+exports.fstatSync = fstatSync;
+
+/**
+ * Asynchronous fstat(2). The callback gets two arguments (err, stats) where
+ * stats is a fs.Stats object.
+ */
+let fstat = Async(fstatSync);
+exports.fstat = fstat;
+
+/**
+ * Synchronous link(2).
+ */
+function linkSync(source, target) {
+  throw Error("Not implemented yet!!");
+};
+exports.linkSync = linkSync;
+
+/**
+ * Asynchronous link(2). No arguments other than a possible exception are given
+ * to the completion callback.
+ */
+let link = Async(linkSync);
+exports.link = link;
+
+/**
+ * Synchronous symlink(2).
+ */
+function symlinkSync(source, target) {
+  throw Error("Not implemented yet!!");
+};
+exports.symlinkSync = symlinkSync;
+
+/**
+ * Asynchronous symlink(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let symlink = Async(symlinkSync);
+exports.symlink = symlink;
+
+/**
+ * Synchronous readlink(2). Returns the resolved path.
+ */
+function readlinkSync(path) {
+  return new nsILocalFile(path).target;
+};
+exports.readlinkSync = readlinkSync;
+
+/**
+ * Asynchronous readlink(2). The callback gets two arguments
+ * `(error, resolvedPath)`.
+ */
+let readlink = Async(readlinkSync);
+exports.readlink = readlink;
+
+/**
+ * Synchronous realpath(2). Returns the resolved path.
+ */
+function realpathSync(path) {
+  return new nsILocalFile(path).path;
+};
+exports.realpathSync = realpathSync;
+
+/**
+ * Asynchronous realpath(2). The callback gets two arguments
+ * `(err, resolvedPath)`.
+ */
+let realpath = Async(realpathSync);
+exports.realpath = realpath;
+
+/**
+ * Synchronous unlink(2).
+ */
+let unlinkSync = remove;
+exports.unlinkSync = unlinkSync;
+
+/**
+ * Asynchronous unlink(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let unlink = Async(remove);
+exports.unlink = unlink;
+
+/**
+ * Synchronous rmdir(2).
+ */
+let rmdirSync = remove;
+exports.rmdirSync = rmdirSync;
+
+/**
+ * Asynchronous rmdir(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let rmdir = Async(rmdirSync);
+exports.rmdir = rmdir;
+
+/**
+ * Synchronous mkdir(2).
+ */
+function mkdirSync(path, mode) {
+  try {
+    return nsILocalFile(path).create(DIRECTORY_TYPE, Mode(mode));
+  } catch (error) {
+    // Adjust exception thorw to match ones thrown by node.
+    if (error.name === "NS_ERROR_FILE_ALREADY_EXISTS") {
+      let { fileName, lineNumber } = error;
+      error = FSError("mkdir", "EEXIST", 47, path, fileName, lineNumber);
+    }
+    throw error;
+  }
+};
+exports.mkdirSync = mkdirSync;
+
+/**
+ * Asynchronous mkdir(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let mkdir = Async(mkdirSync);
+exports.mkdir = mkdir;
+
+/**
+ * Synchronous readdir(3). Returns an array of filenames excluding `"."` and
+ * `".."`.
+ */
+function readdirSync(path) {
+  try {
+    return toArray(new nsILocalFile(path).directoryEntries).map(getFileName);
+  }
+  catch (error) {
+    // Adjust exception thorw to match ones thrown by node.
+    if (error.name === "NS_ERROR_FILE_TARGET_DOES_NOT_EXIST" ||
+        error.name === "NS_ERROR_FILE_NOT_FOUND")
+    {
+      let { fileName, lineNumber } = error;
+      error = FSError("readdir", "ENOENT", 34, path, fileName, lineNumber);
+    }
+    throw error;
+  }
+};
+exports.readdirSync = readdirSync;
+
+/**
+ * Asynchronous readdir(3). Reads the contents of a directory. The callback
+ * gets two arguments `(error, files)` where `files` is an array of the names
+ * of the files in the directory excluding `"."` and `".."`.
+ */
+let readdir = Async(readdirSync);
+exports.readdir = readdir;
+
+/**
+ * Synchronous close(2).
+ */
+ function closeSync(fd) {
+   let input = nsIFileInputStream(fd);
+   let output = nsIFileOutputStream(fd);
+
+   // Closing input stream and removing reference.
+   if (input) input.close();
+   // Closing output stream and removing reference.
+   if (output) output.close();
+
+   nsIFile(fd, null);
+   nsIFileInputStream(fd, null);
+   nsIFileOutputStream(fd, null);
+   nsIBinaryInputStream(fd, null);
+   nsIBinaryOutputStream(fd, null);
+};
+exports.closeSync = closeSync;
+/**
+ * Asynchronous close(2). No arguments other than a possible exception are
+ * given to the completion callback.
+ */
+let close = Async(closeSync);
+exports.close = close;
+
+/**
+ * Synchronous open(2).
+ */
+function openSync(path, flags, mode) {
+  let [ fd, flags, mode, file ] =
+      [ { path: path }, Flags(flags), Mode(mode), nsILocalFile(path) ];
+
+  // If trying to open file for just read that does not exists
+  // need to throw exception as node does.
+  if (!file.exists() && !isWritable(flags))
+    throw FSError("open", "ENOENT", 34, path);
+
+  // If we want to open file in read mode we initialize input stream.
+  if (isReadable(flags)) {
+    let input = FileInputStream(file, flags, mode, DEFER_OPEN);
+    nsIFileInputStream(fd, input);
+  }
+
+  // If we want to open file in write mode we initialize output stream for it.
+  if (isWritable(flags)) {
+    let output = FileOutputStream(file, flags, mode, DEFER_OPEN);
+    nsIFileOutputStream(fd, output);
+  }
+
+  return fd;
+}
+exports.openSync = openSync;
+/**
+ * Asynchronous file open. See open(2). Flags can be
+ * `"r", "r+", "w", "w+", "a"`, or `"a+"`. mode defaults to `0666`.
+ * The callback gets two arguments `(error, fd).
+ */
+let open = Async(openSync);
+exports.open = open;
+
+/**
+ * Synchronous version of buffer-based fs.write(). Returns the number of bytes
+ * written.
+ */
+function writeSync(fd, buffer, offset, length, position) {
+  if (length + offset > buffer.length) {
+    throw Error("Length is extends beyond buffer");
+  }
+  else if (length + offset !== buffer.length) {
+    buffer = buffer.slice(offset, offset + length);
+  }
+  let writeStream = new WriteStream(fd, { position: position,
+                                          length: length });
+  let output = nsIBinaryOutputStream(fd);
+  // We write content as a byte array as this will avoid any transcoding
+  // if content was a buffer.
+  output.writeByteArray(buffer.valueOf(), buffer.length);
+  output.flush();
+};
+exports.writeSync = writeSync;
+
+/**
+ * Write buffer to the file specified by fd.
+ *
+ * `offset` and `length` determine the part of the buffer to be written.
+ *
+ * `position` refers to the offset from the beginning of the file where this
+ * data should be written. If `position` is `null`, the data will be written
+ * at the current position. See pwrite(2).
+ *
+ * The callback will be given three arguments `(error, written, buffer)` where
+ * written specifies how many bytes were written into buffer.
+ *
+ * Note that it is unsafe to use `fs.write` multiple times on the same file
+ * without waiting for the callback.
+ */
+function write(fd, buffer, offset, length, position, callback) {
+  if (!Buffer.isBuffer(buffer)) {
+    // (fd, data, position, encoding, callback)
+    let encoding = null;
+    [ position, encoding, callback ] = Array.slice(arguments, 1);
+    buffer = new Buffer(String(buffer), encoding);
+    offset = 0;
+  } else if (length + offset > buffer.length) {
+    throw Error("Length is extends beyond buffer");
+  } else if (length + offset !== buffer.length) {
+    buffer = buffer.slice(offset, offset + length);
+  }
+
+  let writeStream = new WriteStream(fd, { position: position,
+                                          length: length });
+  writeStream.on("error", callback);
+  writeStream.write(buffer, function onEnd() {
+    writeStream.destroy();
+    if (callback)
+      callback(null, buffer.length, buffer);
+  });
+};
+exports.write = write;
+
+/**
+ * Synchronous version of string-based fs.read. Returns the number of
+ * bytes read.
+ */
+function readSync(fd, buffer, offset, length, position) {
+  let input = nsIFileInputStream(fd);
+  // Setting a stream position, unless it"s `-1` which means current position.
+  if (position >= 0)
+    input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
+  // We use `nsIStreamTransportService` service to transform blocking
+  // file input stream into a fully asynchronous stream that can be written
+  // without blocking the main thread.
+  let binaryInputStream = BinaryInputStream(input);
+  let count = length === ALL ? binaryInputStream.available() : length;
+  var bytes = binaryInputStream.readByteArray(count);
+  buffer.copy.call(bytes, buffer, offset);
+
+  return bytes;
+};
+exports.readSync = readSync;
+
+/**
+ * Read data from the file specified by `fd`.
+ *
+ * `buffer` is the buffer that the data will be written to.
+ * `offset` is offset within the buffer where writing will start.
+ *
+ * `length` is an integer specifying the number of bytes to read.
+ *
+ * `position` is an integer specifying where to begin reading from in the file.
+ * If `position` is `null`, data will be read from the current file position.
+ *
+ * The callback is given the three arguments, `(error, bytesRead, buffer)`.
+ */
+function read(fd, buffer, offset, length, position, callback) {
+  let bytesRead = 0;
+  let readStream = new ReadStream(fd, { position: position, length: length });
+  readStream.on("data", function onData(chunck) {
+      chunck.copy(buffer, offset + bytesRead);
+      bytesRead += chunck.length;
+  });
+  readStream.on("end", function onEnd() {
+    callback(null, bytesRead, buffer);
+    readStream.destroy();
+  });
+};
+exports.read = read;
+
+/**
+ * Asynchronously reads the entire contents of a file.
+ * The callback is passed two arguments `(error, data)`, where data is the
+ * contents of the file.
+ */
+function readFile(path, encoding, callback) {
+  if (isFunction(encoding)) {
+    callback = encoding
+    encoding = null
+  }
+
+  let buffer = new Buffer();
+  try {
+    let readStream = new ReadStream(path);
+    readStream.on("data", function(chunck) {
+      chunck.copy(buffer, buffer.length);
+    });
+    readStream.on("error", function onError(error) {
+      callback(error);
+      readStream.destroy();
+    });
+    readStream.on("end", function onEnd() {
+      callback(null, buffer);
+      readStream.destroy();
+    });
+  } catch (error) {
+    setTimeout(callback, 0, error);
+  }
+};
+exports.readFile = readFile;
+
+/**
+ * Synchronous version of `fs.readFile`. Returns the contents of the path.
+ * If encoding is specified then this function returns a string.
+ * Otherwise it returns a buffer.
+ */
+function readFileSync(path, encoding) {
+  let buffer = new Buffer();
+  let fd = openSync(path, "r");
+  try {
+    readSync(fd, buffer, 0, ALL, 0);
+  }
+  finally {
+    closeSync(fd);
+  }
+  return buffer;
+};
+exports.readFileSync = readFileSync;
+
+/**
+ * Asynchronously writes data to a file, replacing the file if it already
+ * exists. data can be a string or a buffer.
+ */
+function writeFile(path, content, encoding, callback) {
+  try {
+    if (isFunction(encoding)) {
+      callback = encoding
+      encoding = null
+    }
+    if (isString(content))
+      content = new Buffer(content, encoding);
+
+    let writeStream = new WriteStream(path);
+    writeStream.on("error", function onError(error) {
+      callback(error);
+      writeStream.destroy();
+    });
+    writeStream.write(content, function onDrain() {
+      writeStream.destroy();
+      callback(null);
+    });
+  } catch (error) {
+    callback(error);
+  }
+};
+exports.writeFile = writeFile;
+
+/**
+ * The synchronous version of `fs.writeFile`.
+ */
+function writeFileSync(filename, data, encoding) {
+  throw Error("Not implemented");
+};
+exports.writeFileSync = writeFileSync;
+
+
+function utimesSync(path, atime, mtime) {
+  throw Error("Not implemented");
+}
+exports.utimesSync = utimesSync;
+
+let utimes = Async(utimesSync);
+exports.utimes = utimes;
+
+function futimesSync(fd, atime, mtime, callback) {
+  throw Error("Not implemented");
+}
+exports.futimesSync = futimesSync;
+
+let futimes = Async(futimesSync);
+exports.futimes = futimes;
+
+function fsyncSync(fd, atime, mtime, callback) {
+  throw Error("Not implemented");
+}
+exports.fsyncSync = fsyncSync;
+
+let fsync = Async(fsyncSync);
+exports.fsync = fsync;
+
+
+/**
+ * Watch for changes on filename. The callback listener will be called each
+ * time the file is accessed.
+ *
+ * The second argument is optional. The options if provided should be an object
+ * containing two members a boolean, persistent, and interval, a polling value
+ * in milliseconds. The default is { persistent: true, interval: 0 }.
+ */
+function watchFile(path, options, listener) {
+  throw Error("Not implemented");
+};
+exports.watchFile = watchFile;
+
+
+function unwatchFile(path, listener) {
+  throw Error("Not implemented");
+}
+exports.unwatchFile = unwatchFile;
+
+function watch(path, options, listener) {
+  throw Error("Not implemented");
+}
+exports.watch = watch;
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/stream.js
@@ -0,0 +1,324 @@
+/* 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/.
+ */
+"use strict";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { EventTarget } = require("../event/target");
+const { emit } = require("../event/core");
+const { Buffer } = require("./buffer");
+const { Class } = require("../core/heritage");
+const { setTimeout } = require("../timers");
+const { ns } = require("../core/namespace");
+
+function isFunction(value) typeof value === "function"
+
+function accessor() {
+  let map = new WeakMap();
+  return function(fd, value) {
+    if (value === null) map.delete(fd);
+    if (value !== undefined) map.set(fd, value);
+    return map.get(fd);
+  }
+}
+
+let nsIInputStreamPump = accessor();
+let nsIAsyncOutputStream = accessor();
+let nsIInputStream = accessor();
+let nsIOutputStream = accessor();
+
+
+/**
+ * Utility function / hack that we use to figure if output stream is closed.
+ */
+function isClosed(stream) {
+  // We assume that stream is not closed.
+  let isClosed = false;
+  stream.asyncWait({
+    // If `onClose` callback is called before outer function returns
+    // (synchronously) `isClosed` will be set to `true` identifying
+    // that stream is closed.
+    onOutputStreamReady: function onClose() isClosed = true
+
+  // `WAIT_CLOSURE_ONLY` flag overrides the default behavior, causing the
+  // `onOutputStreamReady` notification to be suppressed until the stream
+  // becomes closed.
+  }, stream.WAIT_CLOSURE_ONLY, 0, null);
+  return isClosed;
+}
+/**
+ * Utility function takes output `stream`, `onDrain`, `onClose` callbacks and
+ * calls one of this callbacks depending on stream state. It is guaranteed
+ * that only one called will be called and it will be called asynchronously.
+ * @param {nsIAsyncOutputStream} stream
+ * @param {Function} onDrain
+ *    callback that is called when stream becomes writable.
+ * @param {Function} onClose
+ *    callback that is called when stream becomes closed.
+ */
+function onStateChange(stream, target) {
+  let isAsync = false;
+  stream.asyncWait({
+    onOutputStreamReady: function onOutputStreamReady() {
+      // If `isAsync` was not yet set to `true` by the last line we know that
+      // `onOutputStreamReady` was called synchronously. In such case we just
+      // defer execution until next turn of event loop.
+      if (!isAsync)
+        return setTimeout(onOutputStreamReady, 0);
+
+      // As it"s not clear what is a state of the stream (TODO: Is there really
+      // no better way ?) we employ hack (see details in `isClosed`) to verify
+      // if stream is closed.
+      emit(target, isClosed(stream) ? "close" : "drain");
+    }
+  }, 0, 0, null);
+  isAsync = true;
+}
+
+function pump(stream) {
+  let input = nsIInputStream(stream);
+  nsIInputStreamPump(stream).asyncRead({
+    onStartRequest: function onStartRequest() {
+      emit(stream, "start");
+    },
+    onDataAvailable: function onDataAvailable(req, c, is, offset, count) {
+      try {
+        let bytes = input.readByteArray(count);
+        emit(stream, "data", new Buffer(bytes, stream.encoding));
+      } catch (error) {
+        emit(stream, "error", error);
+        stream.readable = false;
+      }
+    },
+    onStopRequest: function onStopRequest() {
+      stream.readable = false;
+      emit(stream, "end");
+    }
+  }, null);
+}
+
+const Stream = Class({
+  extends: EventTarget,
+  initialize: function() {
+    this.readable = false;
+    this.writable = false;
+    this.encoding = null;
+  },
+  setEncoding: function setEncoding(encoding) {
+    this.encoding = String(encoding).toUpperCase();
+  },
+  pipe: function pipe(target, options) {
+    let source = this;
+    function onData(chunk) {
+      if (target.writable) {
+        if (false === target.write(chunk))
+          source.pause();
+      }
+    }
+    function onDrain() {
+      if (source.readable) source.resume();
+    }
+    function onEnd() {
+      target.end();
+    }
+    function onPause() {
+      source.pause();
+    }
+    function onResume() {
+      if (source.readable)
+        source.resume();
+    }
+
+    function cleanup() {
+      source.removeListener("data", onData);
+      target.removeListener("drain", onDrain);
+      source.removeListener("end", onEnd);
+
+      target.removeListener("pause", onPause);
+      target.removeListener("resume", onResume);
+
+      source.removeListener("end", cleanup);
+      source.removeListener("close", cleanup);
+
+      target.removeListener("end", cleanup);
+      target.removeListener("close", cleanup);
+    }
+
+    if (!options || options.end !== false)
+      target.on("end", onEnd);
+
+    source.on("data", onData);
+    target.on("drain", onDrain);
+    target.on("resume", onResume);
+    target.on("pause", onPause);
+
+    source.on("end", cleanup);
+    source.on("close", cleanup);
+
+    target.on("end", cleanup);
+    target.on("close", cleanup);
+
+    emit(target, "pipe", source);
+  },
+  pause: function pause() {
+    emit(this, "pause");
+  },
+  resume: function resume() {
+    emit(this, "resume");
+  },
+  destroySoon: function destroySoon() {
+    this.destroy();
+  }
+});
+exports.Stream = Stream;
+
+const InputStream = Class({
+  extends: Stream,
+  initialize: function initialize(options) {
+    let { input, pump } = options;
+
+    this.readable = true;
+    this.paused = false;
+    nsIInputStream(this, input);
+    nsIInputStreamPump(this, pump);
+  },
+  get status() nsIInputStreamPump(this).status,
+  read: function() pump(this),
+  pause: function pause() {
+    this.paused = true;
+    nsIInputStreamPump(this).suspend();
+    emit(this, "paused");
+  },
+  resume: function resume() {
+    this.paused = false;
+    nsIInputStreamPump(this).resume();
+    emit(this, "resume");
+  },
+  destroy: function destroy() {
+    this.readable = false;
+    try {
+      emit(this, "close", null);
+      nsIInputStreamPump(this).cancel(null);
+      nsIInputStreamPump(this, null);
+
+      nsIInputStream(this).close();
+      nsIInputStream(this, null);
+    } catch (error) {
+      emit(this, "error", error);
+    }
+  }
+});
+exports.InputStream = InputStream;
+
+const OutputStream = Class({
+  extends: Stream,
+  initialize: function initialize(options) {
+    let { output, asyncOutputStream } = options;
+
+    this.writable = true;
+    nsIOutputStream(this, output);
+    nsIAsyncOutputStream(this, asyncOutputStream);
+  },
+  write: function write(content, encoding, callback) {
+    let output = nsIOutputStream(this);
+    let asyncOutputStream = nsIAsyncOutputStream(this);
+
+    if (isFunction(encoding)) {
+      callback = encoding;
+      encoding = callback;
+    }
+
+    // Flag indicating whether or not content has been flushed to the kernel
+    // buffer.
+    let isWritten = false;
+    // If stream is not writable we throw an error.
+    if (!this.writable)
+      throw Error("stream not writable");
+
+    try {
+      // If content is not a buffer then we create one out of it.
+      if (!Buffer.isBuffer(content))
+        content = new Buffer(content, encoding);
+
+      // We write content as a byte array as this will avoid any transcoding
+      // if content was a buffer.
+      output.writeByteArray(content.valueOf(), content.length);
+      output.flush();
+
+      if (callback) this.once("drain", callback);
+      onStateChange(asyncOutputStream, this);
+      return true;
+    } catch (error) {
+      // If errors occur we emit appropriate event.
+      emit(this, "error", error);
+    }
+  },
+  flush: function flush() {
+    nsIOutputStream(this).flush();
+  },
+  end: function end(content, encoding, callback) {
+    if (isFunction(content)) {
+      callback = content
+      content = callback
+    }
+    if (isFunction(encoding)) {
+      callback = encoding
+      encoding = callback
+    }
+
+    // Setting a listener to "close" event if passed.
+    if (isFunction(callback))
+      this.once("close", callback);
+
+    // If content is passed then we defer closing until we finish with writing.
+    if (content)
+      this.write(content, encoding, end.bind(this));
+    // If we don"t write anything, then we close an outputStream.
+    else
+      nsIOutputStream(this).close();
+  },
+  destroy: function destroy(callback) {
+    try {
+      this.end(callback);
+      nsIOutputStream(this, null);
+      nsIAsyncOutputStream(this, null);
+    } catch (error) {
+      emit(this, "error", error);
+    }
+  }
+});
+exports.OutputStream = OutputStream;
+
+const DuplexStream = Class({
+  extends: Stream,
+  initialize: function initialize(options) {
+    let { input, output, pump } = options;
+
+    this.writable = true;
+    this.readable = true;
+    this.encoding = null;
+
+    nsIInputStream(this, input);
+    nsIOutputStream(this, output);
+    nsIInputStreamPump(this, pump);
+  },
+  read: InputStream.prototype.read,
+  pause: InputStream.prototype.pause,
+  resume: InputStream.prototype.resume,
+
+  write: OutputStream.prototype.write,
+  flush: OutputStream.prototype.flush,
+  end: OutputStream.prototype.end,
+
+  destroy: function destroy(error) {
+    if (error)
+      emit(this, "error", error);
+    InputStream.prototype.destroy.call(this);
+    OutputStream.prototype.destroy.call(this);
+  }
+});
+exports.DuplexStream = DuplexStream;
--- a/addon-sdk/source/lib/sdk/lang/weak-set.js
+++ b/addon-sdk/source/lib/sdk/lang/weak-set.js
@@ -1,8 +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/. */
+
+module.metadata = {
+  "stability": "experimental"
+};
+
 "use strict";
 
 const { Cu } = require("chrome");
 
 function makeGetterFor(Type) {
   let cache = new WeakMap();
 
   return function getFor(target) {
@@ -42,15 +50,21 @@ function clear(target) {
 exports.clear = clear;
 
 function iterator(target) {
   let refs = getRefsFor(target);
 
   for (let ref of refs) {
     let value = ref.get();
 
-    if (has(target, value))
+    // If `value` is already gc'ed, it would be `null`.
+    // The `has` function is using a WeakMap as lookup table, so passing `null`
+    // would raise an exception because WeakMap accepts as value only non-null
+    // object.
+    // Plus, if `value` is already gc'ed, we do not have to take it in account
+    // during the iteration, and remove it from the references.
+    if (value !== null && has(target, value))
       yield value;
     else
       refs.delete(ref);
   }
 }
 exports.iterator = iterator;
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -7,68 +7,49 @@
 
 module.metadata = {
   "stability": "stable"
 };
 
 const observers = require('./deprecated/observer-service');
 const { Loader, validationAttributes } = require('./content/loader');
 const { Worker } = require('./content/worker');
+const { Registry } = require('./util/registry');
 const { EventEmitter } = require('./deprecated/events');
-const { List } = require('./deprecated/list');
-const { Registry } = require('./util/registry');
-const { MatchPattern } = require('./page-mod/match-pattern');
+const { on, emit } = require('./event/core');
 const { validateOptions : validate } = require('./deprecated/api-utils');
 const { Cc, Ci } = require('chrome');
 const { merge } = require('./util/object');
 const { readURISync } = require('./net/url');
 const { windowIterator } = require('./deprecated/window-utils');
 const { isBrowser, getFrames } = require('./window/utils');
 const { getTabs, getTabContentWindow, getTabForContentWindow,
         getURI: getTabURI } = require('./tabs/utils');
-const { has, hasAny } = require('./util/array');
 const { ignoreWindow } = require('sdk/private-browsing/utils');
 const { Style } = require("./stylesheet/style");
 const { attach, detach } = require("./content/mod");
+const { has, hasAny } = require("./util/array");
+const { Rules } = require("./util/rules");
 
 // Valid values for `attachTo` option
 const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
 
+const mods = new WeakMap();
+
 // contentStyle* / contentScript* are sharing the same validation constraints,
 // so they can be mostly reused, except for the messages.
 const validStyleOptions = {
   contentStyle: merge(Object.create(validationAttributes.contentScript), {
     msg: 'The `contentStyle` option must be a string or an array of strings.'
   }),
   contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), {
     msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
   })
 };
 
-// rules registry
-const RULES = {};
-
-const Rules = EventEmitter.resolve({ toString: null }).compose(List, {
-  add: function() Array.slice(arguments).forEach(function onAdd(rule) {
-    if (this._has(rule))
-      return;
-    // registering rule to the rules registry
-    if (!(rule in RULES))
-      RULES[rule] = new MatchPattern(rule);
-    this._add(rule);
-    this._emit('add', rule);
-  }.bind(this)),
-  remove: function() Array.slice(arguments).forEach(function onRemove(rule) {
-    if (!this._has(rule))
-      return;
-    this._remove(rule);
-    this._emit('remove', rule);
-  }.bind(this)),
-});
-
 /**
  * PageMod constructor (exported below).
  * @constructor
  */
 const PageMod = Loader.compose(EventEmitter, {
   on: EventEmitter.required,
   _listeners: EventEmitter.required,
   attachTo: [],
@@ -116,61 +97,55 @@ const PageMod = Loader.compose(EventEmit
                         ' `top` or `frame` value');
     }
     else {
       this.attachTo = ["top", "frame"];
     }
 
     let include = options.include;
     let rules = this.include = Rules();
-    rules.on('add', this._onRuleAdd = this._onRuleAdd.bind(this));
-    rules.on('remove', this._onRuleRemove = this._onRuleRemove.bind(this));
+    
+    if (!include)
+      throw new Error('The `include` option must always contain atleast one rule');
 
-    if (Array.isArray(include))
-      rules.add.apply(null, include);
-    else
-      rules.add(include);
+    rules.add.apply(rules, [].concat(include));
 
     if (contentStyle || contentStyleFile) {
       this._style = Style({
         uri: contentStyleFile,
         source: contentStyle
       });
     }
 
     this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this));
     pageModManager.add(this._public);
+    mods.set(this._public, this);
 
     // `_applyOnExistingDocuments` has to be called after `pageModManager.add()`
     // otherwise its calls to `_onContent` method won't do anything.
     if ('attachTo' in options && has(options.attachTo, 'existing'))
       this._applyOnExistingDocuments();
   },
 
   destroy: function destroy() {
-
     if (this._style)
       detach(this._style);
 
-    for each (let rule in this.include)
-      this.include.remove(rule);
+    for (let i in this.include)
+      this.include.remove(this.include[i]);
+
+    mods.delete(this._public);
     pageModManager.remove(this._public);
   },
 
   _applyOnExistingDocuments: function _applyOnExistingDocuments() {
     let mod = this;
     // Returns true if the tab match one rule
-    function isMatchingURI(uri) {
-      // Use Array.some as `include` isn't a native array
-      return Array.some(mod.include, function (rule) {
-        return RULES[rule].test(uri);
-      });
-    }
     let tabs = getAllTabs().filter(function (tab) {
-      return isMatchingURI(getTabURI(tab));
+      return mod.include.matchesAny(getTabURI(tab));
     });
 
     tabs.forEach(function (tab) {
       // Fake a newly created document
       let window = getTabContentWindow(tab);
       if (has(mod.attachTo, "top"))
         mod._onContent(window);
       if (has(mod.attachTo, "frame"))
@@ -225,22 +200,16 @@ const PageMod = Loader.compose(EventEmit
       onError: this._onUncaughtError
     });
     this._emit('attach', worker);
     let self = this;
     worker.once('detach', function detach() {
       worker.destroy();
     });
   },
-  _onRuleAdd: function _onRuleAdd(url) {
-    pageModManager.on(url, this._onContent);
-  },
-  _onRuleRemove: function _onRuleRemove(url) {
-    pageModManager.off(url, this._onContent);
-  },
   _onUncaughtError: function _onUncaughtError(e) {
     if (this._listeners('error').length == 1)
       console.exception(e);
   }
 });
 exports.PageMod = function(options) PageMod(options)
 exports.PageMod.prototype = PageMod.prototype;
 
@@ -253,19 +222,16 @@ const PageModManager = Registry.resolve(
     observers.add(
       'document-element-inserted',
       this._onContentWindow = this._onContentWindow.bind(this)
     );
   },
   _destructor: function _destructor() {
     observers.remove('document-element-inserted', this._onContentWindow);
     this._removeAllListeners();
-    for (let rule in RULES) {
-      delete RULES[rule];
-    }
 
     // We need to do some cleaning er PageMods, like unregistering any
     // `contentStyle*`
     this._registry.forEach(function(pageMod) {
       pageMod.destroy();
     });
 
     this._registryDestructor();
@@ -280,24 +246,23 @@ const PageModManager = Registry.resolve(
       return;
 
     // When the tab is private, only addons with 'private-browsing' flag in
     // their package.json can apply content script to private documents
     if (ignoreWindow(window)) {
       return;
     }
 
-    for (let rule in RULES)
-      if (RULES[rule].test(document.URL))
-        this._emit(rule, window);
+    this._registry.forEach(function(mod) {
+      if (mod.include.matchesAny(document.URL))
+        mods.get(mod)._onContent(window);
+    });
   },
   off: function off(topic, listener) {
     this.removeListener(topic, listener);
-    if (!this._listeners(topic).length)
-      delete RULES[topic];
   }
 });
 const pageModManager = PageModManager();
 
 // Returns all tabs on all currently opened windows
 function getAllTabs() {
   let tabs = [];
   // Iterate over all chrome windows
--- a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js
+++ b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js
@@ -1,115 +1,5 @@
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-"use strict";
-
-module.metadata = {
-  "stability": "unstable"
-};
-
-const { URL } = require("../url");
-
-exports.MatchPattern = MatchPattern;
-
-function MatchPattern(pattern) {
-  if (typeof pattern.test == "function") {
-
-    // For compatibility with -moz-document rules, we require the RegExp's
-    // global, ignoreCase, and multiline flags to be set to false.
-    if (pattern.global) {
-      throw new Error("A RegExp match pattern cannot be set to `global` " +
-                      "(i.e. //g).");
-    }
-    if (pattern.ignoreCase) {
-      throw new Error("A RegExp match pattern cannot be set to `ignoreCase` " +
-                      "(i.e. //i).");
-    }
-    if (pattern.multiline) {
-      throw new Error("A RegExp match pattern cannot be set to `multiline` " +
-                      "(i.e. //m).");
-    }
-
-    this.regexp = pattern;
-  }
-  else {
-    let firstWildcardPosition = pattern.indexOf("*");
-    let lastWildcardPosition = pattern.lastIndexOf("*");
-    if (firstWildcardPosition != lastWildcardPosition)
-      throw new Error("There can be at most one '*' character in a wildcard.");
-
-    if (firstWildcardPosition == 0) {
-      if (pattern.length == 1)
-        this.anyWebPage = true;
-      else if (pattern[1] != ".")
-        throw new Error("Expected a *.<domain name> string, got: " + pattern);
-      else
-        this.domain = pattern.substr(2);
-    }
-    else {
-      if (pattern.indexOf(":") == -1) {
-        throw new Error("When not using *.example.org wildcard, the string " +
-                        "supplied is expected to be either an exact URL to " +
-                        "match or a URL prefix. The provided string ('" +
-                        pattern + "') is unlikely to match any pages.");
-      }
+let { deprecateUsage } = require("../util/deprecate");
 
-      if (firstWildcardPosition == -1)
-        this.exactURL = pattern;
-      else if (firstWildcardPosition == pattern.length - 1)
-        this.urlPrefix = pattern.substr(0, pattern.length - 1);
-      else {
-        throw new Error("The provided wildcard ('" + pattern + "') has a '*' " +
-                        "in an unexpected position. It is expected to be the " +
-                        "first or the last character in the wildcard.");
-      }
-    }
-  }
-}
-
-MatchPattern.prototype = {
-
-  test: function MatchPattern_test(urlStr) {
-    try {
-      var url = URL(urlStr);
-    }
-    catch (err) {
-      return false;
-    }
+deprecateUsage("Module 'sdk/page-mod/match-pattern' is deprecated use 'sdk/util/match-pattern' instead");
 
-    // Test the URL against a RegExp pattern.  For compatibility with
-    // -moz-document rules, we require the RegExp to match the entire URL,
-    // so we not only test for a match, we also make sure the matched string
-    // is the entire URL string.
-    //
-    // Assuming most URLs don't match most match patterns, we call `test` for
-    // speed when determining whether or not the URL matches, then call `exec`
-    // for the small subset that match to make sure the entire URL matches.
-    //
-    if (this.regexp && this.regexp.test(urlStr) &&
-        this.regexp.exec(urlStr)[0] == urlStr)
-      return true;
-
-    if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme))
-      return true;
-    if (this.exactURL && this.exactURL == urlStr)
-      return true;
-
-    // Tests the urlStr against domain and check if
-    // wildcard submitted (*.domain.com), it only allows
-    // subdomains (sub.domain.com) or from the root (http://domain.com)
-    // and reject non-matching domains (otherdomain.com)
-    // bug 856913
-    if (this.domain && url.host &&
-         (url.host === this.domain ||
-          url.host.slice(-this.domain.length - 1) === "." + this.domain))
-      return true;
-    if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix))
-      return true;
-
-    return false;
-  }
-
-};
+module.exports = require("../page-mod/match-pattern");
--- a/addon-sdk/source/lib/sdk/page-worker.js
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -4,59 +4,166 @@
  * 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/. */
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const { Symbiont } = require("./content/symbiont");
-const { Trait } = require("./deprecated/traits");
+const { Class } = require('./core/heritage');
+const { on, emit, off, setListeners } = require('./event/core');
+const { filter, pipe, map, merge: streamMerge } = require('./event/utils');
+const { WorkerHost, Worker, detach, attach } = require('./worker/utils');
+const { Disposable } = require('./core/disposable');
+const { EventTarget } = require('./event/target');
+const { unload } = require('./system/unload');
+const { events, streamEventsFrom } = require('./content/events');
+const { getAttachEventType } = require('./content/utils');
+const { window } = require('./addon/window');
+const { getParentWindow } = require('./window/utils');
+const { create: makeFrame, getDocShell } = require('./frame/utils');
+const { contract } = require('./util/contract');
+const { contract: loaderContract } = require('./content/loader');
+const { has } = require('./util/array');
+const { Rules } = require('./util/rules');
+const { merge } = require('./util/object');
+
+const views = WeakMap();
+const workers = WeakMap();
+const pages = WeakMap();
+
+const readyEventNames = [
+  'DOMContentLoaded',
+  'document-element-inserted',
+  'load'
+];
+
+function workerFor(page) workers.get(page)
+function pageFor(view) pages.get(view)
+function viewFor(page) views.get(page)
+function isDisposed (page) !views.get(page, false)
 
-const Page = Trait.compose(
-  Symbiont.resolve({
-    constructor: '_initSymbiont'
-  }),
-  {
-    _frame: Trait.required,
-    _initFrame: Trait.required,
-    postMessage: Symbiont.required,
-    on: Symbiont.required,
-    destroy: Symbiont.required,
+let pageContract = contract(merge({
+  allow: {
+    is: ['object', 'undefined', 'null'],
+    map: function (allow) { return { script: !allow || allow.script !== false }}
+  },
+  onMessage: {
+    is: ['function', 'undefined']
+  },
+  include: {
+    is: ['string', 'array', 'undefined']
+  },
+  contentScriptWhen: {
+    is: ['string', 'undefined']
+  }
+}, loaderContract.rules));
+
+function enableScript (page) {
+  getDocShell(viewFor(page)).allowJavascript = true;
+}
 
-    constructor: function Page(options) {
-      options = options || {};
+function disableScript (page) {
+  getDocShell(viewFor(page)).allowJavascript = false;
+}
+
+function Allow (page) {
+  return {
+    get script() getDocShell(viewFor(page)).allowJavascript,
+    set script(value) value ? enableScript(page) : disableScript(page)
+  };
+}
+
+function injectWorker ({page}) {
+  let worker = workerFor(page);
+  let view = viewFor(page);
+  if (isValidURL(page, view.contentDocument.URL))
+    attach(worker, view.contentWindow);
+}
+
+function isValidURL(page, url) !page.rules || page.rules.matchesAny(url)
 
-      this.contentURL = 'contentURL' in options ? options.contentURL
-        : 'about:blank';
-      if ('contentScriptWhen' in options)
-        this.contentScriptWhen = options.contentScriptWhen;
-      if ('contentScriptFile' in options)
-        this.contentScriptFile = options.contentScriptFile;
-      if ('contentScriptOptions' in options)
-        this.contentScriptOptions = options.contentScriptOptions;
-      if ('contentScript' in options)
-        this.contentScript = options.contentScript;
-      if ('allow' in options)
-        this.allow = options.allow;
-      if ('onError' in options)
-        this.on('error', options.onError);
-      if ('onMessage' in options)
-        this.on('message', options.onMessage);
+const Page = Class({
+  implements: [
+    EventTarget,
+    Disposable
+  ],
+  extends: WorkerHost(workerFor),
+  setup: function Page(options) {
+    let page = this;
+    options = pageContract(options);
+    setListeners(this, options);
+    let view = makeFrame(window.document, {
+      nodeName: 'iframe',
+      type: 'content',
+      uri: options.contentURL,
+      allowJavascript: options.allow.script,
+      allowPlugins: true,
+      allowAuth: true
+    });
+
+    ['contentScriptFile', 'contentScript', 'contentScriptWhen']
+      .forEach(function (prop) page[prop] = options[prop]);
+
+    views.set(this, view);
+    pages.set(view, this);
+
+    let worker = new Worker(options);
+    workers.set(this, worker);
+    pipe(worker, this);
 
-      this.on('propertyChange', this._onChange.bind(this));
+    if (this.include || options.include) {
+      this.rules = Rules();
+      this.rules.add.apply(this.rules, [].concat(this.include || options.include));
+    }
+  },
+  get allow() Allow(this),
+  set allow(value) {
+    let allowJavascript = pageContract({ allow: value }).allow.script;
+    return allowJavascript ? enableScript(this) : disableScript(this);
+  },
+  get contentURL() { return viewFor(this).getAttribute('src'); },
+  set contentURL(value) {
+    if (!isValidURL(this, value)) return;
+    let view = viewFor(this);
+    let contentURL = pageContract({ contentURL: value }).contentURL;
+    view.setAttribute('src', contentURL);
+  },
+  dispose: function () {
+    if (isDisposed(this)) return;
+    let view = viewFor(this);
+    if (view.parentNode) view.parentNode.removeChild(view);
+    views.delete(this);
+    detach(workers.get(this));
+  },
+  toString: function () '[object Page]'
+});
 
-      this._initSymbiont();
-    },
-    
-    _onChange: function _onChange(e) {
-      if ('contentURL' in e && this._frame) {
-        // Cleanup the worker before injecting the content script in the new
-        // document
-        this._workerCleanup();
-        this._initFrame(this._frame);
-      }
-    }
-  }
-);
-exports.Page = function(options) Page(options);
-exports.Page.prototype = Page.prototype;
+exports.Page = Page;
+
+let pageEvents = streamMerge([events, streamEventsFrom(window)]);
+let readyEvents = filter(pageEvents, isReadyEvent);
+let formattedEvents = map(readyEvents, function({target, type}) {
+  return { type: type, page: pageFromDoc(target) };
+});
+let pageReadyEvents = filter(formattedEvents, function({page, type}) {
+  return getAttachEventType(page) === type});
+on(pageReadyEvents, 'data', injectWorker);
+
+function isReadyEvent ({type}) {
+  return has(readyEventNames, type);
+}
+
+/*
+ * Takes a document, finds its doc shell tree root and returns the
+ * matching Page instance if found
+ */
+function pageFromDoc(doc) {
+  let parentWindow = getParentWindow(doc.defaultView), page;
+  if (!parentWindow) return;
+
+  let frames = parentWindow.document.getElementsByTagName('iframe');
+  for (let i = frames.length; i--;)
+    if (frames[i].contentDocument === doc && (page = pageFor(frames[i])))
+      return page;
+  return null;
+}
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -27,26 +27,17 @@ const { contract } = require("./util/con
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
 const { filter, pipe } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject } = require("./lang/type");
-
-function getAttachEventType(model) {
-  let when = model.contentScriptWhen;
-  return requiresAddonGlobal(model) ? "sdk-panel-content-changed" :
-         when === "start" ? "sdk-panel-content-changed" :
-         when === "end" ? "sdk-panel-document-loaded" :
-         when === "ready" ? "sdk-panel-content-loaded" :
-         null;
-}
-
+const { getAttachEventType } = require("./content/utils");
 
 let number = { is: ['number', 'undefined', 'null'] };
 let boolean = { is: ['boolean', 'undefined', 'null'] };
 
 let rectContract = contract({
   top: number,
   right: number,
   bottom: number,
@@ -89,24 +80,24 @@ let setupAutoHide = new function() {
 
   return function setupAutoHide(panel) {
     // Create system event listener that reacts to any panel showing and
     // hides given `panel` if it's not the one being shown.
     function listener({subject}) {
       // It could be that listener is not GC-ed in the same cycle as
       // panel in such case we remove listener manually.
       let view = viewFor(panel);
-      if (!view) systemEvents.off("sdk-panel-show", listener);
+      if (!view) systemEvents.off("popupshowing", listener);
       else if (subject !== view) panel.hide();
     }
 
     // system event listener is intentionally weak this way we'll allow GC
     // to claim panel if it's no longer referenced by an add-on code. This also
     // helps minimizing cleanup required on unload.
-    systemEvents.on("sdk-panel-show", listener);
+    systemEvents.on("popupshowing", listener);
     // To make sure listener is not claimed by GC earlier than necessary we
     // associate it with `panel` it's associated with. This way it won't be
     // GC-ed earlier than `panel` itself.
     refs.set(panel, listener);
   }
 }
 
 const Panel = Class({
@@ -179,16 +170,28 @@ const Panel = Class({
     domPanel.setURL(viewFor(this), model.contentURL);
   },
 
   /* Public API: Panel.isShowing */
   get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
 
   /* Public API: Panel.show */
   show: function show(options, anchor) {
+    if (options instanceof Ci.nsIDOMElement) {
+      [anchor, options] = [options, null];
+    }
+
+    if (anchor instanceof Ci.nsIDOMElement) {
+      console.warn(
+        "Passing a DOM node to Panel.show() method is an unsupported " +
+        "feature that will be soon replaced. " +
+        "See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
+      );
+    }
+
     let model = modelFor(this);
     let view = viewFor(this);
     let anchorView = getNodeView(anchor);
 
     options = merge({
       position: model.position,
       width: model.width,
       height: model.height,
@@ -229,31 +232,31 @@ const Panel = Class({
   }
 });
 exports.Panel = Panel;
 
 // Filter panel events to only panels that are create by this module.
 let panelEvents = filter(events, function({target}) panelFor(target));
 
 // Panel events emitted after panel has being shown.
-let shows = filter(panelEvents, function({type}) type === "sdk-panel-shown");
+let shows = filter(panelEvents, function({type}) type === "popupshown");
 
 // Panel events emitted after panel became hidden.
-let hides = filter(panelEvents, function({type}) type === "sdk-panel-hidden");
+let hides = filter(panelEvents, function({type}) type === "popuphidden");
 
 // Panel events emitted after content inside panel is ready. For different
 // panels ready may mean different state based on `contentScriptWhen` attribute.
 // Weather given event represents readyness is detected by `getAttachEventType`
 // helper function.
 let ready = filter(panelEvents, function({type, target})
   getAttachEventType(modelFor(panelFor(target))) === type);
 
 // Panel events emitted after content document in the panel has changed.
 let change = filter(panelEvents, function({type})
-  type === "sdk-panel-content-changed");
+  type === "document-element-inserted");
 
 // Forward panel show / hide events to panel's own event listeners.
 on(shows, "data", function({target}) emit(panelFor(target), "show"));
 on(hides, "data", function({target}) emit(panelFor(target), "hide"));
 
 on(ready, "data", function({target}) {
   let worker = workerFor(panelFor(target));
   attach(worker, domPanel.getContentDocument(target).defaultView);
--- a/addon-sdk/source/lib/sdk/panel/events.js
+++ b/addon-sdk/source/lib/sdk/panel/events.js
@@ -14,14 +14,13 @@ module.metadata = {
 const events = require("../system/events");
 const { emit } = require("../event/core");
 
 let channel = {};
 
 function forward({ subject, type, data })
   emit(channel, "data", { target: subject, type: type, data: data });
 
-["sdk-panel-show", "sdk-panel-hide", "sdk-panel-shown",
- "sdk-panel-hidden", "sdk-panel-content-changed", "sdk-panel-content-loaded",
- "sdk-panel-document-loaded"
+["popupshowing", "popuphiding", "popupshown", "popuphidden",
+"document-element-inserted", "DOMContentLoaded", "load"
 ].forEach(function(type) events.on(type, forward));
 
 exports.events = channel;
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -200,26 +200,16 @@ function setupPanelFrame(frame) {
   frame.setAttribute("showcaret", true);
   frame.setAttribute("autocompleteenabled", true);
   if (platform === "darwin") {
     frame.style.borderRadius = "6px";
     frame.style.padding = "1px";
   }
 }
 
-let EVENT_NAMES = {
-  "popupshowing": "sdk-panel-show",
-  "popuphiding": "sdk-panel-hide",
-  "popupshown": "sdk-panel-shown",
-  "popuphidden": "sdk-panel-hidden",
-  "document-element-inserted": "sdk-panel-content-changed",
-  "DOMContentLoaded": "sdk-panel-content-loaded",
-  "load": "sdk-panel-document-loaded"
-};
-
 function make(document) {
   document = document || getMostRecentBrowserWindow().document;
   let panel = document.createElementNS(XUL_NS, "panel");
   panel.setAttribute("type", "arrow");
 
   // Note that panel is a parent of `viewFrame` who's `docShell` will be
   // configured at creation time. If `panel` and there for `viewFrame` won't
   // have an owner document attempt to access `docShell` will throw. There
@@ -244,39 +234,39 @@ function make(document) {
   setupPanelFrame(backgroundFrame);
 
   let viewFrame = createFrame(panel, frameOptions);
   setupPanelFrame(viewFrame);
 
   function onDisplayChange({type}) {
     try { swapFrameLoaders(backgroundFrame, viewFrame); }
     catch(error) { console.exception(error); }
-    events.emit(EVENT_NAMES[type], { subject: panel });
+    events.emit(type, { subject: panel });
   }
 
   function onContentReady({target, type}) {
     if (target === getContentDocument(panel)) {
       style(panel);
-      events.emit(EVENT_NAMES[type], { subject: panel });
+      events.emit(type, { subject: panel });
     }
   }
 
   function onContentLoad({target, type}) {
     if (target === getContentDocument(panel))
-      events.emit(EVENT_NAMES[type], { subject: panel });
+      events.emit(type, { subject: panel });
   }
 
   function onContentChange({subject, type}) {
     let document = subject;
     if (document === getContentDocument(panel) && document.defaultView)
-      events.emit(EVENT_NAMES[type], { subject: panel });
+      events.emit(type, { subject: panel });
   }
 
   function onPanelStateChange({type}) {
-    events.emit(EVENT_NAMES[type], { subject: panel })
+    events.emit(type, { subject: panel })
   }
 
   panel.addEventListener("popupshowing", onDisplayChange, false);
   panel.addEventListener("popuphiding", onDisplayChange, false);
   panel.addEventListener("popupshown", onPanelStateChange, false);
   panel.addEventListener("popuphidden", onPanelStateChange, false);
 
   // Panel content document can be either in panel `viewFrame` or in
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/bookmarks.js
@@ -0,0 +1,394 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { send } = require('../addon/events');
+const { defer, reject, all, resolve, promised } = require('../core/promise');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { identity, defer:async } = require('../lang/functional');
+const { extend, merge } = require('../util/object');
+const { fromIterator } = require('../util/array');
+const {
+  constructTree, fetchItem, createQuery,
+  isRootGroup, createQueryOptions
+} = require('./utils');
+const {
+  bookmarkContract, groupContract, separatorContract
+} = require('./contract');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+                getService(Ci.nsINavBookmarksService);
+
+/*
+ * Mapping of uncreated bookmarks with their created
+ * counterparts
+ */
+const itemMap = new WeakMap();
+
+/*
+ * Constant used by nsIHistoryQuery; 1 is a bookmark query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const BOOKMARK_QUERY = 1;
+
+/*
+ * Bookmark Item classes
+ */
+
+const Bookmark = Class({
+  extends: [
+    bookmarkContract.properties(identity)
+  ],
+  initialize: function initialize (options) {
+    merge(this, bookmarkContract(extend(defaults, options)));
+  },
+  type: 'bookmark',
+  toString: function () '[object Bookmark]'
+});
+exports.Bookmark = Bookmark;
+
+const Group = Class({
+  extends: [
+    groupContract.properties(identity)
+  ],
+  initialize: function initialize (options) {
+    // Don't validate if root group
+    if (isRootGroup(options))
+      merge(this, options);
+    else
+      merge(this, groupContract(extend(defaults, options)));
+  },
+  type: 'group',
+  toString: function () '[object Group]'
+});
+exports.Group = Group;
+
+const Separator = Class({
+  extends: [
+    separatorContract.properties(identity)
+  ],
+  initialize: function initialize (options) {
+    merge(this, separatorContract(extend(defaults, options)));
+  },
+  type: 'separator',
+  toString: function () '[object Separator]'
+});
+exports.Separator = Separator;
+
+/*
+ * Functions
+ */
+
+function save (items, options) {
+  items = [].concat(items);
+  options = options || {};
+  let emitter = EventTarget();
+  let results = [];
+  let errors = [];
+  let root = constructTree(items);
+  let cache = new Map();
+
+  let isExplicitSave = item => !!~items.indexOf(item);
+  // `walk` returns an aggregate promise indicating the completion
+  // of the `commitItem` on each node, not whether or not that
+  // commit was successful
+
+  // Force this to be async, as if a ducktype fails validation,
+  // the promise implementation will fire an error event, which will
+  // not trigger the handler as it's not yet bound
+  //
+  // Can remove after `Promise.jsm` is implemented in Bug 881047,
+  // which will guarantee next tick execution
+  async(() => root.walk(preCommitItem).then(commitComplete))();
+
+  function preCommitItem ({value:item}) {
+    // Do nothing if tree root, default group (unsavable),
+    // or if it's a dependency and not explicitly saved (in the list
+    // of items to be saved), and not needed to be saved
+    if (item === null || // node is the tree root
+        isRootGroup(item) ||
+        (getId(item) && !isExplicitSave(item)))
+      return;
+
+    return promised(validate)(item)
+      .then(() => commitItem(item, options))
+      .then(data => construct(data, cache))
+      .then(savedItem => {
+        // If item was just created, make a map between
+        // the creation object and created object,
+        // so we can reference the item that doesn't have an id
+        if (!getId(item))
+          saveId(item, savedItem.id);
+
+        // Emit both the processed item, and original item
+        // so a mapping can be understood in handler
+        emit(emitter, 'data', savedItem, item);
+       
+        // Push to results iff item was explicitly saved
+        if (isExplicitSave(item))
+          results[items.indexOf(item)] = savedItem;
+      }, reason => {
+        // Force reason to be a string for consistency
+        reason = reason + '';
+        // Emit both the reason, and original item
+        // so a mapping can be understood in handler
+        emit(emitter, 'error', reason + '', item);
+        // Store unsaved item in results list
+        results[items.indexOf(item)] = item;
+        errors.push(reason);
+      });
+  }
+
+  // Called when traversal of the node tree is completed and all
+  // items have been committed
+  function commitComplete () {
+    emit(emitter, 'end', results);
+  }
+
+  return emitter;
+}
+exports.save = save;
+
+function search (queries, options) {
+  queries = [].concat(queries);
+  let emitter = EventTarget();
+  let cache = new Map();
+  let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
+  let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
+
+  // Can remove after `Promise.jsm` is implemented in Bug 881047,
+  // which will guarantee next tick execution
+  async(() => {
+    send('sdk-places-query', { queries: queryObjs, options: optionsObj })
+      .then(handleQueryResponse);
+  })();
+    
+  function handleQueryResponse (data) {
+    let deferreds = data.map(item => {
+      return construct(item, cache).then(bookmark => {
+        emit(emitter, 'data', bookmark);
+        return bookmark;
+      }, reason => {
+        emit(emitter, 'error', reason);
+        errors.push(reason);
+      });
+    });
+
+    all(deferreds).then(data => {
+      emit(emitter, 'end', data);
+    }, () => emit(emitter, 'end', []));
+  }
+
+  return emitter;
+}
+exports.search = search;
+
+function remove (items) {
+  return [].concat(items).map(item => {
+    item.remove = true;
+    return item;
+  });
+}
+
+exports.remove = remove;
+
+/*
+ * Internal Utilities
+ */
+
+function commitItem (item, options) {
+  // Get the item's ID, or getId it's saved version if it exists
+  let id = getId(item);
+  let data = normalize(item);
+  let promise;
+
+  data.id = id;
+
+  if (!id) {
+    promise = send('sdk-places-bookmarks-create', data);
+  } else if (item.remove) {
+    promise = send('sdk-places-bookmarks-remove', { id: id });
+  } else {
+    promise = send('sdk-places-bookmarks-last-updated', {
+      id: id
+    }).then(function (updated) {
+      // If attempting to save an item that is not the 
+      // latest snapshot of a bookmark item, execute
+      // the resolution function
+      if (updated !== item.updated && options.resolve)
+        return fetchItem(id)
+          .then(options.resolve.bind(null, data));
+      else
+        return data;
+    }).then(send.bind(null, 'sdk-places-bookmarks-save'));
+  }
+
+  return promise;
+}
+
+/*
+ * Turns a bookmark item into a plain object,
+ * converts `tags` from Set to Array, group instance to an id
+ */
+function normalize (item) {
+  let data = merge({}, item);
+  // Circumvent prototype property of `type`
+  delete data.type;
+  data.type = item.type;
+  data.tags = [];
+  if (item.tags) {
+    data.tags = fromIterator(item.tags);
+  }
+  data.group = getId(data.group) || exports.UNSORTED.id;
+
+  return data;
+}
+
+/*
+ * Takes a data object and constructs a BookmarkItem instance
+ * of it, recursively generating parent instances as well.
+ *
+ * Pass in a `cache` Map to reuse instances of
+ * bookmark items to reduce overhead;
+ * The cache object is a map of id to a deferred with a 
+ * promise that resolves to the bookmark item.
+ */
+function construct (object, cache, forced) {
+  let item = instantiate(object);
+  let deferred = defer();
+
+  // Item could not be instantiated
+  if (!item)
+    return resolve(null);
+
+  // Return promise for item if found in the cache,
+  // and not `forced`. `forced` indicates that this is the construct
+  // call that should not read from cache, but should actually perform
+  // the construction, as it was set before several async calls
+  if (cache.has(item.id) && !forced)
+    return cache.get(item.id).promise;
+  else if (cache.has(item.id))
+    deferred = cache.get(item.id);
+  else
+    cache.set(item.id, deferred);
+
+  // When parent group is found in cache, use
+  // the same deferred value
+  if (item.group && cache.has(item.group)) {
+    cache.get(item.group).promise.then(group => {
+      item.group = group;
+      deferred.resolve(item);
+    });
+
+  // If not in the cache, and a root group, return
+  // the premade instance
+  } else if (rootGroups.get(item.group)) {
+    item.group = rootGroups.get(item.group);
+    deferred.resolve(item);
+
+  // If not in the cache or a root group, fetch the parent
+  } else {
+    cache.set(item.group, defer());
+    fetchItem(item.group).then(group => {
+      return construct(group, cache, true);
+    }).then(group => {
+      item.group = group;
+      deferred.resolve(item);
+    }, deferred.reject);
+  }
+
+  return deferred.promise;
+}
+
+function instantiate (object) {
+  if (object.type === 'bookmark')
+    return Bookmark(object);
+  if (object.type === 'group')
+    return Group(object);
+  if (object.type === 'separator')
+    return Separator(object);
+  return null;
+}
+
+/**
+ * Validates a bookmark item; will throw an error if ininvalid,
+ * to be used with `promised`. As bookmark items check on their class,
+ * this only checks ducktypes
+ */
+function validate (object) {
+  if (!isDuckType(object)) return true;
+  let contract = object.type === 'bookmark' ? bookmarkContract :
+                 object.type === 'group' ? groupContract :
+                 object.type === 'separator' ? separatorContract :
+                 null;
+  if (!contract) {
+    throw Error('No type specified');
+  }
+
+  // If object has a property set, and undefined,
+  // manually override with default as it'll fail otherwise
+  let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
+    if (obj[prop] == null) obj[prop] = defaults[prop];
+    return obj;
+  }, extend(object));
+
+  contract(withDefaults);
+}
+
+function isDuckType (item) {
+  return !(item instanceof Bookmark) &&
+    !(item instanceof Group) &&
+    !(item instanceof Separator);
+}
+
+function saveId (unsaved, id) {
+  itemMap.set(unsaved, id);
+}
+
+// Fetches an item's ID from itself, or from the mapped items
+function getId (item) {
+  return typeof item === 'number' ? item :
+    item ? item.id || itemMap.get(item) :
+    null;
+}
+
+/*
+ * Set up the default, root groups
+ */
+
+let defaultGroupMap = {
+  MENU: bmsrv.bookmarksMenuFolder,
+  TOOLBAR: bmsrv.toolbarFolder,
+  UNSORTED: bmsrv.unfiledBookmarksFolder
+};
+
+let rootGroups = new Map();
+
+for (let i in defaultGroupMap) {
+  let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
+  rootGroups.set(defaultGroupMap[i], group);
+  exports[i] = group;
+}
+
+let defaults = {
+  group: exports.UNSORTED,
+  index: -1
+};
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/contract.js
@@ -0,0 +1,77 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { Cc, Ci } = require('chrome');
+const { EventEmitter } = require('../deprecated/events');
+const { isValidURI, URL } = require('../url');
+const { contract } = require('../util/contract');
+const { extend } = require('../util/object');
+
+// map of property validations
+const validItem = {
+  id: {
+    is: ['number', 'undefined', 'null'],
+  },
+  group: {
+    is: ['object', 'number', 'undefined', 'null'],
+    ok: function (value) {
+      return value && 
+        (value.toString && value.toString() === '[object Group]') ||
+        typeof value === 'number' ||
+        value.type === 'group';
+    },
+    msg: 'The `group` property must be a valid Group object'
+  },
+  index: {
+    is: ['undefined', 'null', 'number'],
+    map: function (value) value == null ? -1 : value,
+    msg: 'The `index` property must be a number.'
+  },
+  updated: {
+    is: ['number', 'undefined']
+  }
+};
+
+const validTitle = {
+  title: {
+    is: ['string'],
+    msg: 'The `title` property must be defined.'
+  }
+};
+
+const validURL = {
+  url: {
+    is: ['string'], 
+    ok: isValidURI,
+    msg: 'The `url` property must be a valid URL.'
+  }
+};
+
+const validTags = {
+  tags: {
+    is: ['object'],
+    ok: function (tags) tags instanceof Set,
+    map: function (tags) {
+      if (Array.isArray(tags))
+        return new Set(tags);
+      if (tags == null)
+        return new Set();
+      return tags;
+    },
+    msg: 'The `tags` property must be a Set, or an array'
+  }
+};
+
+exports.bookmarkContract = contract(
+  extend(validItem, validTitle, validURL, validTags));
+exports.separatorContract = contract(validItem);
+exports.groupContract = contract(extend(validItem, validTitle));
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/history.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { events, send } = require('../addon/events');
+const { defer, reject, all } = require('../core/promise');
+const { uuid } = require('../util/uuid');
+const { flatten } = require('../util/array');
+const { has, extend, merge, pick } = require('../util/object');
+const { emit } = require('../event/core');
+const { defer: async } = require('../lang/functional');
+const { EventTarget } = require('../event/target');
+const {
+  urlQueryParser, createQuery, createQueryOptions
+} = require('./utils');
+
+/*
+ * Constant used by nsIHistoryQuery; 0 is a history query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const HISTORY_QUERY = 0;
+
+let search = function query (queries, options) {
+  queries = [].concat(queries);
+  let emitter = EventTarget();
+  let queryObjs = queries.map(createQuery.bind(null, HISTORY_QUERY));
+  let optionsObj = createQueryOptions(HISTORY_QUERY, options);
+
+  // Can remove after `Promise.jsm` is implemented in Bug 881047,
+  // which will guarantee next tick execution
+  async(() => {
+    send('sdk-places-query', {
+      query: queryObjs,
+      options: optionsObj
+    }).then(results => {
+      results.map(item => emit(emitter, 'data', item));
+      emit(emitter, 'end', results);
+    }, reason => {
+      emit(emitter, 'error', reason);
+      emit(emitter, 'end', []);
+    });
+  })();
+
+  return emitter;
+};
+exports.search = search;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
@@ -0,0 +1,236 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "experimental",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+const { Cc, Ci } = require('chrome');
+const browserHistory = Cc["@mozilla.org/browser/nav-history-service;1"].
+                       getService(Ci.nsIBrowserHistory);
+const asyncHistory = Cc["@mozilla.org/browser/history;1"].
+                     getService(Ci.mozIAsyncHistory);
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+                        getService(Ci.nsINavBookmarksService);
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+                       getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+            getService(Ci.nsIIOService);
+const { query } = require('./host-query');
+const {
+  defer, all, resolve, promised, reject
+} = require('../../core/promise');
+const { request, response } = require('../../addon/host');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+const { URL, isValidURI } = require('../../url');
+const { newURI } = require('../../url/utils');
+
+const DEFAULT_INDEX = bmsrv.DEFAULT_INDEX;
+const UNSORTED_ID = bmsrv.unfiledBookmarksFolder;
+const ROOT_FOLDERS = [
+  bmsrv.unfiledBookmarksFolder, bmsrv.toolbarFolder,
+  bmsrv.tagsFolder, bmsrv.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+  'sdk-places-bookmarks-create': createBookmarkItem,
+  'sdk-places-bookmarks-save': saveBookmarkItem,
+  'sdk-places-bookmarks-last-updated': getBookmarkLastUpdated,
+  'sdk-places-bookmarks-get': getBookmarkItem,
+  'sdk-places-bookmarks-remove': removeBookmarkItem,
+  'sdk-places-bookmarks-get-all': getAllBookmarks,
+  'sdk-places-bookmarks-get-children': getChildren
+};
+
+function typeMap (type) {
+  if (typeof type === 'number') {
+    if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
+    if (bmsrv.TYPE_FOLDER === type) return 'group';
+    if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
+  } else {
+    if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
+    if ('group' === type) return bmsrv.TYPE_FOLDER;
+    if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
+  }
+}
+
+function getBookmarkLastUpdated ({id})
+  resolve(bmsrv.getItemLastModified(id))
+exports.getBookmarkLastUpdated;
+
+function createBookmarkItem (data) {
+  let error;
+
+  if (data.group == null) data.group = UNSORTED_ID;
+  if (data.index == null) data.index = DEFAULT_INDEX;
+
+  if (data.type === 'group')
+    data.id = bmsrv.createFolder(
+      data.group, data.title, data.index
+    );
+  else if (data.type === 'separator')
+    data.id = bmsrv.insertSeparator(
+      data.group, data.index
+    );
+  else
+    data.id = bmsrv.insertBookmark(
+      data.group, newURI(data.url), data.index, data.title
+    );
+
+  // In the event where default or no index is provided (-1),
+  // query the actual index for the response
+  if (data.index === -1)
+    data.index = bmsrv.getItemIndex(data.id);
+
+  data.updated = bmsrv.getItemLastModified(data.id);
+
+  return tag(data, true).then(() => data);
+}
+exports.createBookmarkItem = createBookmarkItem;
+
+function saveBookmarkItem (data) {
+  let id = data.id;
+  if (!id)
+    reject('Item is missing id');
+
+  let group = bmsrv.getFolderIdForItem(id);
+  let index = bmsrv.getItemIndex(id);
+  let type = bmsrv.getItemType(id);
+
+  if (data.url) {
+    bmsrv.changeBookmarkURI(id, newURI(data.url));
+  }
+  else if (typeMap(type) === 'bookmark')
+    data.url = bmsrv.getBookmarkURI(id).spec;
+
+  if (data.title)
+    bmsrv.setItemTitle(id, data.title);
+  else if (typeMap(type) !== 'separator')
+    data.title = bmsrv.getItemTitle(id);
+
+  if (data.group && data.group !== group)
+    bmsrv.moveItem(id, data.group, data.index || -1);
+  else if (data.index != null && data.index !== index) {
+    // We use moveItem here instead of setItemIndex
+    // so we don't have to manage the indicies of the siblings
+    bmsrv.moveItem(id, group, data.index);
+  } else if (data.index == null)
+    data.index = bmsrv.getItemIndex(id);
+
+  data.updated = bmsrv.getItemLastModified(data.id);
+
+  return tag(data).then(() => data);
+}
+exports.saveBookmarkItem = saveBookmarkItem;
+
+function removeBookmarkItem (data) {
+  let id = data.id;
+
+  if (!id)
+    reject('Item is missing id');
+
+  bmsrv.removeItem(id);
+  return resolve(null);
+}
+exports.removeBookmarkItem = removeBookmarkItem;
+
+function getBookmarkItem (data) {
+  let id = data.id;
+
+  if (!id)
+    reject('Item is missing id');
+
+  let type = bmsrv.getItemType(id);
+
+  data.type = typeMap(type);
+
+  if (type === bmsrv.TYPE_BOOKMARK || type === bmsrv.TYPE_FOLDER)
+    data.title = bmsrv.getItemTitle(id);
+
+  if (type === bmsrv.TYPE_BOOKMARK) {
+    data.url = bmsrv.getBookmarkURI(id).spec;
+    // Should be moved into host-tags as a method
+    data.tags = taggingService.getTagsForURI(newURI(data.url), {});
+  }
+
+  data.group = bmsrv.getFolderIdForItem(id);
+  data.index = bmsrv.getItemIndex(id);
+  data.updated = bmsrv.getItemLastModified(data.id);
+
+  return resolve(data);
+}
+exports.getBookmarkItem = getBookmarkItem;
+
+function getAllBookmarks () {
+  return query({}, { queryType: 1 }).then(bookmarks =>
+    all(bookmarks.map(getBookmarkItem)));
+}
+exports.getAllBookmarks = getAllBookmarks;
+
+function getChildren ({ id }) {
+  if (typeMap(bmsrv.getItemType(id)) !== 'group') return [];
+  let ids = [];
+  for (let i = 0; ids[ids.length - 1] !== -1; i++)
+    ids.push(bmsrv.getIdForItemAt(id, i));
+  ids.pop();
+  return all(ids.map(id => getBookmarkItem({ id: id })));
+}
+exports.getChildren = getChildren;
+
+/*
+ * Hook into host
+ */
+
+let reqStream = filter(request, function (data) /sdk-places-bookmarks/.test(data.event));
+on(reqStream, 'data', function ({event, id, data}) {
+  if (!EVENT_MAP[event]) return;
+
+  let resData = {
+    id: id,
+    event: event
+  };
+
+  promised(EVENT_MAP[event])(data).then(res => {
+    resData.data = res;
+    respond(resData);
+  }, reason => {
+    resData.error = reason;
+    respond(resData);
+  });
+});
+
+function respond (data) {
+  emit(response, 'data', data);
+}
+
+function tag (data, isNew) {
+  // If a new item, we can skip checking what other tags
+  // are on the item
+  if (data.type !== 'bookmark') {
+    return resolve();
+  } else if (!isNew) {
+    return send('sdk-places-tags-get-tags-by-url', { url: data.url })
+      .then(tags => {
+        return send('sdk-places-tags-untag', {
+          tags: tags.filter(tag => !~data.tags.indexOf(tag)),
+          url: data.url
+        });
+      }).then(() => send('sdk-places-tags-tag', {
+        url: data.url, tags: data.tags
+      }));
+  }
+  else if (data.tags && data.tags.length) {
+    return send('sdk-places-tags-tag', { url: data.url, tags: data.tags });
+  }
+  else
+    return resolve();
+}
+
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-query.js
@@ -0,0 +1,171 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "experimental",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+const { Cc, Ci } = require('chrome');
+const { defer, all, resolve } = require('../../core/promise');
+const { safeMerge, omit } = require('../../util/object');
+const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
+                     .getService(Ci.nsINavHistoryService);
+const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
+                         .getService(Ci.nsINavBookmarksService);
+const { request, response } = require('../../addon/host');
+const { newURI } = require('../../url/utils');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const ROOT_FOLDERS = [
+  bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
+  bookmarksService.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+  'sdk-places-query': queryReceiver
+};
+
+// Properties that need to be manually
+// copied into a nsINavHistoryQuery object
+const MANUAL_QUERY_PROPERTIES = [
+  'uri', 'folder', 'tags', 'url', 'folder'
+];
+
+const PLACES_PROPERTIES = [
+  'uri', 'title', 'accessCount', 'time'
+];
+
+function execute (queries, options) {
+  let deferred = defer();
+  let root = historyService
+    .executeQueries(queries, queries.length, options).root;
+
+  let items = collect([], root);
+  deferred.resolve(items);
+  return deferred.promise;
+}
+
+function collect (acc, node) {
+  node.containerOpen = true;
+  for (let i = 0; i < node.childCount; i++) {
+    let child = node.getChild(i);
+    acc.push(child);
+    if (child.type === child.RESULT_TYPE_FOLDER) {
+      let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+      collect(acc, container);
+    }
+  }
+  node.containerOpen = false;
+  return acc;
+}
+
+function query (queries, options) {
+  queries = queries || [];
+  options = options || {}; 
+  let deferred = defer();
+  let optionsObj, queryObjs;
+
+  try {
+    optionsObj = historyService.getNewQueryOptions();
+    queryObjs = [].concat(queries).map(createQuery);
+    if (!queryObjs.length) {
+      queryObjs = [historyService.getNewQuery()];
+    }
+    safeMerge(optionsObj, options);
+  } catch (e) {
+    deferred.reject(e);
+    return deferred.promise;
+  }
+
+  /*
+   * Currently `places:` queries are not supported
+   */
+  optionsObj.excludeQueries = true;
+
+  execute(queryObjs, optionsObj).then(function (results) {
+    if (optionsObj.queryType === 0) {
+      return results.map(normalize);
+    } else if (optionsObj.queryType === 1) {
+      // Formats query results into more standard
+      // data structures for returning
+      return all(results.map(({itemId}) =>
+        send('sdk-places-bookmarks-get', { id: itemId })));
+    }
+  }).then(deferred.resolve, deferred.reject);
+  
+  return deferred.promise;
+}
+exports.query = query;
+
+function createQuery (query) {
+  query = query || {};
+  let queryObj = historyService.getNewQuery();
+
+  safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));
+
+  if (query.tags && Array.isArray(query.tags))
+    queryObj.tags = query.tags;
+  if (query.uri || query.url)
+    queryObj.uri = newURI(query.uri || query.url);
+  if (query.folder)
+    queryObj.setFolders([query.folder], 1);
+  return queryObj;
+}
+
+function queryReceiver (message) {
+  let queries = message.data.queries || message.data.query;
+  let options = message.data.options;
+  let resData = {
+    id: message.id,
+    event: message.event
+  };
+
+  query(queries, options).then(results => {
+    resData.data = results;
+    respond(resData);
+  }, reason => {
+    resData.error = reason;
+    respond(resData);
+  });
+}
+
+/*
+ * Converts a nsINavHistoryResultNode into a plain object
+ * 
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
+ */
+function normalize (historyObj) {
+  return PLACES_PROPERTIES.reduce((obj, prop) => {
+    if (prop === 'uri')
+      obj.url = historyObj.uri;
+    else if (prop === 'time') {
+      // Cast from microseconds to milliseconds
+      obj.time = Math.floor(historyObj.time / 1000)
+    } else if (prop === 'accessCount')
+      obj.visitCount = historyObj[prop];
+    else
+      obj[prop] = historyObj[prop];
+    return obj;
+  }, {});
+}
+
+/*
+ * Hook into host
+ */
+
+let reqStream = filter(request, function (data) /sdk-places-query/.test(data.event));
+on(reqStream, 'data', function (e) {
+  if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+  emit(response, 'data', data);
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-tags.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "experimental",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+const { Cc, Ci } = require('chrome');
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+                       getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+            getService(Ci.nsIIOService);
+const { URL } = require('../../url');
+const { newURI } = require('../../url/utils');
+const { request, response } = require('../../addon/host');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const EVENT_MAP = {
+  'sdk-places-tags-tag': tag,
+  'sdk-places-tags-untag': untag,
+  'sdk-places-tags-get-tags-by-url': getTagsByURL,
+  'sdk-places-tags-get-urls-by-tag': getURLsByTag
+};
+
+function tag (message) {
+  let data = message.data;
+  let resData = {
+    id: message.id,
+    event: message.event
+  };
+
+  resData.data = taggingService.tagURI(newURI(data.url), data.tags);
+  respond(resData);
+}
+
+function untag (message) {
+  let data = message.data;
+  let resData = {
+    id: message.id,
+    event: message.event
+  };
+
+  resData.data = taggingService.untagURI(newURI(data.url), data.tags);
+  respond(resData);
+}
+
+function getURLsByTag (message) {
+  let data = message.data;
+  let resData = {
+    id: message.id,
+    event: message.event
+  };
+
+  resData.data = taggingService
+    .getURIsForTag(data.tag).map(function (uri) uri.spec);
+  respond(resData);
+}
+
+function getTagsByURL (message) {
+  let data = message.data;
+  let resData = {
+    id: message.id,
+    event: message.event
+  };
+
+  resData.data = taggingService.getTagsForURI(newURI(data.url), {});
+  respond(resData);
+}
+
+/*
+ * Hook into host
+ */
+
+let reqStream = filter(request, function (data) {
+  return /sdk-places-tags/.test(data.event);
+});
+
+on(reqStream, 'data', function (e) {
+  if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+  emit(response, 'data', data);
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/utils.js
@@ -0,0 +1,237 @@
+/* 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/. */
+
+'use strict';
+
+module.metadata = {
+  "stability": "experimental",
+  "engines": {
+    "Firefox": "*"
+  }
+};
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { method } = require('../lang/functional');
+const { defer, promised, all } = require('../core/promise');
+const { send } = require('../addon/events');
+const { EventTarget } = require('../event/target');
+const { merge } = require('../util/object');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+                getService(Ci.nsINavBookmarksService);
+
+/*
+ * TreeNodes are used to construct dependency trees
+ * for BookmarkItems
+ */
+let TreeNode = Class({
+  initialize: function (value) {
+    this.value = value;
+    this.children = [];
+  },
+  add: function (values) {
+    [].concat(values).forEach(value => {
+      this.children.push(value instanceof TreeNode ? value : TreeNode(value));
+    });
+  },
+  get length () {
+    let count = 0;
+    this.walk(() => count++);
+    // Do not count the current node
+    return --count;
+  },
+  get: method(get),
+  walk: method(walk),
+  toString: function () '[object TreeNode]'
+});
+exports.TreeNode = TreeNode;
+
+/*
+ * Descends down from `node` applying `fn` to each in order.
+ * Can be asynchronous if `fn` returns a promise. `fn` is passed 
+ * one argument, the current node, `curr`
+ */
+function walk (curr, fn) {
+  return promised(fn)(curr).then(val => {
+    return all(curr.children.map(child => walk(child, fn)));
+  });
+} 
+
+/*
+ * Descends from the TreeNode `node`, returning
+ * the node with value `value` if found or `null`
+ * otherwise
+ */
+function get (node, value) {
+  if (node.value === value) return node;
+  for (let child of node.children) {
+    let found = get(child, value);
+    if (found) return found;
+  }
+  return null;
+}
+
+/*
+ * Constructs a tree of bookmark nodes
+ * returning the root (value: null);
+ */
+
+function constructTree (items) {
+  let root = TreeNode(null);
+  items.forEach(treeify.bind(null, root));
+
+  function treeify (root, item) {
+    // If node already exists, skip
+    let node = root.get(item);
+    if (node) return node;
+    node = TreeNode(item);
+
+    let parentNode = item.group ? treeify(root, item.group) : root;
+    parentNode.add(node);
+
+    return node;
+  }
+
+  return root;
+}
+exports.constructTree = constructTree;
+
+/*
+ * Shortcut for converting an id, or an object with an id, into
+ * an object with corresponding bookmark data
+ */
+function fetchItem (item)
+  send('sdk-places-bookmarks-get', { id: item.id || item })
+exports.fetchItem = fetchItem;
+
+/*
+ * Takes an ID or an object with ID and checks it against
+ * the root bookmark folders
+ */
+function isRootGroup (id) {
+  id = id && id.id;
+  return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
+    bmsrv.unfiledBookmarksFolder
+  ].indexOf(id);
+}
+exports.isRootGroup = isRootGroup;
+
+/*
+ * Merges appropriate options into query based off of url
+ * 4 scenarios:
+ * 
+ * 'moz.com' // domain: moz.com, domainIsHost: true
+ *    --> 'http://moz.com', 'http://moz.com/thunderbird'
+ * '*.moz.com' // domain: moz.com, domainIsHost: false
+ *    --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
+ * 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false
+ *    --> 'http://moz.com/'
+ * 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true
+ *    --> 'http://moz.com/', 'http://moz.com/thunderbird'
+ */
+
+function urlQueryParser (query, url) {
+  if (!url) return;
+  if (/^https?:\/\//.test(url)) {
+    query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
+    if (/\*$/.test(url)) {
+      query.uri = url.replace(/\*$/, '');
+      query.uriIsPrefix = true;
+    }
+  } else {
+    if (/^\*/.test(url)) {
+      query.domain = url.replace(/^\*\./, '');
+      query.domainIsHost = false;
+    } else {
+      query.domain = url;
+      query.domainIsHost = true;
+    }
+  }
+}
+exports.urlQueryParser = urlQueryParser;
+
+/*
+ * Takes an EventEmitter and returns a promise that
+ * aggregates results and handles a bulk resolve and reject
+ */
+
+function promisedEmitter (emitter) {
+  let { promise, resolve, reject } = defer();
+  let errors = [];
+  emitter.on('error', error => errors.push(error));
+  emitter.on('end', (items) => {
+    if (errors.length) reject(errors[0]);
+    else resolve(items);
+  });
+  return promise;
+}
+exports.promisedEmitter = promisedEmitter;
+
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+function createQuery (type, query) {
+  query = query || {};
+  let qObj = {
+    searchTerms: query.query
+  };
+     
+  urlQueryParser(qObj, query.url);
+  
+  // 0 === history
+  if (type === 0) {
+    // PRTime used by query is in microseconds, not milliseconds
+    qObj.beginTime = (query.from || 0) * 1000;
+    qObj.endTime = (query.to || new Date()) * 1000;
+
+    // Set reference time to Epoch
+    qObj.beginTimeReference = 0;
+    qObj.endTimeReference = 0;
+  }
+  // 1 === bookmarks
+  else if (type === 1) {
+    qObj.tags = query.tags;
+    qObj.folder = query.group && query.group.id;
+  } 
+  // 2 === unified (not implemented on platform)
+  else if (type === 2) {
+
+  }
+
+  return qObj;
+}
+exports.createQuery = createQuery;
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+
+const SORT_MAP = {
+  title: 1,
+  date: 3, // sort by visit date
+  url: 5,
+  visitCount: 7,
+  // keywords currently unsupported
+  // keyword: 9,
+  dateAdded: 11, // bookmarks only
+  lastModified: 13 // bookmarks only
+};
+
+function createQueryOptions (type, options) {
+  options = options || {};
+  let oObj = {};
+  oObj.sortingMode = SORT_MAP[options.sort] || 0;
+  if (options.descending && options.sort)
+    oObj.sortingMode++;
+
+  // Resolve to default sort if ineligible based on query type
+  if (type === 0 && // history
+      (options.sort === 'dateAdded' || options.sort === 'lastModified'))
+    oObj.sortingMode = 0;
+
+  oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
+
+  oObj.queryType = type;
+
+  return oObj;
+}
+exports.createQueryOptions = createQueryOptions;
+
--- a/addon-sdk/source/lib/sdk/private-browsing.js
+++ b/addon-sdk/source/lib/sdk/private-browsing.js
@@ -2,17 +2,17 @@
  * 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/. */
 'use strict';
 
 module.metadata = {
   "stability": "stable"
 };
 
-const { setMode, getMode, on: onStateChange } = require('./private-browsing/utils');
+const { setMode, getMode, on: onStateChange, isPermanentPrivateBrowsing } = require('./private-browsing/utils');
 const { isWindowPrivate } = require('./window/utils');
 const { emit, on, once, off } = require('./event/core');
 const { when: unload } = require('./system/unload');
 const { deprecateUsage, deprecateFunction, deprecateEvent } = require('./util/deprecate');
 const { getOwnerWindow } = require('./private-browsing/window/utils');
 
 onStateChange('start', function onStart() {
   emit(exports, 'start');
@@ -60,16 +60,20 @@ exports.isPrivate = function(thing) {
     }
 
     // can we find an associated window?
     let window = getOwnerWindow(thing);
     if (window)
       return isWindowPrivate(window);
   }
 
+  // check if the post pwpb, global pb service is enabled.
+  if (isPermanentPrivateBrowsing())
+    return true;
+
   // if we get here, and global private browsing
   // is available, and it is true, then return
   // true otherwise false is returned here
   return getMode();
 };
 
 function deprecateEvents(func) deprecateEvent(
   func,
--- a/addon-sdk/source/lib/sdk/private-browsing/utils.js
+++ b/addon-sdk/source/lib/sdk/private-browsing/utils.js
@@ -47,16 +47,20 @@ let isGlobalPBSupported = exports.isGlob
 
 // checks that per-window private browsing is implemented
 let isWindowPBSupported = exports.isWindowPBSupported =
                           !pbService && !!PrivateBrowsingUtils && is('Firefox');
 
 // checks that per-tab private browsing is implemented
 let isTabPBSupported = exports.isTabPBSupported =
                        !pbService && !!PrivateBrowsingUtils && is('Fennec') && satisfiesVersion(version, '>=20.0*');
+
+exports.isPermanentPrivateBrowsing = function() {
+ return !!(PrivateBrowsingUtils && PrivateBrowsingUtils.permanentPrivateBrowsing);
+}
                        
 function ignoreWindow(window) {
   return !isPrivateBrowsingSupported && isWindowPrivate(window) && !isGlobalPBSupported;
 }
 exports.ignoreWindow = ignoreWindow;
 
 function onChange() {
   // Emit event with in next turn of event loop.
--- a/addon-sdk/source/lib/sdk/system.js
+++ b/addon-sdk/source/lib/sdk/system.js
@@ -80,17 +80,17 @@ exports.stdout = new function() {
     }
   }
   return Object.freeze({ write: write });
 };
 
 /**
  * Returns a path of the system's or application's special directory / file
  * associated with a given `id`. For list of possible `id`s please see:
- * https://developer.mozilla.org/en/Code_snippets/File_I%2F%2FO#Getting_special_files
+ * https://developer.mozilla.org/en-US/docs/Code_snippets/File_I_O#Getting_files_in_special_directories
  * http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAppDirectoryServiceDefs.h
  * @example
  *
  *    // get firefox profile path
  *    let profilePath = require('system').pathFor('ProfD');
  *    // get OS temp files directory (/tmp)
  *    let temps = require('system').pathFor('TmpD');
  *    // get OS desktop path for an active user (~/Desktop on linux
--- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
@@ -57,18 +57,26 @@ const Tab = Class({
   get url() getTabURL(tabNS(this).tab),
   set url(url) setTabURL(tabNS(this).tab, url),
 
   /**
    * URI of the favicon for the page currently loaded in this tab.
    * @type {String}
    */
   get favicon() {
-    // TODO: provide the real favicon when it is available
-    console.error(ERR_FENNEC_MSG);
+    /*
+     * Synchronous favicon services were never supported on Fennec,
+     * and as of FF22, are now deprecated. When/if favicon services
+     * are supported for Fennec, this getter should reference
+     * `require('sdk/places/favicon').getFavicon`
+     */
+    console.error(
+      'tab.favicon is deprecated, and currently ' +
+      'favicon helpers are not yet supported by Fennec'
+    );
 
     // return 16x16 blank default
     return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC';
   },
 
   getThumbnail: function() {
     // TODO: implement!
     console.error(ERR_FENNEC_MSG);
--- a/addon-sdk/source/lib/sdk/test.js
+++ b/addon-sdk/source/lib/sdk/test.js
@@ -7,16 +7,18 @@
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const BaseAssert = require("sdk/test/assert").Assert;
 const { isFunction, isObject } = require("sdk/lang/type");
 
+exports.Assert = BaseAssert;
+
 function extend(target) {
   let descriptor = {}
   Array.slice(arguments, 1).forEach(function(source) {
     Object.getOwnPropertyNames(source).forEach(function onEach(name) {
       descriptor[name] = Object.getOwnPropertyDescriptor(source, name);
     });
   });
   return Object.create(target, descriptor);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/utils.js
@@ -0,0 +1,95 @@
+/* vim:ts=2:sts=2:sw=2:
+ * 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/. */
+
+'use strict';
+
+module.metadata = {
+  'stability': 'unstable'
+};
+
+function getTestNames (exports)
+  Object.keys(exports).filter(name => /^test/.test(name))
+
+function isAsync (fn) fn.length > 1
+
+/*
+ * Takes an `exports` object of a test file and a function `beforeFn`
+ * to be run before each test. `beforeFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function before (exports, beforeFn) {
+  getTestNames(exports).map(name => {
+    let testFn = exports[name];
+    if (!isAsync(testFn) && !isAsync(beforeFn)) {
+      exports[name] = function (assert) {
+        beforeFn(name);
+        testFn(assert);
+      };
+    }
+    else if (isAsync(testFn) && !isAsync(beforeFn)) {
+      exports[name] = function (assert, done) {
+        beforeFn(name);
+        testFn(assert, done);
+      }
+    }
+    else if (!isAsync(testFn) && isAsync(beforeFn)) {
+      exports[name] = function (assert, done) {
+        beforeFn(name, () => {
+          testFn(assert);
+          done();
+        });
+      }
+    } else if (isAsync(testFn) && isAsync(beforeFn)) {
+      exports[name] = function (assert, done) {
+        beforeFn(name, () => {
+          testFn(assert, done);
+        });
+      }
+    }
+  });
+}
+exports.before = before;
+
+/*
+ * Takes an `exports` object of a test file and a function `afterFn`
+ * to be run after each test. `afterFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function after (exports, afterFn) {
+  getTestNames(exports).map(name => {
+    let testFn = exports[name];
+    if (!isAsync(testFn) && !isAsync(afterFn)) {
+      exports[name] = function (assert) {
+        testFn(assert);
+        afterFn(name);
+      };
+    }
+    else if (isAsync(testFn) && !isAsync(afterFn)) {
+      exports[name] = function (assert, done) {
+        testFn(assert, () => {
+          afterFn(name);
+          done();
+        });
+      }
+    }
+    else if (!isAsync(testFn) && isAsync(afterFn)) {
+      exports[name] = function (assert, done) {
+        testFn(assert);
+        afterFn(name, done);
+      }
+    } else if (isAsync(testFn) && isAsync(afterFn)) {
+      exports[name] = function (assert, done) {
+        testFn(assert, () => {
+          afterFn(name, done);
+        });
+      }
+    }
+  });
+}
+exports.after = after;
--- a/addon-sdk/source/lib/sdk/util/array.js
+++ b/addon-sdk/source/lib/sdk/util/array.js
@@ -96,17 +96,16 @@ function fromIterator(iterator) {
   else {
     for (let item of iterator)
       array.push(item);
   }
   return array;
 }
 exports.fromIterator = fromIterator;
 
-
 function find(array, predicate) {
   var index = 0;
   var count = array.length;
   while (index < count) {
     var value = array[index];
     if (predicate(value)) return value;
     else index = index + 1;
   }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/match-pattern.js
@@ -0,0 +1,122 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { URL } = require('../url');
+const cache = {};
+
+function MatchPattern(pattern) {
+  if (cache[pattern]) return cache[pattern];
+
+  if (typeof pattern.test == "function") {
+
+    // For compatibility with -moz-document rules, we require the RegExp's
+    // global, ignoreCase, and multiline flags to be set to false.
+    if (pattern.global) {
+      throw new Error("A RegExp match pattern cannot be set to `global` " +
+                      "(i.e. //g).");
+    }
+    if (pattern.ignoreCase) {
+      throw new Error("A RegExp match pattern cannot be set to `ignoreCase` " +
+                      "(i.e. //i).");
+    }
+    if (pattern.multiline) {
+      throw new Error("A RegExp match pattern cannot be set to `multiline` " +
+                      "(i.e. //m).");
+    }
+
+    this.regexp = pattern;
+  }
+  else {
+    let firstWildcardPosition = pattern.indexOf("*");
+    let lastWildcardPosition = pattern.lastIndexOf("*");
+    if (firstWildcardPosition != lastWildcardPosition)
+      throw new Error("There can be at most one '*' character in a wildcard.");
+
+    if (firstWildcardPosition == 0) {
+      if (pattern.length == 1)
+        this.anyWebPage = true;
+      else if (pattern[1] != ".")
+        throw new Error("Expected a *.<domain name> string, got: " + pattern);
+      else
+        this.domain = pattern.substr(2);
+    }
+    else {
+      if (pattern.indexOf(":") == -1) {
+        throw new Error("When not using *.example.org wildcard, the string " +
+                        "supplied is expected to be either an exact URL to " +
+                        "match or a URL prefix. The provided string ('" +
+                        pattern + "') is unlikely to match any pages.");
+      }
+
+      if (firstWildcardPosition == -1)
+        this.exactURL = pattern;
+      else if (firstWildcardPosition == pattern.length - 1)
+        this.urlPrefix = pattern.substr(0, pattern.length - 1);
+      else {
+        throw new Error("The provided wildcard ('" + pattern + "') has a '*' " +
+                        "in an unexpected position. It is expected to be the " +
+                        "first or the last character in the wildcard.");
+      }
+    }
+  }
+
+  cache[pattern] = this;
+}
+
+MatchPattern.prototype = {
+
+  test: function MatchPattern_test(urlStr) {
+    try {
+      var url = URL(urlStr);
+    }
+    catch (err) {
+      return false;
+    }
+
+    // Test the URL against a RegExp pattern.  For compatibility with
+    // -moz-document rules, we require the RegExp to match the entire URL,
+    // so we not only test for a match, we also make sure the matched string
+    // is the entire URL string.
+    //
+    // Assuming most URLs don't match most match patterns, we call `test` for
+    // speed when determining whether or not the URL matches, then call `exec`
+    // for the small subset that match to make sure the entire URL matches.
+    //
+    if (this.regexp && this.regexp.test(urlStr) &&
+        this.regexp.exec(urlStr)[0] == urlStr)
+      return true;
+
+    if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme))
+      return true;
+    if (this.exactURL && this.exactURL == urlStr)
+      return true;
+
+    // Tests the urlStr against domain and check if
+    // wildcard submitted (*.domain.com), it only allows
+    // subdomains (sub.domain.com) or from the root (http://domain.com)
+    // and reject non-matching domains (otherdomain.com)
+    // bug 856913
+    if (this.domain && url.host &&
+         (url.host === this.domain ||
+          url.host.slice(-this.domain.length - 1) === "." + this.domain))
+      return true;
+    if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix))
+      return true;
+
+    return false;
+  },
+
+  toString: function () '[object MatchPattern]'
+
+};
+
+exports.MatchPattern = MatchPattern;
--- a/addon-sdk/source/lib/sdk/util/object.js
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
+const { flatten } = require('./array');
+
 /**
  * Merges all the properties of all arguments into first argument. If two or
  * more argument objects have own properties with the same name, the property
  * is overridden, with precedence from right to left, implying, that properties
  * of the object on the left are overridden by a same named property of the
  * object on the right.
  *
  * Any argument given with "falsy" value - commonly `null` and `undefined` in
@@ -24,16 +26,17 @@ module.metadata = {
  *    b === a   // true
  *    b.a       // 'a'
  *    b.foo     // 'bar'
  *    b.bar     // 1
  *    b.name    // 'b'
  */
 function merge(source) {
   let descriptor = {};
+
   // `Boolean` converts the first parameter to a boolean value. Any object is
   // converted to `true` where `null` and `undefined` becames `false`. Therefore
   // the `filter` method will keep only objects that are defined and not null.
   Array.slice(arguments, 1).filter(Boolean).forEach(function onEach(properties) {
     Object.getOwnPropertyNames(properties).forEach(function(name) {
       descriptor[name] = Object.getOwnPropertyDescriptor(properties, name);
     });
   });
@@ -49,9 +52,41 @@ exports.merge = merge;
  */
 function extend(source) {
   let rest = Array.slice(arguments, 1);
   rest.unshift(Object.create(source));
   return merge.apply(null, rest);
 }
 exports.extend = extend;
 
+function has(obj, key) obj.hasOwnProperty(key);
+exports.has = has;
 
+function each(obj, fn) {
+  for (let key in obj) has(obj, key) && fn(obj[key], key, obj);
+}
+exports.each = each;
+
+/**
+ * Like `merge`, except no property descriptors are manipulated, for use
+ * with platform objects. Identical to underscore's `extend`. Useful for
+ * merging XPCOM objects
+ */
+function safeMerge(source) {
+  Array.slice(arguments, 1).forEach(function onEach (obj) {
+    for (let prop in obj) source[prop] = obj[prop];
+  });
+  return source;
+}
+exports.safeMerge = safeMerge;
+
+/*
+ * Returns a copy of the object without blacklisted properties
+ */
+function omit(source, ...values) {
+  let copy = {};
+  let keys = flatten(values);
+  for (let prop in source)
+    if (!~keys.indexOf(prop)) 
+      copy[prop] = source[prop];
+  return copy;
+}
+exports.omit = omit;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/rules.js
@@ -0,0 +1,52 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { Class } = require('../core/heritage');
+const { MatchPattern } = require('./match-pattern');
+const { on, off, emit } = require('../event/core');
+const { method } = require('../lang/functional');
+const objectUtil = require('./object');
+const { EventTarget } = require('../event/target');
+const { List, addListItem, removeListItem } = require('./list');
+
+// Should deprecate usage of EventEmitter/compose
+const Rules = Class({
+  implements: [
+    EventTarget,
+    List
+  ],
+  add: function(...rules) [].concat(rules).forEach(function onAdd(rule) {
+    addListItem(this, rule);
+    emit(this, 'add', rule);
+  }, this),
+  remove: function(...rules) [].concat(rules).forEach(function onRemove(rule) {
+    removeListItem(this, rule);
+    emit(this, 'remove', rule);
+  }, this),
+  get: function(rule) {
+    let found = false;
+    for (let i in this) if (this[i] === rule) found = true;
+    return found;
+  },
+  // Returns true if uri matches atleast one stored rule
+  matchesAny: function(uri) !!filterMatches(this, uri).length,
+  toString: function() '[object Rules]'
+});
+exports.Rules = Rules;
+
+function filterMatches(instance, uri) {
+  let matches = [];
+  for (let i in instance) {
+    if (new MatchPattern(instance[i]).test(uri)) matches.push(instance[i]);
+  }
+  return matches;
+}
--- a/addon-sdk/source/lib/sdk/widget.js
+++ b/addon-sdk/source/lib/sdk/widget.js
@@ -599,16 +599,52 @@ BrowserWindow.prototype = {
   },
 
   _insertNodeInToolbar: function BW__insertNodeInToolbar(node) {
     // Add to the customization palette
     let toolbox = this.doc.getElementById("navigator-toolbox");
     let palette = toolbox.palette;
     palette.appendChild(node);
 
+    if (this.window.CustomizableUI) {
+      let placement = this.window.CustomizableUI.getPlacementOfWidget(node.id);
+      if (!placement) {
+        placement = {area: 'nav-bar', position: undefined};
+      }
+      this.window.CustomizableUI.addWidgetToArea(node.id, placement.area, placement.position);
+
+      // Depending on when this gets called, we might be in the right place now. In that case,
+      // don't run the following code.
+      if (node.parentNode != palette) {
+        return;
+      }
+      // Otherwise, insert:
+      let container = this.doc.getElementById(placement.area);
+      if (container.customizationTarget) {
+        container = container.customizationTarget;
+      }
+
+      if (placement.position !== undefined) {
+        // Find a position:
+        let items = this.window.CustomizableUI.getWidgetIdsInArea(placement.area);
+        let itemIndex = placement.position;
+        for (let l = items.length; itemIndex < l; itemIndex++) {
+          let realItems = container.getElementsByAttribute("id", items[itemIndex]);
+          if (realItems[0]) {
+            container.insertBefore(node, realItems[0]);
+            break;
+          }
+        }
+      }
+      if (node.parentNode != container) {
+        container.appendChild(node);
+      }
+      return;
+    }
+
     // Search for widget toolbar by reading toolbar's currentset attribute
     let container = null;
     let toolbars = this.doc.getElementsByTagName("toolbar");
     let id = node.getAttribute("id");
     for (let i = 0, l = toolbars.length; i < l; i++) {
       let toolbar = toolbars[i];
       if (toolbar.getAttribute("currentset").indexOf(id) == -1)
         continue;
--- a/addon-sdk/source/lib/sdk/window/utils.js
+++ b/addon-sdk/source/lib/sdk/window/utils.js
@@ -384,8 +384,21 @@ function getOwnerBrowserWindow(node) {
   // contains browser for the anchored window document.
   let document = window.document;
   let browsers = windows("navigator:browser", { includePrivate: true });
   return array.find(browsers, function isTargetBrowser(window) {
     return !!window.gBrowser.getBrowserForDocument(document);
   });
 }
 exports.getOwnerBrowserWindow = getOwnerBrowserWindow;
+
+function getParentWindow(window) {
+  try {
+    return window.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShellTreeItem).parent
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindow);
+  }
+  catch (e) {}
+  return null;
+}
+exports.getParentWindow = getParentWindow;
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -345,17 +345,17 @@ const Require = iced(function Require(lo
   require.main = loader.main === requirer ? requirer : undefined;
   return iced(require);
 });
 exports.Require = Require;
 
 const main = iced(function main(loader, id) {
   let uri = resolveURI(id, loader.mapping)
   let module = loader.main = loader.modules[uri] = Module(id, uri);
-  return load(loader, module).exports;
+  return loader.load(loader, module).exports;
 });
 exports.main = main;
 
 // Makes module object that is made available to CommonJS modules when they
 // are evaluated, along with `exports` and `require`.
 const Module = iced(function Module(id, uri) {
   return create(null, {
     id: { enumerable: true, value: id },
--- a/addon-sdk/source/mapping.json
+++ b/addon-sdk/source/mapping.json
@@ -28,17 +28,17 @@
   "window-utils": "sdk/deprecated/window-utils",
   "window/utils": "sdk/window/utils",
   "windows/dom": "sdk/windows/dom",
   "windows/loader": "sdk/windows/loader",
   "xul-app": "sdk/system/xul-app",
   "url": "sdk/url",
   "traceback": "sdk/console/traceback",
   "xhr": "sdk/net/xhr",
-  "match-pattern": "sdk/page-mod/match-pattern",
+  "match-pattern": "sdk/util/match-pattern",
   "tab-browser": "sdk/deprecated/tab-browser",
   "file": "sdk/io/file",
   "runtime": "sdk/system/runtime",
   "xpcom": "sdk/platform/xpcom",
   "querystring": "sdk/querystring",
   "text-streams": "sdk/io/text-streams",
   "app-strings": "sdk/deprecated/app-strings",
   "light-traits": "sdk/deprecated/light-traits",
--- a/addon-sdk/source/python-lib/cuddlefish/docs/renderapi.readme.md
+++ b/addon-sdk/source/python-lib/cuddlefish/docs/renderapi.readme.md
@@ -1,16 +1,15 @@
 <!-- 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/. -->
 
 
 This document describes the structure of the HTML generated by the renderapi.py
-tool, both for use in the API docs shown by "cfx docs" and as exported by
-"cfx sdocs". The particular HTML id and class attributes embedded in the files,
+tool. The particular HTML id and class attributes embedded in the files,
 as well as their organization, represent the interface between the tool and any
 front-end code wanting to style the docs in some particular way.
 
 renderapi generates two sorts of files:
 
 - a file called "<module-name>.div": this is the contents of the parsed
 Markdown file rendered inside a well-defined DIV tag
 
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
@@ -218,17 +218,18 @@ class SmallXPI(unittest.TestCase):
                      ("three-deps", "three-c", "lib", "sub", "foo.js")
                      ]]
 
         add_addon_sdk= lambda path: os.path.join(addon_sdk_dir, path)
         expected.extend([add_addon_sdk(module) for module in [
             os.path.join("sdk", "self.js"),
             os.path.join("sdk", "core", "promise.js"),
             os.path.join("sdk", "net", "url.js"),
-            os.path.join("sdk", "util", "object.js")
+            os.path.join("sdk", "util", "object.js"),
+            os.path.join("sdk", "util", "array.js")
             ]])
 
         missing = set(expected) - set(used_files)
         extra = set(used_files) - set(expected)
 
         self.failUnlessEqual(list(missing), [])
         self.failUnlessEqual(list(extra), [])
         used_deps = m.get_used_packages()
@@ -260,16 +261,17 @@ class SmallXPI(unittest.TestCase):
                     "resources/addon-sdk/lib/",
                     "resources/addon-sdk/lib/sdk/",
                     "resources/addon-sdk/lib/sdk/self.js",
                     "resources/addon-sdk/lib/sdk/core/",
                     "resources/addon-sdk/lib/sdk/util/",
                     "resources/addon-sdk/lib/sdk/net/",
                     "resources/addon-sdk/lib/sdk/core/promise.js",
                     "resources/addon-sdk/lib/sdk/util/object.js",
+                    "resources/addon-sdk/lib/sdk/util/array.js",
                     "resources/addon-sdk/lib/sdk/net/url.js",
                     "resources/three/",
                     "resources/three/lib/",
                     "resources/three/lib/main.js",
                     "resources/three/data/",
                     "resources/three/data/msg.txt",
                     "resources/three/data/subdir/",
                     "resources/three/data/subdir/submsg.txt",
--- a/addon-sdk/source/python-lib/jetpack_sdk_env.py
+++ b/addon-sdk/source/python-lib/jetpack_sdk_env.py
@@ -55,12 +55,12 @@ def welcome():
         # Python 2.x and 3.x, so we'll have to use the traceback module.
 
         import traceback
         _, e, _ = sys.exc_info()
         print ("Verification of Add-on SDK environment failed (%s)." % e)
         print ("Your SDK may not work properly.")
         return
 
-    print ("Welcome to the Add-on SDK. Run 'cfx docs' for assistance.")
+    print ("Welcome to the Add-on SDK. For the docs, visit https://addons.mozilla.org/en-US/developers/docs/sdk/latest/")
 
 if __name__ == '__main__':
     welcome()
--- a/addon-sdk/source/test/addons/layout-change/main.js
+++ b/addon-sdk/source/test/addons/layout-change/main.js
@@ -1,13 +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/. */
 
 "use strict";
+
+const { LoaderWithHookedConsole } = require('sdk/test/loader');
+const { loader } = LoaderWithHookedConsole(module);
 const app = require("sdk/system/xul-app");
 
 // This test makes sure that require statements used by all AMO hosted
 // add-ons will be able to use old require statements.
 // Tests are based on following usage data:
 // https://docs.google.com/spreadsheet/ccc?key=0ApEBy-GRnGxzdHlRMHJ5RXN1aWJ4RGhINkxSd0FCQXc#gid=0
 
 exports["test compatibility"] = function(assert) {
@@ -99,17 +102,17 @@ exports["test compatibility"] = function
 
   assert.equal(require("passwords"),
                require("sdk/passwords"), "sdk/passwords -> passwords");
 
   assert.equal(require("events"),
                require("sdk/deprecated/events"), "sdk/deprecated/events -> events");
 
   assert.equal(require("match-pattern"),
-               require("sdk/page-mod/match-pattern"), "sdk/page-mod/match-pattern -> match-pattern");
+               require("sdk/util/match-pattern"), "sdk/util/match-pattern -> match-pattern");
 
   if (app.is("Firefox")) {
     assert.equal(require("tab-browser"),
                  require("sdk/deprecated/tab-browser"), "sdk/deprecated/tab-browser -> tab-browser");
   }
 
   assert.equal(require("file"),
                require("sdk/io/file"), "sdk/io/file -> file");
@@ -136,18 +139,18 @@ exports["test compatibility"] = function
                require("sdk/keyboard/utils"), "sdk/keyboard/utils -> keyboard/utils");
 
   assert.equal(require("system"),
                require("sdk/system"), "sdk/system -> system");
 
   assert.equal(require("querystring"),
                require("sdk/querystring"), "sdk/querystring -> querystring");
 
-  assert.equal(require("addon-page"),
-               require("sdk/addon-page"), "sdk/addon-page -> addon-page");
+  assert.equal(loader.require("addon-page"),
+               loader.require("sdk/addon-page"), "sdk/addon-page -> addon-page");
 
   assert.equal(require("tabs/utils"),
                require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils");
 
   assert.equal(require("app-strings"),
                require("sdk/deprecated/app-strings"), "sdk/deprecated/app-strings -> app-strings");
 
   assert.equal(require("dom/events"),
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/unsupported/fennec.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+module.metadata = {
+  "engines": {
+    "Fennec": "*"
+  }
+};
+module.exports = {};
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/unsupported/firefox.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+module.metadata = {
+  "engines": {
+    "Firefox": "*"
+  }
+};
+module.exports = {};
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/places-helper.js
@@ -0,0 +1,222 @@
+/* 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/. */
+ 'use strict'
+
+const { Cc, Ci } = require('chrome');
+const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1'].
+                    getService(Ci.nsINavBookmarksService);
+const hsrv = Cc['@mozilla.org/browser/nav-history-service;1'].
+              getService(Ci.nsINavHistoryService);
+const brsrv = Cc["@mozilla.org/browser/nav-history-service;1"]
+                     .getService(Ci.nsIBrowserHistory);
+const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
+              getService(Ci.nsITaggingService);
+const asyncHistory = Cc['@mozilla.org/browser/history;1'].
+              getService(Ci.mozIAsyncHistory);
+const { send } = require('sdk/addon/events');
+const { setTimeout } = require('sdk/timers');
+const { newURI } = require('sdk/url/utils');
+const { defer, all } = require('sdk/core/promise');
+const { once } = require('sdk/system/events');
+const {
+  Bookmark, Group, Separator,
+  save, search,
+  MENU, TOOLBAR, UNSORTED
+} = require('sdk/places/bookmarks');
+
+function invalidResolve (assert) {
+  return function (e) {
+    assert.fail('Resolve state should not be called: ' + e);
+  };
+}
+exports.invalidResolve = invalidResolve;
+
+function invalidReject (assert) {
+  return function (e) {
+    assert.fail('Reject state should not be called: ' + e);
+  };
+}
+exports.invalidReject = invalidReject;
+
+// Removes all children of group
+function clearBookmarks (group) {
+  group
+   ? bmsrv.removeFolderChildren(group.id)
+   : clearAllBookmarks();
+}
+exports.clearBookmarks = clearBookmarks;
+
+function clearAllBookmarks () {
+  [MENU, TOOLBAR, UNSORTED].forEach(clearBookmarks);
+}
+exports.clearAllBookmarks = clearAllBookmarks;
+
+function compareWithHost (assert, item) {
+  let id = item.id;
+  let type = item.type === 'group' ? bmsrv.TYPE_FOLDER : bmsrv['TYPE_' + item.type.toUpperCase()];
+  let url = item.url && !item.url.endsWith('/') ? item.url + '/' : item.url;
+
+  if (type === bmsrv.TYPE_BOOKMARK) {
+    assert.equal(url, bmsrv.getBookmarkURI(id).spec.toString(), 'Matches host url');
+    let tags = tagsrv.getTagsForURI(newURI(item.url));
+    for (let tag of tags) {
+      // Handle both array for raw data and set for instances
+      if (Array.isArray(item.tags))
+        assert.ok(~item.tags.indexOf(tag), 'has correct tag');
+      else
+        assert.ok(item.tags.has(tag), 'has correct tag');
+    }
+    assert.equal(tags.length,
+      Array.isArray(item.tags) ? item.tags.length : item.tags.size,
+      'matches tag count');
+  }
+  if (type !== bmsrv.TYPE_SEPARATOR) {
+    assert.equal(item.title, bmsrv.getItemTitle(id), 'Matches host title');
+  }
+  assert.equal(item.index, bmsrv.getItemIndex(id), 'Matches host index');
+  assert.equal(item.group.id || item.group, bmsrv.getFolderIdForItem(id), 'Matches host group id');
+  assert.equal(type, bmsrv.getItemType(id), 'Matches host type');
+}
+exports.compareWithHost = compareWithHost;
+
+function addVisits (urls) {
+  var deferred = defer();
+  asyncHistory.updatePlaces([].concat(urls).map(createVisit), {
+    handleResult: function () {},
+    handleError: deferred.reject,
+    handleCompletion: deferred.resolve
+  });
+
+  return deferred.promise;
+}
+exports.addVisits = addVisits;
+
+// Creates a mozIVisitInfo object
+function createVisit (url) {
+  let place = {}
+  place.uri = newURI(url);
+  place.title = "Test visit for " + place.uri.spec;
+  place.visits = [{
+    transitionType: hsrv.TRANSITION_LINK,
+    visitDate: +(new Date()) * 1000,
+    referredURI: undefined
+  }];
+  return place;
+}
+
+function clearHistory (done) {
+  hsrv.removeAllPages();
+  once('places-expiration-finished', done);
+}
+exports.clearHistory = clearHistory;
+
+function createBookmark (data) {
+  data = data || {};
+  let item = {
+    title: data.title || 'Moz',
+    url: data.url || (!data.type || data.type === 'bookmark' ?
+      'http://moz.com/' :
+      undefined),
+    tags: data.tags || (!data.type || data.type === 'bookmark' ?
+      ['firefox'] :
+      undefined),
+    type: data.type || 'bookmark',
+    group: data.group
+  };
+  return send('sdk-places-bookmarks-create', item);
+}
+exports.createBookmark = createBookmark;
+
+function createBookmarkItem (data) {
+  let deferred = defer();
+  data = data || {};
+  save({
+    title: data.title || 'Moz',
+    url: data.url || 'http://moz.com/',
+    tags: data.tags || (!data.type || data.type === 'bookmark' ?
+      ['firefox'] :
+      undefined),
+    type: data.type || 'bookmark',
+    group: data.group
+  }).on('end', function (bookmark) {
+    deferred.resolve(bookmark[0]);
+  });
+  return deferred.promise;
+}
+exports.createBookmarkItem = createBookmarkItem;
+
+function createBookmarkTree () {
+  let agg = [];
+  return createBookmarkItem({ type: 'group', title: 'mozgroup' })
+    .then(group => {
+    agg.push(group);
+    return all([createBookmarkItem({
+      title: 'mozilla.com',
+      url: 'http://mozilla.com/',
+      group: group,
+      tags: ['mozilla', 'firefox', 'thunderbird', 'rust']
+    }), createBookmarkItem({
+      title: 'mozilla.org',
+      url: 'http://mozilla.org/',
+      group: group,
+      tags: ['mozilla', 'firefox', 'thunderbird', 'rust']
+    }), createBookmarkItem({
+      title: 'firefox',
+      url: 'http://firefox.com/',
+      group: group,
+      tags: ['mozilla', 'firefox', 'browser']
+    }), createBookmarkItem({
+      title: 'thunderbird',
+      url: 'http://mozilla.org/thunderbird/',
+      group: group,
+      tags: ['mozilla', 'thunderbird', 'email']
+    }), createBookmarkItem({
+      title: 'moz subfolder',
+      group: group,
+      type: 'group'
+    })
+    ]);
+  })
+  .then(results => {
+    agg = agg.concat(results);
+    let subfolder = results.filter(item => item.type === 'group')[0];
+    return createBookmarkItem({
+      title: 'dark javascript secrets',
+      url: 'http://w3schools.com',
+      group: subfolder,
+      tags: []
+    });
+  }).then(item => {
+    agg.push(item);
+    return createBookmarkItem(
+      { type: 'group', group: MENU, title: 'other stuff' }
+    );
+  }).then(newGroup => {
+    agg.push(newGroup);
+    return all([
+      createBookmarkItem({
+        title: 'mdn',
+        url: 'http://developer.mozilla.org/en-US/',
+        group: newGroup,
+        tags: ['javascript']
+      }),
+      createBookmarkItem({
+        title: 'web audio',
+        url: 'http://webaud.io',
+        group: newGroup,
+        tags: ['javascript', 'web audio']
+      }),
+      createBookmarkItem({
+        title: 'web audio components',
+        url: 'http://component.fm',
+        group: newGroup,
+        tags: ['javascript', 'web audio', 'components']
+      })
+    ]);
+  }).then(results => {
+    agg = agg.concat(results);
+    return agg;
+  });
+}
+exports.createBookmarkTree = createBookmarkTree;
--- a/addon-sdk/source/test/tabs/test-fennec-tabs.js
+++ b/addon-sdk/source/test/tabs/test-fennec-tabs.js
@@ -108,17 +108,19 @@ exports.testTabProperties = function(tes
     url: url,
     onReady: function(tab) {
       test.assertEqual(tab.title, "foo", "title of the new tab matches");
       test.assertEqual(tab.url, url, "URL of the new tab matches");
       test.assert(tab.favicon, "favicon of the new tab is not empty");
       // TODO: remove need for this test by implementing the favicon feature
       // Poors man deepEqual with JSON.stringify...
       test.assertEqual(JSON.stringify(messages),
-                       JSON.stringify([ERR_FENNEC_MSG]),
+                       JSON.stringify(['tab.favicon is deprecated, and ' +
+                          'currently favicon helpers are not yet supported ' +
+                          'by Fennec']),
                        "favicon logs an error for now");
       test.assertEqual(tab.style, null, "style of the new tab matches");
       test.assertEqual(tab.index, tabsLen, "index of the new tab matches");
       test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
       test.assertNotEqual(tab.id, null, "a tab object always has an id property");
 
       tab.close(function() {
         loader.unload();
@@ -571,39 +573,16 @@ exports.testPerTabEvents = function(test
 
         // end test
         tab.close(function() test.done());
       });
     }
   });
 };
 
-// TEST: tabs.activeTab getter
-exports.testActiveTab_getter_alt = function(test) {
-  test.waitUntilDone();
-
-  let url = URL.replace("#title#", "foo");
-  tabs.open({
-    url: url,
-    onActivate: function(tab) {
-      test.assertEqual(tabs.activeTab.url, tab.url, 'the active tab is correct');
-
-      tab.once('ready', function() {
-        test.assertEqual(tab.url, url);
-        test.assertEqual(tab.title, "foo");
-
-        tab.close(function() {
-          // end test
-          test.done();
-        });
-      });
-    }
-  });
-};
-
 exports.testUniqueTabIds = function(test) {
   test.waitUntilDone();
   var tabs = require('sdk/tabs');
   var tabIds = {};
   var steps = [
     function (index) {
       tabs.open({
         url: "data:text/html;charset=utf-8,foo",
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -1,220 +1,151 @@
 /* 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/. */
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const timer = require('sdk/timers');
+const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
+const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { open, focus } = require('sdk/window/helpers');
 const { StringBundle } = require('sdk/deprecated/app-strings');
+const tabs = require('sdk/tabs');
 
 const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
                   "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
                   "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
                   "bWRR9AAAAABJRU5ErkJggg%3D%3D";
 
-// TEST: tabs.activeTab getter
-exports.testActiveTab_getter = function(test) {
-  test.waitUntilDone();
-
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-
-    let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head></html>";
-    require("sdk/deprecated/tab-browser").addTab(
-      url,
-      {
-        onLoad: function(e) {
-          test.assert(tabs.activeTab);
-          test.assertEqual(tabs.activeTab.url, url);
-          test.assertEqual(tabs.activeTab.title, "foo");
-          closeBrowserWindow(window, function() test.done());
-        }
-      }
-    );
-  });
-};
-
 // Bug 682681 - tab.title should never be empty
 exports.testBug682681_aboutURI = function(test) {
   test.waitUntilDone();
 
   let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
 
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+  tabs.on('ready', function onReady(tab) {
+    tabs.removeListener('ready', onReady);
 
-    tabs.on('ready', function onReady(tab) {
-      tabs.removeListener('ready', onReady);
-
-      test.assertEqual(tab.title,
-                       tabStrings.get('tabs.emptyTabTitle'),
-                       "title of about: tab is not blank");
+    test.assertEqual(tab.title,
+                     tabStrings.get('tabs.emptyTabTitle'),
+                     "title of about: tab is not blank");
 
-      // end of test
-      closeBrowserWindow(window, function() test.done());
-    });
+    tab.close(function() test.done());
+  });
 
-    // open a about: url
-    tabs.open({
-      url: "about:blank",
-      inBackground: true
-    });
+  // open a about: url
+  tabs.open({
+    url: "about:blank",
+    inBackground: true
   });
 };
 
 // related to Bug 682681
 exports.testTitleForDataURI = function(test) {
   test.waitUntilDone();
 
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-
-    tabs.on('ready', function onReady(tab) {
-      tabs.removeListener('ready', onReady);
-
+  tabs.open({
+    url: "data:text/html;charset=utf-8,<title>tab</title>",
+    inBackground: true,
+    onReady: function(tab) {
       test.assertEqual(tab.title, "tab", "data: title is not Connecting...");
-
-      // end of test
-      closeBrowserWindow(window, function() test.done());
-    });
-
-    // open a about: url
-    tabs.open({
-      url: "data:text/html;charset=utf-8,<title>tab</title>",
-      inBackground: true
-    });
+      tab.close(function() test.done());
+    }
   });
 };
 
 // TEST: 'BrowserWindow' instance creation on tab 'activate' event
 // See bug 648244: there was a infinite loop.
 exports.testBrowserWindowCreationOnActivate = function(test) {
   test.waitUntilDone();
 
   let windows = require("sdk/windows").browserWindows;
-  let tabs = require("sdk/tabs");
-
   let gotActivate = false;
 
   tabs.once('activate', function onActivate(eventTab) {
     test.assert(windows.activeWindow, "Is able to fetch activeWindow");
     gotActivate = true;
   });
 
-  openBrowserWindow(function(window, browser) {
+  open().then(function(window) {
     test.assert(gotActivate, "Received activate event before openBrowserWindow's callback is called");
     closeBrowserWindow(window, function () test.done());
   });
 }
 
-// TEST: tab.activate()
-exports.testActiveTab_setter = function(test) {
-  test.waitUntilDone();
-
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-    let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head></html>";
-
-    tabs.on('ready', function onReady(tab) {
-      tabs.removeListener('ready', onReady);
-      test.assertEqual(tabs.activeTab.url, "about:blank", "activeTab url has not changed");
-      test.assertEqual(tab.url, url, "url of new background tab matches");
-      tabs.on('activate', function onActivate(eventTab) {
-        tabs.removeListener('activate', onActivate);
-        test.assertEqual(tabs.activeTab.url, url, "url after activeTab setter matches");
-        test.assertEqual(eventTab, tab, "event argument is the activated tab");
-        test.assertEqual(eventTab, tabs.activeTab, "the tab is the active one");
-        closeBrowserWindow(window, function() test.done());
-      });
-      tab.activate();
-    })
-
-    tabs.open({
-      url: url,
-      inBackground: true
-    });
-  });
-};
-
 // TEST: tab unloader
 exports.testAutomaticDestroy = function(test) {
   test.waitUntilDone();
 
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-
-    // Create a second tab instance that we will destroy
-    let called = false;
+  // Create a second tab instance that we will destroy
+  let called = false;
 
-    let loader = Loader(module);
-    let tabs2 = loader.require("sdk/tabs");
-    tabs2.on('open', function onOpen(tab) {
-      called = true;
-    });
+  let loader = Loader(module);
+  let tabs2 = loader.require("sdk/tabs");
+  tabs2.on('open', function onOpen(tab) {
+    called = true;
+  });
 
-    loader.unload();
+  loader.unload();
 
-    // Fire a tab event and ensure that the destroyed tab is inactive
-    tabs.once('open', function () {
-      timer.setTimeout(function () {
-        test.assert(!called, "Unloaded tab module is destroyed and inactive");
-        closeBrowserWindow(window, function() test.done());
-      }, 0);
-    });
+  // Fire a tab event and ensure that the destroyed tab is inactive
+  tabs.once('open', function (tab) {
+    timer.setTimeout(function () {
+      test.assert(!called, "Unloaded tab module is destroyed and inactive");
+      tab.close(test.done.bind(test));
+    }, 0);
+  });
 
-    tabs.open("data:text/html;charset=utf-8,foo");
-  });
+  tabs.open("data:text/html;charset=utf-8,foo");
 };
 
 // test tab properties
 exports.testTabProperties = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require('sdk/tabs');
-    let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
-    tabs.open({
-      url: url,
-      onReady: function(tab) {
-        test.assertEqual(tab.title, "foo", "title of the new tab matches");
-        test.assertEqual(tab.url, url, "URL of the new tab matches");
-        test.assert(tab.favicon, "favicon of the new tab is not empty");
-        test.assertEqual(tab.style, null, "style of the new tab matches");
-        test.assertEqual(tab.index, 1, "index of the new tab matches");
-        test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
-        test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
-        onReadyOrLoad(window);
-      },
-      onLoad: function(tab) {
-        test.assertEqual(tab.title, "foo", "title of the new tab matches");
-        test.assertEqual(tab.url, url, "URL of the new tab matches");
-        test.assert(tab.favicon, "favicon of the new tab is not empty");
-        test.assertEqual(tab.style, null, "style of the new tab matches");
-        test.assertEqual(tab.index, 1, "index of the new tab matches");
-        test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
-        test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
-        onReadyOrLoad(window);
-      }
-    });
-  });
 
   let count = 0;
-  function onReadyOrLoad (window) {
-    if (count++)
-      closeBrowserWindow(window, function() test.done());
+  function onReadyOrLoad (tab) {
+    if (count++) {
+      tab.close(test.done.bind(test));
+    }
   }
+
+  let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
+  tabs.open({
+    url: url,
+    onReady: function(tab) {
+      test.assertEqual(tab.title, "foo", "title of the new tab matches");
+      test.assertEqual(tab.url, url, "URL of the new tab matches");
+      test.assert(tab.favicon, "favicon of the new tab is not empty");
+      test.assertEqual(tab.style, null, "style of the new tab matches");
+      test.assertEqual(tab.index, 1, "index of the new tab matches");
+      test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
+      onReadyOrLoad(tab);
+    },
+    onLoad: function(tab) {
+      test.assertEqual(tab.title, "foo", "title of the new tab matches");
+      test.assertEqual(tab.url, url, "URL of the new tab matches");
+      test.assert(tab.favicon, "favicon of the new tab is not empty");
+      test.assertEqual(tab.style, null, "style of the new tab matches");
+      test.assertEqual(tab.index, 1, "index of the new tab matches");
+      test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
+      onReadyOrLoad(tab);
+    }
+  });
 };
 
 // TEST: tab properties
 exports.testTabContentTypeAndReload = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+
+  open().then(focus).then(function(window) {
     let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
     let urlXML = "data:text/xml;charset=utf-8,<foo>bar</foo>";
     tabs.open({
       url: url,
       onReady: function(tab) {
         if (tab.url === url) {
           test.assertEqual(tab.contentType, "text/html");
           tab.url = urlXML;
@@ -225,42 +156,44 @@ exports.testTabContentTypeAndReload = fu
       }
     });
   });
 };
 
 // TEST: tabs iterator and length property
 exports.testTabsIteratorAndLength = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+
+  open(null, { features: { chrome: true, toolbar: true } }).then(focus).then(function(window) {
     let startCount = 0;
     for each (let t in tabs) startCount++;
     test.assertEqual(startCount, tabs.length, "length property is correct");
     let url = "data:text/html;charset=utf-8,default";
+
     tabs.open(url);
     tabs.open(url);
     tabs.open({
       url: url,
       onOpen: function(tab) {
         let count = 0;
         for each (let t in tabs) count++;
         test.assertEqual(count, startCount + 3, "iterated tab count matches");
         test.assertEqual(startCount + 3, tabs.length, "iterated tab count matches length property");
+
         closeBrowserWindow(window, function() test.done());
       }
     });
   });
 };
 
 // TEST: tab.url setter
 exports.testTabLocation = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+
+  open().then(focus).then(function(window) {
     let url1 = "data:text/html;charset=utf-8,foo";
     let url2 = "data:text/html;charset=utf-8,bar";
 
     tabs.on('ready', function onReady(tab) {
       if (tab.url != url2)
         return;
       tabs.removeListener('ready', onReady);
       test.pass("tab.load() loaded the correct url");
@@ -274,55 +207,49 @@ exports.testTabLocation = function(test)
       }
     });
   });
 };
 
 // TEST: tab.close()
 exports.testTabClose = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-    let url = "data:text/html;charset=utf-8,foo";
+
+  let url = "data:text/html;charset=utf-8,foo";
+
+  test.assertNotEqual(tabs.activeTab.url, url, "tab is not the active tab");
+  tabs.on('ready', function onReady(tab) {
+    tabs.removeListener('ready', onReady);
+    test.assertEqual(tabs.activeTab.url, tab.url, "tab is now the active tab");
+    let secondOnCloseCalled = false;
 
-    test.assertNotEqual(tabs.activeTab.url, url, "tab is not the active tab");
-    tabs.on('ready', function onReady(tab) {
-      tabs.removeListener('ready', onReady);
-      test.assertEqual(tabs.activeTab.url, tab.url, "tab is now the active tab");
-      let secondOnCloseCalled = false;
-      tab.close(function() {
-        closeBrowserWindow(window, function() {
-          test.assert(secondOnCloseCalled,
-            "The immediate second call to tab.close gots its callback fired");
-          test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
-          test.done()
-        });
+    // Bug 699450: Multiple calls to tab.close should not throw
+    tab.close(function() secondOnCloseCalled = true);
+    try {
+      tab.close(function () {
+        test.assert(secondOnCloseCalled,
+          "The immediate second call to tab.close gots its callback fired");
+        test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
+        test.done();
       });
+    }
+    catch(e) {
+      test.fail("second call to tab.close() thrown an exception: " + e);
+    }
+    test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
+  });
 
-      // Bug 699450: Multiple calls to tab should not throw
-      try {
-        tab.close(function () {
-          secondOnCloseCalled = true;
-        });
-      }
-      catch(e) {
-        test.fail("second call to tab.close() thrown an exception: " + e);
-      }
-      test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
-    });
-
-    tabs.open(url);
-  });
+  tabs.open(url);
 };
 
 // TEST: tab.move()
 exports.testTabMove = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+
+  open().then(function(window) {
     let url = "data:text/html;charset=utf-8,foo";
 
     tabs.open({
       url: url,
       onOpen: function(tab) {
         test.assertEqual(tab.index, 1, "tab index before move matches");
         tab.index = 0;
         test.assertEqual(tab.index, 0, "tab index after move matches");
@@ -330,186 +257,141 @@ exports.testTabMove = function(test) {
       }
     });
   });
 };
 
 // TEST: open tab with default options
 exports.testOpen = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-    let url = "data:text/html;charset=utf-8,default";
-    tabs.open({
-      url: url,
-      onReady: function(tab) {
-        test.assertEqual(tab.url, url, "URL of the new tab matches");
-        test.assertEqual(window.content.location, url, "URL of active tab in the current window matches");
-        test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
 
-        closeBrowserWindow(window, function() test.done());
-      }
-    });
+  let url = "data:text/html;charset=utf-8,default";
+  tabs.open({
+    url: url,
+    onReady: function(tab) {
+      test.assertEqual(tab.url, url, "URL of the new tab matches");
+      test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
+
+      tab.close(function() test.done());
+    }
   });
 };
 
-// TEST: open pinned tab
+// TEST: opening a pinned tab
 exports.testOpenPinned = function(test) {
-  const xulApp = require("sdk/system/xul-app");
-  if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) {
-    // test tab pinning
-    test.waitUntilDone();
-    openBrowserWindow(function(window, browser) {
-      let tabs = require("sdk/tabs");
-      let url = "data:text/html;charset=utf-8,default";
-      tabs.open({
-        url: url,
-        isPinned: true,
-        onOpen: function(tab) {
-          test.assertEqual(tab.isPinned, true, "The new tab is pinned");
-          closeBrowserWindow(window, function() test.done());
-        }
-      });
-    });
-  }
-  else {
-    test.pass("Pinned tabs are not supported in this application.");
-  }
+  test.waitUntilDone();
+
+  let url = "data:text/html;charset=utf-8,default";
+  tabs.open({
+    url: url,
+    isPinned: true,
+    onOpen: function(tab) {
+      test.assertEqual(tab.isPinned, true, "The new tab is pinned");
+      tab.close(test.done.bind(test));
+    }
+  });
 };
 
 // TEST: pin/unpin opened tab
 exports.testPinUnpin = function(test) {
-  const xulApp = require("sdk/system/xul-app");
-  if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) {
-    test.waitUntilDone();
-    openBrowserWindow(function(window, browser) {
-      let tabs = require("sdk/tabs");
-      let url = "data:text/html;charset=utf-8,default";
-      tabs.open({
-        url: url,
-        onOpen: function(tab) {
-          tab.pin();
-          test.assertEqual(tab.isPinned, true, "The tab was pinned correctly");
-          tab.unpin();
-          test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly");
-          closeBrowserWindow(window, function() test.done());
-        }
-      });
-    });
-  }
-  else {
-    test.pass("Pinned tabs are not supported in this application.");
-  }
-};
+  test.waitUntilDone();
+
+  let url = "data:text/html;charset=utf-8,default";
+  tabs.open({
+    url: url,
+    inBackground: true,
+    onOpen: function(tab) {
+      tab.pin();
+      test.assertEqual(tab.isPinned, true, "The tab was pinned correctly");
+      tab.unpin();
+      test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly");
+      tab.close(test.done.bind(test));
+    }
+  });
+}
 
 // TEST: open tab in background
 exports.testInBackground = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-    let activeUrl = tabs.activeTab.url;
-    let url = "data:text/html;charset=utf-8,background";
-    test.assertEqual(activeWindow, window, "activeWindow matches this window");
-    tabs.on('ready', function onReady(tab) {
-      tabs.removeListener('ready', onReady);
-      test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
-      test.assertEqual(tab.url, url, "URL of the new background tab matches");
-      test.assertEqual(activeWindow, window, "a new window was not opened");
-      test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
-      closeBrowserWindow(window, function() test.done());
-    });
-    tabs.open({
-      url: url,
-      inBackground: true
-    });
+
+  let window = getMostRecentBrowserWindow();
+  let activeUrl = tabs.activeTab.url;
+  let url = "data:text/html;charset=utf-8,background";
+  test.assertEqual(activeWindow, window, "activeWindow matches this window");
+  tabs.on('ready', function onReady(tab) {
+    tabs.removeListener('ready', onReady);
+    test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
+    test.assertEqual(tab.url, url, "URL of the new background tab matches");
+    test.assertEqual(activeWindow, window, "a new window was not opened");
+    test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
+    tab.close(test.done.bind(test));
   });
-};
+
+  tabs.open({
+    url: url,
+    inBackground: true
+  });
+}
 
 // TEST: open tab in new window
 exports.testOpenInNewWindow = function(test) {
   test.waitUntilDone();
-  openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
+
+  let startWindowCount = windows().length;
 
-    let cache = [];
-    let windowUtils = require("sdk/deprecated/window-utils");
-    let wt = new windowUtils.WindowTracker({
-      onTrack: function(win) {
-        cache.push(win);
-      },
-      onUntrack: function(win) {
-        cache.splice(cache.indexOf(win), 1)
-      }
-    });
-    let startWindowCount = cache.length;
+  let url = "data:text/html;charset=utf-8,testOpenInNewWindow";
+  tabs.open({
+    url: url,
+    inNewWindow: true,
+    onReady: function(tab) {
+      let newWindow = getOwnerWindow(tab);
+      test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
 
-    let url = "data:text/html;charset=utf-8,newwindow";
-    tabs.open({
-      url: url,
-      inNewWindow: true,
-      onReady: function(tab) {
-        let newWindow = cache[cache.length - 1];
-        test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened");
+      onFocus(newWindow).then(function() {
         test.assertEqual(activeWindow, newWindow, "new window is active");
         test.assertEqual(tab.url, url, "URL of the new tab matches");
         test.assertEqual(newWindow.content.location, url, "URL of new tab in new window matches");
         test.assertEqual(tabs.activeTab.url, url, "URL of activeTab matches");
-        for (let i in cache) cache[i] = null;
-        wt.unload();
-        closeBrowserWindow(newWindow, function() {
-          closeBrowserWindow(window, function() test.done());
-        });
-      }
-    });
+
+        closeBrowserWindow(newWindow, test.done.bind(test));
+      }, test.fail).then(null, test.fail);
+    }
   });
-};
+
+}
 
 // Test tab.open inNewWindow + onOpen combination
 exports.testOpenInNewWindowOnOpen = function(test) {
   test.waitUntilDone();
-  let tabs = require("sdk/tabs");
+
+  let startWindowCount = windows().length;
 
-  openBrowserWindow(function(window, browser) {
-    let cache = [];
-    let windowUtils = require("sdk/deprecated/window-utils");
-    let wt = new windowUtils.WindowTracker({
-      onTrack: function(win) {
-        cache.push(win);
-      },
-      onUntrack: function(win) {
-        cache.splice(cache.indexOf(win), 1)
-      }
-    });
-    let startWindowCount = cache.length;
+  let url = "data:text/html;charset=utf-8,newwindow";
+  tabs.open({
+    url: url,
+    inNewWindow: true,
+    onOpen: function(tab) {
+      let newWindow = getOwnerWindow(tab);
 
-    let url = "data:text/html;charset=utf-8,newwindow";
-    tabs.open({
-      url: url,
-      inNewWindow: true,
-      onOpen: function(tab) {
-        let newWindow = cache[cache.length - 1];
-        test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened");
+      onFocus(newWindow).then(function() {
+        test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
         test.assertEqual(activeWindow, newWindow, "new window is active");
 
-        for (let i in cache) cache[i] = null;
-        wt.unload();
-
         closeBrowserWindow(newWindow, function() {
-          closeBrowserWindow(window, function() test.done());
+          test.done();
         });
-      }
-    });
+      });
+    }
   });
 };
 
 // TEST: onOpen event handler
 exports.testTabsEvent_onOpen = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,1";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('open', listener1);
@@ -525,17 +407,16 @@ exports.testTabsEvent_onOpen = function(
     tabs.open(url);
   });
 };
 
 // TEST: onClose event handler
 exports.testTabsEvent_onClose = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,onclose";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     }
     tabs.on('close', listener1);
@@ -557,18 +438,16 @@ exports.testTabsEvent_onClose = function
   });
 };
 
 // TEST: onClose event handler when a window is closed
 exports.testTabsEvent_onCloseWindow = function(test) {
   test.waitUntilDone();
 
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
-
     let closeCount = 0, individualCloseCount = 0;
     function listener() {
       closeCount++;
     }
     tabs.on('close', listener);
 
     // One tab is already open with the window
     let openTabs = 1;
@@ -611,17 +490,16 @@ exports.testTabsEvent_onCloseWindow = fu
 
   });
 }
 
 // TEST: onReady event handler
 exports.testTabsEvent_onReady = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,onready";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('ready', listener1);
@@ -637,17 +515,16 @@ exports.testTabsEvent_onReady = function
     tabs.open(url);
   });
 };
 
 // TEST: onActivate event handler
 exports.testTabsEvent_onActivate = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,onactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('activate', listener1);
@@ -663,17 +540,16 @@ exports.testTabsEvent_onActivate = funct
     tabs.open(url);
   });
 };
 
 // onDeactivate event handler
 exports.testTabsEvent_onDeactivate = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,ondeactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('deactivate', listener1);
@@ -694,17 +570,16 @@ exports.testTabsEvent_onDeactivate = fun
     tabs.open(url);
   });
 };
 
 // pinning
 exports.testTabsEvent_pinning = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,1";
 
     tabs.on('open', function onOpen(tab) {
       tabs.removeListener('open', onOpen);
       tab.pin();
     });
 
     tabs.on('pinned', function onPinned(tab) {
@@ -722,17 +597,16 @@ exports.testTabsEvent_pinning = function
     tabs.open(url);
   });
 };
 
 // TEST: per-tab event handlers
 exports.testPerTabEvents = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    var tabs = require("sdk/tabs");
     let eventCount = 0;
 
     tabs.open({
       url: "data:text/html;charset=utf-8,foo",
       onOpen: function(tab) {
         // add listener via property assignment
         function listener1() {
           eventCount++;
@@ -750,18 +624,16 @@ exports.testPerTabEvents = function(test
     });
   });
 };
 
 exports.testAttachOnOpen = function (test) {
   // Take care that attach has to be called on tab ready and not on tab open.
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
-
     tabs.open({
       url: "data:text/html;charset=utf-8,foobar",
       onOpen: function (tab) {
         let worker = tab.attach({
           contentScript: 'self.postMessage(document.location.href); ',
           onMessage: function (msg) {
             test.assertEqual(msg, "about:blank",
               "Worker document url is about:blank on open");
@@ -774,17 +646,16 @@ exports.testAttachOnOpen = function (tes
 
   });
 }
 
 exports.testAttachOnMultipleDocuments = function (test) {
   // Example of attach that process multiple tab documents
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
     let firstLocation = "data:text/html;charset=utf-8,foobar";
     let secondLocation = "data:text/html;charset=utf-8,bar";
     let thirdLocation = "data:text/html;charset=utf-8,fox";
     let onReadyCount = 0;
     let worker1 = null;
     let worker2 = null;
     let detachEventCount = 0;
     tabs.open({
@@ -856,17 +727,16 @@ exports.testAttachOnMultipleDocuments = 
   });
 }
 
 
 exports.testAttachWrappers = function (test) {
   // Check that content script has access to wrapped values by default
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
     let document = "data:text/html;charset=utf-8,<script>var globalJSVar = true; " +
                    "                       document.getElementById = 3;</script>";
     let count = 0;
 
     tabs.open({
       url: document,
       onReady: function (tab) {
         let worker = tab.attach({
@@ -890,17 +760,16 @@ exports.testAttachWrappers = function (t
 
 /*
 // We do not offer unwrapped access to DOM since bug 601295 landed
 // See 660780 to track progress of unwrap feature
 exports.testAttachUnwrapped = function (test) {
   // Check that content script has access to unwrapped values through unsafeWindow
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
-    let tabs = require("sdk/tabs");
     let document = "data:text/html;charset=utf-8,<script>var globalJSVar=true;</script>";
     let count = 0;
 
     tabs.open({
       url: document,
       onReady: function (tab) {
         let worker = tab.attach({
           contentScript: 'try {' +
@@ -919,17 +788,16 @@ exports.testAttachUnwrapped = function (
   });
 }
 */
 
 exports['test window focus changes active tab'] = function(test) {
   test.waitUntilDone();
   let win1 = openBrowserWindow(function() {
     let win2 = openBrowserWindow(function() {
-      let tabs = require("sdk/tabs");
       tabs.on("activate", function onActivate() {
         tabs.removeListener("activate", onActivate);
         test.pass("activate was called on windows focus change.");
         closeBrowserWindow(win1, function() {
           closeBrowserWindow(win2, function() { test.done(); });
         });
       });
       win1.focus();
@@ -988,17 +856,16 @@ exports['test unique tab ids'] = functio
   });
 }
 
 // related to Bug 671305
 exports.testOnLoadEventWithDOM = function(test) {
   test.waitUntilDone();
 
   openBrowserWindow(function(window, browser) {
-    let tabs = require('sdk/tabs');
     let count = 0;
     tabs.on('load', function onLoad(tab) {
       test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called');
       if (!count++) {
         tab.reload();
       }
       else {
         // end of test
@@ -1016,17 +883,16 @@ exports.testOnLoadEventWithDOM = functio
   });
 };
 
 // related to Bug 671305
 exports.testOnLoadEventWithImage = function(test) {
   test.waitUntilDone();
 
   openBrowserWindow(function(window, browser) {
-    let tabs = require('sdk/tabs');
     let count = 0;
     tabs.on('load', function onLoad(tab) {
       if (!count++) {
         tab.reload();
       }
       else {
         // end of test
         tabs.removeListener('load', onLoad);
@@ -1045,18 +911,16 @@ exports.testOnLoadEventWithImage = funct
 
 exports.testOnPageShowEvent = function (test) {
   test.waitUntilDone();
 
   let firstUrl = 'data:text/html;charset=utf-8,First';
   let secondUrl = 'data:text/html;charset=utf-8,Second';
 
   openBrowserWindow(function(window, browser) {
-    let tabs = require('sdk/tabs');
-
     let counter = 0;
     tabs.on('pageshow', function onPageShow(tab, persisted) {
       counter++;
       if (counter === 1) {
         test.assert(!persisted, 'page should not be cached on initial load');
         tab.url = secondUrl;
       }
       else if (counter === 2) {
@@ -1145,12 +1009,8 @@ function openBrowserWindow(callback, url
 // Helper for calling code at window close
 function closeBrowserWindow(window, callback) {
   window.addEventListener("unload", function unload() {
     window.removeEventListener("unload", unload, false);
     callback();
   }, false);
   window.close();
 }
-
-// Test disabled on Linux because of bug 882867
-if (require("sdk/system/runtime").OS == "Linux")
-  module.exports = {};
--- a/addon-sdk/source/test/test-addon-page.js
+++ b/addon-sdk/source/test/test-addon-page.js
@@ -50,16 +50,17 @@ exports['test that add-on page has no ch
     activateTab(tab);
 
     assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
       'chrome is not visible for addon page');
 
     closeTab(tab);
     assert.ok(isChromeVisible(window), 'chrome is visible again');
     loader.unload();
+    assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
     done();
   });
 };
 
 exports['test that add-on page with hash has no chrome'] = function(assert, done) {
   let { loader } = LoaderWithHookedConsole(module);
   loader.require('sdk/addon-page');
 
@@ -74,16 +75,17 @@ exports['test that add-on page with hash
     activateTab(tab);
 
     assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
       'chrome is not visible for addon page');
 
     closeTab(tab);
     assert.ok(isChromeVisible(window), 'chrome is visible again');
     loader.unload();
+    assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
     done();
   });
 };
 
 exports['test that add-on page with querystring has no chrome'] = function(assert, done) {
   let { loader } = LoaderWithHookedConsole(module);
   loader.require('sdk/addon-page');
 
@@ -98,16 +100,17 @@ exports['test that add-on page with quer
     activateTab(tab);
 
     assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
       'chrome is not visible for addon page');
 
     closeTab(tab);
     assert.ok(isChromeVisible(window), 'chrome is visible again');
     loader.unload();
+    assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
     done();
   });
 };
 
 exports['test that add-on page with hash and querystring has no chrome'] = function(assert, done) {
   let { loader } = LoaderWithHookedConsole(module);
   loader.require('sdk/addon-page');
 
@@ -122,16 +125,17 @@ exports['test that add-on page with hash
     activateTab(tab);
 
     assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
       'chrome is not visible for addon page');
 
     closeTab(tab);
     assert.ok(isChromeVisible(window), 'chrome is visible again');
     loader.unload();
+    assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
     done();
   });
 };
 
 exports['test that malformed uri is not an addon-page'] = function(assert, done) {
   let { loader } = LoaderWithHookedConsole(module);
   loader.require('sdk/addon-page');
 
@@ -146,24 +150,9 @@ exports['test that malformed uri is not 
     assert.ok(isChromeVisible(window), 'chrome is visible for malformed uri');
 
     closeTab(tab);
     loader.unload();
     done();
   });
 };
 
-exports['test that add-on pages are closed on unload'] = function(assert, done) {
-  let { loader } = LoaderWithHookedConsole(module);
-  loader.require('sdk/addon-page');
-
-  tabs.open({
-    url: uri,
-    onReady: function listener(tab) {
-      loader.unload();
-      assert.ok(!isTabOpen(tab), 'add-on page tabs are closed on unload');
-
-      done();
-    }
-  });
-};
-
 require('test').run(exports);
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-"use stirct";
+"use strict";
 
 const { Cc, Ci } = require("chrome");
 const { setTimeout } = require("sdk/timers");
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
 const { close } = require("sdk/window/helpers");
 
 const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-fs.js
@@ -0,0 +1,462 @@
+/* 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/. */
+
+"use strict";
+
+const { pathFor } = require("sdk/system");
+const fs = require("sdk/io/fs");
+const url = require("sdk/url");
+const path = require("sdk/fs/path");
+const { Buffer } = require("sdk/io/buffer");
+
+// Use profile directory to list / read / write files.
+const profilePath = pathFor("ProfD");
+const fileNameInProfile = "compatibility.ini";
+const dirNameInProfile = "extensions";
+const filePathInProfile = path.join(profilePath, fileNameInProfile);
+const dirPathInProfile = path.join(profilePath, dirNameInProfile);
+const mkdirPath = path.join(profilePath, "sdk-fixture-mkdir");
+const writePath = path.join(profilePath, "sdk-fixture-writeFile");
+const unlinkPath = path.join(profilePath, "sdk-fixture-unlink");
+const truncatePath = path.join(profilePath, "sdk-fixture-truncate");
+const renameFromPath = path.join(profilePath, "sdk-fixture-rename-from");
+const renameToPath = path.join(profilePath, "sdk-fixture-rename-to");
+
+const profileEntries = [
+  "compatibility.ini",
+  "extensions",
+  "extensions.ini",
+  "prefs.js"
+  // There are likely to be a lot more files but we can't really
+  // on consistent list so we limit to this.
+];
+
+exports["test readir"] = function(assert, end) {
+  var async = false;
+  fs.readdir(profilePath, function(error, entries) {
+    assert.ok(async, "readdir is async");
+    assert.ok(!error, "there is no error when reading directory");
+    assert.ok(profileEntries.length <= entries.length,
+              "got et least number of entries we expect");
+    assert.ok(profileEntries.every(function(entry) {
+                return entries.indexOf(entry) >= 0;
+              }), "all profiles are present");
+    end();
+  });
+
+  async = true;
+};
+
+exports["test readdir error"] = function(assert, end) {
+  var async = false;
+  var path = profilePath + "-does-not-exists";
+  fs.readdir(path, function(error, entries) {
+    assert.ok(async, "readdir is async");
+    assert.equal(error.message, "ENOENT, readdir " + path);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+    end();
+  });
+
+  async = true;
+};
+
+exports["test readdirSync"] = function(assert) {
+  var async = false;
+  var entries = fs.readdirSync(profilePath);
+  assert.ok(profileEntries.length <= entries.length,
+            "got et least number of entries we expect");
+  assert.ok(profileEntries.every(function(entry) {
+    return entries.indexOf(entry) >= 0;
+  }), "all profiles are present");
+};
+
+exports["test readirSync error"] = function(assert) {
+  var async = false;
+  var path = profilePath + "-does-not-exists";
+  try {
+    fs.readdirSync(path);
+    assert.fail(Error("No error was thrown"));
+  } catch (error) {
+    assert.equal(error.message, "ENOENT, readdir " + path);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+  }
+};
+
+exports["test readFile"] = function(assert, end) {
+  let async = false;
+  fs.readFile(filePathInProfile, function(error, content) {
+    assert.ok(async, "readFile is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(Buffer.isBuffer(content), "readFile returns buffer");
+    assert.ok(typeof(content.length) === "number", "buffer has length");
+    assert.ok(content.toString().indexOf("[Compatibility]") >= 0,
+              "content contains expected data");
+    end();
+  });
+  async = true;
+};
+
+exports["test readFile error"] = function(assert, end) {
+  let async = false;
+  let path = filePathInProfile + "-does-not-exists";
+  fs.readFile(path, function(error, content) {
+    assert.ok(async, "readFile is async");
+    assert.equal(error.message, "ENOENT, open " + path);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+
+    end();
+  });
+  async = true;
+};
+
+exports["test readFileSync not implemented"] = function(assert) {
+  let buffer = fs.readFileSync(filePathInProfile);
+  assert.ok(buffer.toString().indexOf("[Compatibility]") >= 0,
+            "read content");
+};
+
+exports["test fs.stat file"] = function(assert, end) {
+  let async = false;
+  let path = filePathInProfile;
+  fs.stat(path, function(error, stat) {
+    assert.ok(async, "fs.stat is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!stat.isDirectory(), "not a dir");
+    assert.ok(stat.isFile(), "is a file");
+    assert.ok(!stat.isSymbolicLink(), "isn't a symlink");
+    assert.ok(typeof(stat.size) === "number", "size is a number");
+    assert.ok(stat.exists === true, "file exists");
+    assert.ok(typeof(stat.isBlockDevice()) === "boolean");
+    assert.ok(typeof(stat.isCharacterDevice()) === "boolean");
+    assert.ok(typeof(stat.isFIFO()) === "boolean");
+    assert.ok(typeof(stat.isSocket()) === "boolean");
+    assert.ok(typeof(stat.hidden) === "boolean");
+    assert.ok(typeof(stat.writable) === "boolean")
+    assert.ok(stat.readable === true);
+
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.stat dir"] = function(assert, end) {
+  let async = false;
+  let path = dirPathInProfile;
+  fs.stat(path, function(error, stat) {
+    assert.ok(async, "fs.stat is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(stat.isDirectory(), "is a dir");
+    assert.ok(!stat.isFile(), "not a file");
+    assert.ok(!stat.isSymbolicLink(), "isn't a symlink");
+    assert.ok(typeof(stat.size) === "number", "size is a number");
+    assert.ok(stat.exists === true, "file exists");
+    assert.ok(typeof(stat.isBlockDevice()) === "boolean");
+    assert.ok(typeof(stat.isCharacterDevice()) === "boolean");
+    assert.ok(typeof(stat.isFIFO()) === "boolean");
+    assert.ok(typeof(stat.isSocket()) === "boolean");
+    assert.ok(typeof(stat.hidden) === "boolean");
+    assert.ok(typeof(stat.writable) === "boolean")
+    assert.ok(typeof(stat.readable) === "boolean");
+
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.stat error"] = function(assert, end) {
+  let async = false;
+  let path = filePathInProfile + "-does-not-exists";
+  fs.stat(path, function(error, stat) {
+    assert.ok(async, "fs.stat is async");
+    assert.equal(error.message, "ENOENT, stat " + path);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.exists NO"] = function(assert, end) {
+  let async = false
+  let path = filePathInProfile + "-does-not-exists";
+  fs.exists(path, function(error, value) {
+    assert.ok(async, "fs.exists is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!value, "file does not exists");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.exists YES"] = function(assert, end) {
+  let async = false
+  let path = filePathInProfile
+  fs.exists(path, function(error, value) {
+    assert.ok(async, "fs.exists is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(value, "file exists");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.exists NO"] = function(assert, end) {
+  let async = false
+  let path = filePathInProfile + "-does-not-exists";
+  fs.exists(path, function(error, value) {
+    assert.ok(async, "fs.exists is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!value, "file does not exists");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.existsSync"] = function(assert) {
+  let path = filePathInProfile
+  assert.equal(fs.existsSync(path), true, "exists");
+  assert.equal(fs.existsSync(path + "-does-not-exists"), false, "exists");
+};
+
+exports["test fs.mkdirSync fs.rmdirSync"] = function(assert) {
+  let path = mkdirPath;
+
+  assert.equal(fs.existsSync(path), false, "does not exists");
+  fs.mkdirSync(path);
+  assert.equal(fs.existsSync(path), true, "dir was created");
+  try {
+    fs.mkdirSync(path);
+    assert.fail(Error("mkdir on existing should throw"));
+  } catch (error) {
+    assert.equal(error.message, "EEXIST, mkdir " + path);
+    assert.equal(error.code, "EEXIST", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 47, "error has a errno");
+  }
+  fs.rmdirSync(path);
+  assert.equal(fs.existsSync(path), false, "dir was removed");
+};
+
+exports["test fs.mkdir"] = function(assert, end) {
+  let path = mkdirPath;
+
+  if (!fs.existsSync(path)) {
+    let async = false;
+    fs.mkdir(path, function(error) {
+      assert.ok(async, "mkdir is async");
+      assert.ok(!error, "no error");
+      assert.equal(fs.existsSync(path), true, "dir was created");
+      fs.rmdirSync(path);
+      assert.equal(fs.existsSync(path), false, "dir was removed");
+      end();
+    });
+    async = true;
+  }
+};
+
+exports["test fs.mkdir error"] = function(assert, end) {
+  let path = mkdirPath;
+
+  if (!fs.existsSync(path)) {
+    fs.mkdirSync(path);
+    let async = false;
+    fs.mkdir(path, function(error) {
+      assert.ok(async, "mkdir is async");
+      assert.equal(error.message, "EEXIST, mkdir " + path);
+      assert.equal(error.code, "EEXIST", "error has a code");
+      assert.equal(error.path, path, "error has a path");
+      assert.equal(error.errno, 47, "error has a errno");
+      fs.rmdirSync(path);
+      assert.equal(fs.existsSync(path), false, "dir was removed");
+      end();
+    });
+    async = true;
+  }
+};
+
+exports["test fs.rmdir"] = function(assert, end) {
+  let path = mkdirPath;
+
+  if (!fs.existsSync(path)) {
+    fs.mkdirSync(path);
+    assert.equal(fs.existsSync(path), true, "dir exists");
+    let async = false;
+    fs.rmdir(path, function(error) {
+      assert.ok(async, "mkdir is async");
+      assert.ok(!error, "no error");
+      assert.equal(fs.existsSync(path), false, "dir was removed");
+      end();
+    });
+    async = true;
+  }
+};
+
+
+exports["test fs.rmdir error"] = function(assert, end) {
+  let path = mkdirPath;
+
+  if (!fs.existsSync(path)) {
+    assert.equal(fs.existsSync(path), false, "dir doesn't exists");
+    let async = false;
+    fs.rmdir(path, function(error) {
+      assert.ok(async, "mkdir is async");
+      assert.equal(error.message, "ENOENT, remove " + path);
+      assert.equal(error.code, "ENOENT", "error has a code");
+      assert.equal(error.path, path, "error has a path");
+      assert.equal(error.errno, 34, "error has a errno");
+      assert.equal(fs.existsSync(path), false, "dir is removed");
+      end();
+    });
+    async = true;
+  }
+};
+
+exports["test fs.truncateSync fs.unlinkSync"] = function(assert) {
+  let path = truncatePath;
+
+  assert.equal(fs.existsSync(path), false, "does not exists");
+  fs.truncateSync(path);
+  assert.equal(fs.existsSync(path), true, "file was created");
+  fs.truncateSync(path);
+  fs.unlinkSync(path);
+  assert.equal(fs.existsSync(path), false, "file was removed");
+};
+
+
+exports["test fs.truncate"] = function(assert, end) {
+  let path = truncatePath;
+  if (!fs.existsSync(path)) {
+    let async = false;
+    fs.truncate(path, 0, function(error) {
+      assert.ok(async, "truncate is async");
+      console.log(error);
+      assert.ok(!error, "no error");
+      assert.equal(fs.existsSync(path), true, "file was created");
+      fs.unlinkSync(path);
+      assert.equal(fs.existsSync(path), false, "file was removed");
+      end();
+    })
+    async = true;
+  }
+};
+
+exports["test fs.unlink"] = function(assert, end) {
+  let path = unlinkPath;
+  let async = false;
+  assert.ok(!fs.existsSync(path), "file doesn't exists yet");
+  fs.truncateSync(path, 0);
+  assert.ok(fs.existsSync(path), "file was created");
+  fs.unlink(path, function(error) {
+    assert.ok(async, "fs.unlink is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!fs.existsSync(path), "file was removed");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.unlink error"] = function(assert, end) {
+  let path = unlinkPath;
+  let async = false;
+  assert.ok(!fs.existsSync(path), "file doesn't exists yet");
+  fs.unlink(path, function(error) {
+    assert.ok(async, "fs.unlink is async");
+    assert.equal(error.message, "ENOENT, remove " + path);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, path, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.rename"] = function(assert, end) {
+  let fromPath = renameFromPath;
+  let toPath = renameToPath;
+
+  fs.truncateSync(fromPath);
+  assert.ok(fs.existsSync(fromPath), "source file exists");
+  assert.ok(!fs.existsSync(toPath), "destination doesn't exists");
+  var async = false;
+  fs.rename(fromPath, toPath, function(error) {
+    assert.ok(async, "fs.rename is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!fs.existsSync(fromPath), "source path no longer exists");
+    assert.ok(fs.existsSync(toPath), "destination file exists");
+    fs.unlinkSync(toPath);
+    assert.ok(!fs.existsSync(toPath), "cleaned up properly");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.rename (missing source file)"] = function(assert, end) {
+  let fromPath = renameFromPath;
+  let toPath = renameToPath;
+
+  assert.ok(!fs.existsSync(fromPath), "source file doesn't exists");
+  assert.ok(!fs.existsSync(toPath), "destination doesn't exists");
+  var async = false;
+  fs.rename(fromPath, toPath, function(error) {
+    assert.ok(async, "fs.rename is async");
+    assert.equal(error.message, "ENOENT, rename " + fromPath);
+    assert.equal(error.code, "ENOENT", "error has a code");
+    assert.equal(error.path, fromPath, "error has a path");
+    assert.equal(error.errno, 34, "error has a errno");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.rename (existing target file)"] = function(assert, end) {
+  let fromPath = renameFromPath;
+  let toPath = renameToPath;
+
+  fs.truncateSync(fromPath);
+  fs.truncateSync(toPath);
+  assert.ok(fs.existsSync(fromPath), "source file exists");
+  assert.ok(fs.existsSync(toPath), "destination file exists");
+  var async = false;
+  fs.rename(fromPath, toPath, function(error) {
+    assert.ok(async, "fs.rename is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(!fs.existsSync(fromPath), "source path no longer exists");
+    assert.ok(fs.existsSync(toPath), "destination file exists");
+    fs.unlinkSync(toPath);
+    assert.ok(!fs.existsSync(toPath), "cleaned up properly");
+    end();
+  });
+  async = true;
+};
+
+exports["test fs.writeFile"] = function(assert, end) {
+  let path = writePath;
+  let content = ["hello world",
+                 "this is some text"].join("\n");
+
+  var async = false;
+  fs.writeFile(path, content, function(error) {
+    assert.ok(async, "fs write is async");
+    assert.ok(!error, "error is falsy");
+    assert.ok(fs.existsSync(path), "file was created");
+    assert.equal(fs.readFileSync(path).toString(),
+                 content,
+                 "contet was written");
+    fs.unlinkSync(path);
+    assert.ok(!fs.exists(path), "file was removed");
+
+    end();
+  });
+  async = true;
+};
+
+require("test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-host-events.js
@@ -0,0 +1,99 @@
+/* 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/. */
+'use strict';
+
+const { Cc, Ci } = require('chrome');
+const { defer, all } = require('sdk/core/promise');
+const { setTimeout } = require('sdk/timers');
+const { request, response } = require('sdk/addon/host');
+const { send } = require('sdk/addon/events');
+const { filter } = require('sdk/event/utils');
+const { on, emit, off } = require('sdk/event/core');
+
+let stream = filter(request, (data) => /sdk-x-test/.test(data.event));
+
+exports.testSend = function (assert, done) {
+  on(stream, 'data', handle);
+  send('sdk-x-test-simple', { title: 'my test data' }).then((data) => {
+    assert.equal(data.title, 'my response', 'response is handled');
+    off(stream, 'data', handle);
+    done();
+  }, (reason) => {
+    assert.fail('should not call reject');
+  });
+  function handle (e) {
+    assert.equal(e.event, 'sdk-x-test-simple', 'correct event name');
+    assert.ok(e.id != null, 'message has an ID');
+    assert.equal(e.data.title, 'my test data', 'serialized data passes');
+    e.data.title = 'my response';
+    emit(response, 'data', e);
+  }
+};
+
+exports.testSendError = function (assert, done) {
+  on(stream, 'data', handle);
+  send('sdk-x-test-error', { title: 'my test data' }).then((data) => {
+    assert.fail('should not call success');
+  }, (reason) => {
+    assert.equal(reason, 'ErrorInfo', 'should reject with error/reason');
+    off(stream, 'data', handle);
+    done();
+  });
+  function handle (e) {
+    e.error = 'ErrorInfo';
+    emit(response, 'data', e);
+  }
+};
+
+exports.testMultipleSends = function (assert, done) {
+  let count = 0;
+  let ids = [];
+  on(stream, 'data', handle);
+  ['firefox', 'thunderbird', 'rust'].map(data =>
+    send('sdk-x-test-multi', { data: data }).then(val => {
+    assert.ok(val === 'firefox' || val === 'rust', 'successful calls resolve correctly');
+    if (++count === 3) {
+      off(stream, 'data', handle);
+      done();
+    }
+  }, reason => {
+    assert.equal(reason.error, 'too blue', 'rejected calls are rejected');
+    if (++count === 3) {
+      off(stream, 'data', handle);
+      done();
+    }
+  }));
+
+  function handle (e) {
+    if (e.data !== 'firefox' || e.data !== 'rust')
+      e.error = { data: e.data, error: 'too blue' };
+    assert.ok(!~ids.indexOf(e.id), 'ID should be unique');
+    assert.equal(e.event, 'sdk-x-test-multi', 'has event name');
+    ids.push(e.id);
+    emit(response, 'data', e);
+  }
+};
+
+exports.testSerialization = function (assert, done) {
+  on(stream, 'data', handle);
+  let object = { title: 'my test data' };
+  let resObject;
+  send('sdk-x-test-serialize', object).then(data => {
+    data.title = 'another title';
+    assert.equal(object.title, 'my test data', 'original object not modified');
+    assert.equal(resObject.title, 'new title', 'object passed by value from host');
+    off(stream, 'data', handle);
+    done();
+  }, (reason) => {
+    assert.fail('should not call reject');
+  });
+  function handle (e) {
+    e.data.title = 'new title';
+    assert.equal(object.title, 'my test data', 'object passed by value to host');
+    resObject = e.data;
+    emit(response, 'data', e);
+  }
+};
+
+require('test').run(exports);
--- a/addon-sdk/source/test/test-match-pattern.js
+++ b/addon-sdk/source/test/test-match-pattern.js
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 "use strict";
 
-const { MatchPattern } = require("sdk/page-mod/match-pattern");
+const { MatchPattern } = require("sdk/util/match-pattern");
 
 exports.testMatchPatternTestTrue = function(test) {
   function ok(pattern, url) {
     let mp = new MatchPattern(pattern);
     test.assert(mp.test(url), pattern + " should match " + url);
   }
 
   ok("*", "http://example.com");
@@ -30,16 +30,18 @@ exports.testMatchPatternTestTrue = funct
 
   ok("http://example.com", "http://example.com");
   ok("http://example.com/ice-cream", "http://example.com/ice-cream");
 
   ok(/.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753");
   ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753");
   ok('*.sample.com', 'http://ex.sample.com/foo.html');
   ok('*.amp.le.com', 'http://ex.amp.le.com');
+
+  ok('data:*', 'data:text/html;charset=utf-8,');
 };
 
 exports.testMatchPatternTestFalse = function(test) {
   function ok(pattern, url) {
     let mp = new MatchPattern(pattern);
     test.assert(!mp.test(url), pattern + " should not match " + url);
   }
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-object.js
@@ -0,0 +1,35 @@
+/* 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/. */
+'use strict';
+
+const { merge, extend, has, each } = require('sdk/util/object');
+
+let o = {
+  'paper': 0,
+  'rock': 1,
+  'scissors': 2
+}
+
+//exports.testMerge = function(test) {}
+//exports.testExtend = function(test) {}
+
+exports.testHas = function(test) {
+  test.assertEqual(has(o, 'paper'), true, 'has correctly finds key');
+  test.assertEqual(has(o, 'rock'), true, 'has correctly finds key');
+  test.assertEqual(has(o, 'scissors'), true, 'has correctly finds key');
+  test.assertEqual(has(o, 'nope'), false, 'has correctly does not find key');
+  test.assertEqual(has(o, '__proto__'), false, 'has correctly does not find key');
+  test.assertEqual(has(o, 'isPrototypeOf'), false, 'has correctly does not find key');
+};
+
+exports.testEach = function(test) {
+  var count = 0;
+  var keys = new Set();
+  each(o, function (value, key, object) {
+    keys.add(key);
+    test.assertEqual(o[key], value, 'Key and value pairs passed in');
+    test.assertEqual(o, object, 'Object passed in');
+  });
+  test.assertEqual(keys.size, 3, 'All keys have been iterated upon');
+}
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -138,17 +138,17 @@ exports.testPageModIncludes = function(t
     }
     );
 };
 
 exports.testPageModErrorHandling = function(test) {
   test.assertRaises(function() {
       new PageMod();
     },
-    'pattern is undefined',
+    'The `include` option must always contain atleast one rule',
     "PageMod() throws when 'include' option is not specified.");
 };
 
 /* Tests for internal functions. */
 exports.testCommunication1 = function(test) {
   let workerDone = false,
       callbackDone = null;
 
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -142,17 +142,17 @@ exports.testValidateOptions = function(a
   assert.throws(
     function () Page({ contentURL: 'home' }),
     /The `contentURL` option must be a valid URL\./,
     "Validation correctly denied a non-URL contentURL"
   );
 
   assert.throws(
     function () Page({ onMessage: "This is not a function."}),
-    /The event listener must be a function\./,
+    /The option "onMessage" must be one of the following types: function/,
     "Validation correctly denied a non-function onMessage."
   );
 
   assert.pass("Options validation is working.");
 }
 
 exports.testContentAndAllowGettersAndSetters = function(assert, done) {
   let content = "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3;</script>";
@@ -297,16 +297,118 @@ exports.testPingPong = function(assert, 
       else {
         assert.ok(message, 'pong', 'Callback from contentScript');
         done();
       }
     }
   });
 };
 
+exports.testRedirect = function (assert, done) {
+  let page = Page({
+    contentURL: 'data:text/html;charset=utf-8,first-page',
+    contentScript: '(function () {' +
+      'if (/first-page/.test(document.location.href)) ' +
+      '  document.location.href = "data:text/html;charset=utf-8,redirect";' +
+      'else ' +
+      '  self.port.emit("redirect", document.location.href);' +
+      '})();'
+  });
+
+  page.port.on('redirect', function (url) {
+    assert.equal(url, 'data:text/html;charset=utf-8,redirect', 'Reinjects contentScript on reload');
+    done();
+  });
+};
+
+exports.testRedirectIncludeArrays = function (assert, done) {
+  let firstURL = 'data:text/html;charset=utf-8,first-page';
+  let page = Page({
+    contentURL: firstURL,
+    contentScript: '(function () {' +
+      'self.port.emit("load", document.location.href);' +
+      '  self.port.on("redirect", function (url) {' +
+      '   document.location.href = url;' +
+      '  })' +
+      '})();',
+    include: ['about:blank', 'data:*']
+  });
+
+  page.port.on('load', function (url) {
+    if (url === firstURL) {
+      page.port.emit('redirect', 'about:blank');
+    } else if (url === 'about:blank') {
+      page.port.emit('redirect', 'about:home');
+      assert.ok('`include` property handles arrays');
+      assert.equal(url, 'about:blank', 'Redirects work with accepted domains');
+      done();
+    } else if (url === 'about:home') {
+      assert.fail('Should not redirect to restricted domain');
+    }
+  });
+};
+
+exports.testRedirectFromWorker = function (assert, done) {
+  let firstURL = 'data:text/html;charset=utf-8,first-page';
+  let secondURL = 'data:text/html;charset=utf-8,second-page';
+  let thirdURL = 'data:text/html;charset=utf-8,third-page';
+  let page = Page({
+    contentURL: firstURL,
+    contentScript: '(function () {' +
+      'self.port.emit("load", document.location.href);' +
+      '  self.port.on("redirect", function (url) {' +
+      '   document.location.href = url;' +
+      '  })' +
+      '})();',
+    include: 'data:*'
+  });
+
+  page.port.on('load', function (url) {
+    if (url === firstURL) {
+      page.port.emit('redirect', secondURL);
+    } else if (url === secondURL) {
+      page.port.emit('redirect', thirdURL);
+    } else if (url === thirdURL) {
+      page.port.emit('redirect', 'about:home');
+      assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
+      done();
+    } else {
+      assert.fail('Should not redirect to unauthorized domains');
+    }
+  });
+};
+
+exports.testRedirectWithContentURL = function (assert, done) {
+  let firstURL = 'data:text/html;charset=utf-8,first-page';
+  let secondURL = 'data:text/html;charset=utf-8,second-page';
+  let thirdURL = 'data:text/html;charset=utf-8,third-page';
+  let page = Page({
+    contentURL: firstURL,
+    contentScript: '(function () {' +
+      'self.port.emit("load", document.location.href);' +
+      '})();',
+    include: 'data:*'
+  });
+
+  page.port.on('load', function (url) {
+    if (url === firstURL) {
+      page.contentURL = secondURL;
+    } else if (url === secondURL) {
+      page.contentURL = thirdURL;
+    } else if (url === thirdURL) {
+      page.contentURL = 'about:home';
+      assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
+      done();
+    } else {
+      assert.fail('Should not redirect to unauthorized domains');
+    }
+  });
+};
+
+
 exports.testMultipleDestroys = function(assert) {
   let page = Page();
   page.destroy();
   page.destroy();
   assert.pass("Multiple destroys should not cause an error");
 };
 
 exports.testContentScriptOptionsOption = function(assert, done) {
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -6,24 +6,29 @@
 const { Cc, Ci } = require("chrome");
 const { Loader } = require('sdk/test/loader');
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const timer = require("sdk/timers");
 const self = require('sdk/self');
 const { open, close, focus } = require('sdk/window/helpers');
 const { isPrivate } = require('sdk/private-browsing');
 const { isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
-const { defer } = require('sdk/core/promise');
+const { defer, all } = require('sdk/core/promise');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { getWindow } = require('sdk/panel/window');
 const { pb } = require('./private-browsing/helper');
 const { URL } = require('sdk/url');
 
 const SVG_URL = self.data.url('mofo_logo.SVG');
 
+function ignorePassingDOMNodeWarning(type, message) {
+  if (type !== 'warn' || !message.startsWith('Passing a DOM node'))
+    console[type](message);
+}
+
 function makeEmptyPrivateBrowserWindow(options) {
   options = options || {};
   return open('chrome://browser/content/browser.xul', {
     features: {
       chrome: true,
       toolbar: true,
       private: true
     }
@@ -286,17 +291,18 @@ exports["test Several Show Hides"] = fun
   });
   panel.on('error', function(e) {
     assert.fail('error was emitted:' + e.message + '\n' + e.stack);
   });
   panel.show();
 };
 
 exports["test Anchor And Arrow"] = function(assert, done) {
-  const { Panel } = require('sdk/panel');
+  let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
+  let { Panel } = loader.require('sdk/panel');
 
   let count = 0;
   let queue = [];
   let tab;
 
   function newPanel(anchor) {
     let panel = Panel({
       contentURL: "data:text/html;charset=utf-8,<html><body style='padding: 0; margin: 0; " +
@@ -452,16 +458,34 @@ exports["test Panel Text Color"] = funct
   panel.port.on("color", function (color) {
     assert.equal(color, "rgb(255, 255, 0)",
       "The panel text color style is preserved when a style exists.");
     panel.destroy();
     done();
   });
 };
 
+// Bug 866333
+exports["test watch event name"] = function(assert, done) {
+  const { Panel } = require('sdk/panel');
+
+  let html = "<html><head><style>body {color: yellow}</style></head>" +
+             "<body><p>Foo</p></body></html>";
+
+  let panel = Panel({
+    contentURL: "data:text/html;charset=utf-8," + encodeURI(html),
+    contentScript: "self.port.emit('watch', 'test');"
+  });
+  panel.port.on("watch", function (msg) {
+    assert.equal(msg, "test", 'watch event name works');
+    panel.destroy();
+    done();
+  });
+}
+
 // Bug 696552: Ensure panel.contentURL modification support
 exports["test Change Content URL"] = function(assert, done) {
   const { Panel } = require('sdk/panel');
 
   let panel = Panel({
     contentURL: "about:blank",
     contentScript: "self.port.emit('ready', document.location.href);"
   });
@@ -615,17 +639,17 @@ exports["test console.log in Panel"] = f
     assert.equal(message, text, 'console.log() works');
     panel.destroy();
     done();
   }
 };
 
 if (isWindowPBSupported) {
   exports.testPanelDoesNotShowInPrivateWindowNoAnchor = function(assert, done) {
-    let loader = Loader(module);
+    let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
     let { Panel } = loader.require("sdk/panel");
     let browserWindow = getMostRecentBrowserWindow();
 
     assert.equal(isPrivate(browserWindow), false, 'open window is not private');
 
     let panel = Panel({
       contentURL: SVG_URL
     });
@@ -669,17 +693,17 @@ if (isWindowPBSupported) {
       then(function() {
         assert.pass('private window was closed');
       }).
       then(testShowPanel.bind(null, assert, panel)).
       then(done, assert.fail.bind(assert));
   }
 
   exports.testPanelDoesNotShowInPrivateWindowWithAnchor = function(assert, done) {
-    let loader = Loader(module);
+    let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
     let { Panel } = loader.require("sdk/panel");
     let browserWindow = getMostRecentBrowserWindow();
 
     assert.equal(isPrivate(browserWindow), false, 'open window is not private');
 
     let panel = Panel({
       contentURL: SVG_URL
     });
@@ -805,16 +829,66 @@ exports['test Only One Panel Open Concur
       done();
     }
   });
 
   panelA.show();
   panelB.show();
 };
 
+exports['test passing DOM node as first argument'] = function (assert, done) {
+  let warned = defer();
+  let shown = defer();
+
+  function onMessage(type, message) {
+    let warning = 'Passing a DOM node to Panel.show() method is an unsupported ' +
+                  'feature that will be soon replaced. ' +
+                  'See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877';
+
+    assert.equal(type, 'warn',
+      'the message logged is a warning');
+
+    assert.equal(message, warning,
+      'the warning content is correct');
+
+    warned.resolve();
+  }
+
+  let { loader } = LoaderWithHookedConsole(module, onMessage);
+  let { Panel } = loader.require('sdk/panel');
+  let { Widget } = loader.require('sdk/widget');
+  let { document } = getMostRecentBrowserWindow();
+  let widgetId = 'widget:' + self.id + '-panel-widget';
+
+  let panel = Panel({
+    onShow: function() {
+      let panelNode = document.getElementById('mainPopupSet').lastChild;
+
+      assert.equal(panelNode.anchorNode, widgetNode,
+        'the panel is properly anchored to the widget');
+
+      shown.resolve();
+    }
+  });
+
+  let widget = Widget({
+    id: 'panel-widget',
+    label: 'panel widget',
+    content: '<i></i>',
+  });
+
+  let widgetNode = document.getElementById(widgetId);
+
+  all(warned.promise, shown.promise).
+    then(loader.unload).
+    then(done, assert.fail)
+
+  panel.show(widgetNode);
+};
+
 if (isWindowPBSupported) {
   exports.testGetWindow = function(assert, done) {
     let activeWindow = getMostRecentBrowserWindow();
     open(null, { features: {
       toolbar: true,
       chrome: true,
       private: true
     } }).then(function(window) {
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-path.js
@@ -0,0 +1,428 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// Adapted version of:
+// https://github.com/joyent/node/blob/v0.9.1/test/simple/test-path.js
+
+exports['test path'] = function(assert) {
+
+var system = require('sdk/system');
+var path = require('sdk/fs/path');
+
+// Shim process global from node.
+var process = Object.create(require('sdk/system'));
+process.cwd = process.pathFor.bind(process, 'CurProcD');
+
+var isWindows = require('sdk/system').platform.indexOf('win') === 0;
+
+assert.equal(path.basename(''), '');
+assert.equal(path.basename('/dir/basename.ext'), 'basename.ext');
+assert.equal(path.basename('/basename.ext'), 'basename.ext');
+assert.equal(path.basename('basename.ext'), 'basename.ext');
+assert.equal(path.basename('basename.ext/'), 'basename.ext');
+assert.equal(path.basename('basename.ext//'), 'basename.ext');
+
+if (isWindows) {
+  // On Windows a backslash acts as a path separator.
+  assert.equal(path.basename('\\dir\\basename.ext'), 'basename.ext');
+  assert.equal(path.basename('\\basename.ext'), 'basename.ext');
+  assert.equal(path.basename('basename.ext'), 'basename.ext');
+  assert.equal(path.basename('basename.ext\\'), 'basename.ext');
+  assert.equal(path.basename('basename.ext\\\\'), 'basename.ext');
+
+} else {
+  // On unix a backslash is just treated as any other character.
+  assert.equal(path.basename('\\dir\\basename.ext'), '\\dir\\basename.ext');
+  assert.equal(path.basename('\\basename.ext'), '\\basename.ext');
+  assert.equal(path.basename('basename.ext'), 'basename.ext');
+  assert.equal(path.basename('basename.ext\\'), 'basename.ext\\');
+  assert.equal(path.basename('basename.ext\\\\'), 'basename.ext\\\\');
+}
+
+// POSIX filenames may include control characters
+// c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html
+if (!isWindows) {
+  var controlCharFilename = 'Icon' + String.fromCharCode(13);
+  assert.equal(path.basename('/a/b/' + controlCharFilename),
+               controlCharFilename);
+}
+
+assert.equal(path.dirname('/a/b/'), '/a');
+assert.equal(path.dirname('/a/b'), '/a');
+assert.equal(path.dirname('/a'), '/');
+assert.equal(path.dirname(''), '.');
+assert.equal(path.dirname('/'), '/');
+assert.equal(path.dirname('////'), '/');
+
+if (isWindows) {
+  assert.equal(path.dirname('c:\\'), 'c:\\');
+  assert.equal(path.dirname('c:\\foo'), 'c:\\');
+  assert.equal(path.dirname('c:\\foo\\'), 'c:\\');
+  assert.equal(path.dirname('c:\\foo\\bar'), 'c:\\foo');
+  assert.equal(path.dirname('c:\\foo\\bar\\'), 'c:\\foo');
+  assert.equal(path.dirname('c:\\foo\\bar\\baz'), 'c:\\foo\\bar');
+  assert.equal(path.dirname('\\'), '\\');
+  assert.equal(path.dirname('\\foo'), '\\');
+  assert.equal(path.dirname('\\foo\\'), '\\');
+  assert.equal(path.dirname('\\foo\\bar'), '\\foo');
+  assert.equal(path.dirname('\\foo\\bar\\'), '\\foo');
+  assert.equal(path.dirname('\\foo\\bar\\baz'), '\\foo\\bar');
+  assert.equal(path.dirname('c:'), 'c:');
+  assert.equal(path.dirname('c:foo'), 'c:');
+  assert.equal(path.dirname('c:foo\\'), 'c:');
+  assert.equal(path.dirname('c:foo\\bar'), 'c:foo');
+  assert.equal(path.dirname('c:foo\\bar\\'), 'c:foo');
+  assert.equal(path.dirname('c:foo\\bar\\baz'), 'c:foo\\bar');
+  assert.equal(path.dirname('\\\\unc\\share'), '\\\\unc\\share');
+  assert.equal(path.dirname('\\\\unc\\share\\foo'), '\\\\unc\\share\\');
+  assert.equal(path.dirname('\\\\unc\\share\\foo\\'), '\\\\unc\\share\\');
+  assert.equal(path.dirname('\\\\unc\\share\\foo\\bar'),
+               '\\\\unc\\share\\foo');
+  assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\'),
+               '\\\\unc\\share\\foo');
+  assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\baz'),
+               '\\\\unc\\share\\foo\\bar');
+}
+
+
+assert.equal(path.extname(''), '');
+assert.equal(path.extname('/path/to/file'), '');
+assert.equal(path.extname('/path/to/file.ext'), '.ext');
+assert.equal(path.extname('/path.to/file.ext'), '.ext');
+assert.equal(path.extname('/path.to/file'), '');
+assert.equal(path.extname('/path.to/.file'), '');
+assert.equal(path.extname('/path.to/.file.ext'), '.ext');
+assert.equal(path.extname('/path/to/f.ext'), '.ext');
+assert.equal(path.extname('/path/to/..ext'), '.ext');
+assert.equal(path.extname('file'), '');
+assert.equal(path.extname('file.ext'), '.ext');
+assert.equal(path.extname('.file'), '');
+assert.equal(path.extname('.file.ext'), '.ext');
+assert.equal(path.extname('/file'), '');
+assert.equal(path.extname('/file.ext'), '.ext');
+assert.equal(path.extname('/.file'), '');
+assert.equal(path.extname('/.file.ext'), '.ext');
+assert.equal(path.extname('.path/file.ext'), '.ext');
+assert.equal(path.extname('file.ext.ext'), '.ext');
+assert.equal(path.extname('file.'), '.');
+assert.equal(path.extname('.'), '');
+assert.equal(path.extname('./'), '');
+assert.equal(path.extname('.file.ext'), '.ext');
+assert.equal(path.extname('.file'), '');
+assert.equal(path.extname('.file.'), '.');
+assert.equal(path.extname('.file..'), '.');
+assert.equal(path.extname('..'), '');
+assert.equal(path.extname('../'), '');
+assert.equal(path.extname('..file.ext'), '.ext');
+assert.equal(path.extname('..file'), '.file');
+assert.equal(path.extname('..file.'), '.');
+assert.equal(path.extname('..file..'), '.');
+assert.equal(path.extname('...'), '.');
+assert.equal(path.extname('...ext'), '.ext');
+assert.equal(path.extname('....'), '.');
+assert.equal(path.extname('file.ext/'), '.ext');
+assert.equal(path.extname('file.ext//'), '.ext');
+assert.equal(path.extname('file/'), '');
+assert.equal(path.extname('file//'), '');
+assert.equal(path.extname('file./'), '.');
+assert.equal(path.extname('file.//'), '.');
+
+if (isWindows) {
+  // On windows, backspace is a path separator.
+  assert.equal(path.extname('.\\'), '');
+  assert.equal(path.extname('..\\'), '');
+  assert.equal(path.extname('file.ext\\'), '.ext');
+  assert.equal(path.extname('file.ext\\\\'), '.ext');
+  assert.equal(path.extname('file\\'), '');
+  assert.equal(path.extname('file\\\\'), '');
+  assert.equal(path.extname('file.\\'), '.');
+  assert.equal(path.extname('file.\\\\'), '.');
+
+} else {
+  // On unix, backspace is a valid name component like any other character.
+  assert.equal(path.extname('.\\'), '');
+  assert.equal(path.extname('..\\'), '.\\');
+  assert.equal(path.extname('file.ext\\'), '.ext\\');
+  assert.equal(path.extname('file.ext\\\\'), '.ext\\\\');
+  assert.equal(path.extname('file\\'), '');
+  assert.equal(path.extname('file\\\\'), '');
+  assert.equal(path.extname('file.\\'), '.\\');
+  assert.equal(path.extname('file.\\\\'), '.\\\\');
+}
+
+// path.join tests
+var failures = [];
+var joinTests =
+    // arguments result
+    [[['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'],
+     [['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'],
+     [['/foo', '../../../bar'], '/bar'],
+     [['foo', '../../../bar'], '../../bar'],
+     [['foo/', '../../../bar'], '../../bar'],
+     [['foo/x', '../../../bar'], '../bar'],
+     [['foo/x', './bar'], 'foo/x/bar'],
+     [['foo/x/', './bar'], 'foo/x/bar'],
+     [['foo/x/', '.', 'bar'], 'foo/x/bar'],
+     [['./'], './'],
+     [['.', './'], './'],
+     [['.', '.', '.'], '.'],
+     [['.', './', '.'], '.'],
+     [['.', '/./', '.'], '.'],
+     [['.', '/////./', '.'], '.'],
+     [['.'], '.'],
+     [['', '.'], '.'],
+     [['', 'foo'], 'foo'],
+     [['foo', '/bar'], 'foo/bar'],
+     [['', '/foo'], '/foo'],
+     [['', '', '/foo'], '/foo'],
+     [['', '', 'foo'], 'foo'],
+     [['foo', ''], 'foo'],
+     [['foo/', ''], 'foo/'],
+     [['foo', '', '/bar'], 'foo/bar'],
+     [['./', '..', '/foo'], '../foo'],
+     [['./', '..', '..', '/foo'], '../../foo'],
+     [['.', '..', '..', '/foo'], '../../foo'],
+     [['', '..', '..', '/foo'], '../../foo'],
+     [['/'], '/'],
+     [['/', '.'], '/'],
+     [['/', '..'], '/'],
+     [['/', '..', '..'], '/'],
+     [[''], '.'],
+     [['', ''], '.'],
+     [[' /foo'], ' /foo'],
+     [[' ', 'foo'], ' /foo'],
+     [[' ', '.'], ' '],
+     [[' ', '/'], ' /'],
+     [[' ', ''], ' '],
+     [['/', 'foo'], '/foo'],
+     [['/', '/foo'], '/foo'],
+     [['/', '//foo'], '/foo'],
+     [['/', '', '/foo'], '/foo'],
+     [['', '/', 'foo'], '/foo'],
+     [['', '/', '/foo'], '/foo']
+    ];
+
+// Windows-specific join tests
+if (isWindows) {
+  joinTests = joinTests.concat(
+    [// UNC path expected
+     [['//foo/bar'], '//foo/bar/'],
+     [['\\/foo/bar'], '//foo/bar/'],
+     [['\\\\foo/bar'], '//foo/bar/'],
+     // UNC path expected - server and share separate
+     [['//foo', 'bar'], '//foo/bar/'],
+     [['//foo/', 'bar'], '//foo/bar/'],
+     [['//foo', '/bar'], '//foo/bar/'],
+     // UNC path expected - questionable
+     [['//foo', '', 'bar'], '//foo/bar/'],
+     [['//foo/', '', 'bar'], '//foo/bar/'],
+     [['//foo/', '', '/bar'], '//foo/bar/'],
+     // UNC path expected - even more questionable
+     [['', '//foo', 'bar'], '//foo/bar/'],
+     [['', '//foo/', 'bar'], '//foo/bar/'],
+     [['', '//foo/', '/bar'], '//foo/bar/'],
+     // No UNC path expected (no double slash in first component)
+     [['\\', 'foo/bar'], '/foo/bar'],
+     [['\\', '/foo/bar'], '/foo/bar'],
+     [['', '/', '/foo/bar'], '/foo/bar'],
+     // No UNC path expected (no non-slashes in first component - questionable)
+     [['//', 'foo/bar'], '/foo/bar'],
+     [['//', '/foo/bar'], '/foo/bar'],
+     [['\\\\', '/', '/foo/bar'], '/foo/bar'],
+     [['//'], '/'],
+     // No UNC path expected (share name missing - questionable).
+     [['//foo'], '/foo'],
+     [['//foo/'], '/foo/'],
+     [['//foo', '/'], '/foo/'],
+     [['//foo', '', '/'], '/foo/'],
+     // No UNC path expected (too many leading slashes - questionable)
+     [['///foo/bar'], '/foo/bar'],
+     [['////foo', 'bar'], '/foo/bar'],
+     [['\\\\\\/foo/bar'], '/foo/bar'],
+     // Drive-relative vs drive-absolute paths. This merely describes the
+     // status quo, rather than being obviously right
+     [['c:'], 'c:.'],
+     [['c:.'], 'c:.'],
+     [['c:', ''], 'c:.'],
+     [['', 'c:'], 'c:.'],
+     [['c:.', '/'], 'c:./'],
+     [['c:.', 'file'], 'c:file'],
+     [['c:', '/'], 'c:/'],