Bug 1389418 - Support payment method identifier validation in PaymentRequest API. r=baku
authorEden Chuang <echuang@mozilla.com>
Mon, 11 Sep 2017 09:52:24 +0800
changeset 382690 a2c4317700a209372c56e65b90eda253ff7992c9
parent 382689 66ed9c4a099734cd2ab1972e35aedc1ec97d712b
child 382691 ff2d7b9381fa1c3b181bd6609688af663ef59b70
push id95397
push userryanvm@gmail.com
push dateMon, 25 Sep 2017 13:05:21 +0000
treeherdermozilla-inbound@ff2d7b9381fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1389418
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1389418 - Support payment method identifier validation in PaymentRequest API. r=baku This patch implements payment method identifier validation algorithm according to the spec https://w3c.github.io/payment-method-id/. The steps to validate a payment method identifier with a string pmi are given by the following algorithm. It returns true if the pmi is valid. 1. Let url be the result of running the basic URL parser with pmi. 2. If url is failure, validate a standardized payment method identifier with pmi and return the result. 3. Otherwise, validate a URL-based payment method identifier passing url and return the result.
dom/payments/PaymentRequest.cpp
dom/payments/PaymentRequest.h
--- a/dom/payments/PaymentRequest.cpp
+++ b/dom/payments/PaymentRequest.cpp
@@ -1,19 +1,21 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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/Element.h"
 #include "mozilla/dom/PaymentRequest.h"
 #include "mozilla/dom/PaymentResponse.h"
 #include "nsContentUtils.h"
-#include "BasicCardPayment.h"
+#include "nsIURLParser.h"
+#include "nsNetCID.h"
 #include "PaymentRequestManager.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(PaymentRequest)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentRequest,
