blob: a942771d7b1f6b6f9be939aaa8d7617bb578ae3c [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/webshare/navigator_share.h"
#include <stdint.h>
#include <utility>
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_share_data.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/file.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/mojo/mojo_helper.h"
#include "third_party/blink/renderer/platform/scheduler/public/frame_or_worker_scheduler.h"
namespace blink {
namespace {
constexpr size_t kMaxSharedFileCount = 10;
constexpr uint32_t kMaxSharedFileBytes = 50U * 1024 * 1024;
// Gets the human-friendly error message for a ShareError. |error| must not be
// ShareError::OK.
String ErrorToString(mojom::blink::ShareError error) {
switch (error) {
case mojom::blink::ShareError::OK:
NOTREACHED();
break;
case mojom::blink::ShareError::INTERNAL_ERROR:
return "Share failed";
case mojom::blink::ShareError::PERMISSION_DENIED:
return "Permission denied";
case mojom::blink::ShareError::CANCELED:
return "Share canceled";
}
NOTREACHED();
return String();
}
bool HasFiles(const ShareData& data) {
if (!data.hasFiles())
return false;
return !data.files().IsEmpty();
}
// Returns true unless |share(data)| would reject with TypeError.
// Populates |url| with the result of running the URL parser on |data.url|.
// If the return value is false and |exception_state| is non null, throws
// TypeError.
//
// https://w3c.github.io/web-share/level-2/#canshare-method
// https://w3c.github.io/web-share/level-2/#share-method
bool CanShareInternal(const LocalDOMWindow& window,
const ShareData& data,
KURL& url,
ExceptionState* exception_state) {
if (!data.hasTitle() && !data.hasText() && !data.hasUrl() &&
!HasFiles(data)) {
if (exception_state) {
exception_state->ThrowTypeError(
"No known share data fields supplied. If using only new fields "
"(other than title, text and url), you must feature-detect "
"them first.");
}
return false;
}
if (data.hasUrl()) {
url = window.CompleteURL(data.url());
if (!url.IsValid() ||
(!url.ProtocolIsInHTTPFamily() &&
url.Protocol() != window.document()->BaseURL().Protocol())) {
if (exception_state) {
exception_state->ThrowTypeError("Invalid URL");
}
return false;
}
}
return true;
}
} // namespace
class NavigatorShare::ShareClientImpl final
: public GarbageCollected<ShareClientImpl> {
public:
ShareClientImpl(NavigatorShare*, bool has_files, ScriptPromiseResolver*);
void Callback(mojom::blink::ShareError);
void OnConnectionError();
void Trace(Visitor* visitor) const {
visitor->Trace(navigator_);
visitor->Trace(resolver_);
}
private:
WeakMember<NavigatorShare> navigator_;
bool has_files_;
Member<ScriptPromiseResolver> resolver_;
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle
feature_handle_for_scheduler_;
};
NavigatorShare::ShareClientImpl::ShareClientImpl(
NavigatorShare* navigator_share,
bool has_files,
ScriptPromiseResolver* resolver)
: navigator_(navigator_share),
has_files_(has_files),
resolver_(resolver),
feature_handle_for_scheduler_(
ExecutionContext::From(resolver_->GetScriptState())
->GetScheduler()
->RegisterFeature(
SchedulingPolicy::Feature::kWebShare,
{SchedulingPolicy::DisableBackForwardCache()})) {}
void NavigatorShare::ShareClientImpl::Callback(mojom::blink::ShareError error) {
if (navigator_) {
DCHECK_EQ(navigator_->client_, this);
navigator_->client_ = nullptr;
}
if (error == mojom::blink::ShareError::OK) {
UseCounter::Count(ExecutionContext::From(resolver_->GetScriptState()),
has_files_
? WebFeature::kWebShareSuccessfulContainingFiles
: WebFeature::kWebShareSuccessfulWithoutFiles);
resolver_->Resolve();
} else {
UseCounter::Count(ExecutionContext::From(resolver_->GetScriptState()),
has_files_
? WebFeature::kWebShareUnsuccessfulContainingFiles
: WebFeature::kWebShareUnsuccessfulWithoutFiles);
resolver_->Reject(MakeGarbageCollected<DOMException>(
(error == mojom::blink::ShareError::PERMISSION_DENIED)
? DOMExceptionCode::kNotAllowedError
: DOMExceptionCode::kAbortError,
ErrorToString(error)));
}
}
void NavigatorShare::ShareClientImpl::OnConnectionError() {
resolver_->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError,
"Internal error: could not connect to Web Share interface."));
}
NavigatorShare& NavigatorShare::From(Navigator& navigator) {
NavigatorShare* supplement =
Supplement<Navigator>::From<NavigatorShare>(navigator);
if (!supplement) {
supplement = MakeGarbageCollected<NavigatorShare>();
ProvideTo(navigator, supplement);
}
return *supplement;
}
void NavigatorShare::Trace(Visitor* visitor) const {
visitor->Trace(service_remote_);
visitor->Trace(client_);
Supplement<Navigator>::Trace(visitor);
}
const char NavigatorShare::kSupplementName[] = "NavigatorShare";
bool NavigatorShare::canShare(ScriptState* script_state,
const ShareData* data) {
if (!script_state->ContextIsValid())
return false;
LocalDOMWindow* window = LocalDOMWindow::From(script_state);
KURL unused_url;
return CanShareInternal(*window, *data, unused_url, nullptr);
}
bool NavigatorShare::canShare(ScriptState* script_state,
Navigator& navigator,
const ShareData* data) {
return From(navigator).canShare(script_state, data);
}
ScriptPromise NavigatorShare::share(ScriptState* script_state,
const ShareData* data,
ExceptionState& exception_state) {
if (!script_state->ContextIsValid()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kAbortError,
"Internal error: window frame is missing (the navigator may be "
"detached).");
return ScriptPromise();
}
ExecutionContext* const execution_context =
ExecutionContext::From(script_state);
// The feature policy is currently not enforced.
LocalDOMWindow* const window = LocalDOMWindow::From(script_state);
window->CountUse(execution_context->IsFeatureEnabled(
mojom::blink::FeaturePolicyFeature::kWebShare)
? WebFeature::kWebSharePolicyAllow
: WebFeature::kWebSharePolicyDisallow);
if (client_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"A earlier share had not yet completed.");
return ScriptPromise();
}
if (!LocalFrame::ConsumeTransientUserActivation(window->GetFrame())) {
VLOG(1) << "Share without transient activation (user gesture)";
exception_state.ThrowDOMException(
DOMExceptionCode::kNotAllowedError,
"Must be handling a user gesture to perform a share request.");
return ScriptPromise();
}
KURL url;
if (!CanShareInternal(*window, *data, url, &exception_state)) {
DCHECK(exception_state.HadException());
return ScriptPromise();
}
if (!service_remote_.is_bound()) {
// See https://bit.ly/2S0zRAS for task types.
window->GetFrame()->GetBrowserInterfaceBroker().GetInterface(
service_remote_.BindNewPipeAndPassReceiver(
window->GetTaskRunner(TaskType::kMiscPlatformAPI)));
service_remote_.set_disconnect_handler(WTF::Bind(
&NavigatorShare::OnConnectionError, WrapWeakPersistent(this)));
DCHECK(service_remote_.is_bound());
}
bool has_files = HasFiles(*data);
WTF::Vector<mojom::blink::SharedFilePtr> files;
uint64_t total_bytes = 0;
if (has_files) {
files.ReserveInitialCapacity(data->files().size());
for (const blink::Member<blink::File>& file : data->files()) {
total_bytes += file->size();
files.push_back(mojom::blink::SharedFile::New(file->name(),
file->GetBlobDataHandle()));
}
if (files.size() > kMaxSharedFileCount ||
total_bytes > kMaxSharedFileBytes) {
execution_context->AddConsoleMessage(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning, "Share too large");
exception_state.ThrowDOMException(DOMExceptionCode::kNotAllowedError,
"Permission denied");
return ScriptPromise();
}
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
client_ = MakeGarbageCollected<ShareClientImpl>(this, has_files, resolver);
service_remote_->Share(
data->hasTitle() ? data->title() : g_empty_string,
data->hasText() ? data->text() : g_empty_string, url, std::move(files),
WTF::Bind(&ShareClientImpl::Callback, WrapPersistent(client_.Get())));
return resolver->Promise();
}
ScriptPromise NavigatorShare::share(ScriptState* script_state,
Navigator& navigator,
const ShareData* data,
ExceptionState& exception_state) {
return From(navigator).share(script_state, data, exception_state);
}
void NavigatorShare::OnConnectionError() {
if (client_) {
client_->OnConnectionError();
client_ = nullptr;
}
service_remote_.reset();
}
} // namespace blink