PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.34.3
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.34.3
2.34.3 2.34.2 2.34.1 2.34 2.33.10 2.33.9 2.33.8 2.33.7 2.33.6 2.33.5 2.33.2 2.32.10 2.33 2.33.1 2.32.6 2.32.7 2.32.8 trunk 2.10.3 2.10.4 2.10.5 2.10.6 2.11 2.12 2.13 2.14 2.14.1 2.15 2.15.1 2.16.1 2.16.2 2.17 2.18 2.18.1 2.18.2 2.19 2.20 2.21 2.22 2.23 2.24 2.25 2.25.1 2.25.2 2.26 2.27 2.27.10 2.27.5 2.28 2.28.1 2.29 2.30 2.31 2.32 2.32.5
gptranslate / ajax-handler.php
gptranslate Last commit date
assets 3 days ago flags 3 days ago includes 3 days ago language 3 days ago ajax-handler.php 3 days ago gptranslate.php 3 days ago multilang-routing.php 2 days ago readme.txt 2 days ago serverside-translations.php 3 days ago settings.php 3 days ago simplehtmldom.php 3 days ago uninstall.php 3 days ago
ajax-handler.php
311 lines
1 <?php
2 /**
3 * GPTranslate Lightweight AJAX Handler
4 *
5 * This endpoint bypasses the full WordPress stack for read-only translation queries,
6 * drastically reducing response times from ~1-2s to ~50-100ms.
7 *
8 * Handles: gettranslations, getaliastranslation
9 * All write operations remain on the REST API endpoint.
10 */
11
12 // Security: prevent direct browser access without proper headers
13 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
14 http_response_code(405);
15 header('Content-Type: application/json; charset=utf-8');
16 echo json_encode(['result' => false, 'error' => 'Method not allowed']);
17 exit;
18 }
19
20 // CORS headers
21 header('Content-Type: application/json; charset=utf-8');
22
23 // Load minimal WordPress (database + options only, no plugins/themes/hooks)
24 define('SHORTINIT', true);
25
26 // Resolve wp-load.php path dynamically
27 $wp_load = dirname(__FILE__);
28 for ($i = 0; $i < 10; $i++) {
29 $wp_load = dirname($wp_load);
30 if (file_exists($wp_load . '/wp-load.php')) {
31 require_once $wp_load . '/wp-load.php';
32 break;
33 }
34 }
35
36 if (!defined('ABSPATH')) {
37 http_response_code(500);
38 echo json_encode(['result' => false, 'error' => 'WordPress not found']);
39 exit;
40 }
41
42 // SHORTINIT does not load options API, so we query directly
43 global $wpdb;
44
45 // Validate API key
46 $headers = function_exists('getallheaders') ? getallheaders() : [];
47 $headerApiKey = '';
48 foreach ($headers as $name => $value) {
49 if (strtolower($name) === 'x-gptranslate-key') {
50 $headerApiKey = $value;
51 break;
52 }
53 }
54
55 // Validate API key - cryptographically random secret stored in wp_options.
56 // SHORTINIT does not load the options API, so query the option table directly.
57 $expected_key = $wpdb->get_var(
58 $wpdb->prepare(
59 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
60 'gptranslate_api_secret'
61 )
62 );
63
64 if ($expected_key === null || $expected_key === '' || !is_string($headerApiKey) || !hash_equals((string) $expected_key, $headerApiKey)) {
65 http_response_code(403);
66 echo json_encode(['result' => false, 'error' => 'Forbidden']);
67 exit;
68 }
69
70 // Parse input
71 $raw_input = file_get_contents('php://input');
72 $params = json_decode($raw_input, true);
73
74 if (!$params) {
75 parse_str($raw_input, $params);
76 }
77
78 if (empty($params['task'])) {
79 http_response_code(400);
80 echo json_encode(['result' => false, 'error' => 'Missing task parameter']);
81 exit;
82 }
83
84 $task = preg_replace('/[^a-zA-Z]/', '', $params['task'] ?? '');
85 $pageLink = filter_var($params['pagelink'] ?? '', FILTER_SANITIZE_URL);
86 $languageOriginal = preg_replace('/[^a-zA-Z\-]/', '', $params['language_original'] ?? '');
87 $languageTranslated = preg_replace('/[^a-zA-Z\-]/', '', $params['language_translated'] ?? '');
88 $retriggerTranslation = (int)($params['retrigger'] ?? 0);
89
90 $table = $wpdb->prefix . 'gptranslate';
91 $response = ['result' => false];
92
93 // Lightweight replacement for WP trailingslashit + untrailingslashit + wp_parse_url
94 function gpt_light_trailingslashit($url) {
95 $parsed = parse_url($url);
96 if (empty($parsed['path'])) {
97 $parsed['path'] = '/';
98 } else {
99 $parsed['path'] = rtrim($parsed['path'], '/') . '/';
100 }
101 $rebuilt = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
102 $rebuilt .= $parsed['host'] ?? '';
103 $rebuilt .= $parsed['path'];
104 if (!empty($parsed['query'])) {
105 $rebuilt .= '?' . $parsed['query'];
106 }
107 if (!empty($parsed['fragment'])) {
108 $rebuilt .= '#' . $parsed['fragment'];
109 }
110 return $rebuilt;
111 }
112
113 // ============================================================================
114 // Task: gettranslations
115 // ============================================================================
116 if ($task === 'gettranslations') {
117 // Get plugin options for realtime_translations and rewrite settings
118 $opts_raw = $wpdb->get_var("SELECT option_value FROM {$wpdb->options} WHERE option_name = 'gptranslate_options' LIMIT 1");
119 $opts = $opts_raw ? unserialize($opts_raw) : [];
120
121 if (!empty($opts['realtime_translations']) || $retriggerTranslation === 1) {
122 $response['result'] = false;
123 } else {
124 $pageLinkDecoded = urldecode($pageLink);
125
126 if (!empty($opts['rewrite_language_url']) && $opts['rewrite_language_url'] == 1
127 && !empty($opts['rewrite_language_alias']) && $opts['rewrite_language_alias'] == 1) {
128 // Check 8 variants (with/without slash + encoded/decoded)
129 $row = $wpdb->get_row($wpdb->prepare(
130 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" .
131 " WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s OR translated_alias = %s OR translated_alias = %s)" .
132 " AND languageoriginal = %s" .
133 " AND languagetranslated = %s" .
134 " AND published = 1",
135 rtrim($pageLink, '/'),
136 rtrim($pageLink, '/') . '/',
137 rtrim($pageLinkDecoded, '/'),
138 rtrim($pageLinkDecoded, '/') . '/',
139 rtrim($pageLink, '/'),
140 rtrim($pageLink, '/') . '/',
141 rtrim($pageLinkDecoded, '/'),
142 rtrim($pageLinkDecoded, '/') . '/',
143 $languageOriginal,
144 $languageTranslated
145 ), ARRAY_A);
146 } else {
147 // Check 4 variants (with/without slash + encoded/decoded)
148 $row = $wpdb->get_row($wpdb->prepare(
149 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" .
150 " WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)" .
151 " AND languageoriginal = %s" .
152 " AND languagetranslated = %s" .
153 " AND published = 1",
154 rtrim($pageLink, '/'),
155 rtrim($pageLink, '/') . '/',
156 rtrim($pageLinkDecoded, '/'),
157 rtrim($pageLinkDecoded, '/') . '/',
158 $languageOriginal,
159 $languageTranslated
160 ), ARRAY_A);
161 }
162
163 if ($row) {
164 $response['result'] = true;
165 $response['translations'] = json_decode($row['translations'], true) ?: [];
166 $response['alt_translations'] = json_decode($row['alt_translations'], true) ?: [];
167 $response['translated_alias'] = $row['translated_alias'];
168 $response['pagelink_alias'] = $row['pagelink'];
169 } else {
170 $response['result'] = false;
171 }
172 }
173
174 // ============================================================================
175 // Task: getaliastranslation
176 // ============================================================================
177 } elseif ($task === 'getaliastranslation') {
178 try {
179 $row = $wpdb->get_row($wpdb->prepare(
180 "SELECT translated_alias FROM {$table}" .
181 " WHERE (pagelink = %s OR pagelink = %s)" .
182 " AND languageoriginal = %s" .
183 " AND languagetranslated = %s" .
184 " AND published = 1",
185 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
186 ), ARRAY_A);
187
188 if ($row) {
189 $response['result'] = true;
190 $response['translated_alias'] = $row['translated_alias'] ?? '';
191 } else {
192 $response['result'] = false;
193 }
194 } catch (Exception $e) {
195 $response['result'] = false;
196 $response['exception'] = $e->getMessage();
197 }
198
199 // ============================================================================
200 // Task: gettranslatedaliases
201 // ============================================================================
202 } elseif ($task === 'gettranslatedaliases') {
203 try {
204 if ($languageTranslated) {
205 $rows = $wpdb->get_results(
206 $wpdb->prepare(
207 "SELECT pagelink, translated_alias FROM {$table} WHERE languagetranslated = %s AND published = 1",
208 $languageTranslated
209 ),
210 ARRAY_A
211 );
212 } elseif ($languageOriginal) {
213 $rows = $wpdb->get_results(
214 $wpdb->prepare(
215 "SELECT translated_alias AS pagelink, pagelink AS translated_alias FROM {$table} WHERE languageoriginal = %s AND published = 1",
216 $languageOriginal
217 ),
218 ARRAY_A
219 );
220 } else {
221 $response['result'] = false;
222 echo json_encode($response);
223 exit;
224 }
225
226 if ($rows) {
227 $encodedResult = [];
228
229 foreach ($rows as $row) {
230 // Normalize with trailing slash (replaces WP trailingslashit)
231 $pagelink = gpt_light_trailingslashit($row['pagelink']);
232 $translatedAlias = !empty($row['translated_alias']) ? gpt_light_trailingslashit($row['translated_alias']) : '';
233
234 // Encode pagelink path
235 $parsedUrl = parse_url($pagelink);
236 $encodedPagelink = $pagelink;
237
238 if (!empty($parsedUrl['path'])) {
239 $pathParts = explode('/', $parsedUrl['path']);
240 $encodedParts = array_map('rawurlencode', $pathParts);
241 $encodedPath = implode('/', $encodedParts);
242
243 $encodedPagelink = ($parsedUrl['scheme'] ?? '') . '://' . ($parsedUrl['host'] ?? '');
244 $encodedPagelink .= $encodedPath;
245 if (!empty($parsedUrl['query'])) {
246 $encodedPagelink .= '?' . $parsedUrl['query'];
247 }
248 if (!empty($parsedUrl['fragment'])) {
249 $encodedPagelink .= '#' . $parsedUrl['fragment'];
250 }
251 }
252
253 // Encode translated alias path
254 $encodedAlias = $translatedAlias;
255 if (!empty($translatedAlias)) {
256 $parsedAlias = parse_url($translatedAlias);
257 if (!empty($parsedAlias['path'])) {
258 $pathParts = explode('/', $parsedAlias['path']);
259 $encodedParts = array_map('rawurlencode', $pathParts);
260 $encodedPath = implode('/', $encodedParts);
261
262 $encodedAlias = ($parsedAlias['scheme'] ?? '') . '://' . ($parsedAlias['host'] ?? '');
263 $encodedAlias .= $encodedPath;
264 if (!empty($parsedAlias['query'])) {
265 $encodedAlias .= '?' . $parsedAlias['query'];
266 }
267 if (!empty($parsedAlias['fragment'])) {
268 $encodedAlias .= '#' . $parsedAlias['fragment'];
269 }
270 }
271 }
272
273 // Skip malformed entries: if the pagelink (or, on the swapped
274 // languageOriginal branch, the translated_alias standing in for it)
275 // has no real host, the record's underlying DB column was empty and
276 // this would otherwise surface as a bare "https://" key/value, which
277 // the client could mistake for a valid translated URL and use it as
278 // a broken link instead of leaving the original URL untouched.
279 $encodedPagelinkHost = parse_url($encodedPagelink, PHP_URL_HOST);
280 $encodedAliasHost = !empty($encodedAlias) ? parse_url($encodedAlias, PHP_URL_HOST) : null;
281 if (empty($encodedPagelinkHost) || (!empty($encodedAlias) && empty($encodedAliasHost))) {
282 continue;
283 }
284
285 $encodedResult[$encodedPagelink] = [
286 'pagelink' => $encodedPagelink,
287 'translated_alias' => $encodedAlias
288 ];
289 }
290
291 $response['result'] = true;
292 $response['translated_aliases'] = $encodedResult;
293 } else {
294 $response['result'] = false;
295 }
296 } catch (Exception $e) {
297 $response['result'] = false;
298 $response['exception'] = $e->getMessage();
299 }
300
301 // ============================================================================
302 // Unsupported task: fallback to REST API
303 // ============================================================================
304 } else {
305 http_response_code(400);
306 $response['error'] = 'Unsupported task for lightweight handler';
307 }
308
309 echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
310 exit;
311