Commit fbff2e38 authored by Marcos Cáceres's avatar Marcos Cáceres
Browse files

Bug 1312422 - Web Share Base/DOM implementation r=farre

Web Share base implementation just of DOM stuff - working together with @saschanaz.

@Baku, we would greatly appreciate your review.

-Nika, as she is traveling.

Differential Revision: https://phabricator.services.mozilla.com/D44598

--HG--
extra : moz-landing-system : lando
parent ac1ba867
...@@ -55,6 +55,7 @@ DIRS += [ ...@@ -55,6 +55,7 @@ DIRS += [
'uitour', 'uitour',
'urlbar', 'urlbar',
'translation', 'translation',
'webshare',
] ]
DIRS += ['build'] DIRS += ['build']
......
/* 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/. */
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
class SharePicker {
constructor() {}
get classDescription() {
return "Web Share Picker";
}
get classID() {
return Components.ID("{1201d357-8417-4926-a694-e6408fbedcf8}");
}
get contractID() {
return "@mozilla.org/sharepicker;1";
}
get QueryInterface() {
return ChromeUtils.generateQI([Ci.nsISharePicker]);
}
/**
* The data being shared by the Document.
*
* @param {String?} title - title of the share
* @param {String?} text - text shared
* @param {nsIURI?} url - a URI shared
*/
async share(title, text, url) {
// If anything goes wrong, always throw a real DOMException.
// e.g., throw new DOMException(someL10nMsg, "AbortError");
//
// The possible conditions are:
// - User cancels or timeout: "AbortError"
// - Data error: "DataError"
// - Anything else, please file a bug on the spec:
// https://github.com/w3c/web-share/issues/
//
// Returning without throwing is success.
//
// This mock implementation just rejects - it's just here
// as a guide to do actual platform integration.
throw new DOMException("Not supported.", "AbortError");
}
__init() {}
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([SharePicker]);
component {1201d357-8417-4926-a694-e6408fbedcf8} SharePicker.js
contract @mozilla.org/sharepicker;1 {1201d357-8417-4926-a694-e6408fbedcf8}
\ No newline at end of file
EXTRA_COMPONENTS += [
'SharePicker.js',
'SharePicker.manifest',
]
\ No newline at end of file
...@@ -106,6 +106,7 @@ ...@@ -106,6 +106,7 @@
#include "mozilla/Unused.h" #include "mozilla/Unused.h"
#include "mozilla/webgpu/Instance.h" #include "mozilla/webgpu/Instance.h"
#include "mozilla/dom/WindowGlobalChild.h"
namespace mozilla { namespace mozilla {
namespace dom { namespace dom {
...@@ -142,6 +143,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(Navigator) ...@@ -142,6 +143,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(Navigator)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Navigator) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Navigator)
tmp->Invalidate(); tmp->Invalidate();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSharePromise)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_UNLINK_END
...@@ -167,6 +169,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Navigator) ...@@ -167,6 +169,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Navigator)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepadServiceTest) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepadServiceTest)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRGetDisplaysPromises) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRGetDisplaysPromises)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRServiceTest) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRServiceTest)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSharePromise)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Navigator) NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Navigator)
...@@ -234,6 +237,8 @@ void Navigator::Invalidate() { ...@@ -234,6 +237,8 @@ void Navigator::Invalidate() {
mAddonManager = nullptr; mAddonManager = nullptr;
mWebGpu = nullptr; mWebGpu = nullptr;
mSharePromise = nullptr;
} }
void Navigator::GetUserAgent(nsAString& aUserAgent, CallerType aCallerType, void Navigator::GetUserAgent(nsAString& aUserAgent, CallerType aCallerType,
...@@ -1331,6 +1336,110 @@ Promise* Navigator::GetBattery(ErrorResult& aRv) { ...@@ -1331,6 +1336,110 @@ Promise* Navigator::GetBattery(ErrorResult& aRv) {
return mBatteryPromise; return mBatteryPromise;
} }
//*****************************************************************************
// Navigator::Share() - Web Share API
//*****************************************************************************
Promise* Navigator::Share(const ShareData& aData, ErrorResult& aRv) {
if (NS_WARN_IF(!mWindow || !mWindow->GetDocShell() ||
!mWindow->GetExtantDoc())) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
if (mSharePromise) {
NS_WARNING("Only one share picker at a time per navigator instance");
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return nullptr;
}
// If none of data's members title, text, or url are present, reject p with
// TypeError, and abort these steps.
bool someMemberPassed = aData.mTitle.WasPassed() || aData.mText.WasPassed() ||
aData.mUrl.WasPassed();
if (!someMemberPassed) {
nsAutoString message;
nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"WebShareAPI_NeedOneMember", message);
aRv.ThrowTypeError<MSG_MISSING_REQUIRED_DICTIONARY_MEMBER>(message);
return nullptr;
}
// null checked above
auto doc = mWindow->GetExtantDoc();
// If data's url member is present, try to resolve it...
nsCOMPtr<nsIURI> url;
if (aData.mUrl.WasPassed()) {
auto result = doc->ResolveWithBaseURI(aData.mUrl.Value());
if (NS_WARN_IF(result.isErr())) {
aRv.ThrowTypeError<MSG_INVALID_URL>(aData.mUrl.Value());
return nullptr;
}
url = result.unwrap();
}
// Process the title member...
nsCString title;
if (aData.mTitle.WasPassed()) {
title.Assign(NS_ConvertUTF16toUTF8(aData.mTitle.Value()));
} else {
title.SetIsVoid(true);
}
// Process the text member...
nsCString text;
if (aData.mText.WasPassed()) {
text.Assign(NS_ConvertUTF16toUTF8(aData.mText.Value()));
} else {
text.SetIsVoid(true);
}
// The spec does the "triggered by user activation" after the data checks.
// Unfortunately, both Chrome and Safari behave this way, so interop wins.
// https://github.com/w3c/web-share/pull/118
if (!UserActivation::IsHandlingUserInput()) {
NS_WARNING("Attempt to share not triggered by user activation");
aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return nullptr;
}
// Let mSharePromise be a new promise.
mSharePromise = Promise::Create(mWindow->AsGlobal(), aRv);
if (aRv.Failed()) {
return nullptr;
}
IPCWebShareData data(title, text, url);
auto wgc = mWindow->GetWindowGlobalChild();
if (!wgc) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
auto shareResolver = [self = RefPtr<Navigator>(this)](nsresult aResult) {
MOZ_ASSERT(self->mSharePromise);
if (NS_SUCCEEDED(aResult)) {
self->mSharePromise->MaybeResolveWithUndefined();
} else {
self->mSharePromise->MaybeReject(aResult);
}
self->mSharePromise = nullptr;
};
auto shareRejector = [self = RefPtr<Navigator>(this)](
mozilla::ipc::ResponseRejectReason&& aReason) {
// IPC died or maybe page navigated...
if (self->mSharePromise) {
self->mSharePromise = nullptr;
}
};
// Do the share
wgc->SendShare(data, shareResolver, shareRejector);
return mSharePromise;
}
already_AddRefed<LegacyMozTCPSocket> Navigator::MozTCPSocket() { already_AddRefed<LegacyMozTCPSocket> Navigator::MozTCPSocket() {
RefPtr<LegacyMozTCPSocket> socket = new LegacyMozTCPSocket(GetWindow()); RefPtr<LegacyMozTCPSocket> socket = new LegacyMozTCPSocket(GetWindow());
return socket.forget(); return socket.forget();
......
...@@ -80,6 +80,8 @@ class VRDisplay; ...@@ -80,6 +80,8 @@ class VRDisplay;
class VRServiceTest; class VRServiceTest;
class StorageManager; class StorageManager;
class MediaCapabilities; class MediaCapabilities;
struct ShareData;
class WindowGlobalChild;
class Navigator final : public nsISupports, public nsWrapperCache { class Navigator final : public nsISupports, public nsWrapperCache {
public: public:
...@@ -131,6 +133,8 @@ class Navigator final : public nsISupports, public nsWrapperCache { ...@@ -131,6 +133,8 @@ class Navigator final : public nsISupports, public nsWrapperCache {
Geolocation* GetGeolocation(ErrorResult& aRv); Geolocation* GetGeolocation(ErrorResult& aRv);
Promise* GetBattery(ErrorResult& aRv); Promise* GetBattery(ErrorResult& aRv);
Promise* Share(const ShareData& aData, ErrorResult& aRv);
static void AppName(nsAString& aAppName, nsIPrincipal* aCallerPrincipal, static void AppName(nsAString& aAppName, nsIPrincipal* aCallerPrincipal,
bool aUsePrefOverriddenValue); bool aUsePrefOverriddenValue);
...@@ -276,6 +280,7 @@ class Navigator final : public nsISupports, public nsWrapperCache { ...@@ -276,6 +280,7 @@ class Navigator final : public nsISupports, public nsWrapperCache {
RefPtr<dom::MediaCapabilities> mMediaCapabilities; RefPtr<dom::MediaCapabilities> mMediaCapabilities;
RefPtr<AddonManager> mAddonManager; RefPtr<AddonManager> mAddonManager;
RefPtr<webgpu::Instance> mWebGpu; RefPtr<webgpu::Instance> mWebGpu;
RefPtr<Promise> mSharePromise; // Web Share API related
}; };
} // namespace dom } // namespace dom
......
...@@ -28,6 +28,13 @@ struct JSWindowActorMessageMeta { ...@@ -28,6 +28,13 @@ struct JSWindowActorMessageMeta {
JSWindowActorMessageKind kind; JSWindowActorMessageKind kind;
}; };
struct IPCWebShareData
{
nsCString title;
nsCString text;
nsIURI url;
};
/** /**
* A PWindowGlobal actor has a lifetime matching that of a single Window Global, * A PWindowGlobal actor has a lifetime matching that of a single Window Global,
* specifically a |nsGlobalWindowInner|. These actors will form a parent/child * specifically a |nsGlobalWindowInner|. These actors will form a parent/child
...@@ -77,6 +84,9 @@ parent: ...@@ -77,6 +84,9 @@ parent:
/// Notify the parent that this PWindowGlobal is now the current global. /// Notify the parent that this PWindowGlobal is now the current global.
async BecomeCurrentWindowGlobal(); async BecomeCurrentWindowGlobal();
// Attempts to perform a "Web Share".
async Share(IPCWebShareData aData) returns (nsresult rv);
async Destroy(); async Destroy();
}; };
......
...@@ -31,6 +31,10 @@ ...@@ -31,6 +31,10 @@
#include "nsFrameLoaderOwner.h" #include "nsFrameLoaderOwner.h"
#include "nsSerializationHelper.h" #include "nsSerializationHelper.h"
#include "nsITransportSecurityInfo.h" #include "nsITransportSecurityInfo.h"
#include "nsISharePicker.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMExceptionBinding.h"
#include "mozilla/dom/JSWindowActorBinding.h" #include "mozilla/dom/JSWindowActorBinding.h"
#include "mozilla/dom/JSWindowActorParent.h" #include "mozilla/dom/JSWindowActorParent.h"
...@@ -302,6 +306,78 @@ bool WindowGlobalParent::IsCurrentGlobal() { ...@@ -302,6 +306,78 @@ bool WindowGlobalParent::IsCurrentGlobal() {
return CanSend() && mBrowsingContext->GetCurrentWindowGlobal() == this; return CanSend() && mBrowsingContext->GetCurrentWindowGlobal() == this;
} }
namespace {
class ShareHandler final : public PromiseNativeHandler {
public:
explicit ShareHandler(
mozilla::dom::WindowGlobalParent::ShareResolver&& aResolver)
: mResolver(std::move(aResolver)) {}
NS_DECL_ISUPPORTS
public:
virtual void ResolvedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue) override {
mResolver(NS_OK);
}
virtual void RejectedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue) override {
if (NS_WARN_IF(!aValue.isObject())) {
mResolver(NS_ERROR_FAILURE);
return;
}
// nsresult is stored as Exception internally in Promise
JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
RefPtr<DOMException> unwrapped;
nsresult rv = UNWRAP_OBJECT(DOMException, &obj, unwrapped);
if (NS_WARN_IF(NS_FAILED(rv))) {
mResolver(NS_ERROR_FAILURE);
return;
}
mResolver(unwrapped->GetResult());
}
private:
~ShareHandler() = default;
mozilla::dom::WindowGlobalParent::ShareResolver mResolver;
};
NS_IMPL_ISUPPORTS0(ShareHandler)
} // namespace
mozilla::ipc::IPCResult WindowGlobalParent::RecvShare(
IPCWebShareData&& aData, WindowGlobalParent::ShareResolver&& aResolver) {
// Widget Layer handoff...
nsCOMPtr<nsISharePicker> sharePicker =
do_GetService("@mozilla.org/sharepicker;1");
if (!sharePicker) {
aResolver(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return IPC_OK();
}
// And finally share the data...
RefPtr<Promise> promise;
nsresult rv = sharePicker->Share(aData.title(), aData.text(), aData.url(),
getter_AddRefs(promise));
if (NS_FAILED(rv)) {
aResolver(rv);
return IPC_OK();
}
// Handler finally awaits response...
RefPtr<ShareHandler> handler = new ShareHandler(std::move(aResolver));
promise->AppendNativeHandler(handler);
return IPC_OK();
}
already_AddRefed<Promise> WindowGlobalParent::ChangeFrameRemoteness( already_AddRefed<Promise> WindowGlobalParent::ChangeFrameRemoteness(
dom::BrowsingContext* aBc, const nsAString& aRemoteType, dom::BrowsingContext* aBc, const nsAString& aRemoteType,
uint64_t aPendingSwitchId, ErrorResult& aRv) { uint64_t aPendingSwitchId, ErrorResult& aRv) {
......
...@@ -158,6 +158,10 @@ class WindowGlobalParent final : public WindowGlobalActor, ...@@ -158,6 +158,10 @@ class WindowGlobalParent final : public WindowGlobalActor,
const Maybe<IntRect>& aRect, float aScale, const Maybe<IntRect>& aRect, float aScale,
nscolor aBackgroundColor, uint32_t aFlags); nscolor aBackgroundColor, uint32_t aFlags);
// WebShare API - try to share
mozilla::ipc::IPCResult RecvShare(IPCWebShareData&& aData,
ShareResolver&& aResolver);
private: private:
~WindowGlobalParent(); ~WindowGlobalParent();
......
...@@ -384,3 +384,5 @@ MathML_DeprecatedMencloseNotationRadical=The “radical” value is deprecated f ...@@ -384,3 +384,5 @@ MathML_DeprecatedMencloseNotationRadical=The “radical” value is deprecated f
MathML_DeprecatedStyleAttributeWarning=MathML attributes “background”, “color”, “fontfamily”, “fontsize”, “fontstyle” and “fontweight” are deprecated and will be removed at a future date. MathML_DeprecatedStyleAttributeWarning=MathML attributes “background”, “color”, “fontfamily”, “fontsize”, “fontstyle” and “fontweight” are deprecated and will be removed at a future date.
# LOCALIZATION NOTE: Do not translate MathML and XLink. # LOCALIZATION NOTE: Do not translate MathML and XLink.
MathML_DeprecatedXLinkAttributeWarning=XLink attributes “href”, “type”, “show” and “actuate” are deprecated on MathML elements and will be removed at a future date. MathML_DeprecatedXLinkAttributeWarning=XLink attributes “href”, “type”, “show” and “actuate” are deprecated on MathML elements and will be removed at a future date.
# LOCALIZATION NOTE: Do not translate title, text, url as they are the names of JS properties.
WebShareAPI_NeedOneMember=title or text or url member of the ShareData dictionary. At least one of the members is required.
...@@ -322,3 +322,15 @@ partial interface Navigator { ...@@ -322,3 +322,15 @@ partial interface Navigator {
[Pref="dom.events.asyncClipboard", SecureContext, SameObject] [Pref="dom.events.asyncClipboard", SecureContext, SameObject]
readonly attribute Clipboard clipboard; readonly attribute Clipboard clipboard;
}; };
// https://wicg.github.io/web-share/#navigator-interface
partial interface Navigator {
[SecureContext, Throws, Pref="dom.webshare.enabled"]
Promise<void> share(optional ShareData data = {});
};
// https://wicg.github.io/web-share/#sharedata-dictionary
dictionary ShareData {
USVString title;
USVString text;
USVString url;
};
...@@ -2693,6 +2693,12 @@ ...@@ -2693,6 +2693,12 @@
value: true value: true
mirror: always mirror: always
# WebShare API - exposes navigator.share()
- name: dom.webshare.enabled
type: bool
value: false
mirror: always
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Prefs starting with "editor" # Prefs starting with "editor"
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
......
prefs: [dom.webshare.enabled:true]
\ No newline at end of file
[idlharness.https.window.html]
[Navigator interface: operation share(ShareData)]
expected: FAIL
[Navigator interface: calling share(ShareData) on navigator with too few arguments must throw TypeError]
expected: FAIL
[Navigator interface: navigator must inherit property "share(ShareData)" with the proper type]
expected: FAIL
[share-empty.https.html]
[share with no arguments (same as empty dictionary)]
expected: FAIL
[share with an empty dictionary]
expected: FAIL
[share with a undefined argument (same as empty dictionary)]
expected: FAIL
[share with a null argument (same as empty dictionary)]
expected: FAIL
[share with a dictionary containing only surplus fields]
expected: FAIL
[share-sharePromise-internal-slot.https.html]
[Only allow one share call at a time, which is controlled by the [[sharePromise\]\] internal slot.]
expected: FAIL
[share-url-invalid.https.html]
[share with an invalid URL]
expected: FAIL
[share-without-user-gesture.https.html]
[share without a user gesture]
expected: FAIL
...@@ -10,21 +10,27 @@ ...@@ -10,21 +10,27 @@
<script src="/resources/testdriver-vendor.js"></script> <script src="/resources/testdriver-vendor.js"></script>
</head> </head>
<body> <body>
<button>
<script> <script>
setup({ allow_uncaught_exception:true });
promise_test(async t => { promise_test(async t => {
const [, promise2, promise3] = await test_driver.bless( const button = document.querySelector("button");
"share needs user activation", const p = new Promise(r => {
() => { button.onclick = () => {
return [ const promises = [];
promises.push(
navigator.share({ title: "should be pending" }), navigator.share({ title: "should be pending" }),
navigator.share({ title: "should reject" }), navigator.share({ title: "should reject" }),
navigator.share({ title: "should also reject" }), navigator.share({ title: "should also reject" })
]; );
} r(promises);
); };
});
test_driver.click(button);
const [, promise2, promise3] = await p;
await Promise.all([ await Promise.all([
promise_rejects(t, "InvalidStateError", promise2), promise_rejects(t, "InvalidStateError", promise2),
promise_rejects(t, "InvalidStateError", promise3), promise_rejects(t, "InvalidStateError", promise3)
]); ]);
}, "Only allow one share call at a time, which is controlled by the [[sharePromise]] internal slot."); }, "Only allow one share call at a time, which is controlled by the [[sharePromise]] internal slot.");
</script> </script>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment