summaryrefslogtreecommitdiffstats
path: root/dom/permission
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/permission
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/permission/MidiPermissionStatus.cpp22
-rw-r--r--dom/permission/MidiPermissionStatus.h28
-rw-r--r--dom/permission/PermissionObserver.cpp131
-rw-r--r--dom/permission/PermissionObserver.h43
-rw-r--r--dom/permission/PermissionStatus.cpp174
-rw-r--r--dom/permission/PermissionStatus.h84
-rw-r--r--dom/permission/PermissionUtils.cpp73
-rw-r--r--dom/permission/PermissionUtils.h32
-rw-r--r--dom/permission/Permissions.cpp176
-rw-r--r--dom/permission/Permissions.h55
-rw-r--r--dom/permission/StorageAccessPermissionStatus.cpp72
-rw-r--r--dom/permission/StorageAccessPermissionStatus.h28
-rw-r--r--dom/permission/moz.build31
-rw-r--r--dom/permission/tests/file_empty.html2
-rw-r--r--dom/permission/tests/file_storage_access_notification_helper.html30
-rw-r--r--dom/permission/tests/mochitest.toml18
-rw-r--r--dom/permission/tests/test_cross_origin_iframe.html308
-rw-r--r--dom/permission/tests/test_permissions_api.html254
-rw-r--r--dom/permission/tests/test_storage_access_notification.html50
19 files changed, 1611 insertions, 0 deletions
diff --git a/dom/permission/MidiPermissionStatus.cpp b/dom/permission/MidiPermissionStatus.cpp
new file mode 100644
index 0000000000..dd92c41364
--- /dev/null
+++ b/dom/permission/MidiPermissionStatus.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/dom/MidiPermissionStatus.h"
+
+#include "mozilla/dom/PermissionStatus.h"
+#include "mozilla/Permission.h"
+
+namespace mozilla::dom {
+
+MidiPermissionStatus::MidiPermissionStatus(nsPIDOMWindowInner* aWindow,
+ bool aSysex)
+ : PermissionStatus(aWindow, PermissionName::Midi), mSysex(aSysex) {}
+
+nsLiteralCString MidiPermissionStatus::GetPermissionType() const {
+ return mSysex ? "midi-sysex"_ns : "midi"_ns;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/MidiPermissionStatus.h b/dom/permission/MidiPermissionStatus.h
new file mode 100644
index 0000000000..1171754658
--- /dev/null
+++ b/dom/permission/MidiPermissionStatus.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_MidiPermissionStatus_h_
+#define mozilla_dom_MidiPermissionStatus_h_
+
+#include "mozilla/dom/PermissionStatus.h"
+
+namespace mozilla::dom {
+
+class MidiPermissionStatus final : public PermissionStatus {
+ public:
+ MidiPermissionStatus(nsPIDOMWindowInner* aWindow, bool aSysex);
+
+ private:
+ ~MidiPermissionStatus() {}
+
+ nsLiteralCString GetPermissionType() const override;
+
+ bool mSysex;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MidiPermissionStatus_h_
diff --git a/dom/permission/PermissionObserver.cpp b/dom/permission/PermissionObserver.cpp
new file mode 100644
index 0000000000..26ffd02abc
--- /dev/null
+++ b/dom/permission/PermissionObserver.cpp
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "PermissionObserver.h"
+
+#include "mozilla/dom/PermissionStatus.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/Services.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIObserverService.h"
+#include "nsIPermission.h"
+#include "PermissionUtils.h"
+
+namespace mozilla::dom {
+
+namespace {
+PermissionObserver* gInstance = nullptr;
+} // namespace
+
+NS_IMPL_ISUPPORTS(PermissionObserver, nsIObserver, nsISupportsWeakReference)
+
+PermissionObserver::PermissionObserver() { MOZ_ASSERT(!gInstance); }
+
+PermissionObserver::~PermissionObserver() {
+ MOZ_ASSERT(mSinks.IsEmpty());
+ MOZ_ASSERT(gInstance == this);
+
+ gInstance = nullptr;
+}
+
+/* static */
+already_AddRefed<PermissionObserver> PermissionObserver::GetInstance() {
+ RefPtr<PermissionObserver> instance = gInstance;
+ if (!instance) {
+ instance = new PermissionObserver();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return nullptr;
+ }
+
+ nsresult rv = obs->AddObserver(instance, "perm-changed", true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ rv = obs->AddObserver(instance, "perm-changed-notify-only", true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ gInstance = instance;
+ }
+
+ return instance.forget();
+}
+
+void PermissionObserver::AddSink(PermissionStatus* aSink) {
+ MOZ_ASSERT(aSink);
+ MOZ_ASSERT(!mSinks.Contains(aSink));
+
+ mSinks.AppendElement(aSink);
+}
+
+void PermissionObserver::RemoveSink(PermissionStatus* aSink) {
+ MOZ_ASSERT(aSink);
+ MOZ_ASSERT(mSinks.Contains(aSink));
+
+ mSinks.RemoveElement(aSink);
+}
+
+NS_IMETHODIMP
+PermissionObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(!strcmp(aTopic, "perm-changed") ||
+ !strcmp(aTopic, "perm-changed-notify-only"));
+
+ if (mSinks.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPermission> perm = nullptr;
+ nsCOMPtr<nsPIDOMWindowInner> innerWindow = nullptr;
+ nsAutoCString type;
+
+ if (!strcmp(aTopic, "perm-changed")) {
+ perm = do_QueryInterface(aSubject);
+ if (!perm) {
+ return NS_OK;
+ }
+ perm->GetType(type);
+ } else if (!strcmp(aTopic, "perm-changed-notify-only")) {
+ innerWindow = do_QueryInterface(aSubject);
+ if (!innerWindow) {
+ return NS_OK;
+ }
+ type = NS_ConvertUTF16toUTF8(aData);
+ }
+
+ Maybe<PermissionName> permission = TypeToPermissionName(type);
+ if (permission) {
+ for (auto* sink : mSinks) {
+ if (sink->mName != permission.value()) {
+ continue;
+ }
+ // Check for permissions that are changed for this sink's principal
+ // via the "perm-changed" notification. These permissions affect
+ // the window the sink (PermissionStatus) is held in directly.
+ if (perm && sink->MaybeUpdatedBy(perm)) {
+ sink->PermissionChanged();
+ }
+ // Check for permissions that are changed for this sink's principal
+ // via the "perm-changed-notify-only" notification. These permissions
+ // affect the window the sink (PermissionStatus) is held in indirectly- if
+ // the window is same-party with the secondary key of a permission. For
+ // example, a "3rdPartyFrameStorage^https://example.com" permission would
+ // return true on these checks where sink is in a window that is same-site
+ // with https://example.com.
+ if (innerWindow && sink->MaybeUpdatedByNotifyOnly(innerWindow)) {
+ sink->PermissionChanged();
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/PermissionObserver.h b/dom/permission/PermissionObserver.h
new file mode 100644
index 0000000000..f48f26e76a
--- /dev/null
+++ b/dom/permission/PermissionObserver.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PermissionObserver_h_
+#define mozilla_dom_PermissionObserver_h_
+
+#include "mozilla/dom/PermissionsBinding.h"
+
+#include "nsIObserver.h"
+#include "nsIPrincipal.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+
+namespace mozilla::dom {
+
+class PermissionStatus;
+
+// Singleton that watches for perm-changed notifications in order to notify
+// PermissionStatus objects.
+class PermissionObserver final : public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ static already_AddRefed<PermissionObserver> GetInstance();
+
+ void AddSink(PermissionStatus* aObs);
+ void RemoveSink(PermissionStatus* aObs);
+
+ private:
+ PermissionObserver();
+ virtual ~PermissionObserver();
+
+ nsTArray<PermissionStatus*> mSinks;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/permission/PermissionStatus.cpp b/dom/permission/PermissionStatus.cpp
new file mode 100644
index 0000000000..fd3d2ebcac
--- /dev/null
+++ b/dom/permission/PermissionStatus.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/dom/PermissionStatus.h"
+#include "mozilla/PermissionDelegateHandler.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Permission.h"
+#include "mozilla/Services.h"
+#include "nsIPermissionManager.h"
+#include "PermissionObserver.h"
+#include "PermissionUtils.h"
+
+namespace mozilla::dom {
+
+PermissionStatus::PermissionStatus(nsPIDOMWindowInner* aWindow,
+ PermissionName aName)
+ : DOMEventTargetHelper(aWindow),
+ mName(aName),
+ mState(PermissionState::Denied) {
+ KeepAliveIfHasListenersFor(nsGkAtoms::onchange);
+}
+
+// https://w3c.github.io/permissions/#onchange-attribute and
+// https://w3c.github.io/permissions/#query-method
+RefPtr<PermissionStatus::SimplePromise> PermissionStatus::Init() {
+ // Covers the onchange part
+ // Whenever the user agent is aware that the state of a PermissionStatus
+ // instance status has changed: ...
+ // (The observer calls PermissionChanged() to do the steps)
+ mObserver = PermissionObserver::GetInstance();
+ if (NS_WARN_IF(!mObserver)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ mObserver->AddSink(this);
+
+ // Covers the query part (Step 8.2 - 8.4)
+ return UpdateState();
+}
+
+PermissionStatus::~PermissionStatus() {
+ if (mObserver) {
+ mObserver->RemoveSink(this);
+ }
+}
+
+JSObject* PermissionStatus::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PermissionStatus_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsLiteralCString PermissionStatus::GetPermissionType() const {
+ return PermissionNameToType(mName);
+}
+
+// Covers the calling part of "permission query algorithm" of query() method and
+// update steps, which calls
+// https://w3c.github.io/permissions/#dfn-default-permission-query-algorithm
+// and then https://w3c.github.io/permissions/#dfn-permission-state
+RefPtr<PermissionStatus::SimplePromise> PermissionStatus::UpdateState() {
+ // Step 1: If settings wasn't passed, set it to the current settings object.
+ // Step 2: If settings is a non-secure context, return "denied".
+ // XXX(krosylight): No such steps here, and no WPT coverage?
+
+ // The permission handler covers the rest of the steps, although the model
+ // does not exactly match what the spec has. (Not passing "permission key" for
+ // example)
+
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (NS_WARN_IF(!window)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ RefPtr<Document> document = window->GetExtantDoc();
+ if (NS_WARN_IF(!document)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ uint32_t action = nsIPermissionManager::DENY_ACTION;
+
+ PermissionDelegateHandler* permissionHandler =
+ document->GetPermissionDelegateHandler();
+ if (NS_WARN_IF(!permissionHandler)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ nsresult rv = permissionHandler->GetPermissionForPermissionsAPI(
+ GetPermissionType(), &action);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SimplePromise::CreateAndReject(rv, __func__);
+ }
+
+ mState = ActionToPermissionState(action);
+ return SimplePromise::CreateAndResolve(NS_OK, __func__);
+}
+
+bool PermissionStatus::MaybeUpdatedBy(nsIPermission* aPermission) const {
+ NS_ENSURE_TRUE(aPermission, false);
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (NS_WARN_IF(!window)) {
+ return false;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ Permission::ClonePrincipalForPermission(doc->NodePrincipal());
+ NS_ENSURE_TRUE(principal, false);
+ nsCOMPtr<nsIPrincipal> permissionPrincipal;
+ aPermission->GetPrincipal(getter_AddRefs(permissionPrincipal));
+ if (!permissionPrincipal) {
+ return false;
+ }
+ return permissionPrincipal->Equals(principal);
+}
+
+bool PermissionStatus::MaybeUpdatedByNotifyOnly(
+ nsPIDOMWindowInner* aInnerWindow) const {
+ return false;
+}
+
+// https://w3c.github.io/permissions/#dfn-permissionstatus-update-steps
+void PermissionStatus::PermissionChanged() {
+ auto oldState = mState;
+ RefPtr<PermissionStatus> self(this);
+ // Step 1: If this's relevant global object is a Window object, then:
+ // Step 1.1: Let document be status's relevant global object's associated
+ // Document.
+ // Step 1.2: If document is null or document is not fully active,
+ // terminate this algorithm.
+ // TODO(krosylight): WPT /permissions/non-fully-active.https.html fails
+ // because we don't do this. See bug 1876470.
+
+ // Step 2 - 3 is covered by UpdateState()
+ UpdateState()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, oldState]() {
+ if (self->mState != oldState) {
+ // Step 4: Queue a task on the permissions task source to fire an
+ // event named change at status.
+ RefPtr<AsyncEventDispatcher> eventDispatcher =
+ new AsyncEventDispatcher(self.get(), u"change"_ns,
+ CanBubble::eNo);
+ eventDispatcher->PostDOMEvent();
+ }
+ },
+ []() {
+
+ });
+}
+
+void PermissionStatus::DisconnectFromOwner() {
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onchange);
+
+ if (mObserver) {
+ mObserver->RemoveSink(this);
+ mObserver = nullptr;
+ }
+
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void PermissionStatus::GetType(nsACString& aName) const {
+ aName.Assign(GetPermissionType());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/PermissionStatus.h b/dom/permission/PermissionStatus.h
new file mode 100644
index 0000000000..0b334996d3
--- /dev/null
+++ b/dom/permission/PermissionStatus.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PermissionStatus_h_
+#define mozilla_dom_PermissionStatus_h_
+
+#include "mozilla/dom/PermissionsBinding.h"
+#include "mozilla/dom/PermissionStatusBinding.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/MozPromise.h"
+#include "nsIPermission.h"
+
+namespace mozilla::dom {
+
+class PermissionObserver;
+
+class PermissionStatus : public DOMEventTargetHelper {
+ friend class PermissionObserver;
+
+ public:
+ using SimplePromise = MozPromise<nsresult, nsresult, true>;
+
+ PermissionStatus(nsPIDOMWindowInner* aWindow, PermissionName aName);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ PermissionState State() const { return mState; }
+ void SetState(PermissionState aState) { mState = aState; }
+
+ IMPL_EVENT_HANDLER(change)
+
+ void DisconnectFromOwner() override;
+
+ PermissionName Name() const { return mName; }
+
+ void GetType(nsACString& aName) const;
+
+ RefPtr<SimplePromise> Init();
+
+ protected:
+ ~PermissionStatus();
+
+ /**
+ * This method returns the internal permission type, which should be equal to
+ * the permission name for all but the MIDI permission because of the SysEx
+ * support: internally, we have both "midi" and "midi-sysex" permission types
+ * but we only have a "midi" (public) permission name.
+ *
+ * Note: the `MidiPermissionDescriptor` descriptor has an optional `sysex`
+ * boolean, which is used to determine whether to return "midi" or
+ * "midi-sysex" for the MIDI permission.
+ */
+ virtual nsLiteralCString GetPermissionType() const;
+
+ private:
+ virtual RefPtr<SimplePromise> UpdateState();
+
+ // These functions should be called when an permission is updated which may
+ // change the state of this PermissionStatus. MaybeUpdatedBy accepts the
+ // permission object itself that is update. When the permission's key is not
+ // same-origin with this object's owner window, such as for secondary-keyed
+ // permissions like `3rdPartyFrameStorage^...`, MaybeUpdatedByNotifyOnly will
+ // be called with the updated window as an argument. MaybeUpdatedByNotifyOnly
+ // must be defined by PermissionStatus inheritors that are double-keyed.
+ virtual bool MaybeUpdatedBy(nsIPermission* aPermission) const;
+ virtual bool MaybeUpdatedByNotifyOnly(nsPIDOMWindowInner* aInnerWindow) const;
+
+ void PermissionChanged();
+
+ PermissionName mName;
+
+ RefPtr<PermissionObserver> mObserver;
+
+ protected:
+ PermissionState mState;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_permissionstatus_h_
diff --git a/dom/permission/PermissionUtils.cpp b/dom/permission/PermissionUtils.cpp
new file mode 100644
index 0000000000..26e3ec0157
--- /dev/null
+++ b/dom/permission/PermissionUtils.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "PermissionUtils.h"
+#include "nsIPermissionManager.h"
+
+namespace mozilla::dom {
+
+static const nsLiteralCString kPermissionTypes[] = {
+ // clang-format off
+ "geo"_ns,
+ "desktop-notification"_ns,
+ // Alias `push` to `desktop-notification`.
+ "desktop-notification"_ns,
+ "persistent-storage"_ns,
+ // "midi" is the only public permission but internally we have both "midi"
+ // and "midi-sysex" (and yes, this is confusing).
+ "midi"_ns,
+ "storage-access"_ns,
+ "screen-wake-lock"_ns
+ // clang-format on
+};
+
+const size_t kPermissionNameCount = PermissionNameValues::Count;
+
+static_assert(MOZ_ARRAY_LENGTH(kPermissionTypes) == kPermissionNameCount,
+ "kPermissionTypes and PermissionName count should match");
+
+const nsLiteralCString& PermissionNameToType(PermissionName aName) {
+ MOZ_ASSERT((size_t)aName < ArrayLength(kPermissionTypes));
+ return kPermissionTypes[static_cast<size_t>(aName)];
+}
+
+Maybe<PermissionName> TypeToPermissionName(const nsACString& aType) {
+ // Annoyingly, "midi-sysex" is an internal permission. The public permission
+ // name is "midi" so we have to special-case it here...
+ if (aType.Equals("midi-sysex"_ns)) {
+ return Some(PermissionName::Midi);
+ }
+
+ // "storage-access" permissions are also annoying and require a special case.
+ if (StringBeginsWith(aType, "3rdPartyStorage^"_ns) ||
+ StringBeginsWith(aType, "3rdPartyFrameStorage^"_ns)) {
+ return Some(PermissionName::Storage_access);
+ }
+
+ for (size_t i = 0; i < ArrayLength(kPermissionTypes); ++i) {
+ if (kPermissionTypes[i].Equals(aType)) {
+ return Some(static_cast<PermissionName>(i));
+ }
+ }
+
+ return Nothing();
+}
+
+PermissionState ActionToPermissionState(uint32_t aAction) {
+ switch (aAction) {
+ case nsIPermissionManager::ALLOW_ACTION:
+ return PermissionState::Granted;
+
+ case nsIPermissionManager::DENY_ACTION:
+ return PermissionState::Denied;
+
+ default:
+ case nsIPermissionManager::PROMPT_ACTION:
+ return PermissionState::Prompt;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/PermissionUtils.h b/dom/permission/PermissionUtils.h
new file mode 100644
index 0000000000..18b176f7f6
--- /dev/null
+++ b/dom/permission/PermissionUtils.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PermissionUtils_h_
+#define mozilla_dom_PermissionUtils_h_
+
+#include "mozilla/dom/PermissionsBinding.h"
+#include "mozilla/dom/PermissionStatusBinding.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla::dom {
+
+const nsLiteralCString& PermissionNameToType(PermissionName aName);
+
+/**
+ * Returns the permission name given a permission type.
+ *
+ * Note: the "midi" permission is implemented with two internal permissions
+ * ("midi" and "midi-sysex"). For this reason, when we pass "midi-sysex" to
+ * this function, it unconditionally returns the "midi" permission name,
+ * because that's the only public permission name.
+ */
+Maybe<PermissionName> TypeToPermissionName(const nsACString& aType);
+
+PermissionState ActionToPermissionState(uint32_t aAction);
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/permission/Permissions.cpp b/dom/permission/Permissions.cpp
new file mode 100644
index 0000000000..a74f65b80c
--- /dev/null
+++ b/dom/permission/Permissions.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/dom/Permissions.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MidiPermissionStatus.h"
+#include "mozilla/dom/PermissionSetParametersBinding.h"
+#include "mozilla/dom/PermissionStatus.h"
+#include "mozilla/dom/PermissionsBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/StorageAccessPermissionStatus.h"
+#include "PermissionUtils.h"
+
+namespace mozilla::dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Permissions)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Permissions)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Permissions)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Permissions, mWindow)
+
+Permissions::Permissions(nsPIDOMWindowInner* aWindow) : mWindow(aWindow) {}
+
+Permissions::~Permissions() = default;
+
+JSObject* Permissions::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Permissions_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+namespace {
+
+// Steps to parse PermissionDescriptor in
+// https://w3c.github.io/permissions/#query-method and relevant WebDriver
+// commands
+RefPtr<PermissionStatus> CreatePermissionStatus(
+ JSContext* aCx, JS::Handle<JSObject*> aPermissionDesc,
+ nsPIDOMWindowInner* aWindow, ErrorResult& aRv) {
+ // Step 2: Let rootDesc be the object permissionDesc refers to, converted to
+ // an IDL value of type PermissionDescriptor.
+ PermissionDescriptor rootDesc;
+ JS::Rooted<JS::Value> permissionDescValue(
+ aCx, JS::ObjectOrNullValue(aPermissionDesc));
+ if (NS_WARN_IF(!rootDesc.Init(aCx, permissionDescValue))) {
+ // Step 3: If the conversion throws an exception, return a promise rejected
+ // with that exception.
+ // Step 4: If rootDesc["name"] is not supported, return a promise rejected
+ // with a TypeError. (This is done by `enum PermissionName`, as the spec
+ // note says: "implementers are encouraged to use their own custom enum
+ // here")
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ // Step 5: Let typedDescriptor be the object permissionDesc refers to,
+ // converted to an IDL value of rootDesc's name's permission descriptor type.
+ // Step 6: If the conversion throws an exception, return a promise rejected
+ // with that exception.
+ // Step 8.1: Let status be create a PermissionStatus with typedDescriptor.
+ // (The rest is done by the caller)
+ switch (rootDesc.mName) {
+ case PermissionName::Midi: {
+ MidiPermissionDescriptor midiPerm;
+ if (NS_WARN_IF(!midiPerm.Init(aCx, permissionDescValue))) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ return new MidiPermissionStatus(aWindow, midiPerm.mSysex);
+ }
+ case PermissionName::Storage_access:
+ return new StorageAccessPermissionStatus(aWindow);
+ case PermissionName::Geolocation:
+ case PermissionName::Notifications:
+ case PermissionName::Push:
+ case PermissionName::Persistent_storage:
+ case PermissionName::Screen_wake_lock:
+ return new PermissionStatus(aWindow, rootDesc.mName);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled type");
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ return nullptr;
+ }
+}
+
+} // namespace
+
+// https://w3c.github.io/permissions/#query-method
+already_AddRefed<Promise> Permissions::Query(JSContext* aCx,
+ JS::Handle<JSObject*> aPermission,
+ ErrorResult& aRv) {
+ // Step 1: If this's relevant global object is a Window object, then:
+ // Step 1.1: If the current settings object's associated Document is not fully
+ // active, return a promise rejected with an "InvalidStateError" DOMException.
+ //
+ // TODO(krosylight): The spec allows worker global while we don't, see bug
+ // 1193373.
+ if (!mWindow || !mWindow->IsFullyActive()) {
+ aRv.ThrowInvalidStateError("The document is not fully active.");
+ return nullptr;
+ }
+
+ // Step 2 - 6 and 8.1:
+ RefPtr<PermissionStatus> status =
+ CreatePermissionStatus(aCx, aPermission, mWindow, aRv);
+ if (!status) {
+ return nullptr;
+ }
+
+ // Step 7: Let promise be a new promise.
+ RefPtr<Promise> promise = Promise::Create(mWindow->AsGlobal(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // Step 8.2 - 8.3: (Done by the Init method)
+ // Step 8.4: Queue a global task on the permissions task source with this's
+ // relevant global object to resolve promise with status.
+ status->Init()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [status, promise]() {
+ promise->MaybeResolve(status);
+ return;
+ },
+ [promise](nsresult aError) {
+ MOZ_ASSERT(NS_FAILED(aError));
+ NS_WARNING("Failed PermissionStatus creation");
+ promise->MaybeReject(aError);
+ return;
+ });
+
+ return promise.forget();
+}
+
+already_AddRefed<PermissionStatus> Permissions::ParseSetParameters(
+ JSContext* aCx, const PermissionSetParameters& aParameters,
+ ErrorResult& aRv) {
+ // Step 1: Let parametersDict be the parameters argument, converted to an IDL
+ // value of type PermissionSetParameters. If this throws an exception,
+ // return an invalid argument error.
+ // (Done by IDL layer, and the error type should be handled by the caller)
+
+ // Step 2: If parametersDict.state is an inappropriate permission state for
+ // any implementation-defined reason, return a invalid argument error.
+ // (We don't do this)
+
+ // Step 3: Let rootDesc be parametersDict.descriptor.
+ JS::Rooted<JSObject*> rootDesc(aCx, aParameters.mDescriptor);
+
+ // Step 4: Let typedDescriptor be the object rootDesc refers to, converted
+ // to an IDL value of rootDesc.name's permission descriptor type. If this
+ // throws an exception, return a invalid argument error.
+ //
+ // We use PermissionStatus as the typed object.
+ RefPtr<PermissionStatus> status =
+ CreatePermissionStatus(aCx, rootDesc, nullptr, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Set the state too so that the caller can use it for step 5.
+ status->SetState(aParameters.mState);
+
+ return status.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/Permissions.h b/dom/permission/Permissions.h
new file mode 100644
index 0000000000..f6cfd4ae65
--- /dev/null
+++ b/dom/permission/Permissions.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_Permissions_h_
+#define mozilla_dom_Permissions_h_
+
+#include "nsISupports.h"
+#include "nsPIDOMWindow.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class Promise;
+class PermissionStatus;
+struct PermissionSetParameters;
+
+class Permissions final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Permissions)
+
+ explicit Permissions(nsPIDOMWindowInner* aWindow);
+
+ nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ already_AddRefed<Promise> Query(JSContext* aCx,
+ JS::Handle<JSObject*> aPermission,
+ ErrorResult& aRv);
+
+ // The IDL conversion steps of
+ // https://w3c.github.io/permissions/#webdriver-command-set-permission
+ already_AddRefed<PermissionStatus> ParseSetParameters(
+ JSContext* aCx, const PermissionSetParameters& aParameters,
+ ErrorResult& aRv);
+
+ private:
+ ~Permissions();
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_permissions_h_
diff --git a/dom/permission/StorageAccessPermissionStatus.cpp b/dom/permission/StorageAccessPermissionStatus.cpp
new file mode 100644
index 0000000000..fc39e1440e
--- /dev/null
+++ b/dom/permission/StorageAccessPermissionStatus.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/dom/StorageAccessPermissionStatus.h"
+
+#include "mozilla/AntiTrackingUtils.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/PermissionStatus.h"
+#include "mozilla/dom/PermissionStatusBinding.h"
+#include "nsIPermissionManager.h"
+
+namespace mozilla::dom {
+
+StorageAccessPermissionStatus::StorageAccessPermissionStatus(
+ nsPIDOMWindowInner* aWindow)
+ : PermissionStatus(aWindow, PermissionName::Storage_access) {}
+
+RefPtr<PermissionStatus::SimplePromise>
+StorageAccessPermissionStatus::UpdateState() {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (NS_WARN_IF(!window)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ WindowGlobalChild* wgc = window->GetWindowGlobalChild();
+ if (NS_WARN_IF(!wgc)) {
+ return SimplePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ // Perform a Permission Policy Request
+ if (!FeaturePolicyUtils::IsFeatureAllowed(window->GetExtantDoc(),
+ u"storage-access"_ns)) {
+ mState = PermissionState::Prompt;
+ return SimplePromise::CreateAndResolve(NS_OK, __func__);
+ }
+
+ RefPtr<StorageAccessPermissionStatus> self(this);
+ return wgc->SendGetStorageAccessPermission()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self](uint32_t aAction) {
+ if (aAction == nsIPermissionManager::ALLOW_ACTION) {
+ self->mState = PermissionState::Granted;
+ } else {
+ // We never reveal PermissionState::Denied here
+ self->mState = PermissionState::Prompt;
+ }
+ return SimplePromise::CreateAndResolve(NS_OK, __func__);
+ },
+ [](mozilla::ipc::ResponseRejectReason aError) {
+ return SimplePromise::CreateAndResolve(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+bool StorageAccessPermissionStatus::MaybeUpdatedBy(
+ nsIPermission* aPermission) const {
+ return false;
+}
+
+bool StorageAccessPermissionStatus::MaybeUpdatedByNotifyOnly(
+ nsPIDOMWindowInner* aInnerWindow) const {
+ nsPIDOMWindowInner* owner = GetOwner();
+ NS_ENSURE_TRUE(owner, false);
+ NS_ENSURE_TRUE(aInnerWindow, false);
+ return owner->WindowID() == aInnerWindow->WindowID();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/permission/StorageAccessPermissionStatus.h b/dom/permission/StorageAccessPermissionStatus.h
new file mode 100644
index 0000000000..be984b5762
--- /dev/null
+++ b/dom/permission/StorageAccessPermissionStatus.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_StorageAccessPermissionStatus_h_
+#define mozilla_dom_StorageAccessPermissionStatus_h_
+
+#include "mozilla/dom/PermissionStatus.h"
+
+namespace mozilla::dom {
+
+class StorageAccessPermissionStatus final : public PermissionStatus {
+ public:
+ explicit StorageAccessPermissionStatus(nsPIDOMWindowInner* aWindow);
+
+ private:
+ RefPtr<SimplePromise> UpdateState() override;
+
+ bool MaybeUpdatedBy(nsIPermission* aPermission) const override;
+ bool MaybeUpdatedByNotifyOnly(
+ nsPIDOMWindowInner* aInnerWindow) const override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_StorageAccessPermissionStatus_h_
diff --git a/dom/permission/moz.build b/dom/permission/moz.build
new file mode 100644
index 0000000000..be22a2db03
--- /dev/null
+++ b/dom/permission/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+EXPORTS.mozilla.dom += [
+ "MidiPermissionStatus.h",
+ "Permissions.h",
+ "PermissionStatus.h",
+ "PermissionUtils.h",
+ "StorageAccessPermissionStatus.h",
+]
+
+UNIFIED_SOURCES += [
+ "MidiPermissionStatus.cpp",
+ "PermissionObserver.cpp",
+ "Permissions.cpp",
+ "PermissionStatus.cpp",
+ "PermissionUtils.cpp",
+ "StorageAccessPermissionStatus.cpp",
+]
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.toml"]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/dom/permission/tests/file_empty.html b/dom/permission/tests/file_empty.html
new file mode 100644
index 0000000000..15648ec5aa
--- /dev/null
+++ b/dom/permission/tests/file_empty.html
@@ -0,0 +1,2 @@
+<h1>I'm just a support file</h1>
+<p>I get loaded to do permission testing.</p>
diff --git a/dom/permission/tests/file_storage_access_notification_helper.html b/dom/permission/tests/file_storage_access_notification_helper.html
new file mode 100644
index 0000000000..0a8b978517
--- /dev/null
+++ b/dom/permission/tests/file_storage_access_notification_helper.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Helper for Permissions API Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+ 'use strict';
+ async function helper() {
+ let status = await navigator.permissions
+ .query({ name: "storage-access" });
+ status.onchange = () => {
+ status.onchange = null;
+ parent.postMessage(status.state, "*")
+ };
+ parent.postMessage("ready", "*");
+ }
+
+ </script>
+</head>
+
+<body onload="helper()">
+</body>
+</html>
diff --git a/dom/permission/tests/mochitest.toml b/dom/permission/tests/mochitest.toml
new file mode 100644
index 0000000000..148bd6aba9
--- /dev/null
+++ b/dom/permission/tests/mochitest.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+support-files = [
+ "file_empty.html",
+ "file_storage_access_notification_helper.html",
+]
+prefs = [
+ "dom.security.featurePolicy.header.enabled=true",
+ "dom.security.featurePolicy.webidl.enabled=true",
+]
+
+["test_cross_origin_iframe.html"]
+fail-if = ["xorigin"]
+
+["test_permissions_api.html"]
+skip-if = ["xorigin"] # Hangs
+
+["test_storage_access_notification.html"]
+skip-if = ["xorigin"] # Hangs
diff --git a/dom/permission/tests/test_cross_origin_iframe.html b/dom/permission/tests/test_cross_origin_iframe.html
new file mode 100644
index 0000000000..43ef9f0868
--- /dev/null
+++ b/dom/permission/tests/test_cross_origin_iframe.html
@@ -0,0 +1,308 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test for Permissions API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <pre id="test"></pre>
+ <script type="application/javascript">
+ /*globals SpecialPowers, SimpleTest, is, ok, */
+ 'use strict';
+
+ function setPermission(type, allow) {
+ return new Promise(resolve => {
+ SpecialPowers.popPermissions(() => {
+ SpecialPowers.pushPermissions(
+ [{ type, allow, context: document }],
+ resolve
+ );
+ });
+ });
+ }
+
+ function checkPermission(aIFrame, aExpectedState, aName) {
+ return SpecialPowers.spawn(
+ aIFrame,
+ [{name: aName, expectedState: aExpectedState}],
+ async aInput => {
+ try {
+ let result = await content.navigator
+ .permissions
+ .query({ name: aInput.name });
+ is(
+ SpecialPowers.wrap(result).state,
+ aInput.expectedState,
+ `correct state for '${aInput.name}'`
+ );
+ } catch (e) {
+ ok(false, `query should not have rejected for '${aInput.name}'`)
+ }
+ }
+ );
+ }
+
+ function createIframe(aId, aAllow) {
+ return new Promise((resolve) => {
+ const iframe = document.createElement('iframe');
+ iframe.id = aId;
+ iframe.src = 'https://example.org/tests/dom/permission/tests/file_empty.html';
+ if (aAllow) {
+ iframe.allow = aAllow;
+ }
+ iframe.onload = () => resolve(iframe);
+ document.body.appendChild(iframe);
+ });
+ }
+
+ function removeIframe(aId) {
+ return new Promise((resolve) => {
+ document.body.removeChild(document.getElementById(aId));
+ resolve();
+ });
+ }
+
+ const {
+ UNKNOWN_ACTION,
+ PROMPT_ACTION,
+ ALLOW_ACTION,
+ DENY_ACTION
+ } = SpecialPowers.Ci.nsIPermissionManager;
+
+ const tests = [
+ {
+ id: 'query navigation top unknown',
+ top: UNKNOWN_ACTION,
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'denied',
+ },
+ {
+ id: 'query notifications top unknown',
+ top: UNKNOWN_ACTION,
+ name: 'notifications',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query push top unknown',
+ top: UNKNOWN_ACTION,
+ name: 'push',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query persistent-storage unknown',
+ top: UNKNOWN_ACTION,
+ name: 'persistent-storage',
+ type: 'persistent-storage',
+ expected: 'denied',
+ },
+ {
+ id: 'query storage-access unknown',
+ top: UNKNOWN_ACTION,
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query navigation top prompt',
+ top: PROMPT_ACTION,
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'denied',
+ },
+ {
+ id: 'query notifications top prompt',
+ top: PROMPT_ACTION,
+ name: 'notifications',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query push top prompt',
+ top: PROMPT_ACTION,
+ name: 'push',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query persistent-storage top prompt',
+ top: PROMPT_ACTION,
+ name: 'persistent-storage',
+ type: 'persistent-storage',
+ expected: 'denied',
+ },
+ {
+ id: 'query storage-access top prompt',
+ top: PROMPT_ACTION,
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query navigation top denied',
+ top: DENY_ACTION,
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'denied',
+ },
+ {
+ id: 'query notifications top denied',
+ top: DENY_ACTION,
+ name: 'notifications',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query push top denied',
+ top: DENY_ACTION,
+ name: 'push',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query persistent-storage top denied',
+ top: DENY_ACTION,
+ name: 'persistent-storage',
+ type: 'persistent-storage',
+ expected: 'denied',
+ },
+ {
+ id: 'query storage-access top denied',
+ top: DENY_ACTION,
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query navigation top granted',
+ top: ALLOW_ACTION,
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'denied',
+ },
+ {
+ id: 'query notifications top granted',
+ top: ALLOW_ACTION,
+ name: 'notifications',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query push top granted',
+ top: ALLOW_ACTION,
+ name: 'push',
+ type: 'desktop-notification',
+ expected: 'denied',
+ },
+ {
+ id: 'query persistent-storage top granted',
+ top: ALLOW_ACTION,
+ name: 'persistent-storage',
+ type: 'persistent-storage',
+ expected: 'denied',
+ },
+ {
+ id: 'query storage-access top granted',
+ top: ALLOW_ACTION,
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'granted',
+ },
+ {
+ id: 'query navigation top denied, iframe has allow attribute',
+ top: DENY_ACTION,
+ allow: 'geolocation',
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'denied',
+ },
+ {
+ id: 'query navigation top granted, iframe has allow attribute',
+ top: ALLOW_ACTION,
+ allow: 'geolocation',
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'granted',
+ },
+ {
+ id: 'query navigation top prompt, iframe has allow attribute',
+ top: PROMPT_ACTION,
+ allow: 'geolocation',
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'prompt',
+ },
+ {
+ id: 'query navigation top unknown, iframe has allow attribute',
+ top: UNKNOWN_ACTION,
+ allow: 'geolocation',
+ name: 'geolocation',
+ type: 'geo',
+ expected: 'prompt',
+ },
+ {
+ id: 'query storage-access top denied, iframe has allow none attribute',
+ top: DENY_ACTION,
+ allow: "storage-access 'none'",
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query storage-access top granted, iframe has allow none attribute',
+ top: ALLOW_ACTION,
+ allow: "storage-access 'none'",
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query storage-access top prompt, iframe has allow none attribute',
+ top: PROMPT_ACTION,
+ allow: "storage-access 'none'",
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ {
+ id: 'query storage-access top unknown, iframe has allow none attribute',
+ top: UNKNOWN_ACTION,
+ allow: "storage-access 'none'",
+ name: 'storage-access',
+ type: '3rdPartyFrameStorage^https://example.org',
+ expected: 'prompt',
+ },
+ ];
+
+ SimpleTest.waitForExplicitFinish();
+
+ async function nextTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ let test = tests.shift();
+ await setPermission(test.type, test.top)
+ .then(() => createIframe(test.id, test.allow))
+ .then(iframe => checkPermission(iframe, test.expected, test.name))
+ .then(() => removeIframe(test.id));
+
+ SimpleTest.executeSoon(nextTest);
+ }
+
+ nextTest()
+ </script>
+</body>
+
+</html>
diff --git a/dom/permission/tests/test_permissions_api.html b/dom/permission/tests/test_permissions_api.html
new file mode 100644
index 0000000000..57c45e9d58
--- /dev/null
+++ b/dom/permission/tests/test_permissions_api.html
@@ -0,0 +1,254 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test for Permissions API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <pre id="test"></pre>
+ <script type="application/javascript">
+ /*globals SpecialPowers, SimpleTest, is, ok, */
+ 'use strict';
+
+ const {
+ UNKNOWN_ACTION,
+ PROMPT_ACTION,
+ ALLOW_ACTION,
+ DENY_ACTION
+ } = SpecialPowers.Ci.nsIPermissionManager;
+
+ SimpleTest.waitForExplicitFinish();
+
+ const PERMISSIONS = [{
+ name: 'geolocation',
+ type: 'geo'
+ }, {
+ name: 'notifications',
+ type: 'desktop-notification'
+ }, {
+ name: 'push',
+ type: 'desktop-notification'
+ }, {
+ name: 'persistent-storage',
+ type: 'persistent-storage'
+ }, {
+ name: 'midi',
+ type: 'midi'
+ }, ];
+
+ const UNSUPPORTED_PERMISSIONS = [
+ 'foobarbaz', // Not in spec, for testing only.
+ ];
+
+ // Create a closure, so that tests are run on the correct window object.
+ function createPermissionTester(iframe) {
+ const iframeWindow = iframe.contentWindow;
+ return {
+ async setPermissions(allow, context = iframeWindow.document) {
+ const permissions = PERMISSIONS.map(({ type }) => {
+ return {
+ type,
+ allow,
+ context,
+ };
+ });
+ await SpecialPowers.popPermissions();
+ return SpecialPowers.pushPermissions(permissions);
+ },
+ checkPermissions(expectedState) {
+ const promisesToQuery = PERMISSIONS.map(({ name: expectedName }) => {
+ return iframeWindow.navigator.permissions
+ .query({ name: expectedName })
+ .then(
+ ({ state, name }) => {
+ is(state, expectedState, `correct state for '${expectedName}'`)
+ is(name, expectedName, `correct name for '${expectedName}'`)
+ },
+ () => ok(false, `query should not have rejected for '${name}'`)
+ );
+ });
+ return Promise.all(promisesToQuery);
+ },
+ checkUnsupportedPermissions() {
+ const promisesToQuery = UNSUPPORTED_PERMISSIONS.map(({ name }) => {
+ return iframeWindow.navigator.permissions
+ .query({ name })
+ .then(
+ () => ok(false, `query should not have resolved for '${name}'`),
+ error => {
+ is(error.name, 'TypeError',
+ `query should have thrown TypeError for '${name}'`);
+ }
+ );
+ });
+ return Promise.all(promisesToQuery);
+ },
+ promiseStateChanged(name, state) {
+ return iframeWindow.navigator.permissions
+ .query({ name })
+ .then(status => {
+ return new Promise( resolve => {
+ status.onchange = () => {
+ status.onchange = null;
+ is(status.state, state, `state changed for '${name}'`);
+ resolve();
+ };
+ });
+ },
+ () => ok(false, `query should not have rejected for '${name}'`));
+ },
+ testStatusOnChange() {
+ return new Promise((resolve) => {
+ SpecialPowers.popPermissions(() => {
+ const permission = 'geolocation';
+ const promiseGranted = this.promiseStateChanged(permission, 'granted');
+ this.setPermissions(ALLOW_ACTION);
+ promiseGranted.then(async () => {
+ const promisePrompt = this.promiseStateChanged(permission, 'prompt');
+ await SpecialPowers.popPermissions();
+ return promisePrompt;
+ }).then(resolve);
+ });
+ });
+ },
+ testInvalidQuery() {
+ return iframeWindow.navigator.permissions
+ .query({ name: 'invalid' })
+ .then(
+ () => ok(false, 'invalid query should not have resolved'),
+ () => ok(true, 'invalid query should have rejected')
+ );
+ },
+ async testNotFullyActiveDoc() {
+ const iframe1 = await createIframe();
+ const expectedErrorClass = iframe1.contentWindow.DOMException;
+ const permAPI = iframe1.contentWindow.navigator.permissions;
+ // Document no longer fully active
+ iframe1.remove();
+ await new Promise((res) => {
+ permAPI.query({ name: "geolocation" }).catch((error) => {
+ ok(
+ error instanceof expectedErrorClass,
+ "DOMException from other realm"
+ );
+ is(
+ error.name,
+ "InvalidStateError",
+ "Must reject with a InvalidStateError"
+ );
+ iframe1.remove();
+ res();
+ });
+ });
+ },
+ async testNotFullyActiveChange() {
+ await SpecialPowers.popPermissions();
+ const iframe2 = await createIframe();
+ const initialStatus = await iframe2.contentWindow.navigator.permissions.query(
+ { name: "geolocation" }
+ );
+ await SpecialPowers.pushPermissions([
+ {
+ type: "geo",
+ allow: PROMPT_ACTION,
+ context: iframe2.contentWindow.document,
+ },
+ ]);
+ is(
+ initialStatus.state,
+ "prompt",
+ "Initially the iframe's permission is prompt"
+ );
+
+ // Document no longer fully active
+ const stolenDoc = iframe2.contentWindow.document;
+ iframe2.remove();
+ initialStatus.onchange = () => {
+ ok(false, "onchange must not fire when document is not fully active.");
+ };
+ // We set it to grant for this origin, but the PermissionStatus doesn't change.
+ await SpecialPowers.pushPermissions([
+ {
+ type: "geo",
+ allow: ALLOW_ACTION,
+ context: stolenDoc,
+ },
+ ]);
+ is(
+ initialStatus.state,
+ "prompt",
+ "Inactive document's permission must not change"
+ );
+
+ // Re-attach the iframe
+ document.body.appendChild(iframe2);
+ await new Promise((res) => (iframe2.onload = res));
+ // Fully active again
+ const newStatus = await iframe2.contentWindow.navigator.permissions.query({
+ name: "geolocation",
+ });
+ is(newStatus.state, "granted", "Reflect that we are granted");
+
+ const newEventPromise = new Promise((res) => (newStatus.onchange = res));
+ await SpecialPowers.pushPermissions([
+ {
+ type: "geo",
+ allow: DENY_ACTION,
+ context: iframe2.contentWindow.document,
+ },
+ ]);
+ // Event fires...
+ await newEventPromise;
+ is(initialStatus.state, "prompt", "Remains prompt, as it's actually dead.");
+ is(newStatus.state, "denied", "New status must be 'denied'.");
+ iframe2.remove();
+ },
+ };
+ }
+
+ function createIframe() {
+ return new Promise((resolve) => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'file_empty.html';
+ iframe.onload = () => resolve(iframe);
+ document.body.appendChild(iframe);
+ });
+ }
+
+ window.onload = () => {
+ createIframe()
+ .then(createPermissionTester)
+ .then((tester) => {
+ return tester
+ .checkUnsupportedPermissions()
+ .then(() => tester.setPermissions(UNKNOWN_ACTION))
+ .then(() => tester.checkPermissions('prompt'))
+ .then(() => tester.setPermissions(PROMPT_ACTION))
+ .then(() => tester.checkPermissions('prompt'))
+ .then(() => tester.setPermissions(ALLOW_ACTION))
+ .then(() => tester.checkPermissions('granted'))
+ .then(() => tester.setPermissions(DENY_ACTION))
+ .then(() => tester.checkPermissions('denied'))
+ .then(() => tester.testStatusOnChange())
+ .then(() => tester.testInvalidQuery())
+ .then(() => tester.testNotFullyActiveDoc())
+ .then(() => tester.testNotFullyActiveChange());
+ })
+ .then(SimpleTest.finish)
+ .catch((e) => {
+ ok(false, `Unexpected error ${e}`);
+ SimpleTest.finish();
+ });
+ };
+ </script>
+</body>
+
+</html>
diff --git a/dom/permission/tests/test_storage_access_notification.html b/dom/permission/tests/test_storage_access_notification.html
new file mode 100644
index 0000000000..d8b5588554
--- /dev/null
+++ b/dom/permission/tests/test_storage_access_notification.html
@@ -0,0 +1,50 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test for Permissions API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ 'use strict';
+
+ SimpleTest.waitForExplicitFinish();
+
+ async function setPermission(type, allow) {
+ await SpecialPowers.popPermissions();
+ await SpecialPowers.pushPermissions(
+ [{ type, allow, context: document }]
+ );
+ }
+
+ const {
+ UNKNOWN_ACTION,
+ PROMPT_ACTION,
+ ALLOW_ACTION,
+ DENY_ACTION
+ } = SpecialPowers.Ci.nsIPermissionManager;
+
+ window.addEventListener(
+ "message",
+ (event) => {
+ if (event.data == "ready") {
+ setPermission("3rdPartyFrameStorage^https://example.org", ALLOW_ACTION);
+ } else {
+ is(event.data, "granted", "storage-access permission should change to granted after the permission is set");
+ SimpleTest.finish();
+ }
+ }
+ );
+ </script>
+</head>
+
+<body>
+ <iframe id="frame" src="https://example.org/tests/dom/permission/tests/file_storage_access_notification_helper.html"/>
+</body>
+
+</html>