merge m-c to fig
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Wed, 10 Jul 2013 15:21:53 -0700
changeset 143395 9d72801295c50f992458d07f4cce9ee2a564431d
parent 143394 c2c9842f714215dae71cbfaa06a25bce7a95fbf6 (current diff)
parent 137982 3fc610532baf20310494c840e3650c8168798eb9 (diff)
child 143396 36e84501201e92cb5eb9afe24ccaf6b72a36fc30
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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
merge m-c to fig
browser/devtools/shared/test/browser_telemetry_toolboxtabs.js
browser/metro/theme/images/throbber.png
browser/themes/linux/devtools/checkbox-dark.png
browser/themes/linux/devtools/checkbox-light.png
browser/themes/linux/devtools/dark-theme.css
browser/themes/linux/devtools/light-theme.css
browser/themes/linux/devtools/markup-view.css
browser/themes/osx/devtools/checkbox-dark.png
browser/themes/osx/devtools/checkbox-light.png
browser/themes/osx/devtools/dark-theme.css
browser/themes/osx/devtools/light-theme.css
browser/themes/osx/devtools/markup-view.css
browser/themes/windows/devtools/checkbox-dark.png
browser/themes/windows/devtools/checkbox-light.png
browser/themes/windows/devtools/dark-theme.css
browser/themes/windows/devtools/light-theme.css
browser/themes/windows/devtools/markup-view.css
content/base/test/test_bug383430.html
content/canvas/test/reftest/black.png
content/html/document/test/test_bug571981.html
content/media/test/test_paused_after_removed.html
content/media/webspeech/synth/ipc/ipdl.mk
content/xbl/src/nsXBLChildrenElement.cpp
content/xbl/src/nsXBLChildrenElement.h
dom/bluetooth/ipc/ipdl.mk
dom/devicestorage/ipdl.mk
dom/imptests/failures/html/html/dom/documents/dta/test_document.body-getter.html.json
dom/imptests/failures/html/html/semantics/tabular-data/the-table-element/test_createTBody.html.json
dom/indexedDB/ipc/ipdl.mk
dom/interfaces/base/nsIDOMMimeType.idl
dom/interfaces/base/nsIDOMMimeTypeArray.idl
dom/interfaces/base/nsIDOMPlugin.idl
dom/interfaces/base/nsIDOMPluginArray.idl
dom/interfaces/events/nsIDOMPaintRequestList.idl
dom/ipc/ipdl.mk
dom/mobilemessage/src/ipc/ipdl.mk
dom/mobilemessage/src/ril/MmsPduHelper.jsm
dom/mobilemessage/src/ril/MmsService.js
dom/mobilemessage/src/ril/MmsService.manifest
dom/mobilemessage/src/ril/MobileMessageDatabaseService.js
dom/mobilemessage/src/ril/MobileMessageDatabaseService.manifest
dom/mobilemessage/src/ril/SmsService.cpp
dom/mobilemessage/src/ril/SmsService.h
dom/mobilemessage/src/ril/WspPduHelper.jsm
dom/mobilemessage/src/ril/mms_consts.js
dom/mobilemessage/src/ril/wap_consts.js
dom/network/src/ipdl.mk
dom/plugins/ipc/ipdl.mk
dom/src/storage/ipdl.mk
gfx/layers/ipc/ipdl.mk
hal/sandbox/ipdl.mk
ipc/glue/ipdl.mk
ipc/ipdl/test/cxx/ipdl.mk
ipc/testshell/ipdl.mk
js/ipc/ipdl.mk
js/src/frontend/BytecodeEmitter-inl.h
js/src/frontend/Parser-inl.h
js/src/gc/FindSCCs-inl.h
js/src/ion/LIR-inl.h
js/src/ion/PcScriptCache-inl.h
js/src/jstypedarray.cpp
js/src/jstypedarray.h
js/src/jstypedarrayinlines.h
layout/ipc/ipdl.mk
layout/reftests/forms/button-first-letter-1-noref.html
layout/reftests/forms/button-first-letter-1-ref.html
layout/reftests/forms/button-first-letter-1.html
layout/reftests/forms/button-max-height-ref.html
layout/reftests/forms/button-max-height.html
layout/reftests/forms/checkbox-checked-native-notref.html
layout/reftests/forms/checkbox-checked-native.html
layout/reftests/forms/checkbox-checked-notref.html
layout/reftests/forms/checkbox-checked.html
layout/reftests/forms/checkbox-label-dynamic-ref.html
layout/reftests/forms/checkbox-label-dynamic.html
layout/reftests/forms/checkbox-radio-stretched-ref.html
layout/reftests/forms/checkbox-radio-stretched.html
layout/reftests/forms/indeterminate-checked-notref.html
layout/reftests/forms/indeterminate-checked.html
layout/reftests/forms/indeterminate-native-checked-notref.html
layout/reftests/forms/indeterminate-native-checked.html
layout/reftests/forms/indeterminate-native-unchecked-notref.html
layout/reftests/forms/indeterminate-native-unchecked.html
layout/reftests/forms/indeterminate-selector-ref.html
layout/reftests/forms/indeterminate-selector.html
layout/reftests/forms/indeterminate-unchecked-notref.html
layout/reftests/forms/indeterminate-unchecked.html
layout/reftests/forms/input-hidden-border.html
layout/reftests/forms/input-percentage-padding-ref.html
layout/reftests/forms/input-percentage-padding.html
layout/reftests/forms/input-text-baseline-1-ref.html
layout/reftests/forms/input-text-baseline-1.html
layout/reftests/forms/input-text-bounds-1-ref.html
layout/reftests/forms/input-text-bounds-1.html
layout/reftests/forms/input-text-centering-1-ref.xul
layout/reftests/forms/input-text-centering-1.xul
layout/reftests/forms/input-text-dynamic-height-1-ref.xul
layout/reftests/forms/input-text-dynamic-height-1.xul
layout/reftests/forms/input-text-size-1-ref.html
layout/reftests/forms/input-text-size-1.html
layout/reftests/forms/input-text-size-2-ref.html
layout/reftests/forms/input-text-size-2.html
layout/reftests/forms/input/email/input-email-1.html
layout/reftests/forms/input/email/input-email-2.html
layout/reftests/forms/input/email/input-email-3.html
layout/reftests/forms/input/email/input-email-ref.html
layout/reftests/forms/input/file/input-file-background-ref.xul
layout/reftests/forms/input/file/input-file-background.html
layout/reftests/forms/input/file/input-file-color-inherit-ref.html
layout/reftests/forms/input/file/input-file-color-inherit.html
layout/reftests/forms/input/file/input-file-rtl-ref.xul
layout/reftests/forms/input/file/input-file-rtl.html
layout/reftests/forms/input/file/input-file-simple-ref.xul
layout/reftests/forms/input/file/input-file-simple.html
layout/reftests/forms/input/file/input-file-size.html
layout/reftests/forms/input/file/input-file-style-ref.xul
layout/reftests/forms/input/file/input-file-style.html
layout/reftests/forms/input/file/input-file-width-clip-ref.html
layout/reftests/forms/input/file/input-file-width-clip.html
layout/reftests/forms/input/range/input-75pct-common-ref.html
layout/reftests/forms/input/range/input-75pct-unthemed-common-ref.html
layout/reftests/forms/input/range/input-from-range-to-other-type-unthemed-1-ref.html
layout/reftests/forms/input/range/input-from-range-to-other-type-unthemed-1.html
layout/reftests/forms/input/range/input-range-different-fraction-of-range-unthemed-1-notref.html
layout/reftests/forms/input/range/input-range-different-fraction-of-range-unthemed-1.html
layout/reftests/forms/input/range/input-range-direction-unthemed-1-ref.html
layout/reftests/forms/input/range/input-range-direction-unthemed-1.html
layout/reftests/forms/input/range/input-range-moz-range-progress-1-ref.html
layout/reftests/forms/input/range/input-range-moz-range-progress-1.html
layout/reftests/forms/input/range/input-range-moz-range-progress-2-ref.html
layout/reftests/forms/input/range/input-range-moz-range-progress-2.html
layout/reftests/forms/input/range/input-range-moz-range-progress-3-ref.html
layout/reftests/forms/input/range/input-range-moz-range-progress-3.html
layout/reftests/forms/input/range/input-range-not-other-type-unthemed-1.html
layout/reftests/forms/input/range/input-range-not-other-type-unthemed-1a-notref.html
layout/reftests/forms/input/range/input-range-not-other-type-unthemed-1b-notref.html
layout/reftests/forms/input/range/input-range-not-other-type-unthemed-1c-notref.html
layout/reftests/forms/input/range/input-range-same-fraction-of-range-unthemed-1-ref.html
layout/reftests/forms/input/range/input-range-same-fraction-of-range-unthemed-1.html
layout/reftests/forms/input/range/input-stepDown-unthemed.html
layout/reftests/forms/input/range/input-stepDown.html
layout/reftests/forms/input/range/input-stepUp-unthemed.html
layout/reftests/forms/input/range/input-stepUp.html
layout/reftests/forms/input/range/input-to-range-from-other-type-unthemed-1-ref.html
layout/reftests/forms/input/range/input-to-range-from-other-type-unthemed-1.html
layout/reftests/forms/input/range/input-value-prop-unthemed.html
layout/reftests/forms/input/range/input-value-prop.html
layout/reftests/forms/input/range/input-valueAsNumber-prop-unthemed.html
layout/reftests/forms/input/range/input-valueAsNumber-prop.html
layout/reftests/forms/input/search/input-search-1.html
layout/reftests/forms/input/search/input-search-2.html
layout/reftests/forms/input/search/input-search-3.html
layout/reftests/forms/input/search/input-search-ref.html
layout/reftests/forms/input/tel/input-tel-1.html
layout/reftests/forms/input/tel/input-tel-2.html
layout/reftests/forms/input/tel/input-tel-3.html
layout/reftests/forms/input/tel/input-tel-ref.html
layout/reftests/forms/input/url/input-url-1.html
layout/reftests/forms/input/url/input-url-2.html
layout/reftests/forms/input/url/input-url-3.html
layout/reftests/forms/input/url/input-url-ref.html
layout/reftests/forms/legend-ref.html
layout/reftests/forms/legend.html
layout/reftests/forms/out-of-bounds-selectedindex-ref.html
layout/reftests/forms/out-of-bounds-selectedindex.html
layout/reftests/forms/radio-checked-native-notref.html
layout/reftests/forms/radio-checked-native.html
layout/reftests/forms/radio-checked-notref.html
layout/reftests/forms/radio-checked.html
layout/reftests/forms/radio-label-dynamic-ref.html
layout/reftests/forms/radio-label-dynamic.html
layout/reftests/forms/select-boguskids-ref.html
layout/reftests/forms/select-boguskids.html
layout/reftests/forms/select-dynamic-boguskids.html
layout/reftests/forms/select-multiple-ref.html
layout/reftests/forms/select-multiple.html
layout/reftests/forms/select-option-children-ref.html
layout/reftests/forms/select-option-children.html
layout/reftests/forms/textarea-in-dynamic-rtl-doc.html
layout/reftests/forms/textarea-in-ltr-doc-scrollbar.html
layout/reftests/forms/textarea-in-rtl-doc-scrollbar.html
layout/reftests/forms/textarea-ltr-scrollbar.html
layout/reftests/forms/textarea-ltr.html
layout/reftests/forms/textarea-no-resize.html
layout/reftests/forms/textarea-resize-background-ref.html
layout/reftests/forms/textarea-resize-background.html
layout/reftests/forms/textarea-resize-ref.html
layout/reftests/forms/textarea-resize.html
layout/reftests/forms/textarea-rtl-dynamic-attr.html
layout/reftests/forms/textarea-rtl-dynamic-style.html
layout/reftests/forms/textarea-rtl-scrollbar.html
layout/reftests/forms/textarea-rtl.html
layout/reftests/forms/textarea-setvalue-framereconstruction-1.html
layout/reftests/forms/textarea-setvalue-framereconstruction-ref.html
layout/reftests/forms/textbox-accesskey-1-notref.xul
layout/reftests/forms/textbox-accesskey-1.xul
layout/reftests/forms/textbox-accesskey-2-ref.xul
layout/reftests/forms/textbox-accesskey-2.xul
layout/reftests/forms/textbox-accesskey-3-notref.xul
layout/reftests/forms/textbox-accesskey-3-ref.xul
layout/reftests/forms/textbox-accesskey-3.xul
layout/reftests/forms/textbox-accesskey-4-notref.xul
layout/reftests/forms/textbox-accesskey-4-ref.xul
layout/reftests/forms/textbox-accesskey-4.xul
layout/reftests/forms/textbox-align-baseline-1-ref.xul
layout/reftests/forms/textbox-align-baseline-1.xul
layout/reftests/forms/textbox-setsize-ref.xul
layout/reftests/forms/textbox-setsize.xul
media/webrtc/trunk/tools/gyp/.gitignore
media/webrtc/trunk/webrtc/modules/udp_transport/OWNERS
media/webrtc/trunk/webrtc/modules/udp_transport/interface/udp_transport.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/Android.mk
media/webrtc/trunk/webrtc/modules/udp_transport/source/traffic_control_windows.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/traffic_control_windows.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket2_manager_windows.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket2_manager_windows.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket2_windows.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket2_windows.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_manager_posix.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_manager_posix.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_manager_unittest.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_manager_wrapper.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_manager_wrapper.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_posix.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_posix.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_wrapper.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_wrapper.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_socket_wrapper_unittest.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_transport.gypi
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_transport_impl.cc
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_transport_impl.h
media/webrtc/trunk/webrtc/modules/udp_transport/source/udp_transport_unittest.cc
media/webrtc/trunk/webrtc/modules/udp_transport/test/SocketManagerTest.cc
media/webrtc/trunk/webrtc/modules/video_coding/codecs/vp8/temporal_layers.cc
media/webrtc/trunk/webrtc/modules/video_coding/codecs/vp8/temporal_layers_unittest.cc
media/webrtc/trunk/webrtc/modules/video_coding/main/OWNERS
media/webrtc/trunk/webrtc/modules/video_coding/main/source/event.h
media/webrtc/trunk/webrtc/modules/video_coding/main/source/exp_filter.cc
media/webrtc/trunk/webrtc/modules/video_coding/main/source/exp_filter.h
media/webrtc/trunk/webrtc/modules/video_coding/main/source/frame_dropper.cc
media/webrtc/trunk/webrtc/modules/video_coding/main/source/frame_dropper.h
media/webrtc/trunk/webrtc/modules/video_coding/main/source/jitter_buffer_common.cc
media/webrtc/trunk/webrtc/modules/video_coding/main/source/mock/fake_tick_time.h
media/webrtc/trunk/webrtc/modules/video_coding/main/source/tick_time_base.h
media/webrtc/trunk/webrtc/system_wrappers/interface/cpu_wrapper.h
media/webrtc/trunk/webrtc/system_wrappers/source/condition_variable_win.cc
media/webrtc/trunk/webrtc/system_wrappers/source/condition_variable_win.h
media/webrtc/trunk/webrtc/system_wrappers/source/cpu.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_linux.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_linux.h
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_mac.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_mac.h
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_measurement_harness.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_measurement_harness.h
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_no_op.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_win.cc
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_win.h
media/webrtc/trunk/webrtc/system_wrappers/source/cpu_wrapper_unittest.cc
media/webrtc/trunk/webrtc/system_wrappers/source/trace_unittest.cc
media/webrtc/trunk/webrtc/voice_engine/test/auto_test/standard/network_before_streaming_test.cc
media/webvtt/868629.patch
media/webvtt/LICENSE
media/webvtt/Makefile.in
media/webvtt/README_MOZILLA
media/webvtt/alloc.c
media/webvtt/cue.c
media/webvtt/cue_internal.h
media/webvtt/cuetext.c
media/webvtt/cuetext_internal.h
media/webvtt/error.c
media/webvtt/include/webvtt/cue.h
media/webvtt/include/webvtt/error.h
media/webvtt/include/webvtt/node.h
media/webvtt/include/webvtt/parser.h
media/webvtt/include/webvtt/string.h
media/webvtt/include/webvtt/util.h
media/webvtt/lexer.c
media/webvtt/moz.build
media/webvtt/node.c
media/webvtt/node_internal.h
media/webvtt/parser.c
media/webvtt/parser_internal.h
media/webvtt/string.c
media/webvtt/string_internal.h
media/webvtt/update.sh
mobile/android/base/BrowserApp.java
mobile/android/base/BrowserToolbar.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/Makefile.in
mobile/android/base/Tab.java
mobile/android/base/gfx/LayerView.java
mobile/android/base/resources/values/styles.xml
mobile/android/base/resources/values/themes.xml
mobile/android/base/strings.xml.in
mobile/android/base/widget/TopSitesView.java
netwerk/cookie/ipdl.mk
netwerk/ipc/ipdl.mk
netwerk/protocol/ftp/ipdl.mk
netwerk/protocol/http/ipdl.mk
netwerk/protocol/http/nsHttp.h
netwerk/protocol/websocket/ipdl.mk
netwerk/protocol/wyciwyg/ipdl.mk
toolkit/components/urlformatter/Makefile.in
toolkit/modules/tests/xpcshell/test_newtab-migrate-v1.js
uriloader/exthandler/ipdl.mk
uriloader/prefetch/ipdl.mk
--- a/CLOBBER
+++ b/CLOBBER
@@ -12,9 +12,9 @@
 #          O               O
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
-Bug 848491 - Skia update.
+Bug 887463 - Remove webvtt parser.
--- a/accessible/src/generic/HyperTextAccessible.cpp
+++ b/accessible/src/generic/HyperTextAccessible.cpp
@@ -714,27 +714,32 @@ HyperTextAccessible::GetRelativeOffset(n
     NS_ENSURE_SUCCESS(rv, -1);
   }
 
   nsPeekOffsetStruct pos(aAmount, aDirection, contentOffset,
                          0, kIsJumpLinesOk, kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi,
                          aWordMovementType);
   rv = aFromFrame->PeekOffset(&pos);
   if (NS_FAILED(rv)) {
+    pos.mResultContent = aFromFrame->GetContent();
     if (aDirection == eDirPrevious) {
       // Use passed-in frame as starting point in failure case for now,
       // this is a hack to deal with starting on a list bullet frame,
       // which fails in PeekOffset() because the line iterator doesn't see it.
       // XXX Need to look at our overall handling of list bullets, which are an odd case
-      pos.mResultContent = aFromFrame->GetContent();
       int32_t endOffsetUnused;
       aFromFrame->GetOffsets(pos.mContentOffset, endOffsetUnused);
     }
     else {
-      return -1;
+      // XXX: PeekOffset fails on a last frame in the document for
+      // eSelectLine/eDirNext. DOM selection (up/down arrowing processing) has
+      // similar code to handle this case. One day it should be incorporated
+      // into PeekOffset.
+      int32_t startOffsetUnused;
+      aFromFrame->GetOffsets(startOffsetUnused, pos.mContentOffset);
     }
   }
 
   // Turn the resulting node and offset into a hyperTextOffset
   int32_t hyperTextOffset;
   if (!pos.mResultContent)
     return -1;
 
