Bug 684620 - Implement cross process fullscreen API for B2G apps and web content. r=jlebar
authorChris Pearce <cpearce@mozilla.com>
Tue, 31 Jul 2012 14:09:31 +1200
changeset 102771 df1d8e3c45cb1e76ea97438e48128683aec01075
parent 102770 56e0971c81ead2dafafe93a39a4ee63ced8bb169
child 102772 1ca7e8b00fbbea1b33ff8e5a515833faeb28044e
push id18
push usershu@rfrn.org
push dateMon, 06 Aug 2012 22:42:45 +0000
reviewersjlebar
bugs684620
milestone17.0a1
Bug 684620 - Implement cross process fullscreen API for B2G apps and web content. r=jlebar * * * Bug 684620 - Fix in-process fullscreen in B2G. r=?
b2g/chrome/content/shell.js
content/base/public/nsIDocument.h
content/base/src/nsDocument.cpp
content/base/src/nsDocument.h
content/base/src/nsFrameLoader.cpp
content/base/src/nsGenericElement.cpp
dom/base/nsDOMWindowUtils.cpp
dom/base/nsGlobalWindow.cpp
dom/base/nsGlobalWindow.h
dom/base/nsPIDOMWindow.h
dom/browser-element/BrowserElementChild.js
dom/browser-element/BrowserElementParent.js
dom/interfaces/base/nsIDOMWindowUtils.idl
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -378,16 +378,22 @@ Services.obs.addObserver(function onSyst
   shell.sendChromeEvent({
     type: 'open-app',
     url: msg.uri,
     origin: origin,
     manifest: msg.manifest
   });
 }, 'system-messages-open-app', false);
 
