PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.27
Booktics – Appointment Booking Calendar for Service Businesses v1.0.27
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 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.2 1.0.3 All 31 releases
← All changes | core/cart/cart-session.php +60 -195 trunk → 1.0.27 View file →
@@ -5,14 +5,8 @@
5 5 /**
6 6 * Class Cart_Session
7 7 * Handles cart session management for the Booktics plugin
8 8 *
9 - * Every session this class opens is closed again before the method returns so
10 - * the PHP session file lock is never held for the rest of the request. A held
11 - * lock blocks other requests from the same browser, such as WordPress Site
12 - * Health loopback requests to the REST API. Sessions started by other code are
13 - * left open, because closing them would discard that code's later writes.
14 - *
15 9 * @package Booktics\Cart
16 10 */
17 11 class Cart_Session {
18 12
@@ -44,137 +38,12 @@
44 38 if ( headers_sent() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
45 39 return false;
46 40 }
47 41
48 - return session_start(
49 - array(
50 - 'cookie_httponly' => true,
51 - 'cookie_secure' => is_ssl(),
52 - 'cookie_samesite' => 'Lax',
53 - 'use_strict_mode' => true,
54 - )
55 - );
42 + return session_start();
56 43 }
57 44
58 45 /**
59 - * Open the session, run a callback against it and release it again.
60 - *
61 - * The session is closed only when this call opened it, so a session owned by
62 - * another plugin stays open for its owner.
63 - *
64 - * @param callable $callback Receives nothing and returns the callback result.
65 - *
66 - * @return array{0: bool, 1: mixed} Whether the session was usable, and the callback result.
67 - */
68 - private static function with_session( callable $callback ): array {
69 - $should_close = PHP_SESSION_ACTIVE !== session_status();
70 -
71 - if ( ! self::maybe_start_session() ) {
72 - return array( false, null );
73 - }
74 -
75 - try {
76 - return array( true, $callback() );
77 - } finally {
78 - if ( $should_close ) {
79 - session_write_close();
80 - }
81 - }
82 - }
83 -
84 - /**
85 - * Read the stored cart from the open session.
86 - *
87 - * Must only be called while the session is open.
88 - *
89 - * @return array{items: array, total: float, coupon: string}
90 - */
91 - private static function read_session_cart(): array {
92 - return self::to_cart( self::read_raw_session_cart() );
93 - }
94 -
95 - /**
96 - * Read the unsanitized cart from the open session.
97 - *
98 - * Must only be called while the session is open.
99 - *
100 - * @return mixed Raw session value, or null when nothing is stored.
101 - */
102 - private static function read_raw_session_cart() {
103 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_session_data() in to_cart(). Session data is never slashed, so it is not unslashed.
104 - return ! empty( $_SESSION[ self::SESSION_KEY ] ) ? $_SESSION[ self::SESSION_KEY ] : null;
105 - }
106 -
107 - /**
108 - * Turn raw session data into a sanitized cart.
109 - *
110 - * @param mixed $raw_data Raw session value.
111 - *
112 - * @return array{items: array, total: float, coupon: string}
113 - */
114 - private static function to_cart( $raw_data ): array {
115 - $session_data = self::sanitize_session_data( $raw_data );
116 -
117 - return is_array( $session_data ) ? $session_data : self::empty_cart();
118 - }
119 -
120 - /**
121 - * Read, modify and write the cart while holding the session lock once.
122 - *
123 - * Holding the lock across the whole read-modify-write stops parallel
124 - * requests from the same browser from overwriting each other's changes.
125 - *
126 - * @param callable $callback Receives the current cart and returns the new cart.
127 - *
128 - * @return array|null The stored cart, or null when the session is unavailable.
129 - */
130 - private static function mutate( callable $callback ): ?array {
131 - list( , $cart ) = self::with_session(
132 - function () use ( $callback ) {
133 - $cart = $callback( self::read_session_cart() );
134 - $_SESSION[ self::SESSION_KEY ] = $cart;
135 -
136 - return $cart;
137 - }
138 - );
139 -
140 - return $cart;
141 - }
142 -
143 - /**
144 - * Get the amount a cart item adds to the cart total.
145 - *
146 - * Session data always carries a subtotal, which is 0 when none was stored,
147 - * so a zero subtotal falls back to the item price.
148 - *
149 - * @param array $item Cart item.
150 - *
151 - * @return float
152 - */
153 - private static function item_amount( array $item ): float {
154 - $subtotal = floatval( $item['subtotal'] ?? 0 );
155 -
156 - return $subtotal > 0 ? $subtotal : floatval( $item['price'] ?? 0 );
157 - }
158 -
159 - /**
160 - * Sum the amounts of the given cart items.
161 - *
162 - * @param array $items Cart items.
163 - *
164 - * @return float
165 - */
166 - private static function calculate_total( array $items ): float {
167 - return array_reduce(
168 - $items,
169 - function ( $total, $item ) {
170 - return $total + self::item_amount( $item );
171 - },
172 - 0.00
173 - );
174 - }
175 -
176 - /**
177 46 * Get an empty cart structure
178 47 *
179 48 * @return array{items: array, total: float, coupon: string}
180 49 */
@@ -191,16 +60,21 @@
191 60 *
192 61 * @return array{items: array, total: float}
193 62 */
194 63 public static function get() {
195 - // Sanitize after the lock is released, since sanitization filters may be slow.
196 - list( $available, $raw_data ) = self::with_session(
197 - function () {
198 - return self::read_raw_session_cart();
199 - }
200 - );
64 + if ( ! self::maybe_start_session() ) {
65 + return self::empty_cart();
66 + }
201 67
202 - return $available ? self::to_cart( $raw_data ) : self::empty_cart();
68 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_session_data() on next line.
69 + $raw_data = ! empty( $_SESSION[ self::SESSION_KEY ] ) ? wp_unslash( $_SESSION[ self::SESSION_KEY ] ) : null;
70 + $session_data = self::sanitize_session_data( $raw_data );
71 +
72 + if ( ! is_array( $session_data ) ) {
73 + return self::empty_cart();
74 + }
75 +
76 + return $session_data;
203 77 }
204 78
205 79 /**
206 80 * Sanitize raw cart session data
@@ -251,33 +125,29 @@
251 125 * Set cart data in session
252 126 *
253 127 * @param array $cart Cart data to store in session
254 128 *
255 - * @return bool True when the cart was stored.
129 + * @return void
256 130 */
257 - public static function set( array $cart ): bool {
258 - list( $available ) = self::with_session(
259 - function () use ( $cart ) {
260 - $_SESSION[ self::SESSION_KEY ] = $cart;
261 - }
262 - );
131 + public static function set( array $cart ) {
132 + if ( ! self::maybe_start_session() ) {
133 + return;
134 + }
263 135
264 - return $available;
136 + $_SESSION[ self::SESSION_KEY ] = $cart;
265 137 }
266 138
267 139 /**
268 140 * Clear cart data from session
269 141 *
270 - * @return bool True when the cart was cleared.
142 + * @return void
271 143 */
272 - public static function clear(): bool {
273 - list( $available ) = self::with_session(
274 - function () {
275 - unset( $_SESSION[ self::SESSION_KEY ] );
276 - }
277 - );
144 + public static function clear() {
145 + if ( ! self::maybe_start_session() ) {
146 + return;
147 + }
278 148
279 - return $available;
149 + unset( $_SESSION[ self::SESSION_KEY ] );
280 150 }
281 151
282 152 /**
283 153 * Add an item to the cart session
@@ -286,21 +156,15 @@
286 156 *
287 157 * @return array
288 158 */
289 159 public static function add_item( array $item ): array {
290 - $cart = self::mutate(
291 - function ( $cart ) use ( $item ) {
292 - $cart['items'][] = $item;
293 - $cart['total'] = floatval( $cart['total'] ?? 0 ) + self::item_amount( $item );
160 + $cart = self::get();
161 + $cart['items'][] = $item;
162 + $cart['total'] = floatval( $cart['total'] ?? 0 ) + floatval( $item['subtotal'] ?? $item['price'] ?? 0 );
163 + $cart = apply_filters( 'booktics_pro_cart_session', $cart );
294 164
295 - return apply_filters( 'booktics_pro_cart_session', $cart );
296 - }
297 - );
165 + self::set( $cart );
298 166
299 - if ( null === $cart || empty( $cart['items'] ) ) {
300 - return $item;
301 - }
302 -
303 167 return end( $cart['items'] );
304 168 }
305 169
306 170 /**
@@ -310,23 +174,24 @@
310 174 *
311 175 * @return void
312 176 */
313 177 public static function delete_item( string $id ) {
314 - self::mutate(
315 - function ( $cart ) use ( $id ) {
316 - $cart['items'] = array_values(
317 - array_filter(
318 - $cart['items'],
319 - function ( $item ) use ( $id ) {
320 - return $item['uuid'] !== $id;
321 - }
322 - )
323 - );
324 - $cart['total'] = self::calculate_total( $cart['items'] );
178 + $cart = self::get();
179 + $cart['items'] = array_values(
180 + array_filter(
181 + $cart['items'],
182 + function ( $item ) use ( $id ) {
183 + return $item['uuid'] != $id;
184 + }
185 + )
186 + );
187 + $cart['total'] = array_reduce(
188 + $cart['items'], function ( $total, $item ) {
189 + return $total + $item['price'];
190 + }, 0.00
191 + );
325 192
326 - return $cart;
327 - }
328 - );
193 + self::set( $cart );
329 194 }
330 195
331 196 /**
332 197 * Edit an item in the cart session
@@ -333,24 +198,24 @@
333 198 *
334 199 * @param string $uuid UUID of the item to edit
335 200 * @param array $new_data New data to update the item with
336 201 *
337 - * @return array The updated cart, or an empty cart when the session is unavailable.
202 + * @return void
338 203 */
339 - public static function edit_item( string $uuid, array $new_data ): array {
340 - $cart = self::mutate(
341 - function ( $cart ) use ( $uuid, $new_data ) {
342 - $cart['items'] = array_map(
343 - function ( $item ) use ( $uuid, $new_data ) {
344 - return $item['uuid'] === $uuid ? array_merge( $item, $new_data ) : $item;
345 - },
346 - $cart['items']
347 - );
348 - $cart['total'] = self::calculate_total( $cart['items'] );
204 + public static function edit_item( string $uuid, array $new_data ) {
205 + $cart = self::get();
206 + $cart['total'] = 0;
349 207
350 - return apply_filters( 'booktics_pro_cart_session', $cart );
351 - }
208 + $cart['items'] = array_map(
209 + function ( $item ) use ( $uuid, $new_data, &$cart ) {
210 + if ( $item['uuid'] === $uuid ) {
211 + $item = array_merge( $item, $new_data );
212 + }
213 + $cart['total'] += floatval( $item['subtotal'] ?? $item['price'] ?? 0 );
214 +
215 + return $item;
216 + }, $cart['items']
352 217 );
353 -
354 - return $cart ?? self::empty_cart();
218 + $cart = apply_filters( 'booktics_pro_cart_session', $cart );
219 + self::set( $cart );
355 220 }
356 221 }