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 |