diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/payments/PaymentRequest.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/payments/PaymentRequest.cpp')
-rw-r--r-- | dom/payments/PaymentRequest.cpp | 1261 |
1 files changed, 1261 insertions, 0 deletions
diff --git a/dom/payments/PaymentRequest.cpp b/dom/payments/PaymentRequest.cpp new file mode 100644 index 0000000000..00f6345f0b --- /dev/null +++ b/dom/payments/PaymentRequest.cpp @@ -0,0 +1,1261 @@ +/* -*- 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 "BasicCardPayment.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/PaymentMethodChangeEvent.h" +#include "mozilla/dom/PaymentRequest.h" +#include "mozilla/dom/PaymentRequestChild.h" +#include "mozilla/dom/PaymentRequestManager.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsContentUtils.h" +#include "nsIDUtils.h" +#include "nsImportModule.h" +#include "nsIRegion.h" +#include "nsIScriptError.h" +#include "nsIURLParser.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/MerchantValidationEvent.h" +#include "PaymentResponse.h" + +using mozilla::intl::LocaleService; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PaymentRequest) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResultPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAcceptPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbortPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponse) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFullShippingAddress) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResultPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAcceptPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAbortPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResponse) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFullShippingAddress) + tmp->UnregisterActivityObserver(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentRequest) + NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(PaymentRequest, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(PaymentRequest, DOMEventTargetHelper) + +bool PaymentRequest::PrefEnabled(JSContext* aCx, JSObject* aObj) { +#if defined(NIGHTLY_BUILD) + if (!XRE_IsContentProcess()) { + return false; + } + if (!StaticPrefs::dom_payments_request_enabled()) { + return false; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + + nsCOMPtr<nsIRegion> regionJsm = + do_ImportESModule("resource://gre/modules/Region.sys.mjs", "Region"); + nsAutoString region; + nsresult rv = regionJsm->GetHome(region); + if (NS_FAILED(rv)) { + return false; + } + + if (!manager->IsRegionSupported(region)) { + return false; + } + nsAutoCString locale; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale); + mozilla::intl::Locale loc; + auto result = mozilla::intl::LocaleParser::TryParse(locale, loc); + if (!(result.isOk() && loc.Canonicalize().isOk() && + loc.Language().EqualTo("en") && loc.Region().EqualTo("US"))) { + return false; + } + + return true; +#else + return false; +#endif +} + +void PaymentRequest::IsValidStandardizedPMI(const nsAString& aIdentifier, + ErrorResult& aRv) { + /* + * The syntax of a standardized payment method identifier is given by the + * following [ABNF]: + * + * stdpmi = part *( "-" part ) + * part = 1loweralpha *( DIGIT / loweralpha ) + * loweralpha = %x61-7A + */ + const char16_t* start = aIdentifier.BeginReading(); + const char16_t* end = aIdentifier.EndReading(); + while (start != end) { + // the first char must be in the range %x61-7A + if ((*start < 'a' || *start > 'z')) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral( + "' at the beginning or after the '-' must be in the range [a-z]."); + aRv.ThrowRangeError(error); + return; + } + ++start; + // the rest can be in the range %x61-7A + DIGITs + while (start != end && *start != '-' && + ((*start >= 'a' && *start <= 'z') || + (*start >= '0' && *start <= '9'))) { + ++start; + } + // if the char is not in the range %x61-7A + DIGITs, it must be '-' + if (start != end && *start != '-') { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral("' must be in the range [a-zA-z0-9-]."); + aRv.ThrowRangeError(error); + return; + } + if (*start == '-') { + ++start; + // the last char can not be '-' + if (start == end) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The last character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral("' must be in the range [a-z0-9]."); + aRv.ThrowRangeError(error); + return; + } + } + } +} + +void PaymentRequest::IsValidPaymentMethodIdentifier( + const nsAString& aIdentifier, ErrorResult& aRv) { + if (aIdentifier.IsEmpty()) { + aRv.ThrowTypeError("Payment method identifier is required."); + return; + } + /* + * URL-based payment method identifier + * + * 1. If url's scheme is not "https", return false. + * 2. If url's username or password is not the empty string, return false. + * 3. Otherwise, return true. + */ + nsCOMPtr<nsIURLParser> urlParser = do_GetService(NS_STDURLPARSER_CONTRACTID); + MOZ_ASSERT(urlParser); + uint32_t schemePos = 0; + int32_t schemeLen = 0; + uint32_t authorityPos = 0; + int32_t authorityLen = 0; + NS_ConvertUTF16toUTF8 url(aIdentifier); + nsresult rv = + urlParser->ParseURL(url.get(), url.Length(), &schemePos, &schemeLen, + &authorityPos, &authorityLen, nullptr, nullptr); + if (NS_FAILED(rv)) { + nsAutoCString error; + error.AppendLiteral("Error parsing payment method identifier '"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("'as a URL."); + aRv.ThrowRangeError(error); + return; + } + + if (schemeLen == -1) { + // The PMI is not a URL-based PMI, check if it is a standardized PMI + IsValidStandardizedPMI(aIdentifier, aRv); + return; + } + if (!Substring(aIdentifier, schemePos, schemeLen).EqualsASCII("https")) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The scheme must be 'https'."); + aRv.ThrowRangeError(error); + return; + } + if (Substring(aIdentifier, authorityPos, authorityLen).IsEmpty()) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } + + uint32_t usernamePos = 0; + int32_t usernameLen = 0; + uint32_t passwordPos = 0; + int32_t passwordLen = 0; + uint32_t hostnamePos = 0; + int32_t hostnameLen = 0; + int32_t port = 0; + + NS_ConvertUTF16toUTF8 authority( + Substring(aIdentifier, authorityPos, authorityLen)); + rv = urlParser->ParseAuthority( + authority.get(), authority.Length(), &usernamePos, &usernameLen, + &passwordPos, &passwordLen, &hostnamePos, &hostnameLen, &port); + if (NS_FAILED(rv)) { + // Handle the special cases that URLParser treats it as an invalid URL, but + // are used in web-platform-test + // For exmaple: + // https://:@example.com // should be considered as valid + // https://:password@example.com. // should be considered as invalid + int32_t atPos = authority.FindChar('@'); + if (atPos >= 0) { + // only accept the case https://:@xxx + if (atPos == 1 && authority.CharAt(0) == ':') { + usernamePos = 0; + usernameLen = 0; + passwordPos = 0; + passwordLen = 0; + } else { + // for the fail cases, don't care about what the actual length is. + usernamePos = 0; + usernameLen = INT32_MAX; + passwordPos = 0; + passwordLen = INT32_MAX; + } + } else { + usernamePos = 0; + usernameLen = -1; + passwordPos = 0; + passwordLen = -1; + } + // Parse server information when both username and password are empty or do + // not exist. + if ((usernameLen <= 0) && (passwordLen <= 0)) { + if (authority.Length() - atPos - 1 == 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } + // Re-using nsIURLParser::ParseServerInfo to extract the hostname and port + // information. This can help us to handle complicated IPv6 cases. + nsAutoCString serverInfo( + Substring(authority, atPos + 1, authority.Length() - atPos - 1)); + rv = urlParser->ParseServerInfo(serverInfo.get(), serverInfo.Length(), + &hostnamePos, &hostnameLen, &port); + if (NS_FAILED(rv)) { + // ParseServerInfo returns NS_ERROR_MALFORMED_URI in all fail cases, we + // probably need a followup bug to figure out the fail reason. + nsAutoCString error; + error.AssignLiteral("Error extracting hostname from '"); + error.Append(serverInfo); + error.AppendLiteral("'."); + aRv.ThrowRangeError(error); + return; + } + } + } + // PMI is valid when usernameLen/passwordLen equals to -1 or 0. + if (usernameLen > 0 || passwordLen > 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AssignLiteral("' is not valid. Username and password must be empty."); + aRv.ThrowRangeError(error); + return; + } + + // PMI is valid when hostnameLen is larger than 0 + if (hostnameLen <= 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } +} + +void PaymentRequest::IsValidMethodData( + JSContext* aCx, const Sequence<PaymentMethodData>& aMethodData, + ErrorResult& aRv) { + if (!aMethodData.Length()) { + aRv.ThrowTypeError("At least one payment method is required."); + return; + } + + nsTArray<nsString> methods; + for (const PaymentMethodData& methodData : aMethodData) { + IsValidPaymentMethodIdentifier(methodData.mSupportedMethods, aRv); + if (aRv.Failed()) { + return; + } + + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + if (service->IsBasicCardPayment(methodData.mSupportedMethods)) { + if (!methodData.mData.WasPassed()) { + continue; + } + MOZ_ASSERT(aCx); + nsAutoString error; + if (!service->IsValidBasicCardRequest(aCx, methodData.mData.Value(), + error)) { + aRv.ThrowTypeError(NS_ConvertUTF16toUTF8(error)); + return; + } + } + if (!methods.Contains(methodData.mSupportedMethods)) { + methods.AppendElement(methodData.mSupportedMethods); + } else { + aRv.ThrowRangeError(nsPrintfCString( + "Duplicate payment method '%s'", + NS_ConvertUTF16toUTF8(methodData.mSupportedMethods).get())); + return; + } + } +} + +void PaymentRequest::IsValidNumber(const nsAString& aItem, + const nsAString& aStr, ErrorResult& aRv) { + nsresult error = NS_ERROR_FAILURE; + + if (!aStr.IsEmpty()) { + nsAutoString aValue(aStr); + + // If the beginning character is '-', we will check the second one. + int beginningIndex = (aValue.First() == '-') ? 1 : 0; + + // Ensure + // - the beginning character is a digit in [0-9], and + // - the last character is not '.' + // to follow spec: + // https://w3c.github.io/browser-payment-api/#dfn-valid-decimal-monetary-value + // + // For example, ".1" is not valid for '.' is not in [0-9], + // and " 0.1" either for beginning with ' ' + if (aValue.Last() != '.' && aValue.CharAt(beginningIndex) >= '0' && + aValue.CharAt(beginningIndex) <= '9') { + aValue.ToFloat(&error); + } + } + + if (NS_FAILED(error)) { + nsAutoCString errorMsg; + errorMsg.AssignLiteral("The amount.value of \""); + errorMsg.Append(NS_ConvertUTF16toUTF8(aItem)); + errorMsg.AppendLiteral("\"("); + errorMsg.Append(NS_ConvertUTF16toUTF8(aStr)); + errorMsg.AppendLiteral(") must be a valid decimal monetary value."); + aRv.ThrowTypeError(errorMsg); + return; + } +} + +void PaymentRequest::IsNonNegativeNumber(const nsAString& aItem, + const nsAString& aStr, + ErrorResult& aRv) { + nsresult error = NS_ERROR_FAILURE; + + if (!aStr.IsEmpty()) { + nsAutoString aValue(aStr); + // Ensure + // - the beginning character is a digit in [0-9], and + // - the last character is not '.' + if (aValue.Last() != '.' && aValue.First() >= '0' && + aValue.First() <= '9') { + aValue.ToFloat(&error); + } + } + + if (NS_FAILED(error)) { + nsAutoCString errorMsg; + errorMsg.AssignLiteral("The amount.value of \""); + errorMsg.Append(NS_ConvertUTF16toUTF8(aItem)); + errorMsg.AppendLiteral("\"("); + errorMsg.Append(NS_ConvertUTF16toUTF8(aStr)); + errorMsg.AppendLiteral( + ") must be a valid and non-negative decimal monetary value."); + aRv.ThrowTypeError(errorMsg); + return; + } +} + +void PaymentRequest::IsValidCurrency(const nsAString& aItem, + const nsAString& aCurrency, + ErrorResult& aRv) { + /* + * According to spec in + * https://w3c.github.io/payment-request/#validity-checkers, perform currency + * validation with following criteria + * 1. The currency length must be 3. + * 2. The currency contains any character that must be in the range "A" to + * "Z" (U+0041 to U+005A) or the range "a" to "z" (U+0061 to U+007A) + */ + if (aCurrency.Length() != 3) { + nsAutoCString error; + error.AssignLiteral("The length amount.currency of \""); + error.Append(NS_ConvertUTF16toUTF8(aItem)); + error.AppendLiteral("\"("); + error.Append(NS_ConvertUTF16toUTF8(aCurrency)); + error.AppendLiteral(") must be 3."); + aRv.ThrowRangeError(error); + return; + } + // Don't use nsUnicharUtils::ToUpperCase, it converts the invalid "ınr" PMI to + // to the valid one "INR". + for (uint32_t idx = 0; idx < aCurrency.Length(); ++idx) { + if ((aCurrency.CharAt(idx) >= 'A' && aCurrency.CharAt(idx) <= 'Z') || + (aCurrency.CharAt(idx) >= 'a' && aCurrency.CharAt(idx) <= 'z')) { + continue; + } + nsAutoCString error; + error.AssignLiteral("The character amount.currency of \""); + error.Append(NS_ConvertUTF16toUTF8(aItem)); + error.AppendLiteral("\"("); + error.Append(NS_ConvertUTF16toUTF8(aCurrency)); + error.AppendLiteral( + ") must be in the range 'A' to 'Z'(U+0041 to U+005A) or 'a' to " + "'z'(U+0061 to U+007A)."); + aRv.ThrowRangeError(error); + return; + } +} + +void PaymentRequest::IsValidCurrencyAmount(const nsAString& aItem, + const PaymentCurrencyAmount& aAmount, + const bool aIsTotalItem, + ErrorResult& aRv) { + IsValidCurrency(aItem, aAmount.mCurrency, aRv); + if (aRv.Failed()) { + return; + } + if (aIsTotalItem) { + IsNonNegativeNumber(aItem, aAmount.mValue, aRv); + if (aRv.Failed()) { + return; + } + } else { + IsValidNumber(aItem, aAmount.mValue, aRv); + if (aRv.Failed()) { + return; + } + } +} + +void PaymentRequest::IsValidDetailsInit(const PaymentDetailsInit& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value and amount.currency of detail.total + IsValidCurrencyAmount(u"details.total"_ns, aDetails.mTotal.mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + return IsValidDetailsBase(aDetails, aRequestShipping, aRv); +} + +void PaymentRequest::IsValidDetailsUpdate(const PaymentDetailsUpdate& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value and amount.currency of detail.total + if (aDetails.mTotal.WasPassed()) { + IsValidCurrencyAmount(u"details.total"_ns, aDetails.mTotal.Value().mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + IsValidDetailsBase(aDetails, aRequestShipping, aRv); +} + +void PaymentRequest::IsValidDetailsBase(const PaymentDetailsBase& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value of each item in the display items + if (aDetails.mDisplayItems.WasPassed()) { + const Sequence<PaymentItem>& displayItems = aDetails.mDisplayItems.Value(); + for (const PaymentItem& displayItem : displayItems) { + IsValidCurrencyAmount(displayItem.mLabel, displayItem.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + } + + // Check the shipping option + if (aDetails.mShippingOptions.WasPassed() && aRequestShipping) { + const Sequence<PaymentShippingOption>& shippingOptions = + aDetails.mShippingOptions.Value(); + nsTArray<nsString> seenIDs; + for (const PaymentShippingOption& shippingOption : shippingOptions) { + IsValidCurrencyAmount(u"details.shippingOptions"_ns, + shippingOption.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + if (seenIDs.Contains(shippingOption.mId)) { + nsAutoCString error; + error.AssignLiteral("Duplicate shippingOption id '"); + error.Append(NS_ConvertUTF16toUTF8(shippingOption.mId)); + error.AppendLiteral("'"); + aRv.ThrowTypeError(error); + return; + } + seenIDs.AppendElement(shippingOption.mId); + } + } + + // Check payment details modifiers + if (aDetails.mModifiers.WasPassed()) { + const Sequence<PaymentDetailsModifier>& modifiers = + aDetails.mModifiers.Value(); + for (const PaymentDetailsModifier& modifier : modifiers) { + IsValidPaymentMethodIdentifier(modifier.mSupportedMethods, aRv); + if (aRv.Failed()) { + return; + } + if (modifier.mTotal.WasPassed()) { + IsValidCurrencyAmount(u"details.modifiers.total"_ns, + modifier.mTotal.Value().mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + if (modifier.mAdditionalDisplayItems.WasPassed()) { + const Sequence<PaymentItem>& displayItems = + modifier.mAdditionalDisplayItems.Value(); + for (const PaymentItem& displayItem : displayItems) { + IsValidCurrencyAmount(displayItem.mLabel, displayItem.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + } + } + } +} + +already_AddRefed<PaymentRequest> PaymentRequest::Constructor( + const GlobalObject& aGlobal, const Sequence<PaymentMethodData>& aMethodData, + const PaymentDetailsInit& aDetails, const PaymentOptions& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.ThrowAbortError("No global object for creating PaymentRequest"); + return nullptr; + } + + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (!doc) { + aRv.ThrowAbortError("No document for creating PaymentRequest"); + return nullptr; + } + + // the feature can only be used in an active document + if (!doc->IsCurrentActiveDocument()) { + aRv.ThrowSecurityError( + "Can't create a PaymentRequest for an inactive document"); + return nullptr; + } + + if (!FeaturePolicyUtils::IsFeatureAllowed(doc, u"payment"_ns)) { + aRv.ThrowSecurityError( + "Document's Feature Policy does not allow to create a PaymentRequest"); + return nullptr; + } + + // Get the top same process document + nsCOMPtr<Document> topSameProcessDoc = doc; + topSameProcessDoc = doc; + while (topSameProcessDoc) { + nsCOMPtr<Document> parent = topSameProcessDoc->GetInProcessParentDocument(); + if (!parent || !parent->IsContentDocument()) { + break; + } + topSameProcessDoc = parent; + } + nsCOMPtr<nsIPrincipal> topLevelPrincipal = topSameProcessDoc->NodePrincipal(); + + // Check payment methods and details + IsValidMethodData(aGlobal.Context(), aMethodData, aRv); + if (aRv.Failed()) { + return nullptr; + } + IsValidDetailsInit(aDetails, aOptions.mRequestShipping, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + if (NS_WARN_IF(!manager)) { + return nullptr; + } + + // Create PaymentRequest and set its |mId| + RefPtr<PaymentRequest> request; + manager->CreatePayment(aGlobal.Context(), window, topLevelPrincipal, + aMethodData, aDetails, aOptions, + getter_AddRefs(request), aRv); + if (aRv.Failed()) { + return nullptr; + } + return request.forget(); +} + +already_AddRefed<PaymentRequest> PaymentRequest::CreatePaymentRequest( + nsPIDOMWindowInner* aWindow, ErrorResult& aRv) { + // Generate a unique id for identification + nsID uuid; + if (NS_WARN_IF(NS_FAILED(nsID::GenerateUUIDInPlace(uuid)))) { + aRv.ThrowAbortError( + "Failed to create an internal UUID for the PaymentRequest"); + return nullptr; + } + + NSID_TrimBracketsUTF16 id(uuid); + + // Create payment request with generated id + RefPtr<PaymentRequest> request = new PaymentRequest(aWindow, id); + return request.forget(); +} + +PaymentRequest::PaymentRequest(nsPIDOMWindowInner* aWindow, + const nsAString& aInternalId) + : DOMEventTargetHelper(aWindow), + mInternalId(aInternalId), + mShippingAddress(nullptr), + mUpdating(false), + mRequestShipping(false), + mState(eCreated), + mIPC(nullptr) { + MOZ_ASSERT(aWindow); + RegisterActivityObserver(); +} + +already_AddRefed<Promise> PaymentRequest::CanMakePayment(ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + if (mState != eCreated) { + aRv.ThrowInvalidStateError( + "The PaymentRequest's state should be 'Created'"); + return nullptr; + } + + if (mResultPromise) { + // XXX This doesn't match the spec but does match Chromium. + aRv.ThrowNotAllowedError( + "PaymentRequest.CanMakePayment() has already been called"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->CanMakePayment(this, aRv); + if (aRv.Failed()) { + return nullptr; + } + mResultPromise = promise; + return promise.forget(); +} + +void PaymentRequest::RespondCanMakePayment(bool aResult) { + MOZ_ASSERT(mResultPromise); + mResultPromise->MaybeResolve(aResult); + mResultPromise = nullptr; +} + +already_AddRefed<Promise> PaymentRequest::Show( + const Optional<OwningNonNull<Promise>>& aDetailsPromise, ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global); + Document* doc = win->GetExtantDoc(); + + if (!UserActivation::IsHandlingUserInput()) { + nsString msg = nsLiteralString( + u"User activation is now required to call PaymentRequest.show()"); + nsContentUtils::ReportToConsoleNonLocalized( + msg, nsIScriptError::warningFlag, "Security"_ns, doc); + if (StaticPrefs::dom_payments_request_user_interaction_required()) { + aRv.ThrowSecurityError(NS_ConvertUTF16toUTF8(msg)); + return nullptr; + } + } + + if (mState != eCreated) { + aRv.ThrowInvalidStateError( + "The PaymentRequest's state should be 'Created'"); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + mState = eClosed; + return nullptr; + } + + if (aDetailsPromise.WasPassed()) { + aDetailsPromise.Value().AppendNativeHandler(this); + mUpdating = true; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->ShowPayment(this, aRv); + if (aRv.Failed()) { + mState = eClosed; + return nullptr; + } + + mAcceptPromise = promise; + mState = eInteractive; + return promise.forget(); +} + +void PaymentRequest::RejectShowPayment(ErrorResult&& aRejectReason) { + MOZ_ASSERT(mAcceptPromise || mResponse); + MOZ_ASSERT(mState == eInteractive); + + if (mResponse) { + mResponse->RejectRetry(std::move(aRejectReason)); + } else { + mAcceptPromise->MaybeReject(std::move(aRejectReason)); + } + mState = eClosed; + mAcceptPromise = nullptr; +} + +void PaymentRequest::RespondShowPayment(const nsAString& aMethodName, + const ResponseData& aDetails, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone, + ErrorResult&& aResult) { + MOZ_ASSERT(mState == eInteractive); + + if (aResult.Failed()) { + RejectShowPayment(std::move(aResult)); + return; + } + + // https://github.com/w3c/payment-request/issues/692 + mShippingAddress.swap(mFullShippingAddress); + mFullShippingAddress = nullptr; + + if (mResponse) { + mResponse->RespondRetry(aMethodName, mShippingOption, mShippingAddress, + aDetails, aPayerName, aPayerEmail, aPayerPhone); + } else if (mAcceptPromise) { + RefPtr<PaymentResponse> paymentResponse = new PaymentResponse( + GetOwner(), this, mId, aMethodName, mShippingOption, mShippingAddress, + aDetails, aPayerName, aPayerEmail, aPayerPhone); + mResponse = paymentResponse; + mAcceptPromise->MaybeResolve(paymentResponse); + } else { + // mAccpetPromise could be nulled through document activity changed. And + // there is nothing to do here. + mState = eClosed; + return; + } + + mState = eClosed; + mAcceptPromise = nullptr; +} + +void PaymentRequest::RespondComplete() { + MOZ_ASSERT(mResponse); + mResponse->RespondComplete(); +} + +already_AddRefed<Promise> PaymentRequest::Abort(ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + if (mState != eInteractive) { + aRv.ThrowSecurityError( + "The PaymentRequest's state should be 'Interactive'"); + return nullptr; + } + + if (mAbortPromise) { + aRv.ThrowInvalidStateError( + "PaymentRequest.abort() has already been called"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->AbortPayment(this, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mAbortPromise = promise; + return promise.forget(); +} + +void PaymentRequest::RespondAbortPayment(bool aSuccess) { + // Check whether we are aborting the update: + // + // - If |mUpdateError| is failed, we are aborting the update as + // |mUpdateError| was set in method |AbortUpdate|. + // => Reject |mAcceptPromise| and reset |mUpdateError| to complete + // the action, regardless of |aSuccess|. + // + // - Otherwise, we are handling |Abort| method call from merchant. + // => Resolve/Reject |mAbortPromise| based on |aSuccess|. + if (mUpdateError.Failed()) { + // Respond show with mUpdateError, set mUpdating to false. + mUpdating = false; + RespondShowPayment(u""_ns, ResponseData(), u""_ns, u""_ns, u""_ns, + std::move(mUpdateError)); + return; + } + + if (mState != eInteractive) { + return; + } + + if (mAbortPromise) { + if (aSuccess) { + mAbortPromise->MaybeResolve(JS::UndefinedHandleValue); + mAbortPromise = nullptr; + ErrorResult abortResult; + abortResult.ThrowAbortError("The PaymentRequest is aborted"); + RejectShowPayment(std::move(abortResult)); + } else { + mAbortPromise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + mAbortPromise = nullptr; + } + } +} + +void PaymentRequest::UpdatePayment(JSContext* aCx, + const PaymentDetailsUpdate& aDetails, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + if (mState != eInteractive) { + aRv.ThrowInvalidStateError( + "The PaymentRequest state should be 'Interactive'"); + return; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->UpdatePayment(aCx, this, aDetails, mRequestShipping, aRv); +} + +void PaymentRequest::AbortUpdate(ErrorResult& aReason) { + // AbortUpdate has the responsiblity to call aReason.SuppressException() when + // fail to update. + + MOZ_ASSERT(aReason.Failed()); + + // Completely ignoring the call when the owner document is not fully active. + if (!InFullyActiveDocument()) { + aReason.SuppressException(); + return; + } + + // Completely ignoring the call when the PaymentRequest state is not + // eInteractive. + if (mState != eInteractive) { + aReason.SuppressException(); + return; + } + // Try to close down any remaining user interface. Should recevie + // RespondAbortPayment from chrome process. + // Completely ignoring the call when failed to send action to chrome process. + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + IgnoredErrorResult result; + manager->AbortPayment(this, result); + if (result.Failed()) { + aReason.SuppressException(); + return; + } + + // Remember update error |aReason| and do the following steps in + // RespondShowPayment. + // 1. Set target.state to closed + // 2. Reject the promise target.acceptPromise with exception "aRv" + // 3. Abort the algorithm with update error + mUpdateError = std::move(aReason); +} + +void PaymentRequest::RetryPayment(JSContext* aCx, + const PaymentValidationErrors& aErrors, + ErrorResult& aRv) { + if (mState == eInteractive) { + aRv.ThrowInvalidStateError( + "Call Retry() when the PaymentReqeust state is 'Interactive'"); + return; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->RetryPayment(aCx, this, aErrors, aRv); + if (aRv.Failed()) { + return; + } + mState = eInteractive; +} + +void PaymentRequest::GetId(nsAString& aRetVal) const { aRetVal = mId; } + +void PaymentRequest::GetInternalId(nsAString& aRetVal) { + aRetVal = mInternalId; +} + +void PaymentRequest::SetId(const nsAString& aId) { mId = aId; } + +bool PaymentRequest::Equals(const nsAString& aInternalId) const { + return mInternalId.Equals(aInternalId); +} + +bool PaymentRequest::ReadyForUpdate() { + return mState == eInteractive && !mUpdating; +} + +void PaymentRequest::SetUpdating(bool aUpdating) { mUpdating = aUpdating; } + +already_AddRefed<PaymentResponse> PaymentRequest::GetResponse() const { + RefPtr<PaymentResponse> response = mResponse; + return response.forget(); +} + +nsresult PaymentRequest::DispatchUpdateEvent(const nsAString& aType) { + MOZ_ASSERT(ReadyForUpdate()); + + PaymentRequestUpdateEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<PaymentRequestUpdateEvent> event = + PaymentRequestUpdateEvent::Constructor(this, aType, init); + event->SetTrusted(true); + event->SetRequest(this); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +nsresult PaymentRequest::DispatchMerchantValidationEvent( + const nsAString& aType) { + MOZ_ASSERT(ReadyForUpdate()); + + MerchantValidationEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mValidationURL.Truncate(); + + ErrorResult rv; + RefPtr<MerchantValidationEvent> event = + MerchantValidationEvent::Constructor(this, aType, init, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + event->SetTrusted(true); + event->SetRequest(this); + + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +nsresult PaymentRequest::DispatchPaymentMethodChangeEvent( + const nsAString& aMethodName, const ChangeDetails& aMethodDetails) { + MOZ_ASSERT(ReadyForUpdate()); + + PaymentRequestUpdateEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<PaymentMethodChangeEvent> event = + PaymentMethodChangeEvent::Constructor(this, u"paymentmethodchange"_ns, + init, aMethodName, aMethodDetails); + event->SetTrusted(true); + event->SetRequest(this); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +already_AddRefed<PaymentAddress> PaymentRequest::GetShippingAddress() const { + RefPtr<PaymentAddress> address = mShippingAddress; + return address.forget(); +} + +nsresult PaymentRequest::UpdateShippingAddress( + const nsAString& aCountry, const nsTArray<nsString>& aAddressLine, + const nsAString& aRegion, const nsAString& aRegionCode, + const nsAString& aCity, const nsAString& aDependentLocality, + const nsAString& aPostalCode, const nsAString& aSortingCode, + const nsAString& aOrganization, const nsAString& aRecipient, + const nsAString& aPhone) { + nsTArray<nsString> emptyArray; + mShippingAddress = new PaymentAddress( + GetOwner(), aCountry, emptyArray, aRegion, aRegionCode, aCity, + aDependentLocality, aPostalCode, aSortingCode, u""_ns, u""_ns, u""_ns); + mFullShippingAddress = + new PaymentAddress(GetOwner(), aCountry, aAddressLine, aRegion, + aRegionCode, aCity, aDependentLocality, aPostalCode, + aSortingCode, aOrganization, aRecipient, aPhone); + // Fire shippingaddresschange event + return DispatchUpdateEvent(u"shippingaddresschange"_ns); +} + +void PaymentRequest::SetShippingOption(const nsAString& aShippingOption) { + mShippingOption = aShippingOption; +} + +void PaymentRequest::GetShippingOption(nsAString& aRetVal) const { + aRetVal = mShippingOption; +} + +nsresult PaymentRequest::UpdateShippingOption( + const nsAString& aShippingOption) { + mShippingOption = aShippingOption; + + // Fire shippingaddresschange event + return DispatchUpdateEvent(u"shippingoptionchange"_ns); +} + +nsresult PaymentRequest::UpdatePaymentMethod( + const nsAString& aMethodName, const ChangeDetails& aMethodDetails) { + return DispatchPaymentMethodChangeEvent(aMethodName, aMethodDetails); +} + +void PaymentRequest::SetShippingType( + const Nullable<PaymentShippingType>& aShippingType) { + mShippingType = aShippingType; +} + +Nullable<PaymentShippingType> PaymentRequest::GetShippingType() const { + return mShippingType; +} + +void PaymentRequest::GetOptions(PaymentOptions& aRetVal) const { + aRetVal = mOptions; +} + +void PaymentRequest::SetOptions(const PaymentOptions& aOptions) { + mOptions = aOptions; +} + +void PaymentRequest::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + return; + } + + MOZ_ASSERT(aCx); + mUpdating = false; + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + ErrorResult result; + // Converting value to a PaymentDetailsUpdate dictionary + RootedDictionary<PaymentDetailsUpdate> details(aCx); + if (!details.Init(aCx, aValue)) { + result.StealExceptionFromJSContext(aCx); + AbortUpdate(result); + return; + } + + IsValidDetailsUpdate(details, mRequestShipping, result); + if (result.Failed()) { + AbortUpdate(result); + return; + } + + // Update the PaymentRequest with the new details + UpdatePayment(aCx, details, result); + if (result.Failed()) { + AbortUpdate(result); + return; + } +} + +void PaymentRequest::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + return; + } + + mUpdating = false; + ErrorResult result; + result.ThrowAbortError( + "Details promise for PaymentRequest.show() is rejected by merchant"); + AbortUpdate(result); +} + +bool PaymentRequest::InFullyActiveDocument() { + nsIGlobalObject* global = GetOwnerGlobal(); + if (!global) { + return false; + } + + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global); + + Document* doc = win->GetExtantDoc(); + if (!doc || !doc->IsCurrentActiveDocument()) { + return false; + } + + WindowContext* winContext = win->GetWindowContext(); + if (!winContext) { + return false; + } + + while (winContext) { + if (!winContext->IsCurrent()) { + return false; + } + winContext = winContext->GetParentWindowContext(); + } + + return true; +} + +void PaymentRequest::RegisterActivityObserver() { + if (nsPIDOMWindowInner* window = GetOwner()) { + mDocument = window->GetExtantDoc(); + if (mDocument) { + mDocument->RegisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } + } +} + +void PaymentRequest::UnregisterActivityObserver() { + if (mDocument) { + mDocument->UnregisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } +} + +void PaymentRequest::NotifyOwnerDocumentActivityChanged() { + nsPIDOMWindowInner* window = GetOwner(); + NS_ENSURE_TRUE_VOID(window); + Document* doc = window->GetExtantDoc(); + NS_ENSURE_TRUE_VOID(doc); + + if (!InFullyActiveDocument()) { + if (mState == eInteractive) { + if (mAcceptPromise) { + mAcceptPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mAcceptPromise = nullptr; + } + if (mResponse) { + ErrorResult rejectReason; + rejectReason.ThrowAbortError("The owner documnet is not fully active"); + mResponse->RejectRetry(std::move(rejectReason)); + } + if (mAbortPromise) { + mAbortPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mAbortPromise = nullptr; + } + } + if (mState == eCreated) { + if (mResultPromise) { + mResultPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mResultPromise = nullptr; + } + } + RefPtr<PaymentRequestManager> mgr = PaymentRequestManager::GetSingleton(); + mgr->ClosePayment(this); + } +} + +PaymentRequest::~PaymentRequest() { + // Suppress any pending unreported exception on mUpdateError. We don't use + // IgnoredErrorResult for mUpdateError because that doesn't play very nice + // with move assignment operators. + mUpdateError.SuppressException(); + + if (mIPC) { + // If we're being destroyed, the PaymentRequestManager isn't holding any + // references to us and we can't be waiting for any replies. + mIPC->MaybeDelete(false); + } + UnregisterActivityObserver(); +} + +JSObject* PaymentRequest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaymentRequest_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom |