209bfcf7c7fd99260bc2df31b26134831ed6ba5d
[WebKit-https.git] / Source / WebCore / Modules / paymentrequest / PaymentRequest.cpp
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "PaymentRequest.h"
28
29 #if ENABLE(PAYMENT_REQUEST)
30
31 #include "ApplePayPaymentHandler.h"
32 #include "Document.h"
33 #include "PaymentAddress.h"
34 #include "PaymentCurrencyAmount.h"
35 #include "PaymentDetailsInit.h"
36 #include "PaymentHandler.h"
37 #include "PaymentMethodData.h"
38 #include "PaymentOptions.h"
39 #include "ScriptController.h"
40 #include <JavaScriptCore/JSONObject.h>
41 #include <JavaScriptCore/ThrowScope.h>
42 #include <wtf/ASCIICType.h>
43 #include <wtf/RunLoop.h>
44 #include <wtf/UUID.h>
45
46 namespace WebCore {
47
48 // Implements the IsWellFormedCurrencyCode abstract operation from ECMA 402
49 // https://tc39.github.io/ecma402/#sec-iswellformedcurrencycode
50 static bool isWellFormedCurrencyCode(const String& currency)
51 {
52     if (currency.length() == 3)
53         return currency.isAllSpecialCharacters<isASCIIAlpha>();
54     return false;
55 }
56
57 // Implements the "valid decimal monetary value" validity checker
58 // https://www.w3.org/TR/payment-request/#dfn-valid-decimal-monetary-value
59 static bool isValidDecimalMonetaryValue(StringView value)
60 {
61     enum class State {
62         Start,
63         Sign,
64         Digit,
65         Dot,
66         DotDigit,
67     };
68
69     auto state = State::Start;
70     for (auto character : value.codeUnits()) {
71         switch (state) {
72         case State::Start:
73             if (character == '-') {
74                 state = State::Sign;
75                 break;
76             }
77
78             if (isASCIIDigit(character)) {
79                 state = State::Digit;
80                 break;
81             }
82
83             return false;
84
85         case State::Sign:
86             if (isASCIIDigit(character)) {
87                 state = State::Digit;
88                 break;
89             }
90
91             return false;
92
93         case State::Digit:
94             if (character == '.') {
95                 state = State::Dot;
96                 break;
97             }
98
99             if (isASCIIDigit(character)) {
100                 state = State::Digit;
101                 break;
102             }
103
104             return false;
105
106         case State::Dot:
107             if (isASCIIDigit(character)) {
108                 state = State::DotDigit;
109                 break;
110             }
111
112             return false;
113
114         case State::DotDigit:
115             if (isASCIIDigit(character)) {
116                 state = State::DotDigit;
117                 break;
118             }
119
120             return false;
121         }
122     }
123
124     if (state == State::Digit || state == State::DotDigit)
125         return true;
126
127     return false;
128 }
129
130 // Implements the "check and canonicalize amount" validity checker
131 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-amount
132 static ExceptionOr<void> checkAndCanonicalizeAmount(PaymentCurrencyAmount& amount)
133 {
134     if (amount.currencySystem != "urn:iso:std:iso:4217")
135         return { };
136
137     if (!isWellFormedCurrencyCode(amount.currency))
138         return Exception { RangeError, makeString("\"", amount.currency, "\" is not a valid currency code.") };
139
140     if (!isValidDecimalMonetaryValue(amount.value))
141         return Exception { TypeError, makeString("\"", amount.value, "\" is not a valid decimal monetary value.") };
142
143     amount.currency = amount.currency.convertToASCIIUppercase();
144     return { };
145 }
146
147 // Implements the "check and canonicalize total" validity checker
148 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-total
149 static ExceptionOr<void> checkAndCanonicalizeTotal(PaymentCurrencyAmount& total)
150 {
151     if (total.currencySystem != "urn:iso:std:iso:4217")
152         return { };
153
154     auto exception = checkAndCanonicalizeAmount(total);
155     if (exception.hasException())
156         return exception;
157
158     if (total.value[0] == '-')
159         return Exception { TypeError, ASCIILiteral("Total currency values cannot be negative.") };
160
161     return { };
162 }
163
164 // Implements "validate a standardized payment method identifier"
165 // https://www.w3.org/TR/payment-method-id/#validity-0
166 static bool isValidStandardizedPaymentMethodIdentifier(StringView identifier)
167 {
168     enum class State {
169         Start,
170         Hyphen,
171         LowerAlpha,
172         Digit,
173     };
174
175     auto state = State::Start;
176     for (auto character : identifier.codeUnits()) {
177         switch (state) {
178         case State::Start:
179         case State::Hyphen:
180             if (isASCIILower(character)) {
181                 state = State::LowerAlpha;
182                 break;
183             }
184
185             return false;
186
187         case State::LowerAlpha:
188         case State::Digit:
189             if (isASCIILower(character)) {
190                 state = State::LowerAlpha;
191                 break;
192             }
193
194             if (isASCIIDigit(character)) {
195                 state = State::Digit;
196                 break;
197             }
198
199             if (character == '-') {
200                 state = State::Hyphen;
201                 break;
202             }
203
204             return false;
205         }
206     }
207
208     return state == State::LowerAlpha || state == State::Digit;
209 }
210
211 // Implements "validate a URL-based payment method identifier"
212 // https://www.w3.org/TR/payment-method-id/#validation
213 static bool isValidURLBasedPaymentMethodIdentifier(const URL& url)
214 {
215     if (!url.protocolIs("https"))
216         return false;
217
218     if (!url.user().isEmpty() || !url.pass().isEmpty())
219         return false;
220
221     return true;
222 }
223
224 // Implements "validate a payment method identifier"
225 // https://www.w3.org/TR/payment-method-id/#validity
226 std::optional<PaymentRequest::MethodIdentifier> convertAndValidatePaymentMethodIdentifier(const String& identifier)
227 {
228     URL url { URL(), identifier };
229     if (!url.isValid()) {
230         if (isValidStandardizedPaymentMethodIdentifier(identifier))
231             return { identifier };
232         return std::nullopt;
233     }
234
235     if (isValidURLBasedPaymentMethodIdentifier(url))
236         return { WTFMove(url) };
237
238     return std::nullopt;
239 }
240
241 // Implements the PaymentRequest Constructor
242 // https://www.w3.org/TR/payment-request/#constructor
243 ExceptionOr<Ref<PaymentRequest>> PaymentRequest::create(Document& document, Vector<PaymentMethodData>&& methodData, PaymentDetailsInit&& details, PaymentOptions&& options)
244 {
245     // FIXME: Check if this document is allowed to access the PaymentRequest API based on the allowpaymentrequest attribute.
246
247     if (details.id.isNull())
248         details.id = createCanonicalUUIDString();
249
250     if (methodData.isEmpty())
251         return Exception { TypeError, ASCIILiteral("At least one payment method is required.") };
252
253     Vector<Method> serializedMethodData;
254     serializedMethodData.reserveInitialCapacity(methodData.size());
255     for (auto& paymentMethod : methodData) {
256         auto identifier = convertAndValidatePaymentMethodIdentifier(paymentMethod.supportedMethods);
257         if (!identifier)
258             return Exception { RangeError, makeString("\"", paymentMethod.supportedMethods, "\" is an invalid payment method identifier.") };
259
260         String serializedData;
261         if (paymentMethod.data) {
262             auto scope = DECLARE_THROW_SCOPE(document.execState()->vm());
263             serializedData = JSONStringify(document.execState(), paymentMethod.data.get(), 0);
264             if (scope.exception())
265                 return Exception { ExistingExceptionError };
266         }
267         serializedMethodData.uncheckedAppend({ WTFMove(*identifier), WTFMove(serializedData) });
268     }
269
270     auto exception = checkAndCanonicalizeTotal(details.total.amount);
271     if (exception.hasException())
272         return exception.releaseException();
273
274     for (auto& item : details.displayItems) {
275         auto exception = checkAndCanonicalizeAmount(item.amount);
276         if (exception.hasException())
277             return exception.releaseException();
278     }
279
280     String selectedShippingOption;
281     HashSet<String> seenShippingOptionIDs;
282     for (auto& shippingOption : details.shippingOptions) {
283         auto exception = checkAndCanonicalizeAmount(shippingOption.amount);
284         if (exception.hasException())
285             return exception.releaseException();
286
287         auto addResult = seenShippingOptionIDs.add(shippingOption.id);
288         if (!addResult.isNewEntry) {
289             details.shippingOptions = { };
290             selectedShippingOption = { };
291             break;
292         }
293
294         if (shippingOption.selected)
295             selectedShippingOption = shippingOption.id;
296     }
297
298     Vector<String> serializedModifierData;
299     serializedModifierData.reserveInitialCapacity(details.modifiers.size());
300     for (auto& modifier : details.modifiers) {
301         if (modifier.total) {
302             auto exception = checkAndCanonicalizeTotal(modifier.total->amount);
303             if (exception.hasException())
304                 return exception.releaseException();
305         }
306
307         for (auto& item : modifier.additionalDisplayItems) {
308             auto exception = checkAndCanonicalizeAmount(item.amount);
309             if (exception.hasException())
310                 return exception.releaseException();
311         }
312
313         String serializedData;
314         if (modifier.data) {
315             auto scope = DECLARE_THROW_SCOPE(document.execState()->vm());
316             serializedData = JSONStringify(document.execState(), modifier.data.get(), 0);
317             if (scope.exception())
318                 return Exception { ExistingExceptionError };
319         }
320         serializedModifierData.uncheckedAppend(WTFMove(serializedData));
321     }
322
323     return adoptRef(*new PaymentRequest(document, WTFMove(options), WTFMove(details), WTFMove(serializedModifierData), WTFMove(serializedMethodData), WTFMove(selectedShippingOption)));
324 }
325
326 PaymentRequest::PaymentRequest(Document& document, PaymentOptions&& options, PaymentDetailsInit&& details, Vector<String>&& serializedModifierData, Vector<Method>&& serializedMethodData, String&& selectedShippingOption)
327     : ActiveDOMObject { &document }
328     , m_options { WTFMove(options) }
329     , m_details { WTFMove(details) }
330     , m_serializedModifierData { WTFMove(serializedModifierData) }
331     , m_serializedMethodData { WTFMove(serializedMethodData) }
332     , m_shippingOption { WTFMove(selectedShippingOption) }
333 {
334     suspendIfNeeded();
335 }
336
337 PaymentRequest::~PaymentRequest()
338 {
339     ASSERT(!hasPendingActivity());
340     ASSERT(!m_activePaymentHandler);
341 }
342
343 static ExceptionOr<JSC::JSValue> parse(ScriptExecutionContext& context, const String& string)
344 {
345     auto scope = DECLARE_THROW_SCOPE(context.vm());
346     JSC::JSValue data = JSONParse(context.execState(), string);
347     if (scope.exception())
348         return Exception { ExistingExceptionError };
349     return WTFMove(data);
350 }
351
352 // https://www.w3.org/TR/payment-request/#show()-method
353 void PaymentRequest::show(Document& document, ShowPromise&& promise)
354 {
355     // FIXME: Reject promise with SecurityError if show() was not triggered by a user gesture.
356     // Find a way to do this without breaking the payment-request web platform tests.
357
358     if (m_state != State::Created) {
359         promise.reject(Exception { InvalidStateError });
360         return;
361     }
362
363     if (PaymentHandler::hasActiveSession(document)) {
364         promise.reject(Exception { AbortError });
365         return;
366     }
367
368     m_state = State::Interactive;
369     ASSERT(!m_showPromise);
370     m_showPromise = WTFMove(promise);
371
372     RefPtr<PaymentHandler> selectedPaymentHandler;
373     for (auto& paymentMethod : m_serializedMethodData) {
374         auto data = parse(document, paymentMethod.serializedData);
375         if (data.hasException()) {
376             m_showPromise->reject(data.releaseException());
377             return;
378         }
379
380         auto handler = PaymentHandler::create(*this, paymentMethod.identifier);
381         if (!handler)
382             continue;
383
384         auto result = handler->convertData(*document.execState(), data.releaseReturnValue());
385         if (result.hasException()) {
386             m_showPromise->reject(result.releaseException());
387             return;
388         }
389
390         if (!selectedPaymentHandler)
391             selectedPaymentHandler = WTFMove(handler);
392     }
393
394     if (!selectedPaymentHandler) {
395         m_showPromise->reject(Exception { NotSupportedError });
396         return;
397     }
398
399     auto exception = selectedPaymentHandler->show(document);
400     if (exception.hasException()) {
401         m_showPromise->reject(exception.releaseException());
402         return;
403     }
404
405     ASSERT(!m_activePaymentHandler);
406     m_activePaymentHandler = WTFMove(selectedPaymentHandler);
407     setPendingActivity(this); // unsetPendingActivity() is called below in stop()
408 }
409
410 void PaymentRequest::stop()
411 {
412     if (m_state != State::Interactive)
413         return;
414
415     if (auto paymentHandler = std::exchange(m_activePaymentHandler, nullptr)) {
416         unsetPendingActivity(this);
417         paymentHandler->hide(downcast<Document>(*scriptExecutionContext()));
418     }
419
420     ASSERT(m_state == State::Interactive);
421     m_state = State::Closed;
422     m_showPromise->reject(Exception { AbortError });
423 }
424
425 // https://www.w3.org/TR/payment-request/#abort()-method
426 ExceptionOr<void> PaymentRequest::abort(AbortPromise&& promise)
427 {
428     if (m_state != State::Interactive)
429         return Exception { InvalidStateError };
430
431     stop();
432     promise.resolve();
433     return { };
434 }
435
436 // https://www.w3.org/TR/payment-request/#canmakepayment()-method
437 void PaymentRequest::canMakePayment(Document& document, CanMakePaymentPromise&& promise)
438 {
439     if (m_state != State::Created) {
440         promise.reject(Exception { InvalidStateError });
441         return;
442     }
443
444     for (auto& paymentMethod : m_serializedMethodData) {
445         auto data = parse(document, paymentMethod.serializedData);
446         if (data.hasException())
447             continue;
448
449         auto handler = PaymentHandler::create(*this, paymentMethod.identifier);
450         if (!handler)
451             continue;
452
453         auto exception = handler->convertData(*document.execState(), data.releaseReturnValue());
454         if (exception.hasException())
455             continue;
456
457         handler->canMakePayment(document, [promise = WTFMove(promise)](bool canMakePayment) mutable {
458             promise.resolve(canMakePayment);
459         });
460         return;
461     }
462
463     promise.resolve(false);
464 }
465
466 const String& PaymentRequest::id() const
467 {
468     return m_details.id;
469 }
470
471 std::optional<PaymentShippingType> PaymentRequest::shippingType() const
472 {
473     if (m_options.requestShipping)
474         return m_options.shippingType;
475     return std::nullopt;
476 }
477
478 bool PaymentRequest::canSuspendForDocumentSuspension() const
479 {
480     switch (m_state) {
481     case State::Created:
482     case State::Closed:
483         ASSERT(!m_activePaymentHandler);
484         return true;
485     case State::Interactive:
486         return !m_activePaymentHandler;
487     }
488 }
489
490 } // namespace WebCore
491
492 #endif // ENABLE(PAYMENT_REQUEST)