PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites / 2.33
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites v2.33
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 weeks ago flags 3 weeks ago includes 3 weeks ago language 3 weeks ago ajax-handler.php 3 weeks ago gptranslate.php 3 weeks ago multilang-routing.php 3 weeks ago readme.txt 3 weeks ago serverside-translations.php 3 weeks ago settings.php 3 weeks ago simplehtmldom.php 3 weeks ago uninstall.php 3 weeks ago
ajax-handler.php
299 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 $encodedResult[$encodedPagelink] = [
274 'pagelink' => $encodedPagelink,
275 'translated_alias' => $encodedAlias
276 ];
277 }
278
279 $response['result'] = true;
280 $response['translated_aliases'] = $encodedResult;
281 } else {
282 $response['result'] = false;
283 }
284 } catch (Exception $e) {
285 $response['result'] = false;
286 $response['exception'] = $e->getMessage();
287 }
288
289 // ============================================================================
290 // Unsupported task: fallback to REST API
291 // ============================================================================
292 } else {
293 http_response_code(400);
294 $response['error'] = 'Unsupported task for lightweight handler';
295 }
296
297 echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
298 exit;
299