@@ -757,31 +762,16 @@ HyperTextAccessible::GetRelativeOffset(n
         static_cast<int32_t>(nsAccUtils::TextLength(firstChild)) == hyperTextOffset) {
       // XXX Bullet hack -- we should remove this once list bullets use anonymous content
       hyperTextOffset = 0;
     }
     if (!aNeedsStart && hyperTextOffset > 0) {
       -- hyperTextOffset;
     }
   }
-  else if (aAmount == eSelectEndLine && finalAccessible) { 
-    // If not at very end of hypertext, we may need change the end of line offset by 1, 
-    // to make sure we are in the right place relative to the line ending
-    if (finalAccessible->Role() == roles::WHITESPACE) {  // Landed on <br> hard line break
-      // if aNeedsStart, set end of line exactly 1 character past line break
-      // XXX It would be cleaner if we did not have to have the hard line break check,
-      // and just got the correct results from PeekOffset() for the <br> case -- the returned offset should
-      // come after the new line, as it does in other cases.
-      ++ hyperTextOffset;  // Get past hard line break
-    }
-    // We are now 1 character past the line break
-    if (!aNeedsStart) {
-      -- hyperTextOffset;
-    }
-  }
 
   return hyperTextOffset;
 }
 
 int32_t
 HyperTextAccessible::FindBoundary(int32_t aOffset, nsDirection aDirection,
                                   nsSelectionAmount aAmount,
                                   EWordMovementType aWordMovementType)
