Bug 1381628 - WIP patch for web vr permission prompts. draft
authorJonathan Kingston <jkt@mozilla.com>
Mon, 24 Jul 2017 14:42:39 +0100
changeset 616839 b8f14b9390dd83b693f52f501e52422c80d98186
parent 610973 1b065ffd8a535a0ad4c39a912af18e948e6a42c1
child 639608 588b9109c09b33425ff08ba838fc4820d848b345
push id70826
push userjkingston@mozilla.com
push dateThu, 27 Jul 2017 14:55:20 +0000
bugs1381628
milestone56.0a1
Bug 1381628 - WIP patch for web vr permission prompts. MozReview-Commit-ID: Dz7h6SKfOay
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/locales/en-US/chrome/browser/sitePermissions.properties
browser/modules/PermissionUI.jsm
browser/modules/SitePermissions.jsm
browser/modules/test/browser/browser_PermissionUI_prompts.js
browser/modules/test/unit/test_SitePermissions.js
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html
dom/base/Navigator.cpp
dom/permission/PermissionUtils.cpp
dom/vr/VRPermissionRequest.cpp
dom/vr/VRPermissionRequest.h
dom/vr/moz.build
dom/webidl/Permissions.webidl
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2485,16 +2485,21 @@ const ContentPermissionIntegration = {
       case "desktop-notification": {
         return new PermissionUI.DesktopNotificationPermissionPrompt(request);
       }
       case "persistent-storage": {
         if (Services.prefs.getBoolPref("browser.storageManager.enabled")) {
           return new PermissionUI.PersistentStoragePermissionPrompt(request);
         }
       }
+      case "web-vr": {
+        if (Services.prefs.getBoolPref("dom.vr.enabled")) {
+          return new PermissionUI.WebVRPermissionPrompt(request);
+        }
+      }
     }
     return undefined;
   },
 };
 
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -210,16 +210,17 @@ These should match what Safari and other
 <!ENTITY urlbar.geolocationNotificationAnchor.tooltip     "Open location request panel">
 <!ENTITY urlbar.addonsNotificationAnchor.tooltip          "Open add-on installation message panel">
 <!ENTITY urlbar.indexedDBNotificationAnchor.tooltip       "Open offline storage message panel">
 <!ENTITY urlbar.passwordNotificationAnchor.tooltip        "Open save password message panel">
 <!ENTITY urlbar.pluginsNotificationAnchor.tooltip         "Manage plug-in use">
 <!ENTITY urlbar.webNotificationAnchor.tooltip             "Change whether you can receive notifications from the site">
 <!ENTITY urlbar.persistentStorageNotificationAnchor.tooltip     "Store data in Persistent Storage">
 <!ENTITY urlbar.remoteControlNotificationAnchor.tooltip   "Browser is under remote control">
+<!ENTITY urlbar.webVRNotificationAnchor.tooltip           "Change whether the site can access VR hardware">
 
 <!ENTITY urlbar.webRTCShareDevicesNotificationAnchor.tooltip      "Manage sharing your camera and/or microphone with the site">
 <!ENTITY urlbar.webRTCShareMicrophoneNotificationAnchor.tooltip   "Manage sharing your microphone with the site">
 <!ENTITY urlbar.webRTCShareScreenNotificationAnchor.tooltip       "Manage sharing your windows or screen with the site">
 
 <!ENTITY urlbar.servicesNotificationAnchor.tooltip        "Open install message panel">
 <!ENTITY urlbar.translateNotificationAnchor.tooltip       "Translate this page">
 <!ENTITY urlbar.translatedNotificationAnchor.tooltip      "Manage page translation">
