PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
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 / Payment / SquareOAuthService.php

SquareOAuthService.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Services/Payment/SquareOAuthService.php

270 lines 9.0 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\Payment;
4
5 use IvyForms\Services\Integrations\MiddlewareRequestAuthService;
6 use IvyForms\Services\Integrations\MiddlewareUrl;
7 use IvyForms\Services\Settings\SettingsService;
8 use IvyForms\Services\Translations\BackendStrings;
9
10 /**
11 * Square OAuth via IvyForms-hosted middleware (merchant never configures app keys).
12 */
13 class SquareOAuthService
14 {
15 private const APPLICATION_ID_TRANSIENT = 'ivyforms_square_application_id';
16 private const SANDBOX_TRANSIENT = 'ivyforms_square_sandbox';
17
18 private SettingsService $settingsService;
19 private SquarePaymentService $squarePaymentService;
20 private TokenEncryptionService $tokenEncryption;
21 private MiddlewareRequestAuthService $middlewareRequestAuthService;
22 private SquareMiddlewareClient $squareMiddlewareClient;
23
24 public function __construct(
25 SettingsService $settingsService,
26 SquarePaymentService $squarePaymentService,
27 TokenEncryptionService $tokenEncryption,
28 MiddlewareRequestAuthService $middlewareRequestAuthService,
29 SquareMiddlewareClient $squareMiddlewareClient
30 ) {
31 $this->settingsService = $settingsService;
32 $this->squarePaymentService = $squarePaymentService;
33 $this->tokenEncryption = $tokenEncryption;
34 $this->middlewareRequestAuthService = $middlewareRequestAuthService;
35 $this->squareMiddlewareClient = $squareMiddlewareClient;
36 }
37
38 /**
39 * Public Square application ID for Web Payments SDK (from IvyForms middleware, not per-site config).
40 */
41 public function getPublicApplicationId(): string
42 {
43 $definedApplicationId = $this->readApplicationIdFromConstant();
44 if ($definedApplicationId !== '') {
45 return $definedApplicationId;
46 }
47
48 $cachedApplicationId = $this->readApplicationIdFromTransient();
49 if ($cachedApplicationId !== '') {
50 return $cachedApplicationId;
51 }
52
53 $config = $this->squareMiddlewareClient->fetchApplicationConfig();
54 $applicationId = $config['applicationId'];
55 if ($applicationId !== '') {
56 set_transient(self::APPLICATION_ID_TRANSIENT, $applicationId, DAY_IN_SECONDS);
57 }
58
59 /**
60 * Filter the public Square application ID (OAuth + Web Payments SDK).
61 *
62 * @since 1.0.0
63 *
64 * @param string $applicationId Square application ID.
65 */
66 return (string) apply_filters('ivyforms/payment/square_application_id', $applicationId);
67 }
68
69 private function readApplicationIdFromConstant(): string
70 {
71 if (!defined('IVYFORMS_SQUARE_APP_ID') || IVYFORMS_SQUARE_APP_ID === '') {
72 return '';
73 }
74
75 return (string) IVYFORMS_SQUARE_APP_ID;
76 }
77
78 private function readApplicationIdFromTransient(): string
79 {
80 $cached = get_transient(self::APPLICATION_ID_TRANSIENT);
81
82 return is_string($cached) ? trim($cached) : '';
83 }
84
85 /**
86 * Whether middleware Square credentials target sandbox (from /square/application-config).
87 */
88 public function isMiddlewareSandbox(): bool
89 {
90 if (defined('IVYFORMS_SQUARE_AUTH_URL') && IVYFORMS_SQUARE_AUTH_URL !== '') {
91 return str_contains((string) IVYFORMS_SQUARE_AUTH_URL, 'sandbox');
92 }
93
94 if (defined('IVYFORMS_SQUARE_APP_ID') && IVYFORMS_SQUARE_APP_ID !== '') {
95 return str_starts_with((string) IVYFORMS_SQUARE_APP_ID, 'sandbox-sq');
96 }
97
98 $cached = get_transient(self::SANDBOX_TRANSIENT);
99 if ($cached !== false) {
100 return (bool) $cached;
101 }
102
103 $config = $this->squareMiddlewareClient->fetchApplicationConfig();
104 set_transient(self::SANDBOX_TRANSIENT, !empty($config['sandbox']) ? 1 : 0, DAY_IN_SECONDS);
105
106 return !empty($config['sandbox']);
107 }
108
109 /**
110 * Request Square OAuth URL from ivyforms-middleware (Amelia pattern: CSRF state in middleware DB).
111 *
112 * @return array{success: bool, url?: string, message?: string}
113 */
114 public function getAuthorizationUrl(): array
115 {
116 $callbackUrl = rest_url('ivyforms/v1/integration/square/authorization/token');
117 $authUrl = $this->squareMiddlewareClient->fetchAuthorizationUrl($callbackUrl);
118
119 if ($authUrl === null) {
120 return [
121 'success' => false,
122 'message' => BackendStrings::getPaymentStrings()['square_service_unavailable'],
123 ];
124 }
125
126 return [
127 'success' => true,
128 'url' => $authUrl,
129 ];
130 }
131
132 /**
133 * Persist tokens from middleware server-to-server POST (signed, no browser query params).
134 *
135 * @param array<string, mixed> $params
136 * @return array{success: bool, message?: string}
137 */
138 public function handleAuthCallback(array $params): array
139 {
140 $accessToken = $this->sanitizeOAuthToken((string) ($params['access_token'] ?? ''));
141 $refreshToken = $this->sanitizeOAuthToken((string) ($params['refresh_token'] ?? ''));
142 $merchantId = sanitize_text_field((string) ($params['merchant_id'] ?? ''));
143 $expiresAt = sanitize_text_field((string) ($params['expires_at'] ?? ''));
144
145 if ($accessToken === '' || $merchantId === '') {
146 return [
147 'success' => false,
148 'message' => BackendStrings::getPaymentStrings()['square_connect_failed'],
149 ];
150 }
151
152 $encryptedAccess = $this->tokenEncryption->encrypt($accessToken);
153 $encryptedRefresh = $refreshToken !== ''
154 ? $this->tokenEncryption->encrypt($refreshToken)
155 : '';
156
157 $this->settingsService->setSetting('payment', 'square', [
158 'connected' => true,
159 'merchantId' => $merchantId,
160 'accessToken' => $encryptedAccess,
161 'refreshToken' => $encryptedRefresh,
162 'expiresAt' => $expiresAt,
163 'disconnectedByWebhook' => false,
164 ]);
165
166 $location = $this->squarePaymentService->getPrimaryLocationFromApi();
167 $merchant = $this->squarePaymentService->getMerchantInfo();
168
169 $settings = $this->squarePaymentService->getSquareSettings();
170 $settings['merchantName'] = (string) ($merchant['business_name'] ?? $merchant['name'] ?? '');
171 $settings['locationId'] = (string) ($location['id'] ?? '');
172 $settings['locationName'] = (string) ($location['name'] ?? '');
173 $settings['currency'] = (string) ($location['currency'] ?? 'USD');
174
175 $this->settingsService->setSetting('payment', 'square', $settings);
176
177 $config = $this->squareMiddlewareClient->fetchApplicationConfig();
178 set_transient(self::SANDBOX_TRANSIENT, !empty($config['sandbox']) ? 1 : 0, DAY_IN_SECONDS);
179
180 return ['success' => true];
181 }
182
183 public function revokeAccess(): bool
184 {
185 $accessToken = $this->squarePaymentService->getDecryptedAccessToken();
186 if ($accessToken === '') {
187 return false;
188 }
189
190 $body = wp_json_encode(['access_token' => $accessToken]);
191 if (!is_string($body)) {
192 return false;
193 }
194
195 $revokeUrl = MiddlewareUrl::getBaseUrl() . '/square/revoke';
196 $response = wp_remote_post(
197 $revokeUrl,
198 [
199 'headers' => $this->middlewareRequestAuthService->buildSignedHeaders($body, $revokeUrl, 'POST'),
200 'body' => $body,
201 'timeout' => 30,
202 ]
203 );
204
205 if (is_wp_error($response)) {
206 return false;
207 }
208
209 $decoded = json_decode((string) wp_remote_retrieve_body($response), true);
210
211 return !empty($decoded['result']);
212 }
213
214 public function disconnect(): void
215 {
216 $this->revokeAccess();
217 $this->clearSquareSettings();
218 $this->notifyAdminDisconnected();
219 }
220
221 public function clearSquareSettings(): void
222 {
223 $this->resetSquareSettings(false);
224 }
225
226 public function clearSquareSettingsFromRevocationWebhook(): void
227 {
228 $this->resetSquareSettings(true);
229 }
230
231 private function resetSquareSettings(bool $disconnectedByWebhook): void
232 {
233 $this->settingsService->setSetting('payment', 'square', [
234 'connected' => false,
235 'merchantId' => '',
236 'merchantName' => '',
237 'locationId' => '',
238 'locationName' => '',
239 'currency' => '',
240 'accessToken' => '',
241 'refreshToken' => '',
242 'expiresAt' => '',
243 'disconnectedByWebhook' => $disconnectedByWebhook,
244 ]);
245 }
246
247 private function sanitizeOAuthToken(string $raw): string
248 {
249 $token = wp_unslash($raw);
250 if ($token === '' || strlen($token) > 4096) {
251 return '';
252 }
253
254 return $token;
255 }
256
257 public function notifyAdminDisconnected(): void
258 {
259 $adminEmail = get_option('admin_email');
260 if (!is_email($adminEmail)) {
261 return;
262 }
263
264 $subject = BackendStrings::getPaymentStrings()['square_disconnect_email_subject'];
265 $message = BackendStrings::getPaymentStrings()['square_disconnect_email_body'];
266
267 wp_mail($adminEmail, $subject, $message);
268 }
269 }
270