@@ -1077,27 +1067,59 @@ HyperTextAccessible::GetTextAtOffset(int
       // Ignore the spec and follow what WebKitGtk does because Orca expects it,
       // i.e. return a next word at word end offset of the current word
       // (WebKitGtk behavior) instead the current word (AKT spec).
       *aEndOffset = FindWordBoundary(offset, eDirNext, eEndWord);
       *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
       return GetText(*aStartOffset, *aEndOffset, aText);
 
     case BOUNDARY_LINE_START: {
+      // Empty last line doesn't have own frame (a previous line contains '\n'
+      // character instead) thus we can't operate on last line separately
+      // from previous line.
+      if (offset == CharacterCount()) {
+        nsAutoString lastChar;
+        GetText(offset -1, -1, lastChar);
+        if (lastChar.EqualsLiteral("\n")) {
+          *aStartOffset = *aEndOffset = offset;
+          return NS_OK;
+        }
+      }
+
+      if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
+        offset = AdjustCaretOffset(offset);
+
       // Home key, arrow down and if not on last line then home key.
       *aStartOffset = FindLineBoundary(offset, eDirPrevious, eSelectBeginLine);
       *aEndOffset = FindLineBoundary(offset, eDirNext, eSelectLine);
       int32_t tmpOffset = FindLineBoundary(*aEndOffset, eDirPrevious, eSelectBeginLine);
       if (tmpOffset != *aStartOffset)
         *aEndOffset = tmpOffset;
 
       return GetText(*aStartOffset, *aEndOffset, aText);
     }
 
     case BOUNDARY_LINE_END: {
+      // Empty last line doesn't have own frame (a previous line contains '\n'
+      // character instead) thus we can't operate on last line separately
+      // from the previous line.
+      if (offset == CharacterCount()) {
+        nsAutoString lastChar;
+        GetText(offset -1, -1, lastChar);
+        if (lastChar.EqualsLiteral("\n")) {
+          *aStartOffset = offset - 1;
+          *aEndOffset = offset;
+          aText = lastChar;
+          return NS_OK;
+        }
+      }
+
+      if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
+        offset = AdjustCaretOffset(offset);
+
       // In contrast to word end boundary we follow the spec here. End key,
       // then up arrow and if not on first line then end key.
       *aEndOffset = FindLineBoundary(offset, eDirNext, eSelectEndLine);
       int32_t tmpOffset = FindLineBoundary(offset, eDirPrevious, eSelectLine);
       *aStartOffset = FindLineBoundary(tmpOffset, eDirNext, eSelectEndLine);
       if (*aStartOffset == *aEndOffset)
         *aStartOffset = 0;
 
--- a/accessible/src/generic/HyperTextAccessible.h
+++ b/accessible/src/generic/HyperTextAccessible.h
@@ -259,16 +259,36 @@ protected:
       GetCaretOffset(&caretOffset);
       return caretOffset;
     }
 
     return aOffset;
   }
 
   /**
+   * Adjust an offset the caret stays at to get a text by line boundary.
+   */
+  int32_t AdjustCaretOffset(int32_t aOffset)
+  {
+    // It is the same character offset when the caret is visually at the very
+    // end of a line or the start of a new line (soft line break). Getting text
+    // at the line should provide the line with the visual caret, otherwise
+    // screen readers will announce the wrong line as the user presses up or
+    // down arrow and land at the end of a line.
+    if (aOffset > 0) {
+      nsRefPtr<nsFrameSelection> frameSelection = FrameSelection();
+      if (frameSelection &&
+          frameSelection->GetHint() == nsFrameSelection::HINTLEFT) {
+        return aOffset - 1;
+      }
+    }
+    return aOffset;
+  }
+
+  /**
    * Return an offset of the found word boundary.
    */
   int32_t FindWordBoundary(int32_t aOffset, nsDirection aDirection,
                            EWordMovementType aWordMovementType)
   {
     return FindBoundary(aOffset, aDirection, eSelectWord, aWordMovementType);
   }
 
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -264,17 +264,17 @@ this.AccessFu = {
         break;
       case 'Accessibility:NextObject':
         this.Input.moveCursor('moveNext', 'Simple', 'gesture');
         break;
       case 'Accessibility:PreviousObject':
         this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
         break;
       case 'Accessibility:ActivateObject':
-        this.Input.activateCurrent();
+        this.Input.activateCurrent(JSON.parse(aData));
         break;
       case 'Accessibility:Focus':
         this._focused = JSON.parse(aData);
         if (this._focused) {
           let mm = Utils.getMessageManager(Utils.CurrentBrowser);
           mm.sendAsyncMessage('AccessFu:VirtualCursor',
                               {action: 'whereIsIt', move: true});
         }
@@ -353,16 +353,59 @@ this.AccessFu = {
   _focused: false,
 
   // Keep track of message managers tha already have a 'content-script.js'
   // injected.
   _processedMessageManagers: []
 };
 
 var Output = {
+  brailleState: {
+    startOffset: 0,
+    endOffset: 0,
+    text: '',
+
+    init: function init(aOutput) {
+      if (aOutput && 'output' in aOutput) {
+        this.startOffset = aOutput.startOffset;
+        this.endOffset = aOutput.endOffset;
+        // We need to append a space at the end so that the routing key corresponding
+        // to the end of the output (i.e. the space) can be hit to move the caret there.
+        this.text = aOutput.output + ' ';
+        return this.text;
+      }
+    },
+
+    update: function update(aText) {
+      let newBraille = [];
+      let braille = {};
+
+      let prefix = this.text.substring(0, this.startOffset).trim();
+      if (prefix) {
+        prefix += ' ';
+        newBraille.push(prefix);
+      }
+
+      let newText = aText;
+      newBraille.push(newText);
+
+      let suffix = this.text.substring(this.endOffset).trim();
+      if (suffix) {
+        suffix = ' ' + suffix;
+        newBraille.push(suffix);
+      }
+
+      braille.startOffset = prefix.length;
+      braille.output = newBraille.join('');
+      braille.endOffset = braille.output.length - suffix.length;
+
+      return braille;
+    }
+  },
+
   start: function start() {
     Cu.import('resource://gre/modules/Geometry.jsm');
   },
 
   stop: function stop() {
     if (this.highlightBox) {
       Utils.win.document.documentElement.removeChild(this.highlightBox.get());
       delete this.highlightBox;
@@ -453,36 +496,46 @@ var Output = {
         if (announceBox)
           announceBox.classList.remove('showing');
         break;
       }
     }
   },
 
   Android: function Android(aDetails, aBrowser) {
+    const ANDROID_VIEW_TEXT_CHANGED = 0x10;
+
     if (!this._bridge)
       this._bridge = Cc['@mozilla.org/android/bridge;1'].getService(Ci.nsIAndroidBridge);
 
     for each (let androidEvent in aDetails) {
       androidEvent.type = 'Accessibility:Event';
       if (androidEvent.bounds)
-        androidEvent.bounds = this._adjustBounds(androidEvent.bounds, aBrowser);
+        androidEvent.bounds = this._adjustBounds(androidEvent.bounds, aBrowser, true);
+      if (androidEvent.eventType === ANDROID_VIEW_TEXT_CHANGED) {
+        androidEvent.brailleText = this.brailleState.update(androidEvent.text);
+      }
+      let (output = this.brailleState.init(androidEvent.brailleText)) {
+        if (typeof output === 'string') {
+          androidEvent.brailleText = output;
+        }
+      };
       this._bridge.handleGeckoMessage(JSON.stringify(androidEvent));
     }
   },
 
   Haptic: function Haptic(aDetails, aBrowser) {
     Utils.win.navigator.vibrate(aDetails.pattern);
   },
 
   Braille: function Braille(aDetails, aBrowser) {
     Logger.debug('Braille output: ' + aDetails.text);
   },
 
-  _adjustBounds: function(aJsonBounds, aBrowser) {
+  _adjustBounds: function(aJsonBounds, aBrowser, aIncludeZoom) {
     let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
                           aJsonBounds.right - aJsonBounds.left,
                           aJsonBounds.bottom - aJsonBounds.top);
     let vp = Utils.getViewport(Utils.win) || { zoom: 1.0, offsetY: 0 };
     let root = Utils.win;
     let offset = { left: -root.mozInnerScreenX, top: -root.mozInnerScreenY };
     let scale = 1 / Utils.getPixelsPerCSSPixel(Utils.win);
 
@@ -490,18 +543,23 @@ var Output = {
       // OOP browser, add offset of browser.
       // The offset of the browser element in relation to its parent window.
       let clientRect = aBrowser.getBoundingClientRect();
       let win = aBrowser.ownerDocument.defaultView;
       offset.left += clientRect.left + win.mozInnerScreenX;
       offset.top += clientRect.top + win.mozInnerScreenY;
     }
 
-    return bounds.scale(scale, scale).translate(offset.left, offset.top).
-      scale(vp.zoom, vp.zoom).expandToIntegers();
+    let newBounds = bounds.scale(scale, scale).translate(offset.left, offset.top);
+
+    if (aIncludeZoom) {
+      newBounds = newBounds.scale(vp.zoom, vp.zoom);
+    }
+
+    return newBounds.expandToIntegers();
   }
 };
 
 var Input = {
   editState: {},
 
   start: function start() {
     // XXX: This is too disruptive on desktop for now.
@@ -521,16 +579,19 @@ var Input = {
 
   handleEvent: function Input_handleEvent(aEvent) {
     try {
       switch (aEvent.type) {
       case 'keypress':
         this._handleKeypress(aEvent);
         break;
       case 'mozAccessFuGesture':
+        let vp = Utils.getViewport(Utils.win) || { zoom: 1.0 };
+        aEvent.detail.x *= vp.zoom;
+        aEvent.detail.y *= vp.zoom;
         this._handleGesture(aEvent.detail);
         break;
       }
     } catch (x) {
       Logger.logException(x);
     }
   },
 
@@ -679,19 +740,22 @@ var Input = {
 
     aDetails.atStart = this.editState.atStart;
     aDetails.atEnd = this.editState.atEnd;
 
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:MoveCaret', aDetails);
   },
 
-  activateCurrent: function activateCurrent() {
+  activateCurrent: function activateCurrent(aData) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
-    mm.sendAsyncMessage('AccessFu:Activate', {});
+    let offset = aData && typeof aData.keyIndex === 'number' ?
+                 aData.keyIndex - Output.brailleState.startOffset : -1;
+
+    mm.sendAsyncMessage('AccessFu:Activate', {offset: offset});
   },
 
   sendContextMenuMessage: function sendContextMenuMessage() {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:ContextMenu', {});
   },
 
   activateContextMenu: function activateContextMenu(aMessage) {
--- a/accessible/src/jsat/EventManager.jsm
+++ b/accessible/src/jsat/EventManager.jsm
@@ -135,17 +135,17 @@ this.EventManager.prototype = {
   handleAccEvent: function handleAccEvent(aEvent) {
     if (Logger.logLevel >= Logger.DEBUG)
       Logger.debug('A11yEvent', Logger.eventToString(aEvent),
                    Logger.accessibleToString(aEvent.accessible));
 
     // Don't bother with non-content events in firefox.
     if (Utils.MozBuildApp == 'browser' &&
         aEvent.eventType != EVENT_VIRTUALCURSOR_CHANGED &&
-        aEvent.accessibleDocument != Utils.CurrentContentDoc) {
+        aEvent.accessibleDocument.docType == 'window') {
       return;
     }
 
     switch (aEvent.eventType) {
       case EVENT_VIRTUALCURSOR_CHANGED:
       {
         let pivot = aEvent.accessible.
           QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
--- a/accessible/src/jsat/OutputGenerator.jsm
+++ b/accessible/src/jsat/OutputGenerator.jsm
@@ -26,76 +26,75 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm',
   'resource://gre/modules/PluralForm.jsm');
 
-
-let gUtteranceOrder = new PrefCache('accessibility.accessfu.utterance');
-
 var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
   getService(Ci.nsIStringBundleService).
   createBundle('chrome://global/locale/AccessFu.properties');
 
 this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
 
 this.OutputGenerator = {
 
   /**
    * Generates output for a PivotContext.
    * @param {PivotContext} aContext object that generates and caches
    *    context information for a given accessible and its relationship with
    *    another accessible.
-   * @return {Array} An array of strings. Depending on the utterance order,
+   * @return {Object} An object that neccessarily has an output property which
+   *    is an array of strings. Depending on the utterance order,
    *    the strings describe the context for an accessible object either
    *    starting from the accessible's ancestry or accessible's subtree.
+   *    The object may also have properties specific to the type of output
+   *    generated.
    */
   genForContext: function genForContext(aContext) {
     let output = [];
     let self = this;
     let addOutput = function addOutput(aAccessible) {
       output.push.apply(output, self.genForObject(aAccessible, aContext));
     };
     let ignoreSubtree = function ignoreSubtree(aAccessible) {
       let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
       let nameRule = self.roleRuleMap[roleString] || 0;
       // Ignore subtree if the name is explicit and the role's name rule is the
       // NAME_FROM_SUBTREE_RULE.
       return (nameRule & NAME_FROM_SUBTREE_RULE) &&
         (Utils.getAttributes(aAccessible)['explicit-name'] === 'true');
     };
-    let outputOrder = typeof gUtteranceOrder.value == 'number' ?
-                      gUtteranceOrder.value : this.defaultOutputOrder;
+
     let contextStart = this._getContextStart(aContext);
 
-    if (outputOrder === OUTPUT_DESC_FIRST) {
+    if (this.outputOrder === OUTPUT_DESC_FIRST) {
       contextStart.forEach(addOutput);
       addOutput(aContext.accessible);
       [addOutput(node) for
         (node of aContext.subtreeGenerator(true, ignoreSubtree))];
     } else {
       [addOutput(node) for
         (node of aContext.subtreeGenerator(false, ignoreSubtree))];
       addOutput(aContext.accessible);
       contextStart.reverse().forEach(addOutput);
     }
 
     // Clean up the white space.
     let trimmed;
     output = [trimmed for (word of output) if (trimmed = word.trim())];
-    return output;
+    return {output: output};
   },
 
 
   /**
    * Generates output for an object.
-   * @param {nsIAccessible} aAccessible accessible object to generate utterance
+   * @param {nsIAccessible} aAccessible accessible object to generate output
    *    for.
    * @param {PivotContext} aContext object that generates and caches
    *    context information for a given accessible and its relationship with
    *    another accessible.
    * @return {Array} Two string array. The first string describes the object
    *    and its states. The second string is the object's name. Whether the
    *    object's description or it's role is included is determined by
    *    {@link roleRuleMap}.
@@ -159,37 +158,82 @@ this.OutputGenerator = {
   _addName: function _addName(aOutput, aAccessible, aFlags) {
     let name;
     if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
       (aFlags & INCLUDE_NAME)) {
       name = aAccessible.name;
     }
 
     if (name) {
-      let outputOrder = typeof gUtteranceOrder.value == 'number' ?
-                        gUtteranceOrder.value : this.defaultOutputOrder;
-      aOutput[outputOrder === OUTPUT_DESC_FIRST ?
+      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ?
         'push' : 'unshift'](name);
     }
   },
 
+  /**
+   * Adds a landmark role to the output if available.
+   * @param {Array} aOutput Output array.
+   * @param {nsIAccessible} aAccessible current accessible object.
+   */
+  _addLandmark: function _addLandmark(aOutput, aAccessible) {
+    let getLandmarkName = function getLandmarkName(aAccessible) {
+      let roles = Utils.getAttributes(aAccessible)['xml-roles'];
+      if (!roles) {
+        return;
+      }
+
+      // Looking up a role that would match a landmark.
+      for (let landmark of this.gLandmarks) {
+        if (roles.indexOf(landmark) > -1) {
+          return gStringBundle.GetStringFromName(landmark);
+        }
+      }
+    };
+
+    let landmark = getLandmarkName.apply(this, [aAccessible]);
+
+    if (!landmark) {
+      return;
+    }
+
+    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push'](
+      landmark);
+  },
+
+  get outputOrder() {
+    if (!this._utteranceOrder) {
+      this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
+    }
+    return typeof this._utteranceOrder.value === 'number' ?
+      this._utteranceOrder.value : this.defaultOutputOrder;
+  },
+
   _getOutputName: function _getOutputName(aName) {
     return aName.replace(' ', '');
   },
 
   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
 
   _getLocalizedStates: function _getLocalizedStates(aStates) {},
 
   _getPluralFormString: function _getPluralFormString(aString, aCount) {
     let str = gStringBundle.GetStringFromName(this._getOutputName(aString));
     str = PluralForm.get(aCount, str);
     return str.replace('#1', aCount);
   },
 
+  gLandmarks: [
+    'banner',
+    'complementary',
+    'contentinfo',
+    'main',
+    'navigation',
+    'search'
+  ],
+
   roleRuleMap: {
     'menubar': INCLUDE_DESC,
     'scrollbar': INCLUDE_DESC,
     'grip': INCLUDE_DESC,
     'alert': INCLUDE_DESC | INCLUDE_NAME,
     'menupopup': INCLUDE_DESC,
     'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
     'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
@@ -266,30 +310,32 @@ this.OutputGenerator = {
         let desc = this._getLocalizedStates(aStates);
         let roleStr = this._getLocalizedRole(aRoleStr);
         if (roleStr)
           desc.push(roleStr);
         output.push(desc.join(' '));
       }
 
       this._addName(output, aAccessible, aFlags);
+      this._addLandmark(output, aAccessible);
 
       return output;
     },
 
     entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
       let output = [];
       let desc = this._getLocalizedStates(aStates);
       desc.push(this._getLocalizedRole(
                   (aStates.ext & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) ?
                     'textarea' : 'entry'));
 
       output.push(desc.join(' '));
 
       this._addName(output, aAccessible, aFlags);
+      this._addLandmark(output, aAccessible);
 
       return output;
     },
 
     table: function table(aAccessible, aRoleStr, aStates, aFlags) {
       let output = [];
       let table;
       try {
@@ -306,16 +352,17 @@ this.OutputGenerator = {
         let tableColumnInfo = this._getPluralFormString('tableColumnInfo',
           table.columnCount);
         let tableRowInfo = this._getPluralFormString('tableRowInfo',
           table.rowCount);
         output.push(gStringBundle.formatStringFromName(
           this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr),
             tableColumnInfo, tableRowInfo], 3));
         this._addName(output, aAccessible, aFlags);
+        this._addLandmark(output, aAccessible);
         return output;
       }
     }
   }
 };
 
 /**
  * Generates speech utterances from objects, actions and state changes.
@@ -400,31 +447,33 @@ this.UtteranceGenerator = {
 
     heading: function heading(aAccessible, aRoleStr, aStates, aFlags) {
       let level = {};
       aAccessible.groupPosition(level, {}, {});
       let utterance =
         [gStringBundle.formatStringFromName('headingLevel', [level.value], 1)];
 
       this._addName(utterance, aAccessible, aFlags);
+      this._addLandmark(utterance, aAccessible);
 
       return utterance;
     },
 
     listitem: function listitem(aAccessible, aRoleStr, aStates, aFlags) {
       let itemno = {};
       let itemof = {};
       aAccessible.groupPosition({}, itemof, itemno);
       let utterance = [];
       if (itemno.value == 1) // Start of list
         utterance.push(gStringBundle.GetStringFromName('listStart'));
       else if (itemno.value == itemof.value) // last item
         utterance.push(gStringBundle.GetStringFromName('listEnd'));
 
       this._addName(utterance, aAccessible, aFlags);
+      this._addLandmark(utterance, aAccessible);
 
       return utterance;
     },
 
     list: function list(aAccessible, aRoleStr, aStates, aFlags) {
       return this._getListUtterance
         (aAccessible, aRoleStr, aFlags, aAccessible.childCount);
     },
@@ -474,16 +523,18 @@ this.UtteranceGenerator = {
 
         addHeaders(desc, cell.columnHeaders);
         addHeaders(desc, cell.rowHeaders);
 
         utterance.push(desc.join(' '));
       }
 
       this._addName(utterance, aAccessible, aFlags);
+      this._addLandmark(utterance, aAccessible);
+
       return utterance;
     },
 
     columnheader: function columnheader() {
       return this.objectOutputFunctions.cell.apply(this, arguments);
     },
 
     rowheader: function rowheader() {
@@ -546,27 +597,41 @@ this.UtteranceGenerator = {
     let roleStr = this._getLocalizedRole(aRoleStr);
     if (roleStr)
       desc.push(roleStr);
     desc.push
       (gStringBundle.formatStringFromName('listItemCount', [aItemCount], 1));
     let utterance = [desc.join(' ')];
 
     this._addName(utterance, aAccessible, aFlags);
+    this._addLandmark(utterance, aAccessible);
 
     return utterance;
   }
 };
 
 
 this.BrailleGenerator = {
   __proto__: OutputGenerator,
 
   defaultOutputOrder: OUTPUT_DESC_LAST,
 
+  genForContext: function genForContext(aContext) {
+    let output = OutputGenerator.genForContext.apply(this, arguments);
+
+    let acc = aContext.accessible;
+    if (acc instanceof Ci.nsIAccessibleText) {
+      output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ?
+                         output.output.join(' ').length : acc.characterCount;
+      output.startOffset = output.endOffset - acc.characterCount;
+    }
+
+    return output;
+  },
+
   objectOutputFunctions: {
 
     __proto__: OutputGenerator.objectOutputFunctions,
 
     defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
       let braille = this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
 
       if (aAccessible.indexInParent === 1 &&
@@ -582,16 +647,17 @@ this.BrailleGenerator = {
 
       return braille;
     },
 
     listitem: function listitem(aAccessible, aRoleStr, aStates, aFlags) {
       let braille = [];
 
       this._addName(braille, aAccessible, aFlags);
+      this._addLandmark(braille, aAccessible);
 
       return braille;
     },
 
     cell: function cell(aAccessible, aRoleStr, aStates, aFlags, aContext) {
       let braille = [];
       let cell = aContext.getCellInfo(aAccessible);
       if (cell) {
@@ -607,16 +673,17 @@ this.BrailleGenerator = {
             cell.rowIndex + 1], 2));
 
         addHeaders(desc, cell.columnHeaders);
         addHeaders(desc, cell.rowHeaders);
         braille.push(desc.join(' '));
       }
 
       this._addName(braille, aAccessible, aFlags);
+      this._addLandmark(braille, aAccessible);
       return braille;
     },
 
     columnheader: function columnheader() {
       return this.objectOutputFunctions.cell.apply(this, arguments);
     },
 
     rowheader: function rowheader() {
@@ -635,16 +702,17 @@ this.BrailleGenerator = {
 
     _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aStates, aFlags) {
       let braille = [];
 
       let desc = this._getLocalizedStates(aStates);
       braille.push(desc.join(' '));
 
       this._addName(braille, aAccessible, aFlags);
+      this._addLandmark(braille, aAccessible);
 
       return braille;
     },
 
     checkbutton: function checkbutton(aAccessible, aRoleStr, aStates, aFlags) {
       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
     },
 
--- a/accessible/src/jsat/Presentation.jsm
+++ b/accessible/src/jsat/Presentation.jsm
@@ -104,46 +104,50 @@ Presenter.prototype = {
    */
   announce: function announce(aAnnouncement) {}
 };
 
 /**
  * Visual presenter. Draws a box around the virtual cursor's position.
  */
 
-this.VisualPresenter = function VisualPresenter() {};
+this.VisualPresenter = function VisualPresenter() {
+  this._displayedAccessibles = new WeakMap();
+};
 
 VisualPresenter.prototype = {
   __proto__: Presenter.prototype,
 
   type: 'Visual',
 
   /**
    * The padding in pixels between the object and the highlight border.
    */
   BORDER_PADDING: 2,
 
   viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
-    if (this._currentAccessible) {
-      let context = new PivotContext(this._currentAccessible);
+    let currentAcc = this._displayedAccessibles.get(aWindow);
+    if (Utils.isAliveAndVisible(currentAcc)) {
+      let bounds = Utils.getBounds(currentAcc);
       return {
         type: this.type,
         details: {
           method: 'showBounds',
-          bounds: context.bounds,
+          bounds: bounds,
           padding: this.BORDER_PADDING
         }
       };
     }
 
     return null;
   },
 
   pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
-    this._currentAccessible = aContext.accessible;
+    this._displayedAccessibles.set(aContext.accessible.document.window,
+                                   aContext.accessible);
 
     if (!aContext.accessible)
       return {type: this.type, details: {method: 'hideBounds'}};
 
     try {
       aContext.accessible.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
       return {
@@ -231,22 +235,22 @@ AndroidPresenter.prototype = {
     let state = Utils.getStates(aContext.accessible)[0];
 
     let brailleText = '';
     if (Utils.AndroidSdkVersion >= 16) {
       if (!this._braillePresenter) {
         this._braillePresenter = new BraillePresenter();
       }
       brailleText = this._braillePresenter.pivotChanged(aContext, aReason).
-                         details.text;
+                         details;
     }
 
     androidEvents.push({eventType: (isExploreByTouch) ?
                           this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
-                        text: UtteranceGenerator.genForContext(aContext),
+                        text: UtteranceGenerator.genForContext(aContext).output,
                         bounds: aContext.bounds,
                         clickable: aContext.accessible.actionCount > 0,
                         checkable: !!(state &
                                       Ci.nsIAccessibleStates.STATE_CHECKABLE),
                         checked: !!(state &
                                     Ci.nsIAccessibleStates.STATE_CHECKED),
                         brailleText: brailleText});
 
@@ -388,17 +392,17 @@ SpeechPresenter.prototype = {
       return null;
 
     return {
       type: this.type,
       details: {
         actions: [
           {method: 'playEarcon', data: 'tick', options: {}},
           {method: 'speak',
-            data: UtteranceGenerator.genForContext(aContext).join(' '),
+            data: UtteranceGenerator.genForContext(aContext).output.join(' '),
             options: {enqueue: true}}
         ]
       }
     };
   },
 
   actionInvoked: function SpeechPresenter_actionInvoked(aObject, aActionName) {
     return {
@@ -443,19 +447,20 @@ BraillePresenter.prototype = {
 
   type: 'Braille',
 
   pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
     if (!aContext.accessible) {
       return null;
     }
 
-    let text = BrailleGenerator.genForContext(aContext);
+    let brailleOutput = BrailleGenerator.genForContext(aContext);
+    brailleOutput.output = brailleOutput.output.join(' ');
 
-    return { type: this.type, details: {text: text.join(' ')} };
+    return { type: this.type, details: brailleOutput };
   }
 
 };
 
 this.Presentation = {
   get presenters() {
     delete this.presenters;
     this.presenters = [new VisualPresenter()];
--- a/accessible/src/jsat/Utils.jsm
+++ b/accessible/src/jsat/Utils.jsm
@@ -219,16 +219,52 @@ this.Utils = {
       this.AccRetrieval.getAccessibleFor(aDocument);
 
     return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
   },
 
   getPixelsPerCSSPixel: function getPixelsPerCSSPixel(aWindow) {
     return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel;
+  },
+
+  getBounds: function getBounds(aAccessible) {
+      let objX = {}, objY = {}, objW = {}, objH = {};
+      aAccessible.getBounds(objX, objY, objW, objH);
+      return new Rect(objX.value, objY.value, objW.value, objH.value);
+  },
+
+  inHiddenSubtree: function inHiddenSubtree(aAccessible) {
+    for (let acc=aAccessible; acc; acc=acc.parent) {
+      if (JSON.parse(Utils.getAttributes(acc).hidden)) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  isAliveAndVisible: function isAliveAndVisible(aAccessible) {
+    if (!aAccessible) {
+      return false;
+    }
+
+    try {
+      let extstate = {};
+      let state = {};
+      aAccessible.getState(state, extstate);
+      if (extstate.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT ||
+          state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE ||
+          Utils.inHiddenSubtree(aAccessible)) {
+        return false;
+      }
+    } catch (x) {
+      return false;
+    }
+
+    return true;
   }
 };
 
 this.Logger = {
   DEBUG: 0,
   INFO: 1,
   WARNING: 2,
   ERROR: 3,
@@ -554,31 +590,27 @@ PivotContext.prototype = {
     }
 
     this._cells.set(domNode, cellInfo);
     return cellInfo;
   },
 
   get bounds() {
     if (!this._bounds) {
-      let objX = {}, objY = {}, objW = {}, objH = {};
-
-      this._accessible.getBounds(objX, objY, objW, objH);
-
-      this._bounds = new Rect(objX.value, objY.value, objW.value, objH.value);
+      this._bounds = Utils.getBounds(this._accessible);
     }
 
     return this._bounds.clone();
   },
 
   _isDefunct: function _isDefunct(aAccessible) {
     try {
       let extstate = {};
       aAccessible.getState({}, extstate);
-      return !!(aAccessible.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
+      return !!(extstate.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
     } catch (x) {
       return true;
     }
   }
 };
 
 this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) {
   this.name = aName;
--- a/accessible/src/jsat/content-script.js
+++ b/accessible/src/jsat/content-script.js
@@ -1,15 +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/. */
 
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
+const ROLE_ENTRY = Ci.nsIAccessibleRole.ROLE_ENTRY;
 const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
   'resource://gre/modules/accessibility/Presentation.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
@@ -150,16 +151,34 @@ function activateCurrent(aMessage) {
         node.dispatchEvent(evt);
       }
 
       dispatchMouseEvent('mousedown');
       dispatchMouseEvent('mouseup');
     }
   }
 
+  function moveCaretTo(aAccessible, aOffset) {
+    let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
+    let oldOffset = accText.caretOffset;
+    let text = accText.getText(0, accText.characterCount);
+
+    if (aOffset >= 0 && aOffset <= accText.characterCount) {
+      accText.caretOffset = aOffset;
+    }
+
+    presentCaretChange(text, oldOffset, accText.caretOffset);
+  }
+
+  let focusedAcc = Utils.AccRetrieval.getAccessibleFor(content.document.activeElement);
+  if (focusedAcc && focusedAcc.role === ROLE_ENTRY) {
+    moveCaretTo(focusedAcc, aMessage.json.offset);
+    return;
+  }
+
   let vc = Utils.getVirtualCursor(content.document);
   if (!forwardMessage(vc, aMessage))
     activateAccessible(vc.position);
 }
 
 function activateContextMenu(aMessage) {
   function sendContextMenuCoordinates(aAccessible) {
     let objX = {}, objY = {}, objW = {}, objH = {};
@@ -213,20 +232,23 @@ function moveCaret(aMessage) {
         accText.caretOffset = end.value;
         break;
       case MOVEMENT_GRANULARITY_PARAGRAPH:
         accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1);
         break;
     }
   }
 
-  let newOffset = accText.caretOffset;
-  if (oldOffset !== newOffset) {
-    let msg = Presentation.textSelectionChanged(text, newOffset, newOffset,
-                                                oldOffset, oldOffset);
+  presentCaretChange(text, oldOffset, accText.caretOffset);
+}
+
+function presentCaretChange(aText, aOldOffset, aNewOffset) {
+  if (aOldOffset !== aNewOffset) {
+    let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
+                                                aOldOffset, aOldOffset);
     sendAsyncMessage('AccessFu:Present', msg);
   }
 }
 
 function scroll(aMessage) {
   let vc = Utils.getVirtualCursor(content.document);
 
   function tryToScroll() {
--- a/accessible/src/mac/Makefile.in
+++ b/accessible/src/mac/Makefile.in
@@ -7,28 +7,16 @@ topsrcdir = @top_srcdir@
 srcdir = @srcdir@
 VPATH = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXPORT_LIBRARY = ..
 LIBXUL_LIBRARY = 1
 
-DISABLED_CMMSRCS = \
-          AccessibleWrap.mm \
-          DocAccessibleWrap.mm \
-          mozAccessible.mm \
-          mozDocAccessible.mm \
-          mozActionElements.mm \
-          mozTextAccessible.mm \
-          mozHTMLAccessible.mm \
-          MacUtils.mm \
-          Platform.mm \
-          RootAccessibleWrap.mm \
-          $(NULL)
 
 # we don't want the shared lib, but we want to force the creation of a static lib.
 FORCE_STATIC_LIB = 1
 
 include $(topsrcdir)/config/rules.mk
 
 LOCAL_INCLUDES += \
   -I$(srcdir) \
--- a/accessible/tests/mochitest/hyperlink/test_general.xul
+++ b/accessible/tests/mochitest/hyperlink/test_general.xul
@@ -18,16 +18,19 @@
   <script type="application/javascript"
           src="../events.js" />
 
   <script type="application/javascript"
           src="hyperlink.js" />
 
   <script type="application/javascript">
   <![CDATA[
+    if (MAC)
+      SimpleTest.expectAssertions(1);
+
     function testThis(aID, aAcc, aRole, aAnchorCount, aAnchorName, aURI,
                       aStartIndex, aEndIndex, aValid)
     {
       testRole(aID, aRole);
       is(aAcc.anchorCount, aAnchorCount, "Wrong number of anchors for ID " 
          + aID + "!");
       is(aAcc.getAnchor(0).name, aAnchorName, "Wrong name for ID " + aID + "!");
       is(aAcc.getURI(0).spec, aURI, "URI wrong for ID " + aID + "!");
--- a/accessible/tests/mochitest/jsat/Makefile.in
+++ b/accessible/tests/mochitest/jsat/Makefile.in
@@ -12,13 +12,14 @@ relativesrcdir = @relativesrcdir@
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_A11Y_FILES =\
 jsatcommon.js \
 output.js \
 test_alive.html \
 test_braille.html \
 test_explicit_names.html \
+test_landmarks.html \
 test_tables.html \
 test_utterance_order.html \
 $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/accessible/tests/mochitest/jsat/output.js
+++ b/accessible/tests/mochitest/jsat/output.js
@@ -17,17 +17,18 @@ Cu.import("resource://gre/modules/access
  * Note: if |aOldAccOrElmOrID| is not provided, the |aAccOrElmOrID| must be
  * scoped to the "root" element in markup.
  */
 function testContextOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, aGenerator) {
   aOldAccOrElmOrID = aOldAccOrElmOrID || "root";
   var accessible = getAccessible(aAccOrElmOrID);
   var oldAccessible = getAccessible(aOldAccOrElmOrID);
   var context = new PivotContext(accessible, oldAccessible);
-  var output = aGenerator.genForContext(context);
+  var output = aGenerator.genForContext(context).output;
+
   isDeeply(output, expected,
     "Context output is correct for " + aAccOrElmOrID);
 }
 
 /**
  * Test object output generated array that includes names.
  * Note: test ignores outputs without the name.
  *
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_landmarks.html
@@ -0,0 +1,154 @@
+<html>
+<head>
+  <title> [AccessFu] Speak landmarks</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="output.js"></script>
+  <script type="application/javascript">
+
+    function doTest() {
+      // Test the following accOrElmOrID.
+      var tests = [{
+        accOrElmOrID: "nav",
+        expectedUtterance: [["navigation", "a nav"], ["a nav", "navigation"]],
+        expectedBraille: [["navigation", "a nav"], ["a nav", "navigation"]]
+      }, {
+        accOrElmOrID: "main",
+        expectedUtterance: [["main", "a main area"], ["a main area", "main"]],
+        expectedBraille: [["main", "a main area"], ["a main area", "main"]]
+      }, {
+        accOrElmOrID: "header",
+        expectedUtterance: [["banner", "header", "a header"], ["a header",
+          "header", "banner"]],
+        expectedBraille: [["banner", "header", "a header"], ["a header",
+          "header", "banner"]]
+      }, {
+        accOrElmOrID: "footer",
+        expectedUtterance: [["content info", "footer", "a footer"], [
+          "a footer", "footer", "content info"]],
+        expectedBraille: [["content info", "footer", "a footer"], ["a footer",
+          "footer", "content info"]]
+      }, {
+        accOrElmOrID: "article_header",
+        expectedUtterance: [["header", "a header within an article"], [
+          "a header within an article", "header"]],
+        expectedBraille: [["header", "a header within an article"], [
+          "a header within an article", "header"]],
+      }, {
+        accOrElmOrID: "article_footer",
+        expectedUtterance: [["footer", "a footer within an article"], [
+          "a footer within an article", "footer"]],
+        expectedBraille: [["footer", "a footer within an article"], [
+          "a footer within an article", "footer"]]
+      }, {
+        accOrElmOrID: "section_header",
+        expectedUtterance: [["header", "a header within a section"], [
+          "a header within a section", "header"]],
+        expectedBraille: [["header", "a header within a section"], [
+          "a header within a section", "header"]]
+      }, {
+        accOrElmOrID: "section_footer",
+        expectedUtterance: [["footer", "a footer within a section"], [
+          "a footer within a section", "footer"]],
+        expectedBraille: [["footer", "a footer within a section"], [
+          "a footer within a section", "footer"]]
+      }, {
+        accOrElmOrID: "aside",
+        expectedUtterance: [["complementary", "by the way I am an aside"], [
+          "by the way I am an aside", "complementary"]],
+        expectedBraille: [["complementary", "by the way I am an aside"], [
+          "by the way I am an aside", "complementary"]]
+      }, {
+        accOrElmOrID: "main_element",
+        expectedUtterance: [["main", "another main area"], [
+          "another main area", "main"]],
+        expectedBraille: [["main", "another main area"], ["another main area",
+          "main"]]
+      }, {
+        accOrElmOrID: "complementary",
+        expectedUtterance: [["list 1 items", "complementary", "First item",
+          "A complementary"], ["A complementary", "First item",
+          "complementary", "list 1 items"]],
+        // XXX: The '*' should probably come before all of the context
+        // utterance.
+        expectedBraille: [["complementary", "*", "A complementary"], ["*",
+          "A complementary", "complementary"]]
+      }, {
+        accOrElmOrID: "parent_main",
+        expectedUtterance: [["main", "a parent main", "complementary",
+          "a child complementary"], ["a parent main", "a child complementary",
+          "complementary", "main"]],
+        expectedBraille: [["main", "a parent main", "complementary",
+          "a child complementary"], ["a parent main", "a child complementary",
+          "complementary", "main"]]
+      }, {
+        accOrElmOrID: "child_complementary",
+        expectedUtterance: [["main", "complementary", "a child complementary"],
+          ["a child complementary", "complementary", "main"]],
+        expectedBraille: [["complementary", "a child complementary"],
+          ["a child complementary", "complementary"]]
+      }];
+
+      // Test outputs (utterance and braille) for landmarks.
+      tests.forEach(function run(test) {
+        var outputOrderValues = [0, 1];
+        outputOrderValues.forEach(function testOutputOrder(outputOrder) {
+          SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, outputOrder);
+          testOutput(test.expectedUtterance[outputOrder], test.accOrElmOrID,
+            test.oldAccOrElmOrID, 1);
+          testOutput(test.expectedBraille[outputOrder], test.accOrElmOrID,
+            test.oldAccOrElmOrID, 0);
+        });
+      });
+
+      // If there was an original utterance order preference, revert to it.
+      SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  </script>
+</head>
+<body>
+  <div id="root">
+    <a target="_blank"
+       href="https://bugzilla.mozilla.org/show_bug.cgi?id=888256"
+       title="[AccessFu] Speak landmarks">
+       Mozilla Bug 888256
+       </a>
+    <p id="display"></p>
+    <div id="content" style="display: none"></div>
+    <pre id="test"></pre>
+    <nav id="nav">a nav</nav>
+    <header id="header">a header</header>
+    <footer id="footer">a footer</footer>
+    <article id="article_with_header_and_footer">
+      <header id="article_header">a header within an article</header>
+      <footer id="article_footer">a footer within an article</footer>
+    </article>
+    <section id="section_with_header_and_footer">
+      <header id="section_header">a header within a section</header>
+      <footer id="section_footer">a footer within a section</footer>
+    </section>
+    <aside id="aside">by the way I am an aside</aside>
+    <article id="main" role="main">a main area</article>
+    <main id="main_element">another main area</main>
+    <ul style="list-style-type: none;">
+      <li role="complementary" id="complementary">
+        A complementary
+      </li>
+    </ul>
+    <main id="parent_main">
+      a parent main
+      <p id="child_complementary" role="complementary">a child complementary</article>
+    </main>
+  </div>
+</body>
+</html>
\ No newline at end of file
--- a/accessible/tests/mochitest/text/test_atcaretoffset.html
+++ b/accessible/tests/mochitest/text/test_atcaretoffset.html
@@ -41,21 +41,21 @@
       this.finalCheck = function moveToLastLineEnd_finalCheck()
       {
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15,
                             ["textarea"]);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15,
                             "textarea", kOk, kTodo, kTodo);
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15,
-                         "textarea", kTodo, kTodo, kOk);
+        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
+                         [ "textarea" ]);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
-                         "textarea", kTodo, kTodo, kOk);
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
                              "textarea", kOk, kOk, kOk);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
                              "textarea", kTodo, kTodo, kTodo);
       }
 
@@ -75,18 +75,18 @@
                             "textarea", kTodo, kTodo, kOk);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15,
                             "textarea", kTodo, kTodo, kOk);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
                          [ "textarea" ]);
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                         "textarea", kTodo, kTodo, kTodo);
+        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
                              "textarea", kTodo, kTodo, kOk);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
                              "textarea", kTodo, kTodo, kOk);
       }
 
@@ -105,20 +105,20 @@
       {
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                         "textarea", kOk, kOk, kOk);
+                         [ "textarea" ]);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                         "textarea", kTodo, kTodo, kTodo);
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
                              "textarea", kTodo, kTodo, kOk);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
                              "textarea", kTodo, kTodo, kTodo);
       }
 
@@ -136,20 +136,20 @@
       {
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
                             "textarea", kTodo, kTodo, kOk);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                         "textarea", kTodo, kTodo, kTodo);
+                         [ "textarea" ]);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                         "textarea", kTodo, kTodo, kTodo);
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
                              "textarea", kTodo, kTodo, kTodo);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
                              "textarea", kTodo, kTodo, kTodo);
       }
 
@@ -164,23 +164,23 @@
       this.__proto__ = new moveToTextStart("textarea");
 
       this.finalCheck = function moveToFirstLineStart_finalCheck()
       {
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                            "textarea", kTodo, kOk, kTodo);
+                            "textarea", kOk, kOk, kOk);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                         "textarea", kOk, kOk, kOk);
+                         [ "textarea" ]);
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0,
-                         "textarea", kTodo, kOk, kTodo);
+        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0,
                              "textarea", kOk, kOk, kOk);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0,
                              "textarea", kOk, kOk, kOk);
       }
 
@@ -198,20 +198,20 @@
       {
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
                             "textarea", kTodo, kTodo, kTodo);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                         "textarea", kOk, kOk, kOk);
+                         [ "textarea" ]);
 
         testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                         "textarea", kTodo, kOk, kTodo);
+                         [ "textarea" ]);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0,
                              "textarea", kTodo, kOk, kTodo);
 
         testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0,
                              "textarea", kTodo, kOk, kTodo);
       }
 
--- a/accessible/tests/mochitest/text/test_multiline.html
+++ b/accessible/tests/mochitest/text/test_multiline.html
@@ -11,17 +11,17 @@
   <script type="application/javascript"
           src="../common.js"></script>
   <script type="application/javascript"
           src="../text.js"></script>
   <script type="application/javascript">
 
     function doTest()
     {
-      SimpleTest.expectAssertions(38);
+      SimpleTest.expectAssertions(35);
 
       // __o__n__e__w__o__r__d__\n
       //  0  1  2  3  4  5  6  7
       // __\n
       //  8
       // __t__w__o__ __w__o__r__d__s__\n
       //  9 10 11 12 13 14 15 16 17 18
 
@@ -49,58 +49,58 @@
       testTextAfterOffset(8, BOUNDARY_LINE_START, "two words\n", 9, 19,
                           "div", kTodo, kTodo, kTodo,
                           "divbr", kTodo, kTodo, kTodo,
                           "editable", kTodo, kTodo, kTodo,
                           "editablebr", kTodo, kTodo, kTodo,
                           "textarea", kTodo, kTodo, kTodo);
       testTextAfterOffset(9, BOUNDARY_LINE_START, "", 19, 19,
                           "div", kTodo, kTodo, kTodo,
-                          "divbr", kTodo, kTodo, kOk,
+                          "divbr", kTodo, kTodo, kTodo,
                           "editable", kTodo, kTodo, kTodo,
-                          "editablebr", kTodo, kTodo, kOk,
+                          "editablebr", kTodo, kTodo, kTodo,
                           "textarea", kTodo, kTodo, kTodo);
       testTextAfterOffset(19, BOUNDARY_LINE_START, "", 19, 19,
                           "div", kOk, kOk, kTodo,
-                          "divbr", kOk, kOk, kOk,
+                          "divbr", kOk, kOk, kTodo,
                           "editable", kOk, kOk, kTodo,
-                          "editablebr", kOk, kOk, kOk,
+                          "editablebr", kOk, kOk, kTodo,
                           "textarea", kOk, kOk, kTodo);
 
       // BOUNDARY_LINE_END
       testTextAfterOffset(0, BOUNDARY_LINE_END, "\n", 7, 8,
                           "div", kTodo, kTodo, kTodo,
                           "divbr", kTodo, kTodo, kTodo,
                           "editable", kTodo, kTodo, kTodo,
                           "editablebr", kTodo, kTodo, kTodo,
                           "textarea", kTodo, kTodo, kTodo);
       testTextAfterOffset(7, BOUNDARY_LINE_END, "\n", 7, 8,
                           "div", kOk, kOk, kOk,
                           "divbr", kOk, kOk, kOk,
                           "editable", kOk, kOk, kOk,
                           "editablebr", kOk, kOk, kOk,
                           "textarea", kOk, kOk, kOk);
       testTextAfterOffset(8, BOUNDARY_LINE_END, "\ntwo words", 8, 18,
-                          "div", kTodo, kOk, kTodo,
+                          "div", kOk, kOk, kOk,
                           "divbr", kOk, kOk, kOk,
-                          "editable", kTodo, kOk, kTodo,
+                          "editable", kOk, kOk, kOk,
                           "editablebr", kOk, kOk, kOk,
-                          "textarea", kTodo, kOk, kTodo);
+                          "textarea", kOk, kOk, kOk);
       testTextAfterOffset(9, BOUNDARY_LINE_END, "\n", 18, 19,
                           "div", kTodo, kTodo, kTodo,
                           "divbr", kTodo, kTodo, kTodo,
                           "editable", kTodo, kTodo, kTodo,
                           "editablebr", kTodo, kTodo, kTodo,
                           "textarea", kTodo, kTodo, kTodo);
       testTextAfterOffset(18, BOUNDARY_LINE_END, "\n", 18, 19,
-                          "div", kTodo, kOk, kTodo,
+                          "div", kOk, kOk, kOk,
                           "divbr", kOk, kOk, kOk,
-                          "editable", kTodo, kOk, kTodo,
+                          "editable", kOk, kOk, kOk,
                           "editablebr", kOk, kOk, kOk,
-                          "textarea", kTodo, kOk, kTodo);
+                          "textarea", kOk, kOk, kOk);
       testTextAfterOffset(19, BOUNDARY_LINE_END, "", 19, 19,
                           "div", kOk, kTodo, kTodo,
                           "divbr", kOk, kTodo, kTodo,
                           "editable", kOk, kTodo, kTodo,
                           "editablebr", kOk, kTodo, kTodo,
                           "textarea", kOk, kTodo, kTodo);
 
       ////////////////////////////////////////////////////////////////////////
@@ -181,66 +181,26 @@
 
       // BOUNDARY_LINE_START
       testTextAtOffset(0, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs);
       testTextAtOffset(7, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs);
       testTextAtOffset(8, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
       testTextAtOffset(9, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
       testTextAtOffset(13, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
       testTextAtOffset(18, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-      testTextAtOffset(19, BOUNDARY_LINE_START, "", 19, 19,
-                       "div", kTodo, kTodo, kOk,
-                       "divbr", kTodo, kTodo, kOk,
-                       "editable", kTodo, kTodo, kOk,
-                       "editablebr", kTodo, kTodo, kOk,
-                       "textarea", kTodo, kTodo, kOk);
+      testTextAtOffset(19, BOUNDARY_LINE_START, "", 19, 19, IDs);
 
       // BOUNDARY_LINE_END
-      testTextAtOffset(0, BOUNDARY_LINE_END, "oneword", 0, 7,
-                       "div", kTodo, kOk, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kOk, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kOk, kTodo);
-      testTextAtOffset(7, BOUNDARY_LINE_END, "oneword", 0, 7,
-                       "div", kTodo, kOk, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kOk, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kOk, kTodo);
-      testTextAtOffset(8, BOUNDARY_LINE_END, "\n", 7, 8,
-                       "div", kTodo, kTodo, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kTodo, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kTodo, kTodo);
-      testTextAtOffset(9, BOUNDARY_LINE_END, "\ntwo words", 8, 18,
-                       "div", kTodo, kTodo, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kTodo, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kTodo, kTodo);
-      testTextAtOffset(17, BOUNDARY_LINE_END, "\ntwo words", 8, 18,
-                       "div", kTodo, kTodo, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kTodo, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kTodo, kTodo);
-      testTextAtOffset(18, BOUNDARY_LINE_END, "\ntwo words", 8, 18,
-                       "div", kTodo, kTodo, kTodo,
-                       "divbr", kOk, kOk, kOk,
-                       "editable", kTodo, kTodo, kTodo,
-                       "editablebr", kOk, kOk, kOk,
-                       "textarea", kTodo, kTodo, kTodo);
-      testTextAtOffset(19, BOUNDARY_LINE_END, "\n", 18, 19,
-                       "div", kTodo, kTodo, kTodo,
-                       "divbr", kTodo, kTodo, kTodo,
-                       "editable", kTodo, kTodo, kTodo,
-                       "editablebr", kTodo, kTodo, kTodo,
-                       "textarea", kTodo, kTodo, kTodo);
+      testTextAtOffset(0, BOUNDARY_LINE_END, "oneword", 0, 7, IDs);
+      testTextAtOffset(7, BOUNDARY_LINE_END, "oneword", 0, 7, IDs);
+      testTextAtOffset(8, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
+      testTextAtOffset(9, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
+      testTextAtOffset(17, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
+      testTextAtOffset(18, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
+      testTextAtOffset(19, BOUNDARY_LINE_END, "\n", 18, 19, IDs);
 
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
--- a/accessible/tests/mochitest/text/test_singleline.html
+++ b/accessible/tests/mochitest/text/test_singleline.html
@@ -163,12 +163,11 @@
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <input id="input" value="hello my friend"/>
   <div id="div">hello my friend</div>
   <div id="editable" contenteditable="true">hello my friend</div>
   <textarea id="textarea">hello my friend</textarea>
-  <div>XXX: a hack to make text areas/inputs working</div>
 
 </body>
 </html>
--- 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");