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 +74 -2 1.0.60 → 1.0.64 View file →
@@ -5,8 +5,10 @@
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;
10 12 use Timetics\Core\Bookings\Booking;
11 13 use Timetics\Utils\Singleton;
12 14 use WP_HTTP_Response;
@@ -62,8 +64,19 @@
62 64 *
63 65 * @return JSON
64 66 */
65 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 +
66 79 $data = json_decode( $request->get_body(), true );
67 80
68 81 $amount = ! empty( $data['amount'] ) ? floatval( $data['amount'] ) : 0;
69 82 $currency = ! empty( $data['currency'] ) ? sanitize_text_field( $data['currency'] ) : '';
@@ -71,10 +84,14 @@
71 84 $token = ! empty( $data['security_token'] ) ? sanitize_text_field( $data['security_token'] ) : '';
72 85
73 86 $metadata = [];
74 87
75 - // Bind PaymentIntent to a real booking by metadata so make_payment can
76 - // verify ownership server-side. Reject mismatched / missing bindings.
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.
77 94 if ( $booking_id > 0 && '' !== $token ) {
78 95 $booking = new Booking( $booking_id );
79 96
80 97 if ( ! $booking->is_booking() ) {
@@ -102,10 +119,25 @@
102 119 }
103 120
104 121 $metadata['booking_id'] = $booking_id;
105 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();
106 126 }
107 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 +
108 140 $payment = new StripePayment();
109 141
110 142 $payment = $payment->create_payment(
111 143 [
@@ -124,8 +156,48 @@
124 156
125 157 return new WP_HTTP_Response( $response, 403 );
126 158 }
127 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 +
128 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;
129 201 }
130 202 }
131 203