/* -*- 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 manager = PaymentRequestManager::GetSingleton(); MOZ_ASSERT(manager); nsCOMPtr 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 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& aMethodData, ErrorResult& aRv) { if (!aMethodData.Length()) { aRv.ThrowTypeError("At least one payment method is required."); return; } nsTArray methods; for (const PaymentMethodData& methodData : aMethodData) { IsValidPaymentMethodIdentifier(methodData.mSupportedMethods, aRv); if (aRv.Failed()) { return; } RefPtr 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& 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& shippingOptions = aDetails.mShippingOptions.Value(); nsTArray 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& 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& displayItems = modifier.mAdditionalDisplayItems.Value(); for (const PaymentItem& displayItem : displayItems) { IsValidCurrencyAmount(displayItem.mLabel, displayItem.mAmount, false, // isTotalItem aRv); if (aRv.Failed()) { return; } } } } } } already_AddRefed PaymentRequest::Constructor( const GlobalObject& aGlobal, const Sequence& aMethodData, const PaymentDetailsInit& aDetails, const PaymentOptions& aOptions, ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.ThrowAbortError("No global object for creating PaymentRequest"); return nullptr; } nsCOMPtr 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 topSameProcessDoc = doc; topSameProcessDoc = doc; while (topSameProcessDoc) { nsCOMPtr parent = topSameProcessDoc->GetInProcessParentDocument(); if (!parent || !parent->IsContentDocument()) { break; } topSameProcessDoc = parent; } nsCOMPtr 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 manager = PaymentRequestManager::GetSingleton(); if (NS_WARN_IF(!manager)) { return nullptr; } // Create PaymentRequest and set its |mId| RefPtr 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::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 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 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::Create(global, aRv); if (aRv.Failed()) { return nullptr; } RefPtr 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 PaymentRequest::Show( const Optional>& aDetailsPromise, ErrorResult& aRv) { if (!InFullyActiveDocument()) { aRv.ThrowAbortError("The owner document is not fully active"); return nullptr; } nsIGlobalObject* global = GetOwnerGlobal(); nsCOMPtr 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::Create(global, aRv); if (aRv.Failed()) { mState = eClosed; return nullptr; } if (aDetailsPromise.WasPassed()) { aDetailsPromise.Value().AppendNativeHandler(this); mUpdating = true; } RefPtr 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 = 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 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::Create(global, aRv); if (aRv.Failed()) { return nullptr; } RefPtr 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 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 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 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 PaymentRequest::GetResponse() const { RefPtr response = mResponse; return response.forget(); } nsresult PaymentRequest::DispatchUpdateEvent(const nsAString& aType) { MOZ_ASSERT(ReadyForUpdate()); PaymentRequestUpdateEventInit init; init.mBubbles = false; init.mCancelable = false; RefPtr 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 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 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 PaymentRequest::GetShippingAddress() const { RefPtr address = mShippingAddress; return address.forget(); } nsresult PaymentRequest::UpdateShippingAddress( const nsAString& aCountry, const nsTArray& 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 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& aShippingType) { mShippingType = aShippingType; } Nullable 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 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 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 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 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 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 aGivenProto) { return PaymentRequest_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom