oauthManager = $oauthManager; $this->connectionRepository = $connectionRepository; $this->idParser = new SpreadsheetIdParser(); $this->worksheetManager = new WorksheetManager(); $this->headerManager = new HeaderManager(new WordPressLock(), $this->worksheetManager); } /** * @inheritDoc */ public function testConnection(string $connectionId): bool { $connection = $this->connectionRepository->find($connectionId); if (!$connection) { return false; } try { $accessToken = $this->oauthManager->getAccessToken($connection); $response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, ], 'timeout' => 10, ]); if (is_wp_error($response)) { return false; } return wp_remote_retrieve_response_code($response) === 200; } catch (Exception $e) { return false; } } /** * @inheritDoc */ public function validateSpreadsheet(string $connectionId, string $spreadsheetId): bool { $parsedId = $this->idParser->parse($spreadsheetId); if (!$parsedId) { return false; } $connection = $this->connectionRepository->find($connectionId); if (!$connection) { return false; } try { $accessToken = $this->oauthManager->getAccessToken($connection); $url = sprintf( 'https://sheets.googleapis.com/v4/spreadsheets/%s?fields=spreadsheetId', rawurlencode($parsedId) ); $response = wp_remote_get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'Accept' => 'application/json', ], 'timeout' => 10, ]); if (is_wp_error($response)) { return false; } return wp_remote_retrieve_response_code($response) === 200; } catch (Exception $e) { return false; } } /** * @inheritDoc */ public function validateWorksheet(string $connectionId, string $spreadsheetId, string $worksheetName): bool { $parsedId = $this->idParser->parse($spreadsheetId); if (!$parsedId || !$worksheetName) { return false; } $connection = $this->connectionRepository->find($connectionId); if (!$connection) { return false; } try { $accessToken = $this->oauthManager->getAccessToken($connection); $url = sprintf( 'https://sheets.googleapis.com/v4/spreadsheets/%s?includeGridData=false', rawurlencode($parsedId) ); $response = wp_remote_get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'Accept' => 'application/json', ], 'timeout' => 10, ]); if (is_wp_error($response)) { return false; } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if (empty($body['sheets'])) { return false; } foreach ($body['sheets'] as $sheet) { $title = $sheet['properties']['title'] ?? ''; if (strcasecmp($title, $worksheetName) === 0) { return true; } } return false; } catch (Exception $e) { return false; } } /** * @inheritDoc */ public function appendRow( string $connectionId, string $spreadsheetId, string $worksheetName, array $row, bool $createHeaders = false, string $headerBgColor = '', string $headerFgColor = '', bool $headerSticky = false ): AppendResult { $parsedId = $this->idParser->parse($spreadsheetId); if (!$parsedId) { return AppendResult::createFailure('invalid_spreadsheet_id', __('Invalid Spreadsheet ID format.', '1-click-form-sync'), false); } $connection = $this->connectionRepository->find($connectionId); if (!$connection) { return AppendResult::createFailure('connection_not_found', __('Google connection not found.', '1-click-form-sync'), false); } try { $accessToken = $this->oauthManager->getAccessToken($connection); $headers = array_keys($row); $values = array_values($row); // 1. Create headers if empty sheet check is enabled. if ($createHeaders && !empty($headers)) { $this->headerManager->ensureHeaders( $accessToken, $parsedId, $worksheetName, $headers, $headerBgColor, $headerFgColor, $headerSticky ); } // 2. Append row to worksheet. $range = $this->worksheetManager->getRangeString($worksheetName, 'A:ZZ'); $url = sprintf( 'https://sheets.googleapis.com/v4/spreadsheets/%s/values/%s:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS', rawurlencode($parsedId), rawurlencode($range) ); $response = wp_remote_post($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'Content-Type' => 'application/json', ], 'body' => json_encode([ 'values' => [$values], ]), 'timeout' => 15, ]); if (is_wp_error($response)) { return AppendResult::createFailure('http_error', $response->get_error_message(), true); } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); if ($code === 200) { // Update stats. $connection->setLastSuccessAt(current_time('mysql')); $connection->setLastError(null); $this->connectionRepository->save($connection); return AppendResult::createSuccess(); } // Determine if error is temporary (retryable) or permanent. $isRetryable = in_array($code, [429, 500, 502, 503, 504], true); $decodedBody = json_decode($body, true); $message = $decodedBody['error']['message'] ?? 'API response code: ' . $code; if ($code === 401) { $connection->setStatus('revoked'); $connection->setLastError($message); $this->connectionRepository->save($connection); $isRetryable = false; // Credential revokes are permanent. } return AppendResult::createFailure('api_error_' . $code, $message, $isRetryable); } catch (GoogleRateLimitException $e) { return AppendResult::createFailure('rate_limit', $e->getMessage(), true); } catch (PermissionDeniedException $e) { return AppendResult::createFailure('permission_denied', $e->getMessage(), false); } catch (SpreadsheetNotFoundException $e) { return AppendResult::createFailure('spreadsheet_not_found', $e->getMessage(), false); } catch (WorksheetNotFoundException $e) { return AppendResult::createFailure('worksheet_not_found', $e->getMessage(), false); } catch (GoogleAuthenticationException $e) { return AppendResult::createFailure('auth_failed', $e->getMessage(), false); } catch (Exception $e) { return AppendResult::createFailure('unknown_error', $e->getMessage(), true); } } }