@@ -227,16 +228,17 @@ These should match what Safari and other
 
 <!ENTITY urlbar.cameraBlocked.tooltip            "You have blocked your camera for this website.">
 <!ENTITY urlbar.microphoneBlocked.tooltip        "You have blocked your microphone for this website.">
 <!ENTITY urlbar.screenBlocked.tooltip            "You have blocked this website from sharing your screen.">
 <!ENTITY urlbar.geolocationBlocked.tooltip       "You have blocked location information for this website.">
 <!ENTITY urlbar.indexedDBBlocked.tooltip         "You have blocked data storage for this website.">
 <!ENTITY urlbar.webNotificationsBlocked.tooltip  "You have blocked notifications for this website.">
 <!ENTITY urlbar.persistentStorageBlocked.tooltip "You have blocked persistent storage for this website.">
+<!ENTITY urlbar.webVRBlocked.tooltip             "You have blocked VR hardware for this website.">
 
 <!ENTITY urlbar.openHistoryPopup.tooltip                "Show history">
 
 <!ENTITY searchItem.title             "Search">
 
 <!-- Toolbar items -->
 <!ENTITY homeButton.label             "Home">
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -525,16 +525,24 @@ geolocation.remember=Remember this decis
 # Persistent storage UI
 persistentStorage.allow=Allow
 persistentStorage.allow.accesskey=A
 persistentStorage.dontAllow=Don’t Allow
 persistentStorage.dontAllow.accesskey=n
 persistentStorage.allowWithSite=Will you allow %S to store data in persistent storage?
 persistentStorage.remember=Remember this decision
 
+# WebVR UI
+webVR.allow=Allow
+webVR.allow.accesskey=A
+webVR.dontAllow=Don’t Allow
+webVR.dontAllow.accesskey=n
+webVR.allowWithSite=Will you allow %S to access your VR hardware, this includes information that may be able to uniquely identify you.
+webVR.remember=Remember this decision
+
 webNotifications.allow=Allow Notifications
 webNotifications.allow.accesskey=A
 webNotifications.notNow=Not Now
 webNotifications.notNow.accesskey=n
 webNotifications.never=Never Allow
 webNotifications.never.accesskey=v
 webNotifications.receiveFromSite2=Will you allow %S to send notifications?
 
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -32,8 +32,9 @@ permission.camera.label = Use the Camera
 permission.microphone.label = Use the Microphone
 permission.screen.label = Share the Screen
 permission.install.label = Install Add-ons
 permission.popup.label = Open Pop-up Windows
 permission.geo.label = Access Your Location
 permission.indexedDB.label = Maintain Offline Storage
 permission.focus-tab-by-prompt.label = Switch to this Tab
 permission.persistent-storage.label = Store Data in Persistent Storage
+permission.web-vr.label = Access to VR hardware
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -665,8 +665,80 @@ PersistentStoragePermissionPrompt.protot
           gBrowserBundle.GetStringFromName("persistentStorage.dontAllow.accesskey"),
         action: Ci.nsIPermissionManager.DENY_ACTION
       }
     ];
   }
 };
 
 PermissionUI.PersistentStoragePermissionPrompt = PersistentStoragePermissionPrompt;
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the web VR API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ *        The request for a permission from content.
+ */
+function WebVRPermissionPrompt(request) {
+  this.request = request;
+}
+
+WebVRPermissionPrompt.prototype = {
+  __proto__: PermissionPromptForRequestPrototype,
+
+  get permissionKey() {
+    return "web-vr";
+  },
+
+  get popupOptions() {
+    let checkbox = {
+      // In PB mode, we don't want the "always remember" checkbox
+      show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal)
+    };
+    if (checkbox.show) {
+      checkbox.checked = true;
+      checkbox.label = gBrowserBundle.GetStringFromName("webVR.remember");
+    }
+    let learnMoreURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "web-vr";
+    return {
+      checkbox,
+      learnMoreURL
+    };
+  },
+
+  get notificationID() {
+    return "web-vr";
+  },
+
+  get anchorID() {
+    return "web-vr-notification-icon";
+  },
+
+  get message() {
+    let hostPort = "<>";
+    try {
+      hostPort = this.principal.URI.hostPort;
+    } catch (ex) {}
+    return gBrowserBundle.formatStringFromName(
+      "webVR.allowWithSite", [hostPort], 1);
+  },
+
+  get promptActions() {
+    return [
+      {
+        label: gBrowserBundle.GetStringFromName("webVR.allow"),
+        accessKey:
+          gBrowserBundle.GetStringFromName("webVR.allow.accesskey"),
+        action: Ci.nsIPermissionManager.ALLOW_ACTION
+      },
+      {
+        label: gBrowserBundle.GetStringFromName("webVR.dontAllow"),
+        accessKey:
+          gBrowserBundle.GetStringFromName("webVR.dontAllow.accesskey"),
+        action: Ci.nsIPermissionManager.DENY_ACTION
+      }
+    ];
+  }
+};
+
+PermissionUI.WebVRPermissionPrompt = WebVRPermissionPrompt;
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -611,19 +611,26 @@ var gPermissionObject = {
   "indexedDB": {},
 
   "focus-tab-by-prompt": {
     exactHostMatch: true,
     states: [ SitePermissions.UNKNOWN, SitePermissions.ALLOW ],
   },
   "persistent-storage": {
     exactHostMatch: true
+  },
+  "web-vr": {
+    exactHostMatch: true
   }
 };
 
