| @@ -17,21 +17,61 @@ | ||
| 17 | 17 | */ |
| 18 | 18 | const SESSION_KEY = 'booktics_cart'; |
| 19 | 19 | |
| 20 | 20 | /** |
| 21 | + * Start a PHP session when one is not running yet. | |
| 22 | + * | |
| 23 | + * Sessions are started lazily instead of on every request so that a session | |
| 24 | + * is never opened after output has already been flushed, which triggers a | |
| 25 | + * "Session cannot be started after headers have already been sent" warning. | |
| 26 | + * | |
| 27 | + * @return bool True when $_SESSION is usable. | |
| 28 | + */ | |
| 29 | + private static function maybe_start_session(): bool { | |
| 30 | + if ( PHP_SESSION_ACTIVE === session_status() ) { | |
| 31 | + return true; | |
| 32 | + } | |
| 33 | + | |
| 34 | + if ( PHP_SESSION_DISABLED === session_status() ) { | |
| 35 | + return false; | |
| 36 | + } | |
| 37 | + | |
| 38 | + if ( headers_sent() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { | |
| 39 | + return false; | |
| 40 | + } | |
| 41 | + | |
| 42 | + return session_start(); | |
| 43 | + } | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * Get an empty cart structure | |
| 47 | + * | |
| 48 | + * @return array{items: array, total: float, coupon: string} | |
| 49 | + */ | |
| 50 | + private static function empty_cart(): array { | |
| 51 | + return array( | |
| 52 | + 'items' => array(), | |
| 53 | + 'total' => 0.00, | |
| 54 | + 'coupon' => '', | |
| 55 | + ); | |
| 56 | + } | |
| 57 | + | |
| 58 | + /** | |
| 21 | 59 | * Get cart data from session |
| 22 | 60 | * |
| 23 | 61 | * @return array{items: array, total: float} |
| 24 | 62 | */ |
| 25 | 63 | public static function get() { |
| 26 | - $session_data = ! empty( $_SESSION[ self::SESSION_KEY ] ) ? $_SESSION[ self::SESSION_KEY ] : null; | |
| 64 | + if ( ! self::maybe_start_session() ) { | |
| 65 | + return self::empty_cart(); | |
| 66 | + } | |
| 27 | 67 | |
| 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 | + | |
| 28 | 72 | if ( ! is_array( $session_data ) ) { |
| 29 | - return array( | |
| 30 | - 'items' => array(), | |
| 31 | - 'total' => 0.00, | |
| 32 | - 'coupon' => '', | |
| 33 | - ); | |
| 73 | + return self::empty_cart(); | |
| 34 | 74 | } |
| 35 | 75 | |
| 36 | 76 | return $session_data; |
| 37 | 77 | } |
| @@ -36,8 +76,53 @@ | ||
| 36 | 76 | return $session_data; |
| 37 | 77 | } |
| 38 | 78 | |
| 39 | 79 | /** |
| 80 | + * Sanitize raw cart session data | |
| 81 | + * | |
| 82 | + * @param mixed $data Raw data retrieved from the session. | |
| 83 | + * | |
| 84 | + * @return mixed Sanitized array or null if input is not an array. | |
| 85 | + */ | |
| 86 | + private static function sanitize_session_data( $data ) { | |
| 87 | + if ( ! is_array( $data ) ) { | |
| 88 | + return null; | |
| 89 | + } | |
| 90 | + | |
| 91 | + $sanitized_items = array(); | |
| 92 | + | |
| 93 | + if ( ! empty( $data['items'] ) && is_array( $data['items'] ) ) { | |
| 94 | + foreach ( $data['items'] as $item ) { | |
| 95 | + if ( ! is_array( $item ) ) { | |
| 96 | + continue; | |
| 97 | + } | |
| 98 | + | |
| 99 | + $sanitized_item = array( | |
| 100 | + 'uuid' => sanitize_text_field( $item['uuid'] ?? '' ), | |
| 101 | + 'service_id' => absint( $item['service_id'] ?? 0 ), | |
| 102 | + 'team_member_id' => absint( $item['team_member_id'] ?? 0 ), | |
| 103 | + 'date' => sanitize_text_field( $item['date'] ?? '' ), | |
| 104 | + 'start_time' => sanitize_text_field( $item['start_time'] ?? '' ), | |
| 105 | + 'end_time' => sanitize_text_field( $item['end_time'] ?? '' ), | |
| 106 | + 'price' => floatval( $item['price'] ?? 0 ), | |
| 107 | + 'subtotal' => floatval( $item['subtotal'] ?? 0 ), | |
| 108 | + 'total' => floatval( $item['total'] ?? 0 ), | |
| 109 | + ); | |
| 110 | + | |
| 111 | + $sanitized_item = apply_filters( 'booktics_pro_sanitize_cart_item', $sanitized_item, $item ); | |
| 112 | + | |
| 113 | + $sanitized_items[] = $sanitized_item; | |
| 114 | + } | |
| 115 | + } | |
| 116 | + | |
| 117 | + return array( | |
| 118 | + 'items' => $sanitized_items, | |
| 119 | + 'total' => floatval( $data['total'] ?? 0.00 ), | |
| 120 | + 'coupon' => sanitize_text_field( $data['coupon'] ?? '' ), | |
| 121 | + ); | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** | |
| 40 | 125 | * Set cart data in session |
| 41 | 126 | * |
| 42 | 127 | * @param array $cart Cart data to store in session |
| 43 | 128 | * |
| @@ -43,8 +128,12 @@ | ||
| 43 | 128 | * |
| 44 | 129 | * @return void |
| 45 | 130 | */ |
| 46 | 131 | public static function set( array $cart ) { |
| 132 | + if ( ! self::maybe_start_session() ) { | |
| 133 | + return; | |
| 134 | + } | |
| 135 | + | |
| 47 | 136 | $_SESSION[ self::SESSION_KEY ] = $cart; |
| 48 | 137 | } |
| 49 | 138 | |
| 50 | 139 | /** |
| @@ -52,8 +141,12 @@ | ||
| 52 | 141 | * |
| 53 | 142 | * @return void |
| 54 | 143 | */ |
| 55 | 144 | public static function clear() { |
| 145 | + if ( ! self::maybe_start_session() ) { | |
| 146 | + return; | |
| 147 | + } | |
| 148 | + | |
| 56 | 149 | unset( $_SESSION[ self::SESSION_KEY ] ); |
| 57 | 150 | } |
| 58 | 151 | |
| 59 | 152 | /** |
| @@ -65,9 +158,9 @@ | ||
| 65 | 158 | */ |
| 66 | 159 | public static function add_item( array $item ): array { |
| 67 | 160 | $cart = self::get(); |
| 68 | 161 | $cart['items'][] = $item; |
| 69 | - $cart['total'] += $item['subtotal']; | |
| 162 | + $cart['total'] = floatval( $cart['total'] ?? 0 ) + floatval( $item['subtotal'] ?? $item['price'] ?? 0 ); | |
| 70 | 163 | $cart = apply_filters( 'booktics_pro_cart_session', $cart ); |
| 71 | 164 | |
| 72 | 165 | self::set( $cart ); |
| 73 | 166 | |
| @@ -116,9 +209,9 @@ | ||
| 116 | 209 | function ( $item ) use ( $uuid, $new_data, &$cart ) { |
| 117 | 210 | if ( $item['uuid'] === $uuid ) { |
| 118 | 211 | $item = array_merge( $item, $new_data ); |
| 119 | 212 | } |
| 120 | - $cart['total'] += $item['subtotal']; | |
| 213 | + $cart['total'] += floatval( $item['subtotal'] ?? $item['price'] ?? 0 ); | |
| 121 | 214 | |
| 122 | 215 | return $item; |
| 123 | 216 | }, $cart['items'] |
| 124 | 217 | ); |