diff options
Diffstat (limited to 'dom/permission')
-rw-r--r-- | dom/permission/MidiPermissionStatus.cpp | 22 | ||||
-rw-r--r-- | dom/permission/MidiPermissionStatus.h | 28 | ||||
-rw-r--r-- | dom/permission/PermissionObserver.cpp | 131 | ||||
-rw-r--r-- | dom/permission/PermissionObserver.h | 43 | ||||
-rw-r--r-- | dom/permission/PermissionStatus.cpp | 174 | ||||
-rw-r--r-- | dom/permission/PermissionStatus.h | 84 | ||||
-rw-r--r-- | dom/permission/PermissionUtils.cpp | 73 | ||||
-rw-r--r-- | dom/permission/PermissionUtils.h | 32 | ||||
-rw-r--r-- | dom/permission/Permissions.cpp | 176 | ||||
-rw-r--r-- | dom/permission/Permissions.h | 55 | ||||
-rw-r--r-- | dom/permission/StorageAccessPermissionStatus.cpp | 72 | ||||
-rw-r--r-- | dom/permission/StorageAccessPermissionStatus.h | 28 | ||||
-rw-r--r-- | dom/permission/moz.build | 31 | ||||
-rw-r--r-- | dom/permission/tests/file_empty.html | 2 | ||||
-rw-r--r-- | dom/permission/tests/file_storage_access_notification_helper.html | 30 | ||||
-rw-r--r-- | dom/permission/tests/mochitest.toml | 18 | ||||
-rw-r--r-- | dom/permission/tests/test_cross_origin_iframe.html | 308 | ||||
-rw-r--r-- | dom/permission/tests/test_permissions_api.html | 254 | ||||
-rw-r--r-- | dom/permission/tests/test_storage_access_notification.html | 50 |
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> |