PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | core/integrations/stripe/api-stripe.php +117 -6 1.0.12 → 1.0.64 View file →
@@ -5,9 +5,12 @@
5 5 * @package Timetics
6 6 */
7 7 namespace Timetics\Core\Integrations\Stripe;
8 8
9 +defined( 'ABSPATH' ) || exit;
10 +
9 11 use Timetics\Base\Api;
12 +use Timetics\Core\Bookings\Booking;
10 13 use Timetics\Utils\Singleton;
11 14 use WP_HTTP_Response;
12 15
13 16 /**
@@ -61,23 +64,91 @@
61 64 *
62 65 * @return JSON
63 66 */
64 67 public function create_payment( $request ) {
68 + if ( $this->is_rate_limited() ) {
69 + return new WP_HTTP_Response(
70 + [
71 + 'success' => 0,
72 + 'status_code' => 429,
73 + 'message' => __( 'Too many requests. Please try again later.', 'timetics' ),
74 + ],
75 + 429
76 + );
77 + }
78 +
65 79 $data = json_decode( $request->get_body(), true );
66 80
67 - $amount = ! empty( $data['amount'] ) ? floatval( $data['amount'] ) : 0;
68 - $currency = ! empty( $data['currency'] ) ? sanitize_text_field( $data['currency'] ) : '';
81 + $amount = ! empty( $data['amount'] ) ? floatval( $data['amount'] ) : 0;
82 + $currency = ! empty( $data['currency'] ) ? sanitize_text_field( $data['currency'] ) : '';
83 + $booking_id = ! empty( $data['booking_id'] ) ? absint( $data['booking_id'] ) : 0;
84 + $token = ! empty( $data['security_token'] ) ? sanitize_text_field( $data['security_token'] ) : '';
69 85
86 + $metadata = [];
87 +
88 + // The card form (StripePayment.js) creates this PaymentIntent up front, before
89 + // a booking exists, purely from the meeting's price — so booking_id/token are
90 + // optional here. Binding happens later via bind_payment_intent(), and the booking
91 + // can only be marked paid there after its security_token is verified. An intent
92 + // created without a booking can never complete a payment, so this cannot be used
93 + // to steal funds; it can only let a caller create inert PaymentIntents in Stripe.
94 + if ( $booking_id > 0 && '' !== $token ) {
95 + $booking = new Booking( $booking_id );
96 +
97 + if ( ! $booking->is_booking() ) {
98 + return new WP_HTTP_Response(
99 + [
100 + 'success' => 0,
101 + 'status_code' => 404,
102 + 'message' => __( 'Invalid booking id.', 'timetics' ),
103 + ],
104 + 404
105 + );
106 + }
107 +
108 + $stored = (string) $booking->get_security_token();
109 +
110 + if ( '' === $stored || ! hash_equals( $stored, $token ) ) {
111 + return new WP_HTTP_Response(
112 + [
113 + 'success' => 0,
114 + 'status_code' => 403,
115 + 'message' => __( 'Invalid booking token.', 'timetics' ),
116 + ],
117 + 403
118 + );
119 + }
120 +
121 + $metadata['booking_id'] = $booking_id;
122 + $metadata['security_token'] = $stored;
123 +
124 + // Once bound, trust the booking's own total over whatever the client sent.
125 + $amount = (float) $booking->get_total();
126 + }
127 +
128 + // Sanity bounds — reject nonsense amounts regardless of binding.
129 + if ( $amount <= 0 || $amount > 1000000 || ! preg_match( '/^[A-Za-z]{3}$/', (string) $currency ) ) {
130 + return new WP_HTTP_Response(
131 + [
132 + 'success' => 0,
133 + 'status_code' => 400,
134 + 'message' => __( 'Invalid amount or currency.', 'timetics' ),
135 + ],
136 + 400
137 + );
138 + }
139 +
70 140 $payment = new StripePayment();
71 141
72 142 $payment = $payment->create_payment(
73 143 [
74 - 'amount' => $amount * 100,
75 - 'currency' => $currency,
76 - ]
144 + 'amount' => $amount * 100,
145 + 'currency' => $currency,
146 + 'metadata' => $metadata,
147 + ]
77 148 );
78 149
79 - if ( is_wp_error( $data ) ) {
150 + if ( is_wp_error( $payment ) ) {
80 151 $response = [
81 152 'success' => 0,
82 153 'status_code' => 403,
83 154 'message' => $payment->get_error_message(),
@@ -85,8 +156,48 @@
85 156
86 157 return new WP_HTTP_Response( $response, 403 );
87 158 }
88 159
160 + if ( is_array( $payment ) && ! empty( $payment['error'] ) ) {
161 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
162 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is guarded by WP_DEBUG checks.
163 + error_log( 'Timetics Stripe payment intent failed: ' . wp_json_encode( $payment['error'] ) );
164 + }
165 +
166 + return new WP_HTTP_Response(
167 + [
168 + 'success' => 0,
169 + 'status_code' => 402,
170 + 'message' => __( 'We could not start the payment. Please try again.', 'timetics' ),
171 + ],
172 + 402
173 + );
174 + }
175 +
89 176 return rest_ensure_response( $payment );
177 + }
178 +
179 + /**
180 + * Simple per-IP fixed-window limiter for the public payment-intent route.
181 + *
182 + * @return bool
183 + */
184 + private function is_rate_limited() {
185 + $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
186 +
187 + if ( '' === $ip ) {
188 + return false;
189 + }
190 +
191 + $key = 'tt_stripe_rl_' . md5( $ip );
192 + $count = (int) get_transient( $key );
193 +
194 + if ( $count >= 20 ) {
195 + return true;
196 + }
197 +
198 + set_transient( $key, $count + 1, MINUTE_IN_SECONDS * 10 );
199 +
200 + return false;
90 201 }
91 202 }
92 203