+Services.obs.addObserver(function(aSubject, aTopic, aData) {
+  shell.sendEvent(shell.contentBrowser.contentWindow,
+                  "mozChromeEvent", { type: "fullscreenoriginchange",
+                                      fullscreenorigin: aData } );
+}, "fullscreen-origin-change", false);
+
 (function Repl() {
   if (!Services.prefs.getBoolPref('b2g.remote-js.enabled')) {
     return;
   }
   const prompt = 'JS> ';
   let output;
   let reader = {
     onInputStreamReady : function repl_readInput(input) {
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -87,18 +87,18 @@ class Loader;
 
 namespace dom {
 class Link;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 #define NS_IDOCUMENT_IID \
-{ 0x8c6a1e62, 0xd5ad, 0x4297, \
-  { 0xb9, 0x41, 0x64, 0x49, 0x22, 0x2e, 0xc4, 0xf0 } }
+{ 0xbd70ee06, 0x2a7d, 0x4258, \
+  { 0x86, 0x4b, 0xbd, 0x28, 0xad, 0x9f, 0xd1, 0x41 } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 // Enum for requesting a particular type of document when creating a doc
 enum DocumentFlavor {
   DocumentFlavorLegacyGuess, // compat with old code until made HTML5-compliant
   DocumentFlavorHTML, // HTMLDocument with HTMLness bit set to true
@@ -719,16 +719,37 @@ public:
    * Asynchronously requests that the document make aElement the full-screen
    * element, and move into full-screen mode. The current full-screen element
    * (if any) is pushed onto the full-screen element stack, and it can be
    * returned to full-screen status by calling RestorePreviousFullScreenState().
    */
   virtual void AsyncRequestFullScreen(Element* aElement) = 0;
 
   /**
+   * Called when a frame in a child process has entered fullscreen or when a
+   * fullscreen frame in a child process changes to another origin.
+   * aFrameElement is the frame element which contains the child-process
+   * fullscreen document, and aNewOrigin is the origin of the new fullscreen
+   * document.
+   */
+  virtual nsresult RemoteFrameFullscreenChanged(nsIDOMElement* aFrameElement,
+                                                const nsAString& aNewOrigin) = 0;
+
+  /**
+   * Called when a frame in a remote child document has rolled back fullscreen
+   * so that all its fullscreen element stacks are empty; we must continue the
+   * rollback in this parent process' doc tree branch which is fullscreen.
+   * Note that only one branch of the document tree can have its documents in
+   * fullscreen state at one time. We're in inconsistent state if the a 
+   * fullscreen document has a parent and that parent isn't fullscreen. We
+   * preserve this property across process boundaries.
+   */
+   virtual nsresult RemoteFrameFullscreenReverted() = 0;
+
+  /**
    * Restores the previous full-screen element to full-screen status. If there
    * is no former full-screen element, this exits full-screen, moving the
    * top-level browser window out of full-screen mode.
    */
   virtual void RestorePreviousFullScreenState() = 0;
 
   /**
    * Returns true if this document is in full-screen mode.
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -8494,17 +8494,17 @@ nsDocument::MozCancelFullScreen()
   if (!nsContentUtils::IsRequestFullScreenAllowed()) {
     return NS_OK;
   }
   RestorePreviousFullScreenState();
   return NS_OK;
 }
 
 // Runnable to set window full-screen mode. Used as a script runner
-// to ensure we only call nsGlobalWindow::SetFullScreen() when it's safe to 
+// to ensure we only call nsGlobalWindow::SetFullScreen() when it's safe to
 // run script. nsGlobalWindow::SetFullScreen() dispatches a synchronous event
 // (handled in chome code) which is unsafe to run if this is called in
 // nsGenericElement::UnbindFromTree().
 class nsSetWindowFullScreen : public nsRunnable {
 public:
   nsSetWindowFullScreen(nsIDocument* aDoc, bool aValue)
     : mDoc(aDoc), mValue(aValue) {}
 
@@ -8542,23 +8542,64 @@ nsIDocument::ExitFullScreen(bool aRunAsy
 {
   if (aRunAsync) {
     NS_DispatchToCurrentThread(new nsCallExitFullScreen());
     return;
   }
   nsDocument::ExitFullScreen();
 }
 
+// Returns true if the document is a direct child of a cross process parent
+// mozbrowser iframe. This is the case when the document has a null parent,
+// and its DocShell reports that it is a browser frame.
 static bool
-ResetFullScreen(nsIDocument* aDocument, void* aData) {
+HasCrossProcessParent(nsIDocument* aDocument)
+{
+  if (XRE_GetProcessType() != GeckoProcessType_Content) {
+    return false;
+  }
+  if (aDocument->GetParentDocument() != nullptr) {
+    return false;
+  }
+  nsPIDOMWindow* win = aDocument->GetWindow();
+  if (!win) {
+    return false;
+  }
+  nsCOMPtr<nsIDocShell> docShell = win->GetDocShell();
+  if (!docShell) {
+    return false;
+  }
+  bool isBrowserElement = false;
+  docShell->GetIsBrowserElement(&isBrowserElement);
+  return isBrowserElement;
+}
+
+static bool
+ResetFullScreen(nsIDocument* aDocument, void* aData)
+{
   if (aDocument->IsFullScreenDoc()) {
     static_cast<nsDocument*>(aDocument)->CleanupFullscreenState();
     NS_ASSERTION(!aDocument->IsFullScreenDoc(), "Should reset full-screen");
     nsTArray<nsIDocument*>* changed = reinterpret_cast<nsTArray<nsIDocument*>*>(aData);
     changed->AppendElement(aDocument);
+
+    if (HasCrossProcessParent(aDocument)) {
+      // We're at the top of the content-process side doc tree. Ask the parent
+      // process to exit fullscreen.
+      nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+      os->NotifyObservers(aDocument, "ask-parent-to-exit-fullscreen", nullptr);
+    }
+
+    // Dispatch a notification so that if this document has any
+    // cross-process subdocuments, they'll be notified to exit fullscreen.
+    // The BrowserElementParent listens for this event and performs the
+    // cross process notification if it has a remote child process.
+    nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+    os->NotifyObservers(aDocument, "ask-children-to-exit-fullscreen", nullptr);
+
     aDocument->EnumerateSubDocuments(ResetFullScreen, aData);
   }
   return true;
 }
 
 /* static */
 void
 nsDocument::ExitFullScreen()
@@ -8618,34 +8659,47 @@ nsDocument::RestorePreviousFullScreenSta
 
   // If fullscreen mode is updated the pointer should be unlocked
   nsCOMPtr<Element> pointerLockedElement =
     do_QueryReferent(nsEventStateManager::sPointerLockedElement);
   if (pointerLockedElement) {
     UnlockPointer();
   }
 
-  // Clear full-screen stacks in all descendant documents, bottom up.
   nsCOMPtr<nsIDocument> fullScreenDoc(do_QueryReferent(sFullScreenDoc));
+
+  // The fullscreen document may contain a <iframe mozbrowser> element which
+  // has a cross process child. So send a notification so that its browser
+  // parent will send a message to its child process to also exit fullscreen.
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  os->NotifyObservers(fullScreenDoc, "ask-children-to-exit-fullscreen", nullptr);
+
+  // Clear full-screen stacks in all descendant in process documents, bottom up.
   nsIDocument* doc = fullScreenDoc;
   while (doc != this) {
     NS_ASSERTION(doc->IsFullScreenDoc(), "Should be full-screen doc");
     static_cast<nsDocument*>(doc)->CleanupFullscreenState();
     UnlockPointer();
     DispatchFullScreenChange(doc);
     doc = doc->GetParentDocument();
   }
 
   // Roll-back full-screen state to previous full-screen element.
   NS_ASSERTION(doc == this, "Must have reached this doc.");
   while (doc != nullptr) {
     static_cast<nsDocument*>(doc)->FullScreenStackPop();
     UnlockPointer();
     DispatchFullScreenChange(doc);
     if (static_cast<nsDocument*>(doc)->mFullScreenStack.IsEmpty()) {
+      if (HasCrossProcessParent(doc)) {
+        // Send notification to the parent process to tell it to rollback to
+        // the previous fullscreen elements in its fullscreen element stacks.
+        nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+        os->NotifyObservers(doc, "ask-parent-to-rollback-fullscreen", nullptr);
+      }
       // Full-screen stack in document is empty. Go back up to the parent
       // document. We'll pop the containing element off its stack, and use
       // its next full-screen element as the full-screen element.
       static_cast<nsDocument*>(doc)->CleanupFullscreenState();
       doc = doc->GetParentDocument();
     } else {
       // Else we popped the top of the stack, and there's still another
       // element in there, so that will become the full-screen element.
@@ -8660,16 +8714,28 @@ nsDocument::RestorePreviousFullScreenSta
           nsRefPtr<nsAsyncDOMEvent> e =
             new nsAsyncDOMEvent(doc,
                                 NS_LITERAL_STRING("MozEnteredDomFullscreen"),
                                 true,
                                 true);
           e->PostDOMEvent();
         }
       }
+
+      if (!nsContentUtils::HaveEqualPrincipals(doc, fullScreenDoc)) {
+        // The origin which is fullscreen changed. Send a notification to
+        // the root process so that a warning or approval UI can be shown
+        // as necessary.
+        nsAutoString origin;
+        nsContentUtils::GetUTFOrigin(doc->NodePrincipal(), origin);
+        nsIDocument* root = nsContentUtils::GetRootDocument(doc);
+        nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+        os->NotifyObservers(root, "fullscreen-origin-change", origin.get());
+      }
+
       sFullScreenDoc = do_GetWeakReference(doc);
       break;
     }
   }
 
   if (doc == nullptr) {
     // We moved all documents out of full-screen mode, reset global full-screen
     // state and move the top-level window out of full-screen mode.
@@ -8695,17 +8761,19 @@ public:
       mDoc(aElement->OwnerDoc()),
       mWasCallerChrome(nsContentUtils::IsCallerChrome())
   {
   }
 
   NS_IMETHOD Run()
   {
     nsDocument* doc = static_cast<nsDocument*>(mDoc.get());
-    doc->RequestFullScreen(mElement, mWasCallerChrome);
+    doc->RequestFullScreen(mElement,
+                           mWasCallerChrome,
+                           /* aNotifyOnOriginChange */ true);
     return NS_OK;
   }
 
   nsRefPtr<Element> mElement;
   nsCOMPtr<nsIDocument> mDoc;
   bool mWasCallerChrome;
 };
 
@@ -8862,17 +8930,17 @@ IsInActiveTab(nsIDocument* aDoc)
     return false;
   }
 
   bool isActive = false;
   docshell->GetIsActive(&isActive);
   if (!isActive) {
     return false;
   }
-  
+
   nsCOMPtr<nsIDocShellTreeItem> dsti = do_QueryInterface(container);
   if (!dsti) {
     return false;
   }
   nsCOMPtr<nsIDocShellTreeItem> rootItem;
   dsti->GetRootTreeItem(getter_AddRefs(rootItem));
   if (!rootItem) {
     return false;
@@ -8891,18 +8959,53 @@ IsInActiveTab(nsIDocument* aDoc)
   fm->GetActiveWindow(getter_AddRefs(activeWindow));
   if (!activeWindow) {
     return false;
   }
 
   return activeWindow == rootWin;
 }
 
-void
-nsDocument::RequestFullScreen(Element* aElement, bool aWasCallerChrome)
+nsresult nsDocument::RemoteFrameFullscreenChanged(nsIDOMElement* aFrameElement,
+                                                  const nsAString& aOrigin)
+{
+  // Ensure the frame element is the fullscreen element in this document.
+  // If the frame element is already the fullscreen element in this document,
+  // this has no effect.
+  nsCOMPtr<nsIContent> content(do_QueryInterface(aFrameElement));
+  RequestFullScreen(content->AsElement(),
+                    /* aWasCallerChrome */ false,
+                    /* aNotifyOnOriginChange */ false);
+
+  // Origin changed in child process, send notifiction, so that chrome can
+  // update the UI to reflect the fullscreen origin change if necessary.
+  // The BrowserElementChild listens on this, and forwards it over its
+  // parent process, where it is redispatched. Chrome (in the root process,
+  // which could be *this* process) listens for this notification so that
+  // it can show a warning or approval UI.
+  if (!aOrigin.IsEmpty()) {
+    nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+    os->NotifyObservers(nsContentUtils::GetRootDocument(this),
+                        "fullscreen-origin-change",
+                        PromiseFlatString(aOrigin).get());
+  }
+
+  return NS_OK;
+}
+
+nsresult nsDocument::RemoteFrameFullscreenReverted()
+{
+  RestorePreviousFullScreenState();
+  return NS_OK;
+}
+
+void
+nsDocument::RequestFullScreen(Element* aElement,
+                              bool aWasCallerChrome,
+                              bool aNotifyOnOriginChange)
 {
   NS_ASSERTION(aElement,
     "Must pass non-null element to nsDocument::RequestFullScreen");
   if (!aElement || aElement == GetFullScreenElement()) {
     return;
   }
   if (!aElement->IsInDoc()) {
     LogFullScreenDenied(true, "FullScreenDeniedNotInDocument", this);
@@ -8917,17 +9020,17 @@ nsDocument::RequestFullScreen(Element* a
     return;
   }
   if (!IsFullScreenEnabled(aWasCallerChrome, true)) {
     // IsFullScreenEnabled calls LogFullScreenDenied, no need to log.
     return;
   }
   if (GetFullScreenElement() &&
       !nsContentUtils::ContentIsDescendantOf(aElement, GetFullScreenElement())) {
-    // If this document is full-screen, only grant full-screen requests from 
+    // If this document is full-screen, only grant full-screen requests from
     // a descendent of the current full-screen element.
     LogFullScreenDenied(true, "FullScreenDeniedNotDescendant", this);
     return;
   }
   if (!nsContentUtils::IsChromeDoc(this) && !IsInActiveTab(this)) {
     LogFullScreenDenied(true, "FullScreenDeniedNotFocusedTab", this);
     return;
   }
@@ -8953,22 +9056,22 @@ nsDocument::RequestFullScreen(Element* a
   // too. We're required by the spec to dispatch the events in root-to-leaf
   // order, but we traverse the doctree in a leaf-to-root order, so we save
   // references to the documents we must dispatch to so that we get the order
   // as specified.
   nsAutoTArray<nsIDocument*, 8> changed;
 
   // Remember the root document, so that if a full-screen document is hidden
   // we can reset full-screen state in the remaining visible full-screen documents.
-  nsIDocument* fullScreenDoc = nsContentUtils::GetRootDocument(this);
-  sFullScreenRootDoc = do_GetWeakReference(fullScreenDoc);
+  nsIDocument* fullScreenRootDoc = nsContentUtils::GetRootDocument(this);
+  sFullScreenRootDoc = do_GetWeakReference(fullScreenRootDoc);
 
   // If a document is already in fullscreen, then unlock the mouse pointer
   // before setting a new document to fullscreen
-  if (fullScreenDoc) {
+  if (sFullScreenDoc) {
     UnlockPointer();
   }
 
   // If a document is already in fullscreen, then unlock the mouse pointer
   // before setting a new document to fullscreen
   nsCOMPtr<Element> pointerLockedElement =
     do_QueryReferent(nsEventStateManager::sPointerLockedElement);
   if (pointerLockedElement) {
@@ -8976,25 +9079,28 @@ nsDocument::RequestFullScreen(Element* a
   }
 
   // Set the full-screen element. This sets the full-screen style on the
   // element, and the full-screen-ancestor styles on ancestors of the element
   // in this document.
   DebugOnly<bool> x = FullScreenStackPush(aElement);
   NS_ASSERTION(x, "Full-screen state of requesting doc should always change!");
   changed.AppendElement(this);
-  
+
   // Propagate up the document hierarchy, setting the full-screen element as
   // the element's container in ancestor documents. This also sets the
   // appropriate css styles as well. Note we don't propagate down the
   // document hierarchy, the full-screen element (or its container) is not
-  // visible there.  
+  // visible there. Stop when we reach the root document.
   nsIDocument* child = this;
-  nsIDocument* parent;
-  while ((parent = child->GetParentDocument())) {
+  while (true) {
+    nsIDocument* parent = child->GetParentDocument();
+    if (!parent) {
+      break;
+    }
     Element* element = parent->FindContentForSubDocument(child)->AsElement();
     if (static_cast<nsDocument*>(parent)->FullScreenStackPush(element)) {
       changed.AppendElement(parent);
       child = parent;
     } else {
       // We've reached either the root, or a point in the doctree where the
       // new full-screen element container is the same as the previous
       // full-screen element's container. No more changes need to be made
@@ -9007,20 +9113,22 @@ nsDocument::RequestFullScreen(Element* a
   // order so that the events for the root document arrives before the leaf
   // document, as required by the spec.
   for (PRUint32 i = 0; i < changed.Length(); ++i) {
     DispatchFullScreenChange(changed[changed.Length() - i - 1]);
   }
 
   // If this document hasn't already been approved in this session,
   // check to see if the user has granted the fullscreen access
-  // to the document's principal's host, if it has one.
+  // to the document's principal's host, if it has one. Note that documents
+  // in web apps which are the same origin as the web app are considered
+  // trusted and so are automatically approved.
   if (!mIsApprovedForFullscreen) {
     mIsApprovedForFullscreen =
-      GetWindow()->IsPartOfApp() ||
+      GetWindow()->IsInAppOrigin() ||
       nsContentUtils::IsSitePermAllow(NodePrincipal(), "fullscreen");
   }
 
   // If this document, or a document with the same principal has not
   // already been approved for fullscreen this fullscreen-session, dispatch
   // an event so that chrome knows to pop up a warning/approval UI.
   nsCOMPtr<nsIDocument> previousFullscreenDoc(do_QueryReferent(sFullScreenDoc));
   // Note previousFullscreenDoc=nullptr upon first entry, so we always
@@ -9047,16 +9155,31 @@ nsDocument::RequestFullScreen(Element* a
   NS_ASSERTION(IsFullScreenDoc(), "Should be full-screen doc");
   nsCOMPtr<nsIDOMElement> fse;
   GetMozFullScreenElement(getter_AddRefs(fse));
   nsCOMPtr<nsIContent> c(do_QueryInterface(fse));
   NS_ASSERTION(c->AsElement() == aElement,
     "GetMozFullScreenElement should match GetFullScreenElement()");
 #endif
 
+  // The origin which is fullscreen changed, send a notifiction so that the
+  // root document knows the origin of the document which requested fullscreen.
+  // This is used for the fullscreen approval UI. If we're in a child
+  // process, the root BrowserElementChild listens for this notification,
+  // and forwards it across to its BrowserElementParent, which
+  // re-broadcasts the message for the root document in its process.
+  if (aNotifyOnOriginChange &&
+      !nsContentUtils::HaveEqualPrincipals(previousFullscreenDoc, this)) {
+    nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+    nsIDocument* root = nsContentUtils::GetRootDocument(this);
+    nsAutoString origin;
+    nsContentUtils::GetUTFOrigin(NodePrincipal(), origin);
+    os->NotifyObservers(root, "fullscreen-origin-change", origin.get());
+  }
+
   // Make the window full-screen. Note we must make the state changes above
   // before making the window full-screen, as then the document reports as
   // being in full-screen mode when the chrome "fullscreen" event fires,
   // enabling chrome to distinguish between browser and dom full-screen
   // modes. Also note that nsGlobalWindow::SetFullScreen() (which
   // SetWindowFullScreen() calls) proxies to the root window in its hierarchy,
   // and does not operate on the a per-nsIDOMWindow basis.
   SetWindowFullScreen(this, true);
--- a/content/base/src/nsDocument.h
+++ b/content/base/src/nsDocument.h
@@ -932,24 +932,36 @@ public:
     return mHasAudioAvailableListener;
   }
 
   virtual Element* GetFullScreenElement();
   virtual void AsyncRequestFullScreen(Element* aElement);
   virtual void RestorePreviousFullScreenState();
   virtual bool IsFullScreenDoc();
   virtual void SetApprovedForFullscreen(bool aIsApproved);
+  virtual nsresult RemoteFrameFullscreenChanged(nsIDOMElement* aFrameElement,
+                                                const nsAString& aNewOrigin);
+
+  virtual nsresult RemoteFrameFullscreenReverted();
 
   static void ExitFullScreen();
 
   // This is called asynchronously by nsIDocument::AsyncRequestFullScreen()
-  // to move document into full-screen mode if allowed. aWasCallerChrome
+  // to move this document into full-screen mode if allowed. aWasCallerChrome
   // should be true when nsIDocument::AsyncRequestFullScreen() was called
-  // by chrome code.
-  void RequestFullScreen(Element* aElement, bool aWasCallerChrome);
+  // by chrome code. aNotifyOnOriginChange denotes whether we should send a
+  // fullscreen-origin-change notification if requesting fullscreen in this
+  // document causes the origin which is fullscreen to change. We may want to
+  // *not* send this notification if we're calling RequestFullscreen() as part
+  // of a continuation of a request in a subdocument, whereupon the caller will
+  // need to send the notification with the origin of the document which
+  // originally requested fullscreen, not *this* document's origin.
+  void RequestFullScreen(Element* aElement,
+                         bool aWasCallerChrome,
+                         bool aNotifyOnOriginChange);
 
   // Removes all elements from the full-screen stack, removing full-scren
   // styles from the top element in the stack.
   void CleanupFullscreenState();
 
   // Add/remove "fullscreen-approved" observer service notification listener.
   // Chrome sends us a notification when fullscreen is approved for a
   // document, with the notification subject as the document that was approved.
--- a/content/base/src/nsFrameLoader.cpp
+++ b/content/base/src/nsFrameLoader.cpp
@@ -2337,8 +2337,9 @@ nsFrameLoader::SetRemoteBrowser(nsITabPa
   EnsureMessageManager();
   nsCOMPtr<nsIObserverService> os = services::GetObserverService();
   if (OwnerIsBrowserFrame() && os) {
     mRemoteBrowserInitialized = true;
     os->NotifyObservers(NS_ISUPPORTS_CAST(nsIFrameLoader*, this),
                         "remote-browser-frame-shown", NULL);
   }
 }
+
--- a/content/base/src/nsGenericElement.cpp
+++ b/content/base/src/nsGenericElement.cpp
@@ -4757,38 +4757,44 @@ nsGenericElement::MozRequestPointerLock(
   OwnerDoc()->RequestPointerLock(this);
   return NS_OK;
 }
 
 static const char*
 GetFullScreenError(nsIDocument* aDoc)
 {
   nsCOMPtr<nsPIDOMWindow> win = aDoc->GetWindow();
-  if (win && win->IsPartOfApp()) {
+  if (win && win->IsInAppOrigin()) {
+    // Request is in a web app and in the same origin as the web app.
+    // Don't enforce as strict security checks for web apps, the user
+    // is supposed to have trust in them. However documents cross-origin
+    // to the web app must still confirm to the normal security checks.
     return nullptr;
   }
 
   if (!nsContentUtils::IsRequestFullScreenAllowed()) {
     return "FullScreenDeniedNotInputDriven";
   }
-  
+
   if (nsContentUtils::IsSitePermDeny(aDoc->NodePrincipal(), "fullscreen")) {
     return "FullScreenDeniedBlocked";
   }
 
   return nullptr;
 }
 
 nsresult nsGenericElement::MozRequestFullScreen()
 {
   // Only grant full-screen requests if this is called from inside a trusted
   // event handler (i.e. inside an event handler for a user initiated event).
   // This stops the full-screen from being abused similar to the popups of old,
   // and it also makes it harder for bad guys' script to go full-screen and
   // spoof the browser chrome/window and phish logins etc.
+  // Note that requests for fullscreen inside a web app's origin are exempt
+  // from this restriction.
   const char* error = GetFullScreenError(OwnerDoc());
   if (error) {
     nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
                                     "DOM", OwnerDoc(),
                                     nsContentUtils::eDOM_PROPERTIES,
                                     error);
     nsRefPtr<nsAsyncDOMEvent> e =
       new nsAsyncDOMEvent(OwnerDoc(),
@@ -4798,8 +4804,9 @@ nsresult nsGenericElement::MozRequestFul
     e->PostDOMEvent();
     return NS_OK;
   }
 
   OwnerDoc()->AsyncRequestFullScreen(this);
 
   return NS_OK;
 }
+
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2588,8 +2588,42 @@ nsDOMWindowUtils::GetApp(mozIDOMApplicat
     return NS_ERROR_DOM_SECURITY_ERR;
   }
 
   nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
   NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
 
   return static_cast<nsGlobalWindow*>(window.get())->GetApp(aApplication);
 }
+
+nsresult
+nsDOMWindowUtils::RemoteFrameFullscreenChanged(nsIDOMElement* aFrameElement,
+                                            const nsAString& aNewOrigin)
+{
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
+  NS_ENSURE_STATE(window);
+
+  nsCOMPtr<nsIDocument> doc(do_QueryInterface(window->GetExtantDocument()));
+  NS_ENSURE_STATE(doc);
+
+  doc->RemoteFrameFullscreenChanged(aFrameElement, aNewOrigin);
+  return NS_OK;
+}
+
+nsresult
+nsDOMWindowUtils::RemoteFrameFullscreenReverted()
+{
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
+  NS_ENSURE_STATE(window);
+
+  nsCOMPtr<nsIDocument> doc(do_QueryInterface(window->GetExtantDocument()));
+  NS_ENSURE_STATE(doc);
+
+  doc->RemoteFrameFullscreenReverted();
+  return NS_OK;
+}
+
+nsresult
+nsDOMWindowUtils::ExitFullscreen()
+{
+  nsIDocument::ExitFullScreen(/* async = */ false);
+  return NS_OK;
+}
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -10657,16 +10657,44 @@ nsGlobalWindow::SetIsApp(bool aValue)
 
   // You shouldn't call SetIsApp() more than once.
   MOZ_ASSERT(mIsApp == TriState_Unknown);
 
   mIsApp = aValue ? TriState_True : TriState_False;
 }
 
 bool
+nsGlobalWindow::IsInAppOrigin()
+{
+  FORWARD_TO_OUTER(IsInAppOrigin, (), false);
+
+  nsIPrincipal* principal = GetPrincipal();
+  NS_ENSURE_TRUE(principal != nullptr, false);
+
+  // We go trough all window parents until we find one with |mIsApp| set to
+  // something. If none is found, we are not part of an application.
+  for (nsGlobalWindow* w = static_cast<nsGlobalWindow*>(this); w;
+      w = static_cast<nsGlobalWindow*>(w->GetParentInternal())) {
+    if (w->mIsApp == TriState_True) {
+      // The window should be part of an application.
+      MOZ_ASSERT(w->mApp);
+      bool sameOrigin = false;
+      return w->mAppPrincipal &&
+             principal &&
+             NS_SUCCEEDED(principal->Equals(w->mAppPrincipal, &sameOrigin)) &&
+             sameOrigin;
+    } else if (w->mIsApp == TriState_False) {
+      return false;
+    }
+  }
+
+  return false;
+}
+
+bool
 nsGlobalWindow::IsPartOfApp()
 {
   nsCOMPtr<mozIDOMApplication> app;
 
   return NS_SUCCEEDED(GetApp(getter_AddRefs(app))) ? app != nullptr : false;
 }
 
 nsresult
@@ -10686,16 +10714,24 @@ nsGlobalWindow::SetApp(const nsAString& 
 
   nsCOMPtr<mozIDOMApplication> app;
   appsService->GetAppByManifestURL(aManifestURL, getter_AddRefs(app));
   if (!app) {
     NS_WARNING("No application found with the specified manifest URL");
     return NS_ERROR_FAILURE;
   }
 
+  nsCOMPtr<nsIURI> uri;
+  nsresult res = NS_NewURI(getter_AddRefs(uri), aManifestURL);
+  NS_ENSURE_SUCCESS(res, res);
+
+  nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+  res = ssm->GetSimpleCodebasePrincipal(uri, getter_AddRefs(mAppPrincipal));
+  NS_ENSURE_SUCCESS(res, res);
+
   mApp = app.forget();
 
   return NS_OK;
 }
 
 nsresult
 nsGlobalWindow::GetApp(mozIDOMApplication** aApplication)
 {
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -380,16 +380,17 @@ public:
   virtual NS_HIDDEN_(nsresult) ForceClose();
 
   virtual NS_HIDDEN_(void) MaybeUpdateTouchState();
   virtual NS_HIDDEN_(void) UpdateTouchState();
   virtual NS_HIDDEN_(bool) DispatchCustomEvent(const char *aEventName);
   virtual NS_HIDDEN_(void) RefreshCompartmentPrincipal();
   virtual NS_HIDDEN_(nsresult) SetFullScreenInternal(bool aIsFullScreen, bool aRequireTrust);
   virtual NS_HIDDEN_(bool) IsPartOfApp();
+  virtual NS_HIDDEN_(bool) IsInAppOrigin();
 
   // nsIDOMStorageIndexedDB
   NS_DECL_NSIDOMSTORAGEINDEXEDDB
 
   // nsIInterfaceRequestor
   NS_DECL_NSIINTERFACEREQUESTOR
 
   // Object Management
@@ -973,16 +974,19 @@ protected:
   // whether we've sent the destroy notification for our window id
   bool                   mNotifiedIDDestroyed : 1;
 
   // Whether the window is the window of an application frame.
   // This is TriState_Unknown if the object is the content window of an
   // iframe which is neither mozBrowser nor mozApp.
   TriState               mIsApp : 2;
 
+  // Principal of the web app running in this window, if any.
+  nsCOMPtr<nsIPrincipal>        mAppPrincipal;
+
   nsCOMPtr<nsIScriptContext>    mContext;
   nsWeakPtr                     mOpener;
   nsCOMPtr<nsIControllers>      mControllers;
   nsCOMPtr<nsIArray>            mArguments;
   nsCOMPtr<nsIArray>            mArgumentsLast;
   nsCOMPtr<nsIPrincipal>        mArgumentsOrigin;
   nsRefPtr<Navigator>           mNavigator;
   nsRefPtr<nsScreen>            mScreen;
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -597,16 +597,22 @@ public:
 
   /**
    * Returns if the window is part of an application.
    * It will check for the window app state and its parents until a window has
    * an app state different from |TriState_Unknown|.
    */
   virtual bool IsPartOfApp() = 0;
 
+  /**
+   * Returns true of this window is part of an we app, and this window has
+   * the same origin (principal) as the web app.
+   */
+  virtual bool IsInAppOrigin() = 0;
+
 protected:
   // The nsPIDOMWindow constructor. The aOuterWindow argument should
   // be null if and only if the created window itself is an outer
   // window. In all other cases aOuterWindow should be the outer
   // window for the inner window that is being created.
   nsPIDOMWindow(nsPIDOMWindow *aOuterWindow);
 
   ~nsPIDOMWindow();
--- a/dom/browser-element/BrowserElementChild.js
+++ b/dom/browser-element/BrowserElementChild.js
@@ -57,16 +57,20 @@ function BrowserElementChild() {
   // true.
   this._forcedVisible = true;
   this._ownerVisible = true;
 
   this._init();
 };
 
 BrowserElementChild.prototype = {
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
   _init: function() {
     debug("Starting up.");
     sendAsyncMsg("hello");
 
     BrowserElementPromptService.mapWindowToBrowserElementChild(content, this);
 
     docShell.QueryInterface(Ci.nsIWebProgress)
             .addProgressListener(this._progressListener,
@@ -132,16 +136,17 @@ BrowserElementChild.prototype = {
     addMsgListener("get-can-go-forward", this._recvCanGoForward);
     addMsgListener("go-back", this._recvGoBack);
     addMsgListener("go-forward", this._recvGoForward);
     addMsgListener("reload", this._recvReload);
     addMsgListener("stop", this._recvStop);
     addMsgListener("unblock-modal-prompt", this._recvStopWaiting);
     addMsgListener("fire-ctx-callback", this._recvFireCtxCallback);
     addMsgListener("owner-visibility-change", this._recvOwnerVisibilityChange);
+    addMsgListener("exit-fullscreen", this._recvExitFullscreen.bind(this));
 
     let els = Cc["@mozilla.org/eventlistenerservice;1"]
                 .getService(Ci.nsIEventListenerService);
 
     // We are using the system group for those events so if something in the
     // content called .stopPropagation() this will still be called.
     els.addSystemEventListener(global, 'keydown',
                                this._keyEventHandler.bind(this),
@@ -156,16 +161,45 @@ BrowserElementChild.prototype = {
                                this._closeHandler.bind(this),
                                /* useCapture = */ false);
     els.addSystemEventListener(global, 'contextmenu',
                                this._contextmenuHandler.bind(this),
                                /* useCapture = */ false);
     els.addSystemEventListener(global, 'scroll',
                                this._scrollEventHandler.bind(this),
                                /* useCapture = */ false);
+
+    Services.obs.addObserver(this,
+                             "fullscreen-origin-change",
+                             /* ownsWeak = */ true);
+
+    Services.obs.addObserver(this,
+                             'ask-parent-to-exit-fullscreen',
+                             /* ownsWeak = */ true);
+
+    Services.obs.addObserver(this,
+                             'ask-parent-to-rollback-fullscreen',
+                             /* ownsWeak = */ true);
+  },
+
+  observe: function(subject, topic, data) {
+    // Ignore notifications not about our document.
+    if (subject != content.document)
+      return;
+    switch (topic) {
+      case 'fullscreen-origin-change':
+        sendAsyncMsg('fullscreen-origin-change', data);
+        break;
+      case 'ask-parent-to-exit-fullscreen':
+        sendAsyncMsg('exit-fullscreen');
+        break;
+      case 'ask-parent-to-rollback-fullscreen':
+        sendAsyncMsg('rollback-fullscreen');
+        break;
+    }
   },
 
   _tryGetInnerWindowID: function(win) {
     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils);
     try {
       return utils.currentInnerWindowID;
     }
@@ -288,16 +322,23 @@ BrowserElementChild.prototype = {
       return;
     }
 
     debug("recvStopWaiting " + win);
     win.modalReturnValue = returnValue;
     win.modalDepth--;
   },
 
+  _recvExitFullscreen: function() {
+    var utils = content.document.defaultView
+                       .QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils);
+    utils.exitFullscreen();
+  },
+
   _titleChangedHandler: function(e) {
     debug("Got titlechanged: (" + e.target.title + ")");
     var win = e.target.defaultView;
 
     // Ignore titlechanges which don't come from the top-level
     // <iframe mozbrowser> window.
     if (win == content) {
       sendAsyncMsg('titlechange', e.target.title);
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -78,27 +78,27 @@ BrowserElementParentFactory.prototype = 
     }
     catch(e) {
       return false;
     }
   },
 
   _observeInProcessBrowserFrameShown: function(frameLoader) {
     debug("In-process browser frame shown " + frameLoader);
-    this._createBrowserElementParent(frameLoader);
+    this._createBrowserElementParent(frameLoader, /* hasRemoteFrame = */ false);
   },
 
   _observeRemoteBrowserFrameShown: function(frameLoader) {
     debug("Remote browser frame shown " + frameLoader);
-    this._createBrowserElementParent(frameLoader);
+    this._createBrowserElementParent(frameLoader, /* hasRemoteFrame = */ true);
   },
 
-  _createBrowserElementParent: function(frameLoader) {
+  _createBrowserElementParent: function(frameLoader, hasRemoteFrame) {
     let frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
-    this._bepMap.set(frameElement, new BrowserElementParent(frameLoader));
+    this._bepMap.set(frameElement, new BrowserElementParent(frameLoader, hasRemoteFrame));
   },
 
   observe: function(subject, topic, data) {
     switch(topic) {
     case 'app-startup':
       this._init();
       break;
     case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
@@ -114,20 +114,21 @@ BrowserElementParentFactory.prototype = 
       break;
     case 'content-document-global-created':
       this._observeContentGlobalCreated(subject);
       break;
     }
   },
 };
 
-function BrowserElementParent(frameLoader) {
+function BrowserElementParent(frameLoader, hasRemoteFrame) {
   debug("Creating new BrowserElementParent object for " + frameLoader);
   this._domRequestCounter = 0;
   this._pendingDOMRequests = {};
+  this._hasRemoteFrame = hasRemoteFrame;
 
   this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
   if (!this._frameElement) {
     debug("No frame element?");
     return;
   }
 
   this._mm = frameLoader.messageManager;
@@ -157,16 +158,22 @@ function BrowserElementParent(frameLoade
   addMessageListener("error", this._fireEventFromMsg);
   addMessageListener("scroll", this._fireEventFromMsg);
   addMessageListener("get-mozapp-manifest-url", this._sendMozAppManifestURL);
   addMessageListener("keyevent", this._fireKeyEvent);
   addMessageListener("showmodalprompt", this._handleShowModalPrompt);
   addMessageListener('got-screenshot', this._gotDOMRequestResult);
   addMessageListener('got-can-go-back', this._gotDOMRequestResult);
   addMessageListener('got-can-go-forward', this._gotDOMRequestResult);
+  addMessageListener('fullscreen-origin-change', this._remoteFullscreenOriginChange);
+  addMessageListener('rollback-fullscreen', this._remoteFrameFullscreenReverted);
+  addMessageListener('exit-fullscreen', this._exitFullscreen);
+
+  let os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+  os.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true);
 
   function defineMethod(name, fn) {
     XPCNativeWrapper.unwrap(self._frameElement)[name] = function() {
       if (self._isAlive()) {
         return fn.apply(self, arguments);
       }
     };
   }
@@ -197,30 +204,39 @@ function BrowserElementParent(frameLoade
   // down to the child.
   this._window.addEventListener('mozvisibilitychange',
                                 this._ownerVisibilityChange.bind(this),
                                 /* useCapture = */ false,
                                 /* wantsUntrusted = */ false);
 }
 
 BrowserElementParent.prototype = {
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
   /**
    * You shouldn't touch this._frameElement or this._window if _isAlive is
    * false.  (You'll likely get an exception if you do.)
    */
   _isAlive: function() {
     return !Cu.isDeadWrapper(this._frameElement) &&
            !Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
            !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView);
   },
 
   get _window() {
     return this._frameElement.ownerDocument.defaultView;
   },
 
+  get _windowUtils() {
+    return this._window.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils);
+  },
+
   _sendAsyncMsg: function(msg, data) {
     this._frameElement.QueryInterface(Ci.nsIFrameLoaderOwner)
                       .frameLoader
                       .messageManager
                       .sendAsyncMessage('browser-element-api:' + msg, data);
   },
 
   _recvHello: function(data) {
@@ -430,11 +446,32 @@ BrowserElementParent.prototype = {
 
   /**
    * Called when the visibility of the window which owns this iframe changes.
    */
   _ownerVisibilityChange: function() {
     this._sendAsyncMsg('owner-visibility-change',
                        {visible: !this._window.document.mozHidden});
   },
+
+  _exitFullscreen: function() {
+    this._windowUtils.exitFullscreen();
+  },
+
+  _remoteFullscreenOriginChange: function(data) {
+    let origin = data.json;
+    this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin);
+  },
+
+  _remoteFrameFullscreenReverted: function(data) {
+    this._windowUtils.remoteFrameFullscreenReverted();
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic == 'ask-children-to-exit-fullscreen' &&
+        this._isAlive() &&
+        this._frameElement.ownerDocument == subject &&
+        this._hasRemoteFrame)
+      this._sendAsyncMsg('exit-fullscreen');
+  },
 };
 
 var NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserElementParentFactory]);
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -35,17 +35,17 @@ interface nsIQueryContentEventResult;
 interface nsIDOMWindow;
 interface nsIDOMBlob;
 interface nsIDOMFile;
 interface nsIFile;
 interface nsIDOMTouch;
 interface nsIDOMClientRect;
 interface mozIDOMApplication;
 
-[scriptable, uuid(858578f1-9653-4d5c-821a-07479bf2d9b2)]
+[scriptable, uuid(b276a71e-21ee-4be6-894d-4039523e1ad8)]
 interface nsIDOMWindowUtils : nsISupports {
 
   /**
    * Image animation mode of the window. When this attribute's value
    * is changed, the implementation should set all images in the window
    * to the given value. That is, when set to kDontAnimMode, all images
    * will stop animating. The attribute's value must be one of the
    * animationMode values from imgIContainer.
@@ -756,16 +756,39 @@ interface nsIDOMWindowUtils : nsISupport
    *               for the other parameters and the result.
    */
   nsIQueryContentEventResult sendQueryContentEvent(in unsigned long aType,
                                                    in unsigned long aOffset,
                                                    in unsigned long aLength,
                                                    in long aX,
                                                    in long aY);
 
+
+  /**
+   * Called when the remote child frame has changed its fullscreen state,
+   * when entering fullscreen, and when the origin which is fullscreen changes.
+   * aFrameElement is the iframe element which contains the child-process
+   * fullscreen document, and aNewOrigin is the origin of the new fullscreen
+   * document.
+   */
+  void remoteFrameFullscreenChanged(in nsIDOMElement aFrameElement,
+                                    in AString aNewOrigin);
+
+  /**
+   * Called when the remote frame has popped all fullscreen elements off its
+   * stack, so that the operation can complete on the parent side.
+   */
+  void remoteFrameFullscreenReverted();
+
+  /**
+   * Called when the child frame has fully exit fullscreen, so that the parent
+   * process can also fully exit.
+   */
+  void exitFullscreen();
+
   // NOTE: following values are same as NS_QUERY_* in nsGUIEvent.h
 
   /**
    * QUERY_SELECTED_TEXT queries the first selection range's information.
    *
    * @param aOffset   Not used.
    * @param aLength   Not used.
    * @param aX        Not used.