PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / GoogleSheets / Helpers / GoogleSheetsOAuthStateHelper.php

GoogleSheetsOAuthStateHelper.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Services/GoogleSheets/Helpers/GoogleSheetsOAuthStateHelper.php

115 lines 2.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Services\GoogleSheets\Helpers;
4
5 /**
6 * Creates and validates one-time OAuth state tokens for Google Sheets connect flows.
7 */
8 class GoogleSheetsOAuthStateHelper
9 {
10 private const TRANSIENT_PREFIX = 'ivyforms_gs_oauth_';
11 private const TTL_SECONDS = 900;
12
13 public static function createState(): string
14 {
15 $siteUrl = self::getSiteUrl();
16 $userId = get_current_user_id();
17 $nonce = wp_generate_password(32, false, false);
18 $token = wp_hash($nonce . '|' . $userId . '|' . $siteUrl);
19
20 set_transient(
21 self::TRANSIENT_PREFIX . $token,
22 [
23 'user_id' => $userId,
24 'site' => $siteUrl,
25 'nonce' => $nonce,
26 ],
27 self::TTL_SECONDS
28 );
29
30 return $siteUrl . '|' . $token;
31 }
32
33 public static function validateState(string $state): bool
34 {
35 $parsed = self::parseState($state);
36
37 if ($parsed === null) {
38 return false;
39 }
40
41 [$token, $payload] = $parsed;
42
43 if (!self::isPayloadValid($payload) || !self::isUserAllowed($payload)) {
44 return false;
45 }
46
47 delete_transient(self::TRANSIENT_PREFIX . $token);
48
49 return true;
50 }
51
52 /**
53 * @return array{0: string, 1: array<string, mixed>}|null
54 */
55 private static function parseState(string $state): ?array
56 {
57 $state = trim($state);
58
59 if ($state === '') {
60 return null;
61 }
62
63 $parts = explode('|', $state, 2);
64
65 if (count($parts) !== 2) {
66 return null;
67 }
68
69 [$siteUrl, $token] = $parts;
70
71 if ($siteUrl !== self::getSiteUrl() || $token === '') {
72 return null;
73 }
74
75 $payload = get_transient(self::TRANSIENT_PREFIX . $token);
76
77 if (!is_array($payload)) {
78 return null;
79 }
80
81 return [$token, $payload];
82 }
83
84 /**
85 * @param array<string, mixed> $payload
86 */
87 private static function isPayloadValid(array $payload): bool
88 {
89 return ($payload['site'] ?? '') === self::getSiteUrl();
90 }
91
92 /**
93 * @param array<string, mixed> $payload
94 */
95 private static function isUserAllowed(array $payload): bool
96 {
97 $expectedUserId = (int) ($payload['user_id'] ?? 0);
98 $currentUserId = get_current_user_id();
99
100 // The one-time transient is the primary CSRF guard for this public callback.
101 // Cookie auth may be missing when the callback host differs from the admin host
102 // (e.g. localhost vs ivy-cursor.test); only enforce user match when both are known.
103 return !($expectedUserId > 0 && $currentUserId > 0 && $currentUserId !== $expectedUserId);
104 }
105
106 private static function getSiteUrl(): string
107 {
108 if (defined('IVYFORMS_SITE_URL')) {
109 return (string) IVYFORMS_SITE_URL;
110 }
111
112 return (string) home_url();
113 }
114 }
115