@@ -48,31 +50,217 @@ NS_IMPL_RELEASE_INHERITED(PaymentRequest
 
 bool
 PaymentRequest::PrefEnabled(JSContext* aCx, JSObject* aObj)
 {
   return Preferences::GetBool("dom.payments.request.enabled");
 }
 
 nsresult
+PaymentRequest::IsValidStandardizedPMI(const nsAString& aIdentifier,
+                                       nsAString& aErrorMsg)
+{
+  /*
+   *   The syntax of a standardized payment method identifier is given by the
+   *   following [ABNF]:
+   *
+   *       stdpmi = part *( "-" part )
+   *       part = 1loweralpha *( DIGIT / loweralpha )
+   *       loweralpha =  %x61-7A
+   */
+  nsString::const_iterator start, end;
+  aIdentifier.BeginReading(start);
+  aIdentifier.EndReading(end);
+  while (start != end) {
+    // the first char must be in the range %x61-7A
+    if ((*start < 'a' || *start > 'z')) {
+      aErrorMsg.AssignLiteral("'");
+      aErrorMsg.Append(aIdentifier);
+      aErrorMsg.AppendLiteral("' is not valid. The character '");
+      aErrorMsg.Append(*start);
+      aErrorMsg.AppendLiteral("' at the beginning or after the '-' must be in the range [a-z].");
+      return NS_ERROR_RANGE_ERR;
+    }
+    ++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 != '-') {
+      aErrorMsg.AssignLiteral("'");
+      aErrorMsg.Append(aIdentifier);
+      aErrorMsg.AppendLiteral("' is not valid. The character '");
+      aErrorMsg.Append(*start);
+      aErrorMsg.AppendLiteral("' must be in the range [a-zA-z0-9-].");
+      return NS_ERROR_RANGE_ERR;
+    }
+    if (*start == '-') {
+      ++start;
+      // the last char can not be '-'
+      if (start == end) {
+        aErrorMsg.AssignLiteral("'");
+        aErrorMsg.Append(aIdentifier);
+        aErrorMsg.AppendLiteral("' is not valid. The last character '");
+        aErrorMsg.Append(*start);
+        aErrorMsg.AppendLiteral("' must be in the range [a-z0-9].");
+        return NS_ERROR_RANGE_ERR;
+      }
+    }
+  }
+  return NS_OK;
+}
+
+nsresult
+PaymentRequest::IsValidPaymentMethodIdentifier(const nsAString& aIdentifier,
+                                               nsAString& aErrorMsg)
+{
+  if (aIdentifier.IsEmpty()) {
+    aErrorMsg.AssignLiteral("Payment method identifier is required.");
+    return NS_ERROR_TYPE_ERR;
+  }
+  /*
+   *  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);
+  NS_ENSURE_SUCCESS(rv, NS_ERROR_RANGE_ERR);
+  if (schemeLen == -1) {
+    // The PMI is not a URL-based PMI, check if it is a standardized PMI
+    return IsValidStandardizedPMI(aIdentifier, aErrorMsg);
+  }
+  if (!Substring(aIdentifier, schemePos, schemeLen).EqualsASCII("https")) {
+    aErrorMsg.AssignLiteral("'");
+    aErrorMsg.Append(aIdentifier);
+    aErrorMsg.AppendLiteral("' is not valid. The scheme must be 'https'.");
+    return NS_ERROR_RANGE_ERR;
+  }
+  if (Substring(aIdentifier, authorityPos, authorityLen).IsEmpty()) {
+    aErrorMsg.AssignLiteral("'");
+    aErrorMsg.Append(aIdentifier);
+    aErrorMsg.AppendLiteral("' is not valid. hostname can not be empty.");
+    return NS_ERROR_RANGE_ERR;
+  }
+
+  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) {
+        aErrorMsg.AssignLiteral("'");
+        aErrorMsg.Append(aIdentifier);
+        aErrorMsg.AppendLiteral("' is not valid. hostname can not be empty.");
+        return NS_ERROR_RANGE_ERR;
+      }
+      // 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.
+        return NS_ERROR_RANGE_ERR;
+      }
+    }
+  }
+  // PMI is valid when usernameLen/passwordLen equals to -1 or 0.
+  if (usernameLen > 0 || passwordLen > 0) {
+    aErrorMsg.AssignLiteral("'");
+    aErrorMsg.Append(aIdentifier);
+    aErrorMsg.AssignLiteral("' is not valid. Username and password must be empty.");
+    return NS_ERROR_RANGE_ERR;
+  }
+
+  // PMI is valid when hostnameLen is larger than 0
+  if (hostnameLen <= 0) {
+    aErrorMsg.AssignLiteral("'");
+    aErrorMsg.Append(aIdentifier);
+    aErrorMsg.AppendLiteral("' is not valid. hostname can not be empty.");
+    return NS_ERROR_RANGE_ERR;
+  }
+  return NS_OK;
+}
+
+nsresult
 PaymentRequest::IsValidMethodData(JSContext* aCx,
                                   const Sequence<PaymentMethodData>& aMethodData,
                                   nsAString& aErrorMsg)
 {
   if (!aMethodData.Length()) {
     aErrorMsg.AssignLiteral("At least one payment method is required.");
     return NS_ERROR_TYPE_ERR;
   }
 
   for (const PaymentMethodData& methodData : aMethodData) {
-    if (methodData.mSupportedMethods.IsEmpty()) {
-      aErrorMsg.AssignLiteral(
-        "Payment method identifier is required.");
-      return NS_ERROR_TYPE_ERR;
+    nsresult rv = IsValidPaymentMethodIdentifier(methodData.mSupportedMethods,
+                                                 aErrorMsg);
+    if (NS_FAILED(rv)) {
+      return rv;
     }
+
     RefPtr<BasicCardService> service = BasicCardService::GetService();
     MOZ_ASSERT(service);
     if (service->IsBasicCardPayment(methodData.mSupportedMethods)) {
       if (!methodData.mData.WasPassed()) {
         continue;
       }
       MOZ_ASSERT(aCx);
       if (!service->IsValidBasicCardRequest(aCx,
@@ -294,16 +482,20 @@ PaymentRequest::IsValidDetailsBase(const
       seenIDs.AppendElement(shippingOption.mId);
     }
   }
 
   // Check payment details modifiers
   if (aDetails.mModifiers.WasPassed()) {
     const Sequence<PaymentDetailsModifier>& modifiers = aDetails.mModifiers.Value();
     for (const PaymentDetailsModifier& modifier : modifiers) {
+      rv = IsValidPaymentMethodIdentifier(modifier.mSupportedMethods, aErrorMsg);
+      if (NS_FAILED(rv)) {
+        return rv;
+      }
       rv = IsValidCurrencyAmount(NS_LITERAL_STRING("details.modifiers.total"),
                                  modifier.mTotal.mAmount,
                                  true, // isTotalItem
                                  aErrorMsg);
       if (NS_FAILED(rv)) {
         return rv;
       }
       if (modifier.mAdditionalDisplayItems.WasPassed()) {
@@ -372,17 +564,21 @@ PaymentRequest::Constructor(const Global
   } while (node);
 
   // Check payment methods and details
   nsAutoString message;
   nsresult rv = IsValidMethodData(aGlobal.Context(),
                                   aMethodData,
                                   message);
   if (NS_FAILED(rv)) {
-    aRv.ThrowTypeError<MSG_ILLEGAL_TYPE_PR_CONSTRUCTOR>(message);
+    if (rv == NS_ERROR_TYPE_ERR) {
+      aRv.ThrowTypeError<MSG_ILLEGAL_TYPE_PR_CONSTRUCTOR>(message);
+    } else if (rv == NS_ERROR_RANGE_ERR) {
+      aRv.ThrowRangeError<MSG_ILLEGAL_RANGE_PR_CONSTRUCTOR>(message);
+    }
     return nullptr;
   }
   rv = IsValidDetailsInit(aDetails, message);
   if (NS_FAILED(rv)) {
     if (rv == NS_ERROR_TYPE_ERR) {
       aRv.ThrowTypeError<MSG_ILLEGAL_TYPE_PR_CONSTRUCTOR>(message);
     } else if (rv == NS_ERROR_RANGE_ERR) {
       aRv.ThrowRangeError<MSG_ILLEGAL_RANGE_PR_CONSTRUCTOR>(message);
--- a/dom/payments/PaymentRequest.h
+++ b/dom/payments/PaymentRequest.h
@@ -31,16 +31,22 @@ public:
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<PaymentRequest>
   CreatePaymentRequest(nsPIDOMWindowInner* aWindow, nsresult& aRv);
 
   static bool PrefEnabled(JSContext* aCx, JSObject* aObj);
 
+  static nsresult IsValidStandardizedPMI(const nsAString& aIdentifier,
+                                         nsAString& aErrorMsg);
+
+  static nsresult IsValidPaymentMethodIdentifier(const nsAString& aIdentifier,
+                                                 nsAString& aErrorMsg);
+
   static nsresult IsValidMethodData(JSContext* aCx,
                                     const Sequence<PaymentMethodData>& aMethodData,
                                     nsAString& aErrorMsg);
 
   static nsresult
   IsValidNumber(const nsAString& aItem,
                 const nsAString& aStr,
                 nsAString& aErrorMsg);