blob: d67dd5b2c924b669a2afeb2053c0091c107050d9 [file] [log] [blame]
// Copyright 2017 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/payments/payment_instruments.h"
#include <utility>
#include "base/location.h"
#include "third_party/blink/public/common/feature_policy/feature_policy.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_feature.mojom-blink.h"
#include "third_party/blink/public/platform/web_icon_sizes_parser.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/modules/payments/basic_card_helper.h"
#include "third_party/blink/renderer/modules/payments/payment_instrument.h"
#include "third_party/blink/renderer/modules/payments/payment_manager.h"
#include "third_party/blink/renderer/modules/permissions/permission_utils.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
namespace {
static const char kPaymentManagerUnavailable[] = "Payment manager unavailable";
bool rejectError(ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentHandlerStatus status) {
switch (status) {
case payments::mojom::blink::PaymentHandlerStatus::SUCCESS:
return false;
case payments::mojom::blink::PaymentHandlerStatus::NOT_FOUND:
resolver->Resolve();
return true;
case payments::mojom::blink::PaymentHandlerStatus::NO_ACTIVE_WORKER:
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError, "No active service worker"));
return true;
case payments::mojom::blink::PaymentHandlerStatus::STORAGE_OPERATION_FAILED:
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError, "Storage operation is failed"));
return true;
case payments::mojom::blink::PaymentHandlerStatus::
FETCH_INSTRUMENT_ICON_FAILED: {
ScriptState::Scope scope(resolver->GetScriptState());
resolver->Reject(V8ThrowException::CreateTypeError(
resolver->GetScriptState()->GetIsolate(),
"Fetch or decode instrument icon failed"));
return true;
}
case payments::mojom::blink::PaymentHandlerStatus::
FETCH_PAYMENT_APP_INFO_FAILED:
// FETCH_PAYMENT_APP_INFO_FAILED indicates everything works well except
// fetching payment handler's name and/or icon from its web app manifest.
// The origin or name will be used to label this payment handler in
// UI in this case, so only show warnning message instead of reject the
// promise. The warning message was printed by
// payment_app_info_fetcher.cc.
return false;
}
NOTREACHED();
return false;
}
bool AllowedToUsePaymentFeatures(ScriptState* script_state) {
return ExecutionContext::From(script_state)
->GetSecurityContext()
.GetFeaturePolicy()
->IsFeatureEnabled(mojom::FeaturePolicyFeature::kPayment);
}
ScriptPromise RejectNotAllowedToUsePaymentFeatures(ScriptState* script_state) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(
DOMExceptionCode::kSecurityError,
"Must be in a top-level browsing context or an iframe "
"needs to specify allow=\"payment\" explicitly"));
}
} // namespace
// Class used to convert the placement only |PaymentInstrument| to
// GarbageCollected, so it can be used in WTF::Bind. Otherwise, on-heap objects
// referenced by |PaymentInstrument| will not be traced through the callback and
// can be prematurely destroyed.
// TODO(keishi): Remove this conversion if IDLDictionaryBase situation changes.
class PaymentInstrumentParameter
: public GarbageCollectedFinalized<PaymentInstrumentParameter> {
public:
explicit PaymentInstrumentParameter(const PaymentInstrument* instrument)
: has_icons_(instrument->hasIcons()),
has_capabilities_(instrument->hasCapabilities()),
has_method_(instrument->hasMethod()),
has_name_(instrument->hasName()),
capabilities_(instrument->capabilities()),
method_(instrument->method()),
name_(instrument->name()) {
if (has_icons_)
icons_ = instrument->icons();
}
bool has_capabilities() const { return has_capabilities_; }
ScriptValue capabilities() const { return capabilities_; }
bool has_icons() const { return has_icons_; }
const HeapVector<Member<ImageObject>>& icons() const { return icons_; }
bool has_method() const { return has_method_; }
const String& method() const { return method_; }
bool has_name() const { return has_name_; }
const String& name() const { return name_; }
void Trace(blink::Visitor* visitor) { visitor->Trace(icons_); }
private:
bool has_icons_;
bool has_capabilities_;
bool has_method_;
bool has_name_;
ScriptValue capabilities_;
HeapVector<Member<ImageObject>> icons_;
String method_;
String name_;
};
PaymentInstruments::PaymentInstruments(
const payments::mojom::blink::PaymentManagerPtr& manager)
: manager_(manager) {}
ScriptPromise PaymentInstruments::deleteInstrument(
ScriptState* script_state,
const String& instrument_key) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
manager_->DeletePaymentInstrument(
instrument_key,
WTF::Bind(&PaymentInstruments::onDeletePaymentInstrument,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
ScriptPromise PaymentInstruments::get(ScriptState* script_state,
const String& instrument_key) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
manager_->GetPaymentInstrument(
instrument_key,
WTF::Bind(&PaymentInstruments::onGetPaymentInstrument,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
ScriptPromise PaymentInstruments::keys(ScriptState* script_state) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
manager_->KeysOfPaymentInstruments(
WTF::Bind(&PaymentInstruments::onKeysOfPaymentInstruments,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
ScriptPromise PaymentInstruments::has(ScriptState* script_state,
const String& instrument_key) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
manager_->HasPaymentInstrument(
instrument_key,
WTF::Bind(&PaymentInstruments::onHasPaymentInstrument,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
ScriptPromise PaymentInstruments::set(ScriptState* script_state,
const String& instrument_key,
const PaymentInstrument* details,
ExceptionState& exception_state) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ExecutionContext* context = ExecutionContext::From(script_state);
Document* doc = DynamicTo<Document>(context);
// Should move this permission check to browser process.
// Please see http://crbug.com/795929
GetPermissionService(script_state)
->RequestPermission(
CreatePermissionDescriptor(
mojom::blink::PermissionName::PAYMENT_HANDLER),
LocalFrame::HasTransientUserActivation(doc ? doc->GetFrame()
: nullptr),
WTF::Bind(
&PaymentInstruments::OnRequestPermission, WrapPersistent(this),
WrapPersistent(resolver), instrument_key,
WrapPersistent(
MakeGarbageCollected<PaymentInstrumentParameter>(details))));
return resolver->Promise();
}
ScriptPromise PaymentInstruments::clear(ScriptState* script_state) {
if (!AllowedToUsePaymentFeatures(script_state))
return RejectNotAllowedToUsePaymentFeatures(script_state);
if (!manager_.is_bound()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kPaymentManagerUnavailable));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
manager_->ClearPaymentInstruments(
WTF::Bind(&PaymentInstruments::onClearPaymentInstruments,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
mojom::blink::PermissionService* PaymentInstruments::GetPermissionService(
ScriptState* script_state) {
if (!permission_service_) {
ConnectToPermissionService(ExecutionContext::From(script_state),
mojo::MakeRequest(&permission_service_));
}
return permission_service_.get();
}
void PaymentInstruments::OnRequestPermission(
ScriptPromiseResolver* resolver,
const String& instrument_key,
PaymentInstrumentParameter* details,
mojom::blink::PermissionStatus status) {
DCHECK(resolver);
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState::Scope scope(resolver->GetScriptState());
if (status != mojom::blink::PermissionStatus::GRANTED) {
resolver->Reject(
DOMException::Create(DOMExceptionCode::kNotAllowedError,
"Not allowed to install this payment handler"));
return;
}
payments::mojom::blink::PaymentInstrumentPtr instrument =
payments::mojom::blink::PaymentInstrument::New();
instrument->name =
details->has_name() ? details->name() : WTF::g_empty_string;
if (details->has_icons()) {
ExecutionContext* context =
ExecutionContext::From(resolver->GetScriptState());
for (const ImageObject* image_object : details->icons()) {
KURL parsed_url = context->CompleteURL(image_object->src());
if (!parsed_url.IsValid() || !parsed_url.ProtocolIsInHTTPFamily()) {
resolver->Reject(V8ThrowException::CreateTypeError(
resolver->GetScriptState()->GetIsolate(),
"'" + image_object->src() + "' is not a valid URL."));
return;
}
mojom::blink::ManifestImageResourcePtr icon =
mojom::blink::ManifestImageResource::New();
icon->src = parsed_url;
icon->type = image_object->type();
icon->purpose.push_back(blink::mojom::ManifestImageResource_Purpose::ANY);
WebVector<WebSize> web_sizes =
WebIconSizesParser::ParseIconSizes(image_object->sizes());
for (const auto& web_size : web_sizes) {
icon->sizes.push_back(web_size);
}
instrument->icons.push_back(std::move(icon));
}
}
instrument->method =
details->has_method() ? details->method() : WTF::g_empty_string;
if (details->has_capabilities()) {
v8::Local<v8::String> value;
if (!v8::JSON::Stringify(resolver->GetScriptState()->GetContext(),
details->capabilities().V8Value().As<v8::Object>())
.ToLocal(&value)) {
resolver->Reject(V8ThrowException::CreateTypeError(
resolver->GetScriptState()->GetIsolate(),
"Capabilities should be a JSON-serializable object"));
return;
}
instrument->stringified_capabilities = ToCoreString(value);
if (instrument->method == "basic-card") {
ExceptionState exception_state(resolver->GetScriptState()->GetIsolate(),
ExceptionState::kSetterContext,
"PaymentInstruments", "set");
BasicCardHelper::ParseBasiccardData(
details->capabilities(), instrument->supported_networks,
instrument->supported_types, exception_state);
if (exception_state.HadException()) {
resolver->Reject(exception_state);
return;
}
}
} else {
instrument->stringified_capabilities = WTF::g_empty_string;
}
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kPaymentHandler);
manager_->SetPaymentInstrument(
instrument_key, std::move(instrument),
WTF::Bind(&PaymentInstruments::onSetPaymentInstrument,
WrapPersistent(this), WrapPersistent(resolver)));
}
void PaymentInstruments::onDeletePaymentInstrument(
ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
resolver->Resolve(status ==
payments::mojom::blink::PaymentHandlerStatus::SUCCESS);
}
void PaymentInstruments::onGetPaymentInstrument(
ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentInstrumentPtr stored_instrument,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState::Scope scope(resolver->GetScriptState());
if (rejectError(resolver, status))
return;
PaymentInstrument* instrument = PaymentInstrument::Create();
instrument->setName(stored_instrument->name);
HeapVector<Member<ImageObject>> icons;
for (const auto& icon : stored_instrument->icons) {
ImageObject* image_object = ImageObject::Create();
image_object->setSrc(icon->src.GetString());
image_object->setType(icon->type);
String sizes = WTF::g_empty_string;
for (const auto& size : icon->sizes) {
sizes = sizes + String::Format("%dx%d ", size.width, size.height);
}
image_object->setSizes(sizes.StripWhiteSpace());
icons.push_back(image_object);
}
instrument->setIcons(icons);
instrument->setMethod(stored_instrument->method);
if (!stored_instrument->stringified_capabilities.IsEmpty()) {
ExceptionState exception_state(resolver->GetScriptState()->GetIsolate(),
ExceptionState::kGetterContext,
"PaymentInstruments", "get");
instrument->setCapabilities(
ScriptValue(resolver->GetScriptState(),
FromJSONString(resolver->GetScriptState()->GetIsolate(),
resolver->GetScriptState()->GetContext(),
stored_instrument->stringified_capabilities,
exception_state)));
if (exception_state.HadException()) {
resolver->Reject(exception_state);
return;
}
}
resolver->Resolve(instrument);
}
void PaymentInstruments::onKeysOfPaymentInstruments(
ScriptPromiseResolver* resolver,
const Vector<String>& keys,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
if (rejectError(resolver, status))
return;
resolver->Resolve(keys);
}
void PaymentInstruments::onHasPaymentInstrument(
ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
resolver->Resolve(status ==
payments::mojom::blink::PaymentHandlerStatus::SUCCESS);
}
void PaymentInstruments::onSetPaymentInstrument(
ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
if (rejectError(resolver, status))
return;
resolver->Resolve();
}
void PaymentInstruments::onClearPaymentInstruments(
ScriptPromiseResolver* resolver,
payments::mojom::blink::PaymentHandlerStatus status) {
DCHECK(resolver);
if (rejectError(resolver, status))
return;
resolver->Resolve();
}
} // namespace blink