[Payment Request] Implement the "user aborts the payment request" algorithm
[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 "EventNames.h"
34 #include "JSDOMPromise.h"
35 #include "JSPaymentDetailsUpdate.h"
36 #include "JSPaymentResponse.h"
37 #include "PaymentAddress.h"
38 #include "PaymentCurrencyAmount.h"
39 #include "PaymentDetailsInit.h"
40 #include "PaymentHandler.h"
41 #include "PaymentMethodData.h"
42 #include "PaymentOptions.h"
43 #include "PaymentRequestUpdateEvent.h"
44 #include "PaymentResponse.h"
45 #include "ScriptController.h"
46 #include <JavaScriptCore/JSONObject.h>
47 #include <JavaScriptCore/ThrowScope.h>
48 #include <wtf/ASCIICType.h>
49 #include <wtf/RunLoop.h>
50 #include <wtf/Scope.h>
51 #include <wtf/UUID.h>
52
53 namespace WebCore {
54
55 // Implements the IsWellFormedCurrencyCode abstract operation from ECMA 402
56 // https://tc39.github.io/ecma402/#sec-iswellformedcurrencycode
57 static bool isWellFormedCurrencyCode(const String& currency)
58 {
59     if (currency.length() == 3)
60         return currency.isAllSpecialCharacters<isASCIIAlpha>();
61     return false;
62 }
63
64 // Implements the "valid decimal monetary value" validity checker
65 // https://www.w3.org/TR/payment-request/#dfn-valid-decimal-monetary-value
66 static bool isValidDecimalMonetaryValue(StringView value)
67 {
68     enum class State {
69         Start,
70         Sign,
71         Digit,
72         Dot,
73         DotDigit,
74     };
75
76     auto state = State::Start;
77     for (auto character : value.codeUnits()) {
78         switch (state) {
79         case State::Start:
80             if (character == '-') {
81                 state = State::Sign;
82                 break;
83             }
84
85             if (isASCIIDigit(character)) {
86                 state = State::Digit;
87                 break;
88             }
89
90             return false;
91
92         case State::Sign:
93             if (isASCIIDigit(character)) {
94                 state = State::Digit;
95                 break;
96             }
97
98             return false;
99
100         case State::Digit:
101             if (character == '.') {
102                 state = State::Dot;
103                 break;
104             }
105
106             if (isASCIIDigit(character)) {
107                 state = State::Digit;
108                 break;
109             }
110
111             return false;
112
113         case State::Dot:
114             if (isASCIIDigit(character)) {
115                 state = State::DotDigit;
116                 break;
117             }
118
119             return false;
120
121         case State::DotDigit:
122             if (isASCIIDigit(character)) {
123                 state = State::DotDigit;
124                 break;
125             }
126
127             return false;
128         }
129     }
130
131     if (state == State::Digit || state == State::DotDigit)
132         return true;
133
134     return false;
135 }
136
137 // Implements the "check and canonicalize amount" validity checker
138 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-amount
139 static ExceptionOr<void> checkAndCanonicalizeAmount(PaymentCurrencyAmount& amount)
140 {
141     if (amount.currencySystem != "urn:iso:std:iso:4217")
142         return { };
143
144     if (!isWellFormedCurrencyCode(amount.currency))
145         return Exception { RangeError, makeString("\"", amount.currency, "\" is not a valid currency code.") };
146
147     if (!isValidDecimalMonetaryValue(amount.value))
148         return Exception { TypeError, makeString("\"", amount.value, "\" is not a valid decimal monetary value.") };
149
150     amount.currency = amount.currency.convertToASCIIUppercase();
151     return { };
152 }
153
154 // Implements the "check and canonicalize total" validity checker
155 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-total
156 static ExceptionOr<void> checkAndCanonicalizeTotal(PaymentCurrencyAmount& total)
157 {
158     if (total.currencySystem != "urn:iso:std:iso:4217")
159         return { };
160
161     auto exception = checkAndCanonicalizeAmount(total);
162     if (exception.hasException())
163         return exception;
164
165     if (total.value[0] == '-')
166         return Exception { TypeError, ASCIILiteral("Total currency values cannot be negative.") };
167
168     return { };
169 }
170
171 // Implements "validate a standardized payment method identifier"
172 // https://www.w3.org/TR/payment-method-id/#validity-0
173 static bool isValidStandardizedPaymentMethodIdentifier(StringView identifier)
174 {
175     enum class State {
176         Start,
177         Hyphen,
178         LowerAlpha,
179         Digit,
180     };
181
182     auto state = State::Start;
183     for (auto character : identifier.codeUnits()) {
184         switch (state) {
185         case State::Start:
186         case State::Hyphen:
187             if (isASCIILower(character)) {
188                 state = State::LowerAlpha;
189                 break;
190             }
191
192             return false;
193
194         case State::LowerAlpha:
195         case State::Digit:
196             if (isASCIILower(character)) {
197                 state = State::LowerAlpha;
198                 break;
199             }
200
201             if (isASCIIDigit(character)) {
202                 state = State::Digit;
203                 break;
204             }
205
206             if (character == '-') {
207                 state = State::Hyphen;
208                 break;
209             }
210
211             return false;
212         }
213     }
214
215     return state == State::LowerAlpha || state == State::Digit;
216 }
217
218 // Implements "validate a URL-based payment method identifier"
219 // https://www.w3.org/TR/payment-method-id/#validation
220 static bool isValidURLBasedPaymentMethodIdentifier(const URL& url)
221 {
222     if (!url.protocolIs("https"))
223         return false;
224
225     if (!url.user().isEmpty() || !url.pass().isEmpty())
226         return false;
227
228     return true;
229 }
230
231 // Implements "validate a payment method identifier"
232 // https://www.w3.org/TR/payment-method-id/#validity
233 std::optional<PaymentRequest::MethodIdentifier> convertAndValidatePaymentMethodIdentifier(const String& identifier)
234 {
235     URL url { URL(), identifier };
236     if (!url.isValid()) {
237         if (isValidStandardizedPaymentMethodIdentifier(identifier))
238             return { identifier };
239         return std::nullopt;
240     }
241
242     if (isValidURLBasedPaymentMethodIdentifier(url))
243         return { WTFMove(url) };
244
245     return std::nullopt;
246 }
247
248 enum class ShouldValidatePaymentMethodIdentifier {
249     No,
250     Yes,
251 };
252
253 static ExceptionOr<std::tuple<String, Vector<String>>> checkAndCanonicalizeDetails(JSC::ExecState& execState, PaymentDetailsBase& details, bool requestShipping, ShouldValidatePaymentMethodIdentifier shouldValidatePaymentMethodIdentifier)
254 {
255     for (auto& item : details.displayItems) {
256         auto exception = checkAndCanonicalizeAmount(item.amount);
257         if (exception.hasException())
258             return exception.releaseException();
259     }
260
261     String selectedShippingOption;
262     if (requestShipping) {
263         HashSet<String> seenShippingOptionIDs;
264         for (auto& shippingOption : details.shippingOptions) {
265             auto exception = checkAndCanonicalizeAmount(shippingOption.amount);
266             if (exception.hasException())
267                 return exception.releaseException();
268
269             auto addResult = seenShippingOptionIDs.add(shippingOption.id);
270             if (!addResult.isNewEntry)
271                 return Exception { TypeError, "Shipping option IDs must be unique." };
272
273             if (shippingOption.selected)
274                 selectedShippingOption = shippingOption.id;
275         }
276     }
277
278     Vector<String> serializedModifierData;
279     serializedModifierData.reserveInitialCapacity(details.modifiers.size());
280     for (auto& modifier : details.modifiers) {
281         if (shouldValidatePaymentMethodIdentifier == ShouldValidatePaymentMethodIdentifier::Yes) {
282             auto paymentMethodIdentifier = convertAndValidatePaymentMethodIdentifier(modifier.supportedMethods);
283             if (!paymentMethodIdentifier)
284                 return Exception { RangeError, makeString("\"", modifier.supportedMethods, "\" is an invalid payment method identifier.") };
285         }
286
287         if (modifier.total) {
288             auto exception = checkAndCanonicalizeTotal(modifier.total->amount);
289             if (exception.hasException())
290                 return exception.releaseException();
291         }
292
293         for (auto& item : modifier.additionalDisplayItems) {
294             auto exception = checkAndCanonicalizeAmount(item.amount);
295             if (exception.hasException())
296                 return exception.releaseException();
297         }
298
299         String serializedData;
300         if (modifier.data) {
301             auto scope = DECLARE_THROW_SCOPE(execState.vm());
302             serializedData = JSONStringify(&execState, modifier.data.get(), 0);
303             if (scope.exception())
304                 return Exception { ExistingExceptionError };
305             modifier.data.clear();
306         }
307         serializedModifierData.uncheckedAppend(WTFMove(serializedData));
308     }
309
310     return std::make_tuple(WTFMove(selectedShippingOption), WTFMove(serializedModifierData));
311 }
312
313 // Implements the PaymentRequest Constructor
314 // https://www.w3.org/TR/payment-request/#constructor
315 ExceptionOr<Ref<PaymentRequest>> PaymentRequest::create(Document& document, Vector<PaymentMethodData>&& methodData, PaymentDetailsInit&& details, PaymentOptions&& options)
316 {
317     // FIXME: Check if this document is allowed to access the PaymentRequest API based on the allowpaymentrequest attribute.
318
319     if (details.id.isNull())
320         details.id = createCanonicalUUIDString();
321
322     if (methodData.isEmpty())
323         return Exception { TypeError, ASCIILiteral("At least one payment method is required.") };
324
325     Vector<Method> serializedMethodData;
326     serializedMethodData.reserveInitialCapacity(methodData.size());
327     for (auto& paymentMethod : methodData) {
328         auto identifier = convertAndValidatePaymentMethodIdentifier(paymentMethod.supportedMethods);
329         if (!identifier)
330             return Exception { RangeError, makeString("\"", paymentMethod.supportedMethods, "\" is an invalid payment method identifier.") };
331
332         String serializedData;
333         if (paymentMethod.data) {
334             auto scope = DECLARE_THROW_SCOPE(document.execState()->vm());
335             serializedData = JSONStringify(document.execState(), paymentMethod.data.get(), 0);
336             if (scope.exception())
337                 return Exception { ExistingExceptionError };
338         }
339         serializedMethodData.uncheckedAppend({ WTFMove(*identifier), WTFMove(serializedData) });
340     }
341
342     auto totalResult = checkAndCanonicalizeTotal(details.total.amount);
343     if (totalResult.hasException())
344         return totalResult.releaseException();
345
346     auto detailsResult = checkAndCanonicalizeDetails(*document.execState(), details, options.requestShipping, ShouldValidatePaymentMethodIdentifier::No);
347     if (detailsResult.hasException())
348         return detailsResult.releaseException();
349
350     auto shippingOptionAndModifierData = detailsResult.releaseReturnValue();
351     return adoptRef(*new PaymentRequest(document, WTFMove(options), WTFMove(details), WTFMove(std::get<1>(shippingOptionAndModifierData)), WTFMove(serializedMethodData), WTFMove(std::get<0>(shippingOptionAndModifierData))));
352 }
353
354 PaymentRequest::PaymentRequest(Document& document, PaymentOptions&& options, PaymentDetailsInit&& details, Vector<String>&& serializedModifierData, Vector<Method>&& serializedMethodData, String&& selectedShippingOption)
355     : ActiveDOMObject { &document }
356     , m_options { WTFMove(options) }
357     , m_details { WTFMove(details) }
358     , m_serializedModifierData { WTFMove(serializedModifierData) }
359     , m_serializedMethodData { WTFMove(serializedMethodData) }
360     , m_shippingOption { WTFMove(selectedShippingOption) }
361 {
362     suspendIfNeeded();
363 }
364
365 PaymentRequest::~PaymentRequest()
366 {
367     ASSERT(!hasPendingActivity());
368     ASSERT(!m_activePaymentHandler);
369 }
370
371 static ExceptionOr<JSC::JSValue> parse(ScriptExecutionContext& context, const String& string)
372 {
373     auto scope = DECLARE_THROW_SCOPE(context.vm());
374     JSC::JSValue data = JSONParse(context.execState(), string);
375     if (scope.exception())
376         return Exception { ExistingExceptionError };
377     return WTFMove(data);
378 }
379
380 // https://www.w3.org/TR/payment-request/#show()-method
381 void PaymentRequest::show(Document& document, ShowPromise&& promise)
382 {
383     // FIXME: Reject promise with SecurityError if show() was not triggered by a user gesture.
384     // Find a way to do this without breaking the payment-request web platform tests.
385
386     if (m_state != State::Created) {
387         promise.reject(Exception { InvalidStateError });
388         return;
389     }
390
391     if (PaymentHandler::hasActiveSession(document)) {
392         promise.reject(Exception { AbortError });
393         return;
394     }
395
396     m_state = State::Interactive;
397     ASSERT(!m_showPromise);
398     m_showPromise = WTFMove(promise);
399
400     RefPtr<PaymentHandler> selectedPaymentHandler;
401     for (auto& paymentMethod : m_serializedMethodData) {
402         auto data = parse(document, paymentMethod.serializedData);
403         if (data.hasException()) {
404             m_showPromise->reject(data.releaseException());
405             return;
406         }
407
408         auto handler = PaymentHandler::create(document, *this, paymentMethod.identifier);
409         if (!handler)
410             continue;
411
412         auto result = handler->convertData(data.releaseReturnValue());
413         if (result.hasException()) {
414             m_showPromise->reject(result.releaseException());
415             return;
416         }
417
418         if (!selectedPaymentHandler)
419             selectedPaymentHandler = WTFMove(handler);
420     }
421
422     if (!selectedPaymentHandler) {
423         m_showPromise->reject(Exception { NotSupportedError });
424         return;
425     }
426
427     auto exception = selectedPaymentHandler->show();
428     if (exception.hasException()) {
429         m_showPromise->reject(exception.releaseException());
430         return;
431     }
432
433     ASSERT(!m_activePaymentHandler);
434     m_activePaymentHandler = WTFMove(selectedPaymentHandler);
435     setPendingActivity(this); // unsetPendingActivity() is called below in stop()
436 }
437
438 void PaymentRequest::abortWithException(Exception&& exception)
439 {
440     if (m_state != State::Interactive)
441         return;
442
443     if (auto paymentHandler = std::exchange(m_activePaymentHandler, nullptr)) {
444         unsetPendingActivity(this);
445         paymentHandler->hide();
446     }
447
448     ASSERT(m_state == State::Interactive);
449     m_state = State::Closed;
450     m_showPromise->reject(WTFMove(exception));
451 }
452
453 void PaymentRequest::stop()
454 {
455     abortWithException(Exception { AbortError });
456 }
457
458 // https://www.w3.org/TR/payment-request/#abort()-method
459 ExceptionOr<void> PaymentRequest::abort(AbortPromise&& promise)
460 {
461     if (m_state != State::Interactive)
462         return Exception { InvalidStateError };
463
464     stop();
465     promise.resolve();
466     return { };
467 }
468
469 // https://www.w3.org/TR/payment-request/#canmakepayment()-method
470 void PaymentRequest::canMakePayment(Document& document, CanMakePaymentPromise&& promise)
471 {
472     if (m_state != State::Created) {
473         promise.reject(Exception { InvalidStateError });
474         return;
475     }
476
477     for (auto& paymentMethod : m_serializedMethodData) {
478         auto data = parse(document, paymentMethod.serializedData);
479         if (data.hasException())
480             continue;
481
482         auto handler = PaymentHandler::create(document, *this, paymentMethod.identifier);
483         if (!handler)
484             continue;
485
486         auto exception = handler->convertData(data.releaseReturnValue());
487         if (exception.hasException())
488             continue;
489
490         handler->canMakePayment([promise = WTFMove(promise)](bool canMakePayment) mutable {
491             promise.resolve(canMakePayment);
492         });
493         return;
494     }
495
496     promise.resolve(false);
497 }
498
499 const String& PaymentRequest::id() const
500 {
501     return m_details.id;
502 }
503
504 std::optional<PaymentShippingType> PaymentRequest::shippingType() const
505 {
506     if (m_options.requestShipping)
507         return m_options.shippingType;
508     return std::nullopt;
509 }
510
511 bool PaymentRequest::canSuspendForDocumentSuspension() const
512 {
513     switch (m_state) {
514     case State::Created:
515         ASSERT(!m_activePaymentHandler);
516         return true;
517     case State::Interactive:
518     case State::Closed:
519         return !m_activePaymentHandler;
520     }
521 }
522
523 void PaymentRequest::shippingAddressChanged(Ref<PaymentAddress>&& shippingAddress)
524 {
525     ASSERT(m_state == State::Interactive);
526     m_shippingAddress = WTFMove(shippingAddress);
527     auto event = PaymentRequestUpdateEvent::create(eventNames().shippingaddresschangeEvent, *this);
528     dispatchEvent(event.get());
529 }
530
531 void PaymentRequest::shippingOptionChanged(const String& shippingOption)
532 {
533     ASSERT(m_state == State::Interactive);
534     m_shippingOption = shippingOption;
535     auto event = PaymentRequestUpdateEvent::create(eventNames().shippingoptionchangeEvent, *this);
536     dispatchEvent(event.get());
537 }
538
539 bool PaymentRequest::dispatchEvent(Event& event)
540 {
541     if (!event.isTrusted())
542         return EventTargetWithInlineData::dispatchEvent(event);
543
544     if (m_isUpdating)
545         return false;
546
547     if (m_state != State::Interactive)
548         return false;
549
550     return EventTargetWithInlineData::dispatchEvent(event);
551 }
552
553 ExceptionOr<void> PaymentRequest::updateWith(Event& event, Ref<DOMPromise>&& promise)
554 {
555     if (m_state != State::Interactive)
556         return Exception { InvalidStateError };
557
558     if (m_isUpdating)
559         return Exception { InvalidStateError };
560
561     event.stopPropagation();
562     event.stopImmediatePropagation();
563     m_isUpdating = true;
564
565     m_detailsPromise = WTFMove(promise);
566     m_detailsPromise->whenSettled([this, protectedThis = makeRefPtr(this), type = event.type()]() {
567         settleDetailsPromise(type);
568     });
569
570     return { };
571 }
572
573 void PaymentRequest::settleDetailsPromise(const AtomicString& type)
574 {
575     auto scopeExit = makeScopeExit([&] {
576         m_isUpdating = false;
577     });
578
579     if (m_state != State::Interactive)
580         return;
581
582     if (m_detailsPromise->status() == DOMPromise::Status::Rejected) {
583         stop();
584         return;
585     }
586
587     auto& context = *m_detailsPromise->scriptExecutionContext();
588     auto throwScope = DECLARE_THROW_SCOPE(context.vm());
589     auto paymentDetailsUpdate = convertDictionary<PaymentDetailsUpdate>(*context.execState(), m_detailsPromise->result());
590     if (throwScope.exception()) {
591         abortWithException(Exception { ExistingExceptionError });
592         return;
593     }
594
595     auto totalResult = checkAndCanonicalizeTotal(paymentDetailsUpdate.total.amount);
596     if (totalResult.hasException()) {
597         abortWithException(totalResult.releaseException());
598         return;
599     }
600
601     auto detailsResult = checkAndCanonicalizeDetails(*context.execState(), paymentDetailsUpdate, m_options.requestShipping, ShouldValidatePaymentMethodIdentifier::Yes);
602     if (detailsResult.hasException()) {
603         abortWithException(detailsResult.releaseException());
604         return;
605     }
606
607     auto shippingOptionAndModifierData = detailsResult.releaseReturnValue();
608
609     m_details.total = WTFMove(paymentDetailsUpdate.total);
610     m_details.displayItems = WTFMove(paymentDetailsUpdate.displayItems);
611     if (m_options.requestShipping) {
612         m_details.shippingOptions = WTFMove(paymentDetailsUpdate.shippingOptions);
613         m_shippingOption = WTFMove(std::get<0>(shippingOptionAndModifierData));
614     }
615
616     m_details.modifiers = WTFMove(paymentDetailsUpdate.modifiers);
617     m_serializedModifierData = WTFMove(std::get<1>(shippingOptionAndModifierData));
618
619     auto result = m_activePaymentHandler->detailsUpdated(type, paymentDetailsUpdate.error);
620     if (result.hasException()) {
621         abortWithException(result.releaseException());
622         return;
623     }
624 }
625
626 void PaymentRequest::accept(const String& methodName, JSC::Strong<JSC::JSObject>&& details, Ref<PaymentAddress>&& shippingAddress, const String& payerName, const String& payerEmail, const String& payerPhone)
627 {
628     ASSERT(m_state == State::Interactive);
629
630     auto response = PaymentResponse::create(*this);
631     response->setRequestId(m_details.id);
632     response->setMethodName(methodName);
633     response->setDetails(WTFMove(details));
634
635     if (m_options.requestShipping) {
636         response->setShippingAddress(shippingAddress.ptr());
637         response->setShippingOption(m_shippingOption);
638     }
639
640     if (m_options.requestPayerName)
641         response->setPayerName(payerName);
642
643     if (m_options.requestPayerEmail)
644         response->setPayerEmail(payerEmail);
645
646     if (m_options.requestPayerPhone)
647         response->setPayerPhone(payerPhone);
648
649     m_showPromise->resolve(response.get());
650     m_state = State::Closed;
651 }
652
653 void PaymentRequest::complete(std::optional<PaymentComplete>&& result)
654 {
655     ASSERT(m_state == State::Closed);
656     std::exchange(m_activePaymentHandler, nullptr)->complete(WTFMove(result));
657 }
658
659 void PaymentRequest::cancel()
660 {
661     if (m_state != State::Interactive)
662         return;
663
664     if (m_isUpdating)
665         return;
666
667     m_activePaymentHandler = nullptr;
668     stop();
669 }
670
671 } // namespace WebCore
672
673 #endif // ENABLE(PAYMENT_REQUEST)