PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / trunk
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO vtrunk
2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 1.11.0 All 47 releases
thinkrank / includes / integrations / class-google-api-base-client.php

class-google-api-base-client.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO trunk, at includes/integrations/class-google-api-base-client.php

240 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Google API Base Client Class
5 *
6 * Abstract base class for Google API integrations providing common functionality
7 * for HTTP requests, rate limiting, error handling, and response processing.
8 * Follows ThinkRank patterns established by Claude_Client and OpenAI_Client.
9 *
10 * @package ThinkRank\Integrations
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\Integrations;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Google API Base Client Class
25 *
26 * Single Responsibility: Provide common Google API functionality
27 * Following ThinkRank HTTP client patterns from Claude_Client and OpenAI_Client
28 *
29 * @since 1.0.0
30 */
31 abstract class Google_API_Base_Client {
32
33 /**
34 * API key
35 *
36 * @var string
37 */
38 protected string $api_key;
39
40 /**
41 * OAuth Access Token
42 *
43 * @var string|null
44 */
45 protected ?string $access_token = null;
46
47 /**
48 * Request timeout in seconds
49 *
50 * @var int
51 */
52 protected int $timeout;
53
54 /**
55 * Rate limit configuration
56 *
57 * @var array
58 */
59 protected array $rate_limits;
60
61 /**
62 * Constructor
63 *
64 * @param string $api_key Google API key
65 * @param int $timeout Request timeout in seconds
66 * @param string|null $access_token OAuth Access Token (optional)
67 */
68 public function __construct(string $api_key, int $timeout = 20, ?string $access_token = null) {
69 $this->api_key = $api_key;
70 $this->timeout = $timeout;
71 $this->access_token = $access_token;
72 $this->rate_limits = $this->get_rate_limits();
73 }
74
75 /**
76 * Make HTTP request to Google API
77 * Following Claude_Client and OpenAI_Client patterns
78 *
79 * @param string $url Full API URL
80 * @param array $params Request parameters
81 * @param string $method HTTP method
82 * @return array Response data
83 * @throws \Exception If request fails
84 */
85 protected function make_request(string $url, array $params = [], string $method = 'GET'): array {
86 // Check rate limiting before making request
87 $this->check_rate_limit();
88
89 $args = [
90 'timeout' => $this->timeout,
91 'headers' => [
92 'User-Agent' => 'ThinkRank/' . THINKRANK_VERSION,
93 ],
94 'method' => $method
95 ];
96
97 // Add OAuth Authorization header if token exists; otherwise fall back to
98 // the API key sent in the x-goog-api-key HEADER (never the query string,
99 // which is logged by servers, proxies and referrers).
100 if (!empty($this->access_token)) {
101 $args['headers']['Authorization'] = 'Bearer ' . $this->access_token;
102 } elseif (!empty($this->api_key)) {
103 $args['headers']['x-goog-api-key'] = $this->api_key;
104 }
105
106 // Defensive: never let a key travel in the query string.
107 unset($params['key']);
108
109 if ($method === 'GET' && !empty($params)) {
110 $url .= '?' . http_build_query($params);
111 } elseif ($method === 'POST') {
112 $args['body'] = wp_json_encode($params);
113 $args['headers']['Content-Type'] = 'application/json';
114 }
115
116 $response = wp_remote_request($url, $args);
117
118 if (is_wp_error($response)) {
119 // Not esc_html()'d: an exception message is data, not output. It is
120 // JSON-encoded to the REST layer and rendered as text by React, so
121 // escaping here only smuggled entities into what the user reads —
122 // Google's own wording is full of quotes, and the Performance panels
123 // displayed them as "quota metric &#039;Queries&#039;".
124 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Message is JSON data for the REST layer, escaped at render time by React.
125 throw new \Exception('API request failed: ' . $response->get_error_message());
126 }
127
128 $status_code = wp_remote_retrieve_response_code($response);
129 $response_body = wp_remote_retrieve_body($response);
130
131 if ($status_code >= 400) {
132 $error_data = json_decode($response_body, true);
133 $error_message = $error_data['error']['message'] ?? 'Unknown API error';
134 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Same as above: Google's wording is data, not markup; escaping it leaks entities into the UI.
135 throw new \Exception(sprintf('Google API error (%d): %s', (int) $status_code, $error_message), (int) $status_code);
136 }
137
138 $data = json_decode($response_body, true);
139
140 if (json_last_error() !== JSON_ERROR_NONE) {
141 throw new \Exception('Invalid JSON response from Google API');
142 }
143
144 // A valid-but-scalar body (null/number/string from a proxy/WAF/CDN on a
145 // 2xx) would violate this method's : array return type; reject it here so
146 // it surfaces as a catchable \Exception, not an uncatchable TypeError.
147 if (!is_array($data)) {
148 throw new \Exception('Unexpected non-array response from Google API');
149 }
150
151 // Update rate limit tracking after successful request
152 $this->update_rate_limit();
153
154 return $data;
155 }
156
157 /**
158 * Test API connection
159 * Must be implemented by concrete classes
160 *
161 * @return array Connection test results
162 */
163 abstract public function test_connection(): array;
164
165 /**
166 * Get rate limit configuration
167 * Must be implemented by concrete classes
168 *
169 * @return array Rate limit configuration
170 */
171 abstract protected function get_rate_limits(): array;
172
173 /**
174 * Check if request is within rate limits
175 * Following ThinkRank rate limiting patterns from existing classes
176 *
177 * @throws \Exception If rate limit exceeded
178 */
179 protected function check_rate_limit(): void {
180 $rate_limit_key = $this->get_rate_limit_key();
181 $current_time = time();
182
183 // Reset counter if it's a new day
184 if ($current_time >= $this->rate_limits['reset_time']) {
185 $this->reset_rate_limit_counter();
186 }
187
188 $current_count = get_transient($rate_limit_key . '_count') ?: 0;
189
190 if ($current_count >= $this->rate_limits['max_requests_per_day']) {
191 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plugin-authored message, rendered as text by the admin app.
192 throw new \Exception($this->get_rate_limit_error_message());
193 }
194 }
195
196 /**
197 * Update rate limit counter after successful request
198 * Following ThinkRank transient patterns
199 */
200 protected function update_rate_limit(): void {
201 $rate_limit_key = $this->get_rate_limit_key();
202 $current_count = get_transient($rate_limit_key . '_count') ?: 0;
203 $new_count = $current_count + 1;
204
205 // Set transient to expire at end of day
206 $seconds_until_tomorrow = strtotime('tomorrow') - time();
207 set_transient($rate_limit_key . '_count', $new_count, $seconds_until_tomorrow);
208 }
209
210 /**
211 * Reset rate limit counter for new day
212 */
213 private function reset_rate_limit_counter(): void {
214 $rate_limit_key = $this->get_rate_limit_key();
215 delete_transient($rate_limit_key . '_count');
216 delete_transient($rate_limit_key . '_reset');
217
218 // Set new reset time for tomorrow
219 $seconds_until_tomorrow = strtotime('tomorrow') - time();
220 set_transient($rate_limit_key . '_reset', strtotime('tomorrow'), $seconds_until_tomorrow);
221 $this->rate_limits['reset_time'] = strtotime('tomorrow');
222 }
223
224 /**
225 * Get rate limit transient key
226 * Following ThinkRank option naming patterns
227 *
228 * @return string Rate limit key
229 */
230 abstract protected function get_rate_limit_key(): string;
231
232 /**
233 * Get rate limit error message
234 * Must be implemented by concrete classes
235 *
236 * @return string Error message
237 */
238 abstract protected function get_rate_limit_error_message(): string;
239 }
240