PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.20
ووسلام – همگام سازی ووکامرس و باسلام v1.10.20
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / includes / Admin / Settings / OAuthManager.php

OAuthManager.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.20, at includes/Admin/Settings/OAuthManager.php

247 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Admin\Settings;
4
5 use SyncBasalam\Config\Endpoints;
6 use SyncBasalam\Services\ApiServiceManager;
7
8 defined('ABSPATH') || exit;
9
10 class OAuthManager
11 {
12 /** Signed, browser-bound proof that an administrator started OAuth. */
13 const OAUTH_STATE_COOKIE = 'sync_basalam_oauth_state';
14
15 /** Lifetime of a pending OAuth authorization — the SSO round-trip window. */
16 const OAUTH_STATE_TTL = 600; // 10 * MINUTE_IN_SECONDS
17
18 /**
19 * Remember that the current admin has just started an OAuth authorization.
20 *
21 * This is called only from the nonce-protected initiation flow, so the
22 * cookie it stores cannot be planted by a forged cross-site request. The
23 * callback later requires (and consumes) this cookie, which is what turns
24 * the token-saving callback from "always forgeable" into "only valid for a
25 * flow this admin actually started".
26 *
27 * The proof deliberately lives in a signed HttpOnly cookie instead of a
28 * WordPress transient. Sites with a persistent object-cache drop-in route
29 * transients through Redis/Memcached, where a failed write, eviction, or
30 * cache flush during the OAuth round trip would otherwise invalidate a
31 * legitimate callback.
32 */
33 public static function issueOauthState()
34 {
35 $userId = get_current_user_id();
36 if ($userId <= 0) return false;
37
38 $state = wp_generate_password(64, false);
39 $expiresAt = time() + self::OAUTH_STATE_TTL;
40 $value = self::buildOauthStateCookieValue($state, $userId, $expiresAt);
41
42 if (! self::writeOauthStateCookie($value, $expiresAt)) return false;
43
44 return $state;
45 }
46
47 /**
48 * Validate and consume the pending OAuth authorization for the current user.
49 *
50 * Single use: the cookie is deleted whether or not it was valid, so a
51 * replayed or forged callback cannot reuse it.
52 */
53 private static function verifyOauthState()
54 {
55 $value = isset($_COOKIE[self::OAUTH_STATE_COOKIE])
56 ? (string) wp_unslash($_COOKIE[self::OAUTH_STATE_COOKIE])
57 : '';
58
59 self::clearOauthStateCookie();
60
61 return self::isOauthStateCookieValid(
62 $value,
63 get_current_user_id(),
64 time()
65 );
66 }
67
68 private static function buildOauthStateCookieValue($state, $userId, $expiresAt)
69 {
70 $payload = json_encode([
71 'state' => (string) $state,
72 'user_id' => (int) $userId,
73 'expires_at' => (int) $expiresAt,
74 ]);
75
76 if (! is_string($payload)) return '';
77
78 $encodedPayload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
79 $signature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
80
81 return $encodedPayload . '.' . $signature;
82 }
83
84 private static function isOauthStateCookieValid($value, $userId, $now)
85 {
86 if (! is_string($value) || $value === '' || (int) $userId <= 0) return false;
87
88 $parts = explode('.', $value, 2);
89 if (count($parts) !== 2) return false;
90
91 [$encodedPayload, $signature] = $parts;
92 $expectedSignature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
93
94 if (! hash_equals($expectedSignature, $signature)) return false;
95
96 $padding = strlen($encodedPayload) % 4;
97 if ($padding !== 0) $encodedPayload .= str_repeat('=', 4 - $padding);
98
99 $payload = base64_decode(strtr($encodedPayload, '-_', '+/'), true);
100 $data = is_string($payload) ? json_decode($payload, true) : null;
101
102 if (! is_array($data)) return false;
103
104 return ! empty($data['state'])
105 && (int) ($data['user_id'] ?? 0) === (int) $userId
106 && (int) ($data['expires_at'] ?? 0) >= (int) $now;
107 }
108
109 private static function writeOauthStateCookie($value, $expiresAt)
110 {
111 if (! is_string($value) || $value === '' || headers_sent()) return false;
112
113 $written = setcookie(self::OAUTH_STATE_COOKIE, $value, [
114 'expires' => (int) $expiresAt,
115 'path' => self::oauthStateCookiePath(),
116 'domain' => self::oauthStateCookieDomain(),
117 'secure' => is_ssl(),
118 'httponly' => true,
119 'samesite' => 'Lax',
120 ]);
121
122 // Keep the current request internally consistent for callers and tests.
123 if ($written) $_COOKIE[self::OAUTH_STATE_COOKIE] = $value;
124
125 return $written;
126 }
127
128 private static function clearOauthStateCookie()
129 {
130 unset($_COOKIE[self::OAUTH_STATE_COOKIE]);
131
132 if (headers_sent()) return;
133
134 setcookie(self::OAUTH_STATE_COOKIE, '', [
135 'expires' => time() - HOUR_IN_SECONDS,
136 'path' => self::oauthStateCookiePath(),
137 'domain' => self::oauthStateCookieDomain(),
138 'secure' => is_ssl(),
139 'httponly' => true,
140 'samesite' => 'Lax',
141 ]);
142 }
143
144 private static function oauthStateCookiePath()
145 {
146 return defined('ADMIN_COOKIE_PATH') && ADMIN_COOKIE_PATH
147 ? ADMIN_COOKIE_PATH
148 : '/wp-admin';
149 }
150
151 private static function oauthStateCookieDomain()
152 {
153 return defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : '';
154 }
155
156 public function getOauthData()
157 {
158 $oauthDataUrl = apply_filters('sync_basalam_oauth_data_url', Endpoints::HAMSALAM_OAUTH_DATA);
159 $defaultClientId = apply_filters('sync_basalam_oauth_default_client_id', 779);
160 $defaultRedirectUri = apply_filters('sync_basalam_oauth_default_redirect_uri', Endpoints::HAMSALAM_OAUTH_TOKEN);
161
162 try {
163 $apiservice = syncBasalamContainer()->get(ApiServiceManager::class);
164 $request = $apiservice->get($oauthDataUrl);
165 $clientId = $request['body']['client_id'] ?? $defaultClientId;
166 $redirectUri = $request['body']['redirect_uri'] ?? $defaultRedirectUri;
167 } catch (\Throwable $th) {
168 $clientId = $defaultClientId;
169 $redirectUri = $defaultRedirectUri;
170 }
171
172 return [
173 'client_id' => $clientId,
174 'redirect_uri' => $redirectUri,
175 ];
176 }
177
178 public static function saveOauthData()
179 {
180 // CSRF protection: this callback performs a state-changing write from a
181 // plain GET, so it must be tied to an OAuth flow the current admin
182 // actually initiated. Without this an attacker could lure a logged-in
183 // admin to the callback URL and overwrite the stored Basalam credentials.
184 if (! current_user_can('manage_options') || ! self::verifyOauthState()) {
185 wp_die(
186 esc_html__('درخواست نا�
187 عتبر است.', 'sync-basalam'),
188 esc_html__('خطای ا�
189 نیتی', 'sync-basalam'),
190 ['response' => 403]
191 );
192 }
193
194 $isVendor = isset($_GET['is_vendor']) ? sanitize_text_field(wp_unslash($_GET['is_vendor'])) : true;
195 $vendorId = isset($_GET['vendor_id']) ? sanitize_text_field(intval($_GET['vendor_id'])) : null;
196 $hamsalamToken = isset($_GET['hamsalam_token']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_token'])) : null;
197 $hamsalamBusinessId = isset($_GET['hamsalam_business_id']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_business_id'])) : null;
198 $accessToken = isset($_GET['access_token']) ? sanitize_text_field(wp_unslash($_GET['access_token'])) : null;
199 $refreshToken = isset($_GET['refresh_token']) ? sanitize_text_field(wp_unslash($_GET['refresh_token'])) : null;
200 $expiresIn = isset($_GET['expires_in']) ? sanitize_text_field(intval($_GET['expires_in'])) : null;
201
202 // Allow pro version to handle custom fields
203 $extraData = apply_filters('sync_basalam_oauth_save_extra_data', []);
204
205 if ($isVendor == 'false') {
206 $data = [SettingsConfig::IS_VENDOR => false];
207 $data = apply_filters('sync_basalam_oauth_non_vendor_data', $data, $vendorId, $accessToken, $refreshToken, $extraData);
208 SettingsManager::updateSettings($data);
209 return true;
210 }
211
212 $data = [
213 SettingsConfig::VENDOR_ID => $vendorId,
214 SettingsConfig::IS_VENDOR => $isVendor,
215 SettingsConfig::TOKEN => $accessToken,
216 SettingsConfig::REFRESH_TOKEN => $refreshToken,
217 SettingsConfig::HAMSALAM_TOKEN => $hamsalamToken,
218 SettingsConfig::HAMSALAM_BUSINESS_ID => $hamsalamBusinessId,
219 SettingsConfig::EXPIRE_TOKEN_TIME => $expiresIn,
220 ];
221
222 $data = array_merge($data, $extraData);
223
224 SettingsManager::updateSettings($data);
225
226 return true;
227 }
228
229 public function getOAuthUrls()
230 {
231 $oauthData = $this->getOauthData();
232 $siteUrl = get_site_url();
233
234 $scopes = apply_filters('sync_basalam_oauth_scopes', "vendor.product.write vendor.parcel.write customer.profile.read vendor.profile.read vendor.parcel.read vendor.profile.write customer.chat.read customer.chat.write customer.identity.read");
235
236 return [
237 'redirect_uri' => $oauthData['redirect_uri'],
238 'url_req_token' => Endpoints::oauthLoginUrl(
239 $oauthData['client_id'],
240 $scopes,
241 $oauthData['redirect_uri'],
242 $siteUrl
243 ),
244 ];
245 }
246 }
247