Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(153)

Side by Side Diff: third_party/WebKit/Source/modules/payments/PaymentRequest.cpp

Issue 2516923002: [Merge M-56] Add canMakeActivePayment() method to web payments. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "modules/payments/PaymentRequest.h" 5 #include "modules/payments/PaymentRequest.h"
6 6
7 #include "bindings/core/v8/ExceptionState.h" 7 #include "bindings/core/v8/ExceptionState.h"
8 #include "bindings/core/v8/ScriptPromiseResolver.h" 8 #include "bindings/core/v8/ScriptPromiseResolver.h"
9 #include "bindings/core/v8/ScriptState.h" 9 #include "bindings/core/v8/ScriptState.h"
10 #include "bindings/core/v8/V8StringResource.h" 10 #include "bindings/core/v8/V8StringResource.h"
(...skipping 18 matching lines...) Expand all
29 #include "modules/payments/PaymentsValidators.h" 29 #include "modules/payments/PaymentsValidators.h"
30 #include "mojo/public/cpp/bindings/interface_request.h" 30 #include "mojo/public/cpp/bindings/interface_request.h"
31 #include "mojo/public/cpp/bindings/wtf_array.h" 31 #include "mojo/public/cpp/bindings/wtf_array.h"
32 #include "platform/mojo/MojoHelper.h" 32 #include "platform/mojo/MojoHelper.h"
33 #include "public/platform/InterfaceProvider.h" 33 #include "public/platform/InterfaceProvider.h"
34 #include "public/platform/Platform.h" 34 #include "public/platform/Platform.h"
35 #include "public/platform/WebTraceLocation.h" 35 #include "public/platform/WebTraceLocation.h"
36 #include "wtf/HashSet.h" 36 #include "wtf/HashSet.h"
37 #include <utility> 37 #include <utility>
38 38
39 namespace mojo { 39 using payments::mojom::blink::ActivePaymentQueryResult;
40 40 using payments::mojom::blink::PaymentAddressPtr;
41 using payments::mojom::blink::PaymentCurrencyAmount; 41 using payments::mojom::blink::PaymentCurrencyAmount;
42 using payments::mojom::blink::PaymentCurrencyAmountPtr; 42 using payments::mojom::blink::PaymentCurrencyAmountPtr;
43 using payments::mojom::blink::PaymentDetails; 43 using payments::mojom::blink::PaymentDetails;
44 using payments::mojom::blink::PaymentDetailsModifier; 44 using payments::mojom::blink::PaymentDetailsModifier;
45 using payments::mojom::blink::PaymentDetailsModifierPtr; 45 using payments::mojom::blink::PaymentDetailsModifierPtr;
46 using payments::mojom::blink::PaymentDetailsPtr; 46 using payments::mojom::blink::PaymentDetailsPtr;
47 using payments::mojom::blink::PaymentDetailsPtr;
47 using payments::mojom::blink::PaymentErrorReason; 48 using payments::mojom::blink::PaymentErrorReason;
48 using payments::mojom::blink::PaymentItem; 49 using payments::mojom::blink::PaymentItem;
49 using payments::mojom::blink::PaymentItemPtr; 50 using payments::mojom::blink::PaymentItemPtr;
50 using payments::mojom::blink::PaymentMethodData; 51 using payments::mojom::blink::PaymentMethodData;
51 using payments::mojom::blink::PaymentMethodDataPtr; 52 using payments::mojom::blink::PaymentMethodDataPtr;
52 using payments::mojom::blink::PaymentOptions; 53 using payments::mojom::blink::PaymentOptions;
53 using payments::mojom::blink::PaymentOptionsPtr; 54 using payments::mojom::blink::PaymentOptionsPtr;
55 using payments::mojom::blink::PaymentResponsePtr;
54 using payments::mojom::blink::PaymentShippingOption; 56 using payments::mojom::blink::PaymentShippingOption;
55 using payments::mojom::blink::PaymentShippingOptionPtr; 57 using payments::mojom::blink::PaymentShippingOptionPtr;
56 using payments::mojom::blink::PaymentShippingType; 58 using payments::mojom::blink::PaymentShippingType;
57 59
60 namespace mojo {
61
58 template <> 62 template <>
59 struct TypeConverter<PaymentCurrencyAmountPtr, blink::PaymentCurrencyAmount> { 63 struct TypeConverter<PaymentCurrencyAmountPtr, blink::PaymentCurrencyAmount> {
60 static PaymentCurrencyAmountPtr Convert( 64 static PaymentCurrencyAmountPtr Convert(
61 const blink::PaymentCurrencyAmount& input) { 65 const blink::PaymentCurrencyAmount& input) {
62 PaymentCurrencyAmountPtr output = PaymentCurrencyAmount::New(); 66 PaymentCurrencyAmountPtr output = PaymentCurrencyAmount::New();
63 output->currency = input.currency(); 67 output->currency = input.currency();
64 output->value = input.value(); 68 output->value = input.value();
65 if (input.hasCurrencySystem()) 69 if (input.hasCurrencySystem())
66 output->currencySystem = input.currencySystem(); 70 output->currencySystem = input.currencySystem();
67 return output; 71 return output;
(...skipping 22 matching lines...) Expand all
90 output->selected = input.hasSelected() && input.selected(); 94 output->selected = input.hasSelected() && input.selected();
91 return output; 95 return output;
92 } 96 }
93 }; 97 };
94 98
95 template <> 99 template <>
96 struct TypeConverter<PaymentDetailsModifierPtr, blink::PaymentDetailsModifier> { 100 struct TypeConverter<PaymentDetailsModifierPtr, blink::PaymentDetailsModifier> {
97 static PaymentDetailsModifierPtr Convert( 101 static PaymentDetailsModifierPtr Convert(
98 const blink::PaymentDetailsModifier& input) { 102 const blink::PaymentDetailsModifier& input) {
99 PaymentDetailsModifierPtr output = PaymentDetailsModifier::New(); 103 PaymentDetailsModifierPtr output = PaymentDetailsModifier::New();
100 output->supported_methods = 104 output->supported_methods = input.supportedMethods();
101 WTF::Vector<WTF::String>(input.supportedMethods());
102 105
103 if (input.hasTotal()) 106 if (input.hasTotal())
104 output->total = PaymentItem::From(input.total()); 107 output->total = PaymentItem::From(input.total());
105 108
106 if (input.hasAdditionalDisplayItems()) { 109 if (input.hasAdditionalDisplayItems()) {
107 for (size_t i = 0; i < input.additionalDisplayItems().size(); ++i) { 110 for (size_t i = 0; i < input.additionalDisplayItems().size(); ++i) {
108 output->additional_display_items.append( 111 output->additional_display_items.append(
109 PaymentItem::From(input.additionalDisplayItems()[i])); 112 PaymentItem::From(input.additionalDisplayItems()[i]));
110 } 113 }
111 } 114 }
(...skipping 24 matching lines...) Expand all
136 if (input.hasModifiers()) { 139 if (input.hasModifiers()) {
137 for (size_t i = 0; i < input.modifiers().size(); ++i) { 140 for (size_t i = 0; i < input.modifiers().size(); ++i) {
138 output->modifiers.append( 141 output->modifiers.append(
139 PaymentDetailsModifier::From(input.modifiers()[i])); 142 PaymentDetailsModifier::From(input.modifiers()[i]));
140 } 143 }
141 } 144 }
142 145
143 if (input.hasError()) 146 if (input.hasError())
144 output->error = input.error(); 147 output->error = input.error();
145 else 148 else
146 output->error = WTF::emptyString(); 149 output->error = emptyString();
147 150
148 return output; 151 return output;
149 } 152 }
150 }; 153 };
151 154
152 template <> 155 template <>
153 struct TypeConverter<PaymentOptionsPtr, blink::PaymentOptions> { 156 struct TypeConverter<PaymentOptionsPtr, blink::PaymentOptions> {
154 static PaymentOptionsPtr Convert(const blink::PaymentOptions& input) { 157 static PaymentOptionsPtr Convert(const blink::PaymentOptions& input) {
155 PaymentOptionsPtr output = PaymentOptions::New(); 158 PaymentOptionsPtr output = PaymentOptions::New();
156 output->request_payer_name = input.requestPayerName(); 159 output->request_payer_name = input.requestPayerName();
(...skipping 331 matching lines...) Expand 10 before | Expand all | Expand 10 after
488 static const char* const validValues[] = { 491 static const char* const validValues[] = {
489 "shipping", "delivery", "pickup", 492 "shipping", "delivery", "pickup",
490 }; 493 };
491 for (size_t i = 0; i < WTF_ARRAY_LENGTH(validValues); i++) { 494 for (size_t i = 0; i < WTF_ARRAY_LENGTH(validValues); i++) {
492 if (shippingType == validValues[i]) 495 if (shippingType == validValues[i])
493 return shippingType; 496 return shippingType;
494 } 497 }
495 return validValues[0]; 498 return validValues[0];
496 } 499 }
497 500
498 payments::mojom::blink::PaymentDetailsPtr maybeKeepShippingOptions( 501 PaymentDetailsPtr maybeKeepShippingOptions(PaymentDetailsPtr details,
499 payments::mojom::blink::PaymentDetailsPtr details, 502 bool keep) {
500 bool keep) {
501 if (!keep) 503 if (!keep)
502 details->shipping_options.resize(0); 504 details->shipping_options.resize(0);
503 505
504 return details; 506 return details;
505 } 507 }
506 508
507 bool allowedToUsePaymentRequest(const Frame* frame) { 509 bool allowedToUsePaymentRequest(const Frame* frame) {
508 // To determine whether a Document object |document| is allowed to use the 510 // To determine whether a Document object |document| is allowed to use the
509 // feature indicated by attribute name |allowpaymentrequest|, run these steps: 511 // feature indicated by attribute name |allowpaymentrequest|, run these steps:
510 512
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
584 return ScriptPromise::rejectWithDOMException( 586 return ScriptPromise::rejectWithDOMException(
585 scriptState, 587 scriptState,
586 DOMException::create(InvalidStateError, 588 DOMException::create(InvalidStateError,
587 "Never called show(), so nothing to abort")); 589 "Never called show(), so nothing to abort"));
588 590
589 m_abortResolver = ScriptPromiseResolver::create(scriptState); 591 m_abortResolver = ScriptPromiseResolver::create(scriptState);
590 m_paymentProvider->Abort(); 592 m_paymentProvider->Abort();
591 return m_abortResolver->promise(); 593 return m_abortResolver->promise();
592 } 594 }
593 595
596 ScriptPromise PaymentRequest::canMakeActivePayment(ScriptState* scriptState) {
597 if (!m_paymentProvider.is_bound() || m_canMakeActivePaymentResolver ||
598 !scriptState->contextIsValid()) {
599 return ScriptPromise::rejectWithDOMException(
600 scriptState, DOMException::create(InvalidStateError,
601 "Cannot query payment request"));
602 }
603
604 m_paymentProvider->CanMakeActivePayment();
605
606 m_canMakeActivePaymentResolver = ScriptPromiseResolver::create(scriptState);
607 return m_canMakeActivePaymentResolver->promise();
608 }
609
594 bool PaymentRequest::hasPendingActivity() const { 610 bool PaymentRequest::hasPendingActivity() const {
595 return m_showResolver || m_completeResolver; 611 return m_showResolver || m_completeResolver;
596 } 612 }
597 613
598 const AtomicString& PaymentRequest::interfaceName() const { 614 const AtomicString& PaymentRequest::interfaceName() const {
599 return EventTargetNames::PaymentRequest; 615 return EventTargetNames::PaymentRequest;
600 } 616 }
601 617
602 ExecutionContext* PaymentRequest::getExecutionContext() const { 618 ExecutionContext* PaymentRequest::getExecutionContext() const {
603 return ContextLifecycleObserver::getExecutionContext(); 619 return ContextLifecycleObserver::getExecutionContext();
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after
676 m_completeResolver->reject(DOMException::create(AbortError, error)); 692 m_completeResolver->reject(DOMException::create(AbortError, error));
677 clearResolversAndCloseMojoConnection(); 693 clearResolversAndCloseMojoConnection();
678 } 694 }
679 695
680 DEFINE_TRACE(PaymentRequest) { 696 DEFINE_TRACE(PaymentRequest) {
681 visitor->trace(m_options); 697 visitor->trace(m_options);
682 visitor->trace(m_shippingAddress); 698 visitor->trace(m_shippingAddress);
683 visitor->trace(m_showResolver); 699 visitor->trace(m_showResolver);
684 visitor->trace(m_completeResolver); 700 visitor->trace(m_completeResolver);
685 visitor->trace(m_abortResolver); 701 visitor->trace(m_abortResolver);
702 visitor->trace(m_canMakeActivePaymentResolver);
686 EventTargetWithInlineData::trace(visitor); 703 EventTargetWithInlineData::trace(visitor);
687 ContextLifecycleObserver::trace(visitor); 704 ContextLifecycleObserver::trace(visitor);
688 } 705 }
689 706
690 void PaymentRequest::onCompleteTimeoutForTesting() { 707 void PaymentRequest::onCompleteTimeoutForTesting() {
691 m_completeTimer.stop(); 708 m_completeTimer.stop();
692 onCompleteTimeout(0); 709 onCompleteTimeout(0);
693 } 710 }
694 711
695 PaymentRequest::PaymentRequest(ScriptState* scriptState, 712 PaymentRequest::PaymentRequest(ScriptState* scriptState,
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
732 if (m_options.requestShipping()) { 749 if (m_options.requestShipping()) {
733 if (keepShippingOptions) 750 if (keepShippingOptions)
734 m_shippingOption = getSelectedShippingOption(details); 751 m_shippingOption = getSelectedShippingOption(details);
735 m_shippingType = getValidShippingType(m_options.shippingType()); 752 m_shippingType = getValidShippingType(m_options.shippingType());
736 } 753 }
737 754
738 scriptState->domWindow()->frame()->interfaceProvider()->getInterface( 755 scriptState->domWindow()->frame()->interfaceProvider()->getInterface(
739 mojo::GetProxy(&m_paymentProvider)); 756 mojo::GetProxy(&m_paymentProvider));
740 m_paymentProvider.set_connection_error_handler(convertToBaseCallback( 757 m_paymentProvider.set_connection_error_handler(convertToBaseCallback(
741 WTF::bind(&PaymentRequest::OnError, wrapWeakPersistent(this), 758 WTF::bind(&PaymentRequest::OnError, wrapWeakPersistent(this),
742 payments::mojom::blink::PaymentErrorReason::UNKNOWN))); 759 PaymentErrorReason::UNKNOWN)));
743 m_paymentProvider->Init( 760 m_paymentProvider->Init(
744 m_clientBinding.CreateInterfacePtrAndBind(), 761 m_clientBinding.CreateInterfacePtrAndBind(),
745 std::move(validatedMethodData), 762 std::move(validatedMethodData),
746 maybeKeepShippingOptions( 763 maybeKeepShippingOptions(
747 payments::mojom::blink::PaymentDetails::From(details), 764 payments::mojom::blink::PaymentDetails::From(details),
748 keepShippingOptions && m_options.requestShipping()), 765 keepShippingOptions && m_options.requestShipping()),
749 payments::mojom::blink::PaymentOptions::From(m_options)); 766 payments::mojom::blink::PaymentOptions::From(m_options));
750 } 767 }
751 768
752 void PaymentRequest::contextDestroyed() { 769 void PaymentRequest::contextDestroyed() {
753 clearResolversAndCloseMojoConnection(); 770 clearResolversAndCloseMojoConnection();
754 } 771 }
755 772
756 void PaymentRequest::OnShippingAddressChange( 773 void PaymentRequest::OnShippingAddressChange(PaymentAddressPtr address) {
757 payments::mojom::blink::PaymentAddressPtr address) {
758 DCHECK(m_showResolver); 774 DCHECK(m_showResolver);
759 DCHECK(!m_completeResolver); 775 DCHECK(!m_completeResolver);
760 776
761 String errorMessage; 777 String errorMessage;
762 if (!PaymentsValidators::isValidShippingAddress(address, &errorMessage)) { 778 if (!PaymentsValidators::isValidShippingAddress(address, &errorMessage)) {
763 m_showResolver->reject(DOMException::create(SyntaxError, errorMessage)); 779 m_showResolver->reject(DOMException::create(SyntaxError, errorMessage));
764 clearResolversAndCloseMojoConnection(); 780 clearResolversAndCloseMojoConnection();
765 return; 781 return;
766 } 782 }
767 783
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
833 m_completeTimer.startOneShot(completeTimeoutSeconds, BLINK_FROM_HERE); 849 m_completeTimer.startOneShot(completeTimeoutSeconds, BLINK_FROM_HERE);
834 850
835 m_showResolver->resolve(new PaymentResponse(std::move(response), this)); 851 m_showResolver->resolve(new PaymentResponse(std::move(response), this));
836 852
837 // Do not close the mojo connection here. The merchant website should call 853 // Do not close the mojo connection here. The merchant website should call
838 // PaymentResponse::complete(String), which will be forwarded over the mojo 854 // PaymentResponse::complete(String), which will be forwarded over the mojo
839 // connection to display a success or failure message to the user. 855 // connection to display a success or failure message to the user.
840 m_showResolver.clear(); 856 m_showResolver.clear();
841 } 857 }
842 858
843 void PaymentRequest::OnError(mojo::PaymentErrorReason error) { 859 void PaymentRequest::OnError(PaymentErrorReason error) {
844 if (!Platform::current()) { 860 if (!Platform::current()) {
845 // TODO(rockot): Clean this up once renderer shutdown sequence is fixed. 861 // TODO(rockot): Clean this up once renderer shutdown sequence is fixed.
846 return; 862 return;
847 } 863 }
848 864
849 bool isError = false; 865 bool isError = false;
850 ExceptionCode ec = UnknownError; 866 ExceptionCode ec = UnknownError;
851 String message; 867 String message;
852 868
853 switch (error) { 869 switch (error) {
854 case payments::mojom::blink::PaymentErrorReason::USER_CANCEL: 870 case PaymentErrorReason::USER_CANCEL:
855 message = "Request cancelled"; 871 message = "Request cancelled";
856 break; 872 break;
857 case payments::mojom::blink::PaymentErrorReason::NOT_SUPPORTED: 873 case PaymentErrorReason::NOT_SUPPORTED:
858 isError = true; 874 isError = true;
859 ec = NotSupportedError; 875 ec = NotSupportedError;
860 message = "The payment method is not supported"; 876 message = "The payment method is not supported";
861 break; 877 break;
862 case payments::mojom::blink::PaymentErrorReason::UNKNOWN: 878 case PaymentErrorReason::UNKNOWN:
863 isError = true; 879 isError = true;
864 ec = UnknownError; 880 ec = UnknownError;
865 message = "Request failed"; 881 message = "Request failed";
866 break; 882 break;
867 } 883 }
868 884
869 DCHECK(!message.isEmpty()); 885 DCHECK(!message.isEmpty());
870 886
871 if (isError) { 887 if (isError) {
872 if (m_completeResolver) 888 if (m_completeResolver)
873 m_completeResolver->reject(DOMException::create(ec, message)); 889 m_completeResolver->reject(DOMException::create(ec, message));
874 890
875 if (m_showResolver) 891 if (m_showResolver)
876 m_showResolver->reject(DOMException::create(ec, message)); 892 m_showResolver->reject(DOMException::create(ec, message));
877 893
878 if (m_abortResolver) 894 if (m_abortResolver)
879 m_abortResolver->reject(DOMException::create(ec, message)); 895 m_abortResolver->reject(DOMException::create(ec, message));
896
897 if (m_canMakeActivePaymentResolver)
898 m_canMakeActivePaymentResolver->reject(DOMException::create(ec, message));
880 } else { 899 } else {
881 if (m_completeResolver) 900 if (m_completeResolver)
882 m_completeResolver->reject(message); 901 m_completeResolver->reject(message);
883 902
884 if (m_showResolver) 903 if (m_showResolver)
885 m_showResolver->reject(message); 904 m_showResolver->reject(message);
886 905
887 if (m_abortResolver) 906 if (m_abortResolver)
888 m_abortResolver->reject(message); 907 m_abortResolver->reject(message);
908
909 if (m_canMakeActivePaymentResolver)
910 m_canMakeActivePaymentResolver->reject(message);
889 } 911 }
890 912
891 clearResolversAndCloseMojoConnection(); 913 clearResolversAndCloseMojoConnection();
892 } 914 }
893 915
894 void PaymentRequest::OnComplete() { 916 void PaymentRequest::OnComplete() {
895 DCHECK(m_completeResolver); 917 DCHECK(m_completeResolver);
896 m_completeResolver->resolve(); 918 m_completeResolver->resolve();
897 clearResolversAndCloseMojoConnection(); 919 clearResolversAndCloseMojoConnection();
898 } 920 }
899 921
900 void PaymentRequest::OnAbort(bool abortedSuccessfully) { 922 void PaymentRequest::OnAbort(bool abortedSuccessfully) {
901 DCHECK(m_abortResolver); 923 DCHECK(m_abortResolver);
902 DCHECK(m_showResolver); 924 DCHECK(m_showResolver);
903 925
904 if (!abortedSuccessfully) { 926 if (!abortedSuccessfully) {
905 m_abortResolver->reject(DOMException::create(InvalidStateError)); 927 m_abortResolver->reject(DOMException::create(InvalidStateError));
906 m_abortResolver.clear(); 928 m_abortResolver.clear();
907 return; 929 return;
908 } 930 }
909 931
910 m_showResolver->reject(DOMException::create(AbortError)); 932 m_showResolver->reject(DOMException::create(AbortError));
911 m_abortResolver->resolve(); 933 m_abortResolver->resolve();
912 clearResolversAndCloseMojoConnection(); 934 clearResolversAndCloseMojoConnection();
913 } 935 }
914 936
937 void PaymentRequest::OnCanMakeActivePayment(ActivePaymentQueryResult result) {
938 DCHECK(m_canMakeActivePaymentResolver);
939
940 switch (result) {
941 case ActivePaymentQueryResult::CAN_MAKE_ACTIVE_PAYMENT:
942 m_canMakeActivePaymentResolver->resolve(true);
943 break;
944 case ActivePaymentQueryResult::CANNOT_MAKE_ACTIVE_PAYMENT:
945 m_canMakeActivePaymentResolver->resolve(false);
946 break;
947 case ActivePaymentQueryResult::QUERY_QUOTA_EXCEEDED:
948 m_canMakeActivePaymentResolver->reject(
949 DOMException::create(QuotaExceededError, "Query quota exceeded"));
950 break;
951 }
952
953 m_canMakeActivePaymentResolver.clear();
954 }
955
915 void PaymentRequest::onCompleteTimeout(TimerBase*) { 956 void PaymentRequest::onCompleteTimeout(TimerBase*) {
916 m_paymentProvider->Complete(payments::mojom::blink::PaymentComplete(Fail)); 957 m_paymentProvider->Complete(payments::mojom::blink::PaymentComplete(Fail));
917 clearResolversAndCloseMojoConnection(); 958 clearResolversAndCloseMojoConnection();
918 } 959 }
919 960
920 void PaymentRequest::clearResolversAndCloseMojoConnection() { 961 void PaymentRequest::clearResolversAndCloseMojoConnection() {
921 m_completeTimer.stop(); 962 m_completeTimer.stop();
922 m_completeResolver.clear(); 963 m_completeResolver.clear();
923 m_showResolver.clear(); 964 m_showResolver.clear();
924 m_abortResolver.clear(); 965 m_abortResolver.clear();
966 m_canMakeActivePaymentResolver.clear();
925 if (m_clientBinding.is_bound()) 967 if (m_clientBinding.is_bound())
926 m_clientBinding.Close(); 968 m_clientBinding.Close();
927 m_paymentProvider.reset(); 969 m_paymentProvider.reset();
928 } 970 }
929 971
930 } // namespace blink 972 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698