-// Delete this entry while being pre-off
+// Delete this entries while being pre-off
 // or the persistent-storage permission would appear in Page info's Permission section
 if (!Services.prefs.getBoolPref("browser.storageManager.enabled")) {
   delete gPermissionObject["persistent-storage"];
 }
+// or the web-vr permission would appear in Page info's Permission section
+if (!Services.prefs.getBoolPref("dom.vr.enabled")) {
+  delete gPermissionObject["web-vr"];
+}
 
 XPCOMUtils.defineLazyPreferenceGetter(SitePermissions, "temporaryPermissionExpireTime",
                                       "privacy.temporary_permission_expire_time_ms", 3600 * 1000);
--- a/browser/modules/test/browser/browser_PermissionUI_prompts.js
+++ b/browser/modules/test/browser/browser_PermissionUI_prompts.js
@@ -20,16 +20,21 @@ add_task(async function test_desktop_not
   await testPrompt(PermissionUI.DesktopNotificationPermissionPrompt);
 });
 
 // Tests that PersistentStoragePermissionPrompt works as expected
 add_task(async function test_persistent_storage_permission_prompt() {
   await testPrompt(PermissionUI.PersistentStoragePermissionPrompt);
 });
 
+// Tests that WebVRPermissionPrompt works as expected
+add_task(async function test_web_vr_permission_prompt() {
+  await testPrompt(PermissionUI.WebVRPermissionPrompt);
+});
+
 async function testPrompt(Prompt) {
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: "http://example.com",
   }, async function(browser) {
     let mockRequest = makeMockPermissionRequest(browser);
     let principal = mockRequest.principal;
     let TestPrompt = new Prompt(mockRequest);
--- a/browser/modules/test/unit/test_SitePermissions.js
+++ b/browser/modules/test/unit/test_SitePermissions.js
@@ -2,26 +2,30 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 "use strict";
 
 Components.utils.import("resource:///modules/SitePermissions.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const STORAGE_MANAGER_ENABLED = Services.prefs.getBoolPref("browser.storageManager.enabled");
+const WEB_VR_ENABLED = Services.prefs.getBoolPref("dom.vr.enabled");
 
 add_task(async function testPermissionsListing() {
   let expectedPermissions = ["camera", "cookie", "desktop-notification", "focus-tab-by-prompt",
      "geo", "image", "indexedDB", "install", "microphone", "popup", "screen"];
   if (STORAGE_MANAGER_ENABLED) {
     // The persistent-storage permission is still only pref-on on Nightly
     // so we add it only when it's pref-on.
     // Should remove this checking and add it as default after it is fully pref-on.
     expectedPermissions.push("persistent-storage");
   }
+  if (WEB_VR_ENABLED) {
+    expectedPermissions.push("web-vr");
+  }
   Assert.deepEqual(SitePermissions.listPermissions().sort(), expectedPermissions.sort(),
     "Correct list of all permissions");
 });
 
 add_task(async function testGetAllByURI() {
   // check that it returns an empty array on an invalid URI
   // like a file URI, which doesn't support site permissions
   let wrongURI = Services.io.newURI("file:///example.js")
@@ -83,16 +87,19 @@ add_task(async function testExactHostMat
   let exactHostMatched = ["desktop-notification", "focus-tab-by-prompt", "camera",
                           "microphone", "screen", "geo"];
   if (STORAGE_MANAGER_ENABLED) {
     // The persistent-storage permission is still only pref-on on Nightly
     // so we add it only when it's pref-on.
     // Should remove this checking and add it as default after it is fully pref-on.
     exactHostMatched.push("persistent-storage");
   }
+  if (WEB_VR_ENABLED) {
+    exactHostMatched.push("web-vr");
+  }
   let nonExactHostMatched = ["image", "cookie", "popup", "install", "indexedDB"];
 
   let permissions = SitePermissions.listPermissions();
   for (let permission of permissions) {
     SitePermissions.set(uri, permission, SitePermissions.ALLOW);
 
     if (exactHostMatched.includes(permission)) {
       // Check that the sub-origin does not inherit the permission from its parent.
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
@@ -14,16 +14,17 @@ Cu.import("resource://testing-common/Con
 Cu.import("resource://testing-common/BrowserTestUtils.jsm");
 
 const URL = "https://test1.example.com/browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/chrome/mozscreenshots/lib/permissionPrompts.html";
 let lastTab = null;
 
 this.PermissionPrompts = {
   init(libDir) {
     Services.prefs.setBoolPref("browser.storageManager.enabled", true);
+    Services.prefs.setBoolPref("dom.vr.enabled", true);
     Services.prefs.setBoolPref("media.navigator.permission.fake", true);
     Services.prefs.setCharPref("media.getusermedia.screensharing.allowed_domains",
                                "test1.example.com");
     Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
     Services.prefs.setBoolPref("signon.rememberSignons", true);
   },
 
   configurations: {
@@ -64,16 +65,23 @@ this.PermissionPrompts = {
 
     persistentStorage: {
       async applyConfig() {
         await closeLastTab();
         await clickOn("#persistent-storage");
       },
     },
 
+    webVR: {
+      async applyConfig() {
+        await closeLastTab();
+        await clickOn("#web-vr");
+      },
+    },
+
     loginCapture: {
       async applyConfig() {
         await closeLastTab();
         await clickOn("#login-capture");
       },
     },
 
     notifications: {
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html
@@ -2,16 +2,17 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Permission Prompts</title>
 </head>
 <body>
   <button id="geo" onclick="navigator.geolocation.getCurrentPosition(() => {})">Geolocation</button>
   <button id="persistent-storage" onclick="navigator.storage.persist()">Persistent Storage</button>
+  <button id="web-vr" onclick="navigator.getVRDisplays()">Web VR</button>
   <button id="webRTC-shareDevices" onclick="shareDevice({video: true, fake: true});">Video</button>
   <button id="webRTC-shareMicrophone" onclick="shareDevice({audio: true, fake: true});">Audio</button>
   <button id="webRTC-shareDevices2" onclick="shareDevice({audio: true, video: true, fake: true});">Audio and Video</button>
   <button id="webRTC-shareScreen" onclick="shareDevice({video: {mediaSource: 'screen'}});">Screen</button>
   <button id="web-notifications" onclick="Notification.requestPermission()">web-notifications</button>
   <a id="addons" href="borderify.xpi">Install Add-On</a>
   <form>
     <input type="email" id="email" value="email@example.com" />
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -43,16 +43,17 @@
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/Presentation.h"
 #include "mozilla/dom/ServiceWorkerContainer.h"
 #include "mozilla/dom/StorageManager.h"
 #include "mozilla/dom/TCPSocket.h"
 #include "mozilla/dom/URLSearchParams.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/dom/VRDisplayEvent.h"
+#include "mozilla/dom/VRPermissionRequest.h"
 #include "mozilla/dom/VRServiceTest.h"
 #include "mozilla/dom/workers/RuntimeService.h"
 #include "mozilla/Hal.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/SSE.h"
 #include "mozilla/StaticPtr.h"
 #include "Connection.h"
 #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
@@ -1500,26 +1501,38 @@ Navigator::GetVRDisplays(ErrorResult& aR
     return nullptr;
   }
 
   nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
   win->NotifyVREventListenerAdded();
 
   nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(mWindow);
   RefPtr<Promise> p = Promise::Create(go, aRv);
+
+  nsCOMPtr<nsPIDOMWindowInner> window = GetWindow();
+  if (NS_WARN_IF(!window)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return p.forget();
+  }
+
+  nsCOMPtr<nsIDocument> doc = window->GetExtantDoc();
+  if (NS_WARN_IF(!doc)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return p.forget();
+  }
+
+  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+  nsCOMPtr<nsIRunnable> ev  =
+    new VRPermissionRequest(principal, window, p);
+
   if (aRv.Failed()) {
     return nullptr;
   }
 
-  // We pass mWindow's id to RefreshVRDisplays, so NotifyVRDisplaysUpdated will
-  // be called asynchronously, resolving the promises in mVRGetDisplaysPromises.
-  if (!VRDisplay::RefreshVRDisplays(win->WindowID())) {
-    p->MaybeReject(NS_ERROR_FAILURE);
-    return p.forget();
-  }
+  NS_DispatchToMainThread(ev);
 
   mVRGetDisplaysPromises.AppendElement(p);
   return p.forget();
 }
 
 void
 Navigator::GetActiveVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays) const
 {
--- a/dom/permission/PermissionUtils.cpp
+++ b/dom/permission/PermissionUtils.cpp
@@ -9,16 +9,17 @@
 namespace mozilla {
 namespace dom {
 
 const char* kPermissionTypes[] = {
   "geo",
   "desktop-notification",
   // Alias `push` to `desktop-notification`.
   "desktop-notification",
+  "web-vr",
   "persistent-storage"
 };
 
 // `-1` for the last null entry.
 const size_t kPermissionNameCount =
   MOZ_ARRAY_LENGTH(PermissionNameValues::strings) - 1;
 
 static_assert(MOZ_ARRAY_LENGTH(kPermissionTypes) == kPermissionNameCount,
new file mode 100644
--- /dev/null
+++ b/dom/vr/VRPermissionRequest.cpp
@@ -0,0 +1,131 @@
+#include "VRPermissionRequest.h"
+#include "nsContentPermissionHelper.h"
+
+namespace mozilla {
+namespace dom {
+
+VRPermissionRequest::VRPermissionRequest(nsIPrincipal* aPrincipal,
+                                     nsPIDOMWindowInner* aWindow,
+                                     Promise* aPromise)
+  : mPrincipal(aPrincipal)
+  , mWindow(aWindow)
+  , mPromise(aPromise)
+{
+  MOZ_ASSERT(aPrincipal);
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(aPromise);
+}
+
+VRPermissionRequest::~VRPermissionRequest()
+{
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::Run()
+{
+  MOZ_ASSERT(mPrincipal);
+
+  mRequester = new nsContentPermissionRequester(mWindow);
+
+  return nsContentPermissionUtils::AskPermission(this, mWindow);
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::GetTypes(nsIArray** aTypes)
+{
+  MOZ_ASSERT(aTypes);
+
+  nsTArray<nsString> emptyOptions;
+
+  return nsContentPermissionUtils::CreatePermissionArray(
+                                       NS_LITERAL_CSTRING("web-vr"),
+                                       NS_LITERAL_CSTRING("unused"),
+                                       emptyOptions,
+                                       aTypes);
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::GetPrincipal(nsIPrincipal** aPrincipal)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aPrincipal);
+  MOZ_ASSERT(mPrincipal);
+
+  NS_ADDREF(*aPrincipal = mPrincipal);
+
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
+VRPermissionRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aRequestingWindow);
+  MOZ_ASSERT(mWindow);
+
+  NS_ADDREF(*aRequestingWindow = mWindow);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::GetElement(nsIDOMElement** aElement)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aElement);
+
+  *aElement = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::GetRequester(
+                                     nsIContentPermissionRequester** aRequester)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aRequester);
+
+  nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+  requester.forget(aRequester);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::Cancel()
+{
+  mPromise->MaybeReject(NS_ERROR_FAILURE);
+  mPromise.forget();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+VRPermissionRequest::Allow(JS::HandleValue aChoices)
+{
+  MOZ_ASSERT(aChoices.isUndefined());
+
+  // We pass mWindow's id to RefreshVRDisplays, so NotifyVRDisplaysUpdated will
+  // be called asynchronously, resolving the promises in mVRGetDisplaysPromises.
+  if (!VRDisplay::RefreshVRDisplays(mWindow->WindowID())) {
+    mPromise->MaybeReject(NS_ERROR_FAILURE);
+    mPromise.forget();
+    return NS_OK;
+  }
+
+  return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(VRPermissionRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(VRPermissionRequest)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VRPermissionRequest)
+  NS_INTERFACE_MAP_ENTRY(nsIContentPermissionRequest)
+  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContentPermissionRequest)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(VRPermissionRequest, mWindow, mPromise)
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/vr/VRPermissionRequest.h
@@ -0,0 +1,32 @@
+#include "nsContentPermissionHelper.h"
+#include "mozilla/dom/Promise.h"
+#include "jsapi.h"
+
+namespace mozilla {
+namespace dom {
+
+class VRPermissionRequest final
+  : public nsIContentPermissionRequest
+  , public nsIRunnable
+{
+  nsCOMPtr<nsIPrincipal> mPrincipal;
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  RefPtr<Promise> mPromise;
+  nsCOMPtr<nsIContentPermissionRequester> mRequester;
+
+public:
+  explicit VRPermissionRequest(nsIPrincipal* aPrincipal,
+                                     nsPIDOMWindowInner* aWindow,
+                                     Promise* aPromise);
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS;
+  NS_DECL_NSICONTENTPERMISSIONREQUEST;
+  NS_DECL_NSIRUNNABLE;
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(VRPermissionRequest, nsIContentPermissionRequest);
+
+private:
+  virtual ~VRPermissionRequest();
+};
+
+
+} // namespace dom
+} // namespace mozilla
--- a/dom/vr/moz.build
+++ b/dom/vr/moz.build
@@ -6,27 +6,29 @@
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "WebVR")
 
 EXPORTS.mozilla.dom += [
     'VRDisplay.h',
     'VRDisplayEvent.h',
     'VREventObserver.h',
+    'VRPermissionRequest.h',
     'VRServiceTest.h'
     ]
 
 UNIFIED_SOURCES = [
     'VRDisplay.cpp',
     'VRDisplayEvent.cpp',
     'VREventObserver.cpp',
+    'VRPermissionRequest.cpp',
     'VRServiceTest.cpp'
     ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 LOCAL_INCLUDES += [
     '/dom/base'
 ]
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
-REFTEST_MANIFESTS += ['test/reftest/reftest.list']
\ No newline at end of file
+REFTEST_MANIFESTS += ['test/reftest/reftest.list']
--- a/dom/webidl/Permissions.webidl
+++ b/dom/webidl/Permissions.webidl
@@ -6,16 +6,17 @@
  * The origin of this IDL file is
  * https://w3c.github.io/permissions/#permissions-interface
  */
 
 enum PermissionName {
   "geolocation",
   "notifications",
   "push",
+  "web-vr",
   "persistent-storage"
   // Unsupported: "midi"
 };
 
 dictionary PermissionDescriptor {
   required PermissionName name;
 };