|
@@ -77,6 +77,260 @@ function getReservationByOrderNumber($orderNumber) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get remembered order IDs for the current browser profile.
|
|
|
|
|
+ */
|
|
|
|
|
+function getRememberedOrderIds(): array {
|
|
|
|
|
+ return readSignedOrderHistoryCookie();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Remember a newly created order ID in browser history cookie.
|
|
|
|
|
+ */
|
|
|
|
|
+function rememberOrderId(string $orderId): void {
|
|
|
|
|
+ if (!isValidOrderHistoryOrderId($orderId)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $existingIds = getRememberedOrderIds();
|
|
|
|
|
+ $updatedIds = [$orderId];
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($existingIds as $existingId) {
|
|
|
|
|
+ if ($existingId !== $orderId) {
|
|
|
|
|
+ $updatedIds[] = $existingId;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $maxIds = getOrderHistoryMaxIds();
|
|
|
|
|
+ if (count($updatedIds) > $maxIds) {
|
|
|
|
|
+ $updatedIds = array_slice($updatedIds, 0, $maxIds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ writeSignedOrderHistoryCookie($updatedIds);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Read and validate signed browser order history cookie.
|
|
|
|
|
+ */
|
|
|
|
|
+function readSignedOrderHistoryCookie(): array {
|
|
|
|
|
+ $cookieName = getOrderHistoryCookieName();
|
|
|
|
|
+ if (!isset($_COOKIE[$cookieName]) || !is_string($_COOKIE[$cookieName])) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $secret = getOrderHistorySecret();
|
|
|
|
|
+ if ($secret === '') {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $cookieValue = $_COOKIE[$cookieName];
|
|
|
|
|
+ $parts = explode('.', $cookieValue, 2);
|
|
|
|
|
+ if (count($parts) !== 2) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $encodedPayload = $parts[0];
|
|
|
|
|
+ $signature = $parts[1];
|
|
|
|
|
+ if ($encodedPayload === '' || $signature === '') {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $expectedSignature = hash_hmac('sha256', $encodedPayload, $secret);
|
|
|
|
|
+ if (!hash_equals($expectedSignature, $signature)) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $payloadJson = base64UrlDecode($encodedPayload);
|
|
|
|
|
+ if ($payloadJson === null) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $payload = json_decode($payloadJson, true);
|
|
|
|
|
+ if (!is_array($payload)) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $version = isset($payload['v']) ? (int)$payload['v'] : 0;
|
|
|
|
|
+ if ($version !== 1) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $ids = isset($payload['ids']) && is_array($payload['ids']) ? $payload['ids'] : [];
|
|
|
|
|
+ return sanitizeOrderHistoryIds($ids);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Write signed browser order history cookie.
|
|
|
|
|
+ */
|
|
|
|
|
+function writeSignedOrderHistoryCookie(array $ids): void {
|
|
|
|
|
+ if (headers_sent()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $secret = getOrderHistorySecret();
|
|
|
|
|
+ if ($secret === '') {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $sanitizedIds = sanitizeOrderHistoryIds($ids);
|
|
|
|
|
+ $payload = [
|
|
|
|
|
+ 'v' => 1,
|
|
|
|
|
+ 'ids' => $sanitizedIds,
|
|
|
|
|
+ 'iat' => time()
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
|
|
|
+ if ($payloadJson === false) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $encodedPayload = base64UrlEncode($payloadJson);
|
|
|
|
|
+ $signature = hash_hmac('sha256', $encodedPayload, $secret);
|
|
|
|
|
+ $cookieValue = $encodedPayload . '.' . $signature;
|
|
|
|
|
+
|
|
|
|
|
+ $expires = time() + (getOrderHistoryTtlDays() * 86400);
|
|
|
|
|
+ $success = setcookie(getOrderHistoryCookieName(), $cookieValue, [
|
|
|
|
|
+ 'expires' => $expires,
|
|
|
|
|
+ 'path' => getOrderHistoryCookiePath(),
|
|
|
|
|
+ 'secure' => isHttpsRequest(),
|
|
|
|
|
+ 'httponly' => true,
|
|
|
|
|
+ 'samesite' => 'Lax'
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if ($success) {
|
|
|
|
|
+ $_COOKIE[getOrderHistoryCookieName()] = $cookieValue;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Build a safe, deduplicated order history ID list.
|
|
|
|
|
+ */
|
|
|
|
|
+function sanitizeOrderHistoryIds(array $ids): array {
|
|
|
|
|
+ $result = [];
|
|
|
|
|
+ $seen = [];
|
|
|
|
|
+ $maxIds = getOrderHistoryMaxIds();
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($ids as $id) {
|
|
|
|
|
+ if (!is_string($id) || !isValidOrderHistoryOrderId($id)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isset($seen[$id])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $seen[$id] = true;
|
|
|
|
|
+ $result[] = $id;
|
|
|
|
|
+
|
|
|
|
|
+ if (count($result) >= $maxIds) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Check if order ID matches configured pattern.
|
|
|
|
|
+ */
|
|
|
|
|
+function isValidOrderHistoryOrderId($orderId): bool {
|
|
|
|
|
+ if (!is_string($orderId) || $orderId === '') {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
|
|
|
|
|
+ $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-\d+$/';
|
|
|
|
|
+ return preg_match($pattern, $orderId) === 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Base64url encode helper.
|
|
|
|
|
+ */
|
|
|
|
|
+function base64UrlEncode(string $data): string {
|
|
|
|
|
+ return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Base64url decode helper.
|
|
|
|
|
+ */
|
|
|
|
|
+function base64UrlDecode(string $data): ?string {
|
|
|
|
|
+ $remainder = strlen($data) % 4;
|
|
|
|
|
+ if ($remainder > 0) {
|
|
|
|
|
+ $data .= str_repeat('=', 4 - $remainder);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $decoded = base64_decode(strtr($data, '-_', '+/'), true);
|
|
|
|
|
+ if ($decoded === false) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return $decoded;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get cookie name for order history.
|
|
|
|
|
+ */
|
|
|
|
|
+function getOrderHistoryCookieName(): string {
|
|
|
|
|
+ return defined('ORDER_HISTORY_COOKIE_NAME') ? ORDER_HISTORY_COOKIE_NAME : 'order_history';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get secret for order history cookie signing.
|
|
|
|
|
+ */
|
|
|
|
|
+function getOrderHistorySecret(): string {
|
|
|
|
|
+ return defined('ORDER_HISTORY_COOKIE_SECRET') ? (string) ORDER_HISTORY_COOKIE_SECRET : '';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get maximum number of remembered order IDs.
|
|
|
|
|
+ */
|
|
|
|
|
+function getOrderHistoryMaxIds(): int {
|
|
|
|
|
+ $maxIds = defined('ORDER_HISTORY_MAX_IDS') ? (int) ORDER_HISTORY_MAX_IDS : 10;
|
|
|
|
|
+ return $maxIds > 0 ? $maxIds : 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get order history cookie retention days.
|
|
|
|
|
+ */
|
|
|
|
|
+function getOrderHistoryTtlDays(): int {
|
|
|
|
|
+ $ttlDays = defined('ORDER_HISTORY_COOKIE_TTL_DAYS') ? (int) ORDER_HISTORY_COOKIE_TTL_DAYS : 365;
|
|
|
|
|
+ return $ttlDays > 0 ? $ttlDays : 365;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get cookie path from SITE_URL.
|
|
|
|
|
+ */
|
|
|
|
|
+function getOrderHistoryCookiePath(): string {
|
|
|
|
|
+ $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : '';
|
|
|
|
|
+ if (strpos($siteUrl, '://') !== false) {
|
|
|
|
|
+ $parsedPath = parse_url($siteUrl, PHP_URL_PATH);
|
|
|
|
|
+ $siteUrl = is_string($parsedPath) ? $parsedPath : '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($siteUrl === '' || $siteUrl === '/') {
|
|
|
|
|
+ return '/';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($siteUrl[0] !== '/') {
|
|
|
|
|
+ $siteUrl = '/' . $siteUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return rtrim($siteUrl, '/');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Detect whether current request uses HTTPS.
|
|
|
|
|
+ */
|
|
|
|
|
+function isHttpsRequest(): bool {
|
|
|
|
|
+ if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Check if reservation is hidden (spam/deleted)
|
|
* Check if reservation is hidden (spam/deleted)
|
|
|
*/
|
|
*/
|