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
This commit is contained in:
Marcos Cáceres 2019-10-09 10:57:11 +00:00
parent ac1ba8670f
commit fbff2e387c
24 changed files with 333 additions and 47 deletions

View File

@ -55,6 +55,7 @@ DIRS += [
'uitour',
'urlbar',
'translation',
'webshare',
]
DIRS += ['build']

View File

@ -0,0 +1,55 @@
/* 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]);

View File

@ -0,0 +1,2 @@
component {1201d357-8417-4926-a694-e6408fbedcf8} SharePicker.js
contract @mozilla.org/sharepicker;1 {1201d357-8417-4926-a694-e6408fbedcf8}

View File

@ -0,0 +1,4 @@
EXTRA_COMPONENTS += [
'SharePicker.js',
'SharePicker.manifest',
]

View File

@ -106,6 +106,7 @@
#include "mozilla/Unused.h"
#include "mozilla/webgpu/Instance.h"
#include "mozilla/dom/WindowGlobalChild.h"
namespace mozilla {
namespace dom {
@ -142,6 +143,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(Navigator)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Navigator)
tmp->Invalidate();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSharePromise)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
@ -167,6 +169,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Navigator)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepadServiceTest)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRGetDisplaysPromises)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRServiceTest)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSharePromise)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Navigator)
@ -234,6 +237,8 @@ void Navigator::Invalidate() {
mAddonManager = nullptr;
mWebGpu = nullptr;
mSharePromise = nullptr;
}
void Navigator::GetUserAgent(nsAString& aUserAgent, CallerType aCallerType,
@ -1331,6 +1336,110 @@ Promise* Navigator::GetBattery(ErrorResult& aRv) {
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() {
RefPtr<LegacyMozTCPSocket> socket = new LegacyMozTCPSocket(GetWindow());
return socket.forget();

View File

@ -80,6 +80,8 @@ class VRDisplay;
class VRServiceTest;
class StorageManager;
class MediaCapabilities;
struct ShareData;
class WindowGlobalChild;
class Navigator final : public nsISupports, public nsWrapperCache {
public:
@ -131,6 +133,8 @@ class Navigator final : public nsISupports, public nsWrapperCache {
Geolocation* GetGeolocation(ErrorResult& aRv);
Promise* GetBattery(ErrorResult& aRv);
Promise* Share(const ShareData& aData, ErrorResult& aRv);
static void AppName(nsAString& aAppName, nsIPrincipal* aCallerPrincipal,
bool aUsePrefOverriddenValue);
@ -276,6 +280,7 @@ class Navigator final : public nsISupports, public nsWrapperCache {
RefPtr<dom::MediaCapabilities> mMediaCapabilities;
RefPtr<AddonManager> mAddonManager;
RefPtr<webgpu::Instance> mWebGpu;
RefPtr<Promise> mSharePromise; // Web Share API related
};
} // namespace dom

View File

@ -28,6 +28,13 @@ struct JSWindowActorMessageMeta {
JSWindowActorMessageKind kind;
};
struct IPCWebShareData
{
nsCString title;
nsCString text;
nsIURI url;
};
/**
* A PWindowGlobal actor has a lifetime matching that of a single Window Global,
* specifically a |nsGlobalWindowInner|. These actors will form a parent/child
@ -77,6 +84,9 @@ parent:
/// Notify the parent that this PWindowGlobal is now the current global.
async BecomeCurrentWindowGlobal();
// Attempts to perform a "Web Share".
async Share(IPCWebShareData aData) returns (nsresult rv);
async Destroy();
};

View File

@ -31,6 +31,10 @@
#include "nsFrameLoaderOwner.h"
#include "nsSerializationHelper.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/JSWindowActorParent.h"
@ -302,6 +306,78 @@ bool WindowGlobalParent::IsCurrentGlobal() {
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(
dom::BrowsingContext* aBc, const nsAString& aRemoteType,
uint64_t aPendingSwitchId, ErrorResult& aRv) {

View File

@ -158,6 +158,10 @@ class WindowGlobalParent final : public WindowGlobalActor,
const Maybe<IntRect>& aRect, float aScale,
nscolor aBackgroundColor, uint32_t aFlags);
// WebShare API - try to share
mozilla::ipc::IPCResult RecvShare(IPCWebShareData&& aData,
ShareResolver&& aResolver);
private:
~WindowGlobalParent();

View File

@ -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.
# 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.
# 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.

View File

@ -322,3 +322,15 @@ partial interface Navigator {
[Pref="dom.events.asyncClipboard", SecureContext, SameObject]
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;
};

View File

@ -2693,6 +2693,12 @@
value: true
mirror: always
# WebShare API - exposes navigator.share()
- name: dom.webshare.enabled
type: bool
value: false
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "editor"
#---------------------------------------------------------------------------

View File

@ -0,0 +1 @@
prefs: [dom.webshare.enabled:true]

View File

@ -1,10 +0,0 @@
[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

View File

@ -1,16 +0,0 @@
[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

View File

@ -1,4 +0,0 @@
[share-sharePromise-internal-slot.https.html]
[Only allow one share call at a time, which is controlled by the [[sharePromise\]\] internal slot.]
expected: FAIL

View File

@ -1,4 +0,0 @@
[share-url-invalid.https.html]
[share with an invalid URL]
expected: FAIL

View File

@ -1,4 +0,0 @@
[share-without-user-gesture.https.html]
[share without a user gesture]
expected: FAIL

View File

@ -10,21 +10,27 @@
<script src="/resources/testdriver-vendor.js"></script>
</head>
<body>
<button>
<script>
setup({ allow_uncaught_exception:true });
promise_test(async t => {
const [, promise2, promise3] = await test_driver.bless(
"share needs user activation",
() => {
return [
const button = document.querySelector("button");
const p = new Promise(r => {
button.onclick = () => {
const promises = [];
promises.push(
navigator.share({ title: "should be pending" }),
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([
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.");
</script>

View File

@ -117,6 +117,7 @@ XPIDL_SOURCES += [
'nsIPrintSettingsService.idl',
'nsIScreen.idl',
'nsIScreenManager.idl',
'nsISharePicker.idl',
'nsISound.idl',
'nsITransferable.idl',
]

View File

@ -30,6 +30,7 @@ NS_DEFINE_NAMED_CID(NS_COLORPICKER_CID);
NS_DEFINE_NAMED_CID(NS_DRAGSERVICE_CID);
NS_DEFINE_NAMED_CID(NS_FILEPICKER_CID);
NS_DEFINE_NAMED_CID(NS_SOUND_CID);
NS_DEFINE_NAMED_CID(NS_SHAREPICKER_CID);
NS_DEFINE_NAMED_CID(NS_SCREENMANAGER_CID);
static const mozilla::Module::CIDEntry kWidgetCIDs[] = {
@ -59,6 +60,8 @@ static const mozilla::Module::ContractIDEntry kWidgetContracts[] = {
{"@mozilla.org/sound;1", &kNS_SOUND_CID, Module::CONTENT_PROCESS_ONLY},
{"@mozilla.org/widget/dragservice;1", &kNS_DRAGSERVICE_CID,
Module::CONTENT_PROCESS_ONLY},
{"@mozilla.org/sharepicker;1", &kNS_SHAREPICKER_CID,
Module::CONTENT_PROCESS_ONLY},
{nullptr}};
extern const mozilla::Module kContentProcessWidgetModule = {

19
widget/nsISharePicker.idl Normal file
View File

@ -0,0 +1,19 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* 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 "nsISupports.idl"
interface nsIURI;
[scriptable, uuid(1201d357-8417-4926-a694-e6408fbedcf8)]
interface nsISharePicker : nsISupports
{
/**
* XPCOM Analog of navigator.share() as per:
* https://w3c.github.io/web-share/#share-method
*/
Promise share(in AUTF8String title, in AUTF8String text, in nsIURI url);
};

View File

@ -51,6 +51,14 @@
} \
}
/*1201d357-8417-4926-a694-e6408fbedcf8*/
#define NS_SHAREPICKER_CID \
{ \
0x1201d357, 0x8417, 0x4926, { \
0xa6, 0x94, 0xe6, 0x40, 0x8f, 0xbe, 0xdc, 0xf8 \
} \
}
/* XXX the following CID's are not in order. This needs
to be fixed. */