PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites / 2.28
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites v2.28
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 / gptranslate.php
gptranslate Last commit date
assets 2 months ago flags 2 months ago includes 2 months ago language 2 months ago ajax-handler.php 2 months ago gptranslate.php 2 months ago multilang-routing.php 2 months ago readme.txt 2 months ago serverside-translations.php 2 months ago settings.php 2 months ago simplehtmldom.php 2 months ago uninstall.php 2 months ago
gptranslate.php
3439 lines
1 <?php
2 /*
3 Plugin Name: GPTranslate
4 Plugin URI: https://gptranslate.storejextensions.org/
5 Description: GPTranslate for Wordpress is the revolutionary multilanguage solution to automatically translate your Wordpress website thanks to the power of Artificial Intelligence like ChatGPT, Deepseek, Gemini, Claude, DeepL and more. ⚠️GPTranslate FREE Mode active
6 Author: JExtensions Store
7 Version: 2.28
8 Author URI: https://storejextensions.org
9 License: GPLv2 or later
10 License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13 if (!defined('ABSPATH')) exit;
14
15 require_once plugin_dir_path(__FILE__) . 'includes/class-gptranslate-optimizer-compat.php';
16
17 class GPTranslate {
18 private static $instance = null;
19 private $table_name;
20 private $version;
21
22 private function isSelected($current, $value) {
23 return selected($current, $value, false);
24 }
25
26 public static $pluginVersion = '2.28';
27
28 /**
29 * Class constructor and settings inizializer with register_setting
30 *
31 * @access public
32 */
33 public function __construct() {
34 global $wpdb;
35 $this->table_name = $wpdb->prefix . 'gptranslate';
36
37 $this->version = '2.28';
38
39 $settings = get_option ( 'gptranslate_options', [ ] );
40
41 // Server-side plugin exclusion/inclusion check for frontend pages
42 if (! is_admin ()) {
43 $page_inclusions_raw = $settings ['page_inclusions'] ?? '';
44 $page_exclusions_raw = $settings ['page_exclusions'] ?? '';
45
46 // If page_inclusions is set, it takes priority over page_exclusions
47 if (! empty ( $page_inclusions_raw )) {
48
49 // Normalize input: newlines -> commas, collapse multiple commas
50 $inclusion_patterns = explode ( ',', trim ( preg_replace ( '/,+/', ',', str_ireplace ( [
51 "\r",
52 "\n"
53 ], ',', $page_inclusions_raw ) ), ',' ) );
54
55 // Normalize current request URI (PATH ONLY)
56 $request_uri = $_SERVER ['REQUEST_URI'] ?? '/';
57 $request_path = parse_url ( $request_uri, PHP_URL_PATH ) ?: '/';
58 $request_path = '/' . trim ( $request_path, '/' );
59
60 // Split path into segments
61 $path_segments = array_values ( array_filter ( explode ( '/', trim ( $request_path, '/' ) ) ) );
62
63 // Remove subfolder installation segment if configured
64 if (! empty ( $settings ['subfolder_installation'] ) && ! empty ( $path_segments )) {
65 array_shift ( $path_segments );
66 }
67
68 // Remove language slug if present (use configured languages)
69 $languages = (isset ( $settings ['languages'] ) && is_array ( $settings ['languages'] )) ? array_map ( 'strtolower', $settings ['languages'] ) : [ ];
70
71 if (! empty ( $path_segments ) && in_array ( strtolower ( $path_segments [0] ), $languages, true )) {
72 array_shift ( $path_segments );
73 }
74
75 // HOME detection:
76 // After removing subfolder + language, nothing left = HOME
77 $is_home_request = empty ( $path_segments );
78
79 // Check if current page is explicitly INCLUDED
80 $page_is_included = false;
81
82 // 1) Explicit HOME inclusion via "home" or "/"
83 if ($is_home_request) {
84 foreach ( $inclusion_patterns as $pattern ) {
85 $pattern = strtolower ( trim ( $pattern ) );
86 if ($pattern === 'home' || $pattern === '/') {
87 $page_is_included = true;
88 break;
89 }
90 }
91 }
92
93 // 2) Normal URL-based inclusion (full URL substring match)
94 if (! $page_is_included) {
95 $current_url = esc_url_raw ( ((! empty ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] !== 'off') ? 'https://' : 'http://') . ($_SERVER ['HTTP_HOST'] ?? '') . $request_uri );
96
97 foreach ( $inclusion_patterns as $pattern ) {
98 $pattern = trim ( $pattern );
99
100 // Skip empty and home patterns (already handled)
101 if ($pattern === '' || $pattern === '/' || strtolower ( $pattern ) === 'home') {
102 continue;
103 }
104
105 // Case-insensitive substring match
106 if (stripos ( $current_url, $pattern ) !== false) {
107 $page_is_included = true;
108 break;
109 }
110 }
111 }
112
113 // If page is not included, skip plugin execution
114 if (! $page_is_included) {
115 return;
116 }
117
118 } else if (! empty ( $page_exclusions_raw )) {
119 // If page_inclusions is empty, use page_exclusions logic
120
121 // Normalize input: newlines -> commas, collapse multiple commas
122 $patterns = explode ( ',', trim ( preg_replace ( '/,+/', ',', str_ireplace ( [
123 "\r",
124 "\n"
125 ], ',', $page_exclusions_raw ) ), ',' ) );
126
127 // Normalize current request URI (PATH ONLY)
128 $request_uri = $_SERVER ['REQUEST_URI'] ?? '/';
129 $request_path = parse_url ( $request_uri, PHP_URL_PATH ) ?: '/';
130 $request_path = '/' . trim ( $request_path, '/' );
131
132 // Split path into segments
133 $path_segments = array_values ( array_filter ( explode ( '/', trim ( $request_path, '/' ) ) ) );
134
135 // Remove subfolder installation segment if configured
136 if (! empty ( $settings ['subfolder_installation'] ) && ! empty ( $path_segments )) {
137 array_shift ( $path_segments );
138 }
139
140 // Remove language slug if present (use configured languages)
141 $languages = (isset ( $settings ['languages'] ) && is_array ( $settings ['languages'] )) ? array_map ( 'strtolower', $settings ['languages'] ) : [ ];
142
143 if (! empty ( $path_segments ) && in_array ( strtolower ( $path_segments [0] ), $languages, true )) {
144 array_shift ( $path_segments );
145 }
146
147 // HOME detection:
148 // After removing subfolder + language, nothing left = HOME
149 $is_home_request = empty ( $path_segments );
150
151 // 1) Explicit HOME exclusion via "home" or "/"
152 if ($is_home_request) {
153 foreach ( $patterns as $pattern ) {
154 $pattern = strtolower ( trim ( $pattern ) );
155 if ($pattern === 'home' || $pattern === '/') {
156 return; // Skip plugin execution
157 }
158 }
159 }
160
161 // 2) Normal URL-based exclusion (full URL substring match)
162 $current_url = esc_url_raw ( ((! empty ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] !== 'off') ? 'https://' : 'http://') . ($_SERVER ['HTTP_HOST'] ?? '') . $request_uri );
163
164 foreach ( $patterns as $pattern ) {
165 $pattern = trim ( $pattern );
166
167 // Skip empty and home patterns (already handled)
168 if ($pattern === '' || $pattern === '/' || strtolower ( $pattern ) === 'home') {
169 continue;
170 }
171
172 // Case-insensitive substring match
173 if (stripos ( $current_url, $pattern ) !== false) {
174 return; // Skip plugin execution
175 }
176 }
177 }
178 }
179
180 /**
181 * Global helper function to check if a link should be excluded from rewriting
182 */
183 if (!function_exists('gptranslate_should_exclude_link')) {
184 function gptranslate_should_exclude_link($href, $exclusions_raw) {
185 if (empty($exclusions_raw)) {
186 return false;
187 }
188
189 // Parse exclusion patterns: split by comma and newline, trim whitespace
190 $exclusion_patterns = explode(',', trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ',', $exclusions_raw)), ','));
191
192 // Decode href for comparison
193 $decoded_href = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
194
195 // Check each pattern using case-insensitive substring matching
196 foreach ($exclusion_patterns as $pattern) {
197 $pattern = trim($pattern);
198 if (empty($pattern)) {
199 continue;
200 }
201 // Case-insensitive substring match
202 if (stripos($decoded_href, $pattern) !== false) {
203 return true;
204 }
205 }
206
207 return false;
208 }
209 }
210
211 // Include various functions like multilanguage URLs, hreflang tag, HTML lang attribute rewriting
212 require_once plugin_dir_path(__FILE__) . 'multilang-routing.php';
213
214 if ( isset($settings ['serverside_translations']) && $settings ['serverside_translations'] == 1 ) {
215 require_once plugin_dir_path(__FILE__) . 'serverside-translations.php';
216 }
217
218 register_activation_hook ( __FILE__, [
219 $this,
220 'activate_plugin'
221 ] );
222
223 add_action ( 'admin_init', function () {
224 // Disable WordPress emoji script and styles
225 remove_action('wp_head', 'print_emoji_detection_script', 7);
226 remove_action('admin_print_scripts', 'print_emoji_detection_script');
227 remove_action('wp_print_styles', 'print_emoji_styles');
228 remove_action('admin_print_styles', 'print_emoji_styles');
229 remove_filter('the_content_feed', 'wp_staticize_emoji');
230 remove_filter('comment_text_rss', 'wp_staticize_emoji');
231 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
232
233 function gptranslate_sanitize_options( $options ) {
234 $clean = [];
235 $clean = $options;
236
237 // Ensure original language is always included in enabled languages
238 if ( isset( $clean['language'] ) && ! empty( $clean['language'] ) ) {
239 $originalLang = strtolower( sanitize_text_field( $clean['language'] ) );
240 if ( ! isset( $clean['languages'] ) || ! is_array( $clean['languages'] ) ) {
241 $clean['languages'] = [];
242 }
243 $cleanedLangs = array_map( 'strtolower', $clean['languages'] );
244 if ( ! in_array( $originalLang, $cleanedLangs, true ) ) {
245 array_unshift( $clean['languages'], $originalLang );
246 }
247 }
248
249 return $clean;
250 }
251 register_setting ( 'gptranslate_settings', 'gptranslate_options', [
252 'sanitize_callback' => 'gptranslate_sanitize_options'
253 ]);
254
255 // Register record deletion
256 $page = sanitize_key($_GET['page'] ?? '');
257 $action = sanitize_key($_GET['action'] ?? '');
258 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
259 $translation_id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
260
261 if ($page === 'gptranslate' &&
262 $action === 'delete_translation' &&
263 $translation_id &&
264 wp_verify_nonce($nonce, 'gptranslate_delete_' . $translation_id)
265 ) {
266 $this->gptranslate_handle_deletion($translation_id);
267 exit;
268 }
269
270 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'toggle_published') {
271 // Toggle published state
272 $id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0;
273
274 if (!$id || !isset($_GET['_gptranslate_nonce']) || !wp_verify_nonce(wp_unslash($_GET['_gptranslate_nonce']), 'gptranslate_toggle_' . $id)) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
275 wp_die('Invalid nonce.');
276 }
277
278 global $wpdb;
279 $table = $wpdb->prefix . 'gptranslate';
280
281 // Toggle published flag
282 $current = $wpdb->get_var($wpdb->prepare("SELECT published FROM $table WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
283 $new = ($current == 1) ? 0 : 1;
284
285 $wpdb->update($table, ['published' => $new], ['id' => $id]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
286
287 wp_redirect(admin_url('admin.php?page=gptranslate&action=state_toggled'));
288 exit;
289 }
290
291 // Check for plugin update
292 function check_for_gptranslate_update($currentVersion) {
293 // Start the session if not already started
294 if (session_status() === PHP_SESSION_NONE) {
295 session_start();
296 }
297
298 // Reset session vars after update
299 if (isset($_SESSION['gptranslate_update_version']) && version_compare($currentVersion, $_SESSION['gptranslate_update_version'], '>=')) {
300 unset($_SESSION['gptranslate_update_version']);
301 unset($_SESSION['gptranslate_update_checked']);
302 }
303
304 // Check if the update check has been done in this session
305 if (!isset($_SESSION['gptranslate_update_checked']) || $_SESSION['gptranslate_update_checked'] !== true) {
306
307 // Perform the remote XML check
308 $remote_url = 'https://storejextensions.org/updates/gptranslatewp_updater.xml';
309 $response = wp_remote_get($remote_url);
310
311 if (!is_wp_error($response)) {
312 $body = wp_remote_retrieve_body($response);
313 if (!empty($body)) {
314 $xml = simplexml_load_string($body);
315 if ($xml && !empty($xml->update->version)) {
316 $updateversion = (string)$xml->update->version;
317 if (version_compare($updateversion, $currentVersion, '>')) {
318 // Store the update info in session (version, and flag that update is available)
319 $_SESSION['gptranslate_update_version'] = $updateversion;
320 }
321 $_SESSION['gptranslate_update_checked'] = true;
322 }
323 }
324 }
325 }
326
327 // If update is available, show the notice
328 $gpt_update_version = (isset($_SESSION['gptranslate_update_version']) && sanitize_text_field($_SESSION['gptranslate_update_version']))
329 ? sanitize_text_field($_SESSION['gptranslate_update_version'])
330 : '';
331 session_write_close();
332 if ($gpt_update_version) {
333 add_action('admin_notices', function () use ($gpt_update_version) {
334 echo '<div class="notice notice-warning is-dismissible">';
335 echo '<p>An update for <strong><a href="https://storejextensions.org/extensions/gptranslate.html" target="_blank">GPTranslate</a></strong> is available. The new version <strong>' . esc_html($gpt_update_version) . '</strong> can be downloaded from your reserved area if you have a valid subscription and license for the full version.</p>';
336 echo '</div>';
337 });
338 }
339 }
340 //check_for_gptranslate_update($this->version);
341 } );
342
343 // Post admin notices after actions
344 add_action( 'admin_notices', function() {
345 if ( isset( $_GET['page'], $_GET['deleted'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
346 if ( (int) $_GET['deleted'] === 1 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
347 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED')) . '</p></div>';
348 } elseif ( (int) $_GET['deleted'] === 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
349 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED_ERROR')) . '</p></div>';
350 }
351 }
352
353 if ( isset( $_GET['page'], $_GET['action'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
354 if ( $_GET['action'] === 'state_toggled' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
355 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_STATE_UPDATED_SUCCESSFULLY')) . '</p></div>';
356 }
357 }
358
359 if (isset($_GET['imported']) && $_GET['imported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
360 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_IMPORTED_SUCCESSFULLY')) . '</p></div>';
361 }
362
363 if (isset($_GET['settingsimported']) && $_GET['settingsimported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
364 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_IMPORTED_SUCCESSFULLY')) . '</p></div>';
365 }
366 });
367
368 // Handle review notice dismiss/remind actions
369 add_action('admin_post_gptranslate_review_dismiss', function() {
370 check_admin_referer('gptranslate_review_dismiss');
371 $type = isset($_GET['type']) ? sanitize_key($_GET['type']) : 'forever';
372 if ($type === 'later') {
373 update_option('gptranslate_review_remind_later', time() + 14 * DAY_IN_SECONDS);
374 } else {
375 update_option('gptranslate_review_dismissed', true);
376 }
377 $redirect = wp_get_referer();
378 wp_safe_redirect($redirect ? $redirect : admin_url('admin.php?page=gptranslate-settings'));
379 exit;
380 });
381
382 // Add hook for admin menu links
383 add_action ( 'admin_menu', [
384 $this,
385 'admin_menu'
386 ] );
387
388 // Review button on all GPTranslate admin pages
389 add_action('admin_footer', function() {
390 $screen = get_current_screen();
391 if (!$screen || strpos($screen->id, 'gptranslate') === false) return;
392 $dismissed = get_option('gptranslate_review_dismissed');
393 $remind = get_option('gptranslate_review_remind_later');
394 if ($dismissed || ($remind && time() < (int)$remind)) return;
395 $url_review = 'https://wordpress.org/plugins/gptranslate/';
396 $url_later = esc_url(wp_nonce_url(admin_url('admin-post.php?action=gptranslate_review_dismiss&type=later'), 'gptranslate_review_dismiss'));
397 $url_forever = esc_url(wp_nonce_url(admin_url('admin-post.php?action=gptranslate_review_dismiss&type=forever'), 'gptranslate_review_dismiss'));
398 ?>
399 <button type="button" id="gpt-rate-btn" onclick="document.getElementById('gpt-review-overlay').classList.add('open')">
400 <span class="gpt-heart">&#10084;</span> Rate us <span class="gpt-stars">&#9733;&#9733;&#9733;&#9733;&#9733;</span>
401 </button>
402 <div id="gpt-review-overlay" onclick="if(event.target===this)this.classList.remove('open')">
403 <div id="gpt-review-box">
404 <button type="button" id="gpt-review-close" onclick="document.getElementById('gpt-review-overlay').classList.remove('open')" title="Close">&times;</button>
405 <div class="gpt-review-stars">&#9733;&#9733;&#9733;&#9733;&#9733;</div>
406 <h3 class="gpt-review-title">Enjoying GPTranslate?</h3>
407 <p class="gpt-review-desc">If GPTranslate has been helpful for your site, please take a moment to leave a 5-star review on WordPress.org your feedback really helps the plugin grow and improve!</p>
408 <a href="<?php echo esc_url($url_review); ?>" target="_blank" rel="noopener" class="gpt-review-cta">&#11088; Leave a 5-star review</a>
409 <div class="gpt-review-links">
410 <a href="<?php echo $url_later; ?>">&#128337; Remind me later</a>
411 <a href="<?php echo $url_forever; ?>">&#10003; Already did</a>
412 </div>
413 <div class="gpt-review-footer">Made with <span style="color:#e03e3e;">&#10084;</span> by <a href="https://storejextensions.org" target="_blank" rel="noopener">JExtensions Store</a></div>
414 </div>
415 </div>
416 <?php
417 });
418
419 // Add hook for record saving/deleting
420 add_action ( 'admin_post_save_gptranslate_record', [
421 $this,
422 'save_record'
423 ] );
424
425 add_action( 'admin_post_save_gptranslate_record_and_close', [
426 $this,
427 'save_record'
428 ]);
429
430 add_action('admin_post_cancel_gptranslate_record', [
431 $this,
432 'save_record'
433 ]);
434
435 // Add hook for adding main frontend app scripts
436 add_action ( 'wp_enqueue_scripts', [
437 $this,
438 'enqueue_frontend_scripts'
439 ] );
440 }
441
442 /**
443 * Singleton class instance
444 *
445 * @access public
446 */
447 public static function get_instance() {
448 if (null === self::$instance) {
449 self::$instance = new static();
450 }
451 return self::$instance;
452 }
453
454 /**
455 * Activation plugin hook with db table creation
456 *
457 * @access public
458 */
459 public function activate_plugin() {
460 global $wpdb;
461 $charset_collate = $wpdb->get_charset_collate();
462
463 $sql = "CREATE TABLE " . $this->table_name . " (
464 id int UNSIGNED NOT NULL AUTO_INCREMENT,
465 pagelink varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
466 translated_alias varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
467 translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
468 alt_translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
469 languageoriginal char(20) NOT NULL,
470 languagetranslated char(20) NOT NULL,
471 published tinyint NOT NULL DEFAULT '1',
472 translate_date datetime DEFAULT NULL,
473 translation_engine varchar(20) NOT NULL,
474 PRIMARY KEY (id),
475 INDEX idx_lookup (languageoriginal, languagetranslated, published, pagelink),
476 INDEX idx_alias_lookup (languageoriginal, languagetranslated, published, translated_alias)
477 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
478
479 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
480 dbDelta($sql);
481
482 // Valori di default
483 $default_options = [
484 'google_translate_engine' => '1',
485 'google_translate_method' => '0',
486 'chatgpt_apikey' => '',
487 'chatgpt_model' => 'gpt-3.5-turbo',
488 'chatgpt_request_message' => "Compile this JSON object key-value pairs adding the translation into '{{target}}' language to the empty value from the original '{{source}}' language of the key and return me only a parsable JSON object without any surrounding characters, preserve and return in the JSON object the key in the original '{{source}}' language within double quotes: '{{translations}}'. Pay attention to not skip any key and translate all keys. Return only a parsable JSON object with no surrounding text, explanations, or markdown formatting. Ensure the response is valid JSON and can be parsed directly.",
489 'chatgpt_request_conversation_mode' => 'user',
490 'language' => 'en',
491 'max_translations_per_request' => '100',
492 'max_characters_per_request' => '2048',
493 'detect_browser_language' => '0',
494 'autotranslate_detected_language' => '0',
495 'always_detect_autotranslated_language' => '0',
496 'auto_set_language_direction' => '0',
497 'serverside_translations' => '0',
498 'serverside_translations_method' => 'regex',
499 'serverside_translations_caseinsensitive' => '1',
500 'serverside_translations_matchquotes' => '1',
501 'serverside_translations_urldecode' => '1',
502 'serverside_translations_language_switching_mode' => 'url',
503 'serverside_translations_ignore_querystring' => '0',
504 'serverside_translations_strip_querystring_params' => '',
505 'serverside_translations_urlencode_space' => '0',
506 'css_selector_serverside_leafnodes_excluded' => '',
507 'detect_current_language' => '0',
508 'detect_default_language' => '0',
509 'rewrite_language_url' => '0',
510 'rewrite_language_alias' => '0',
511 'rewrite_language_alias_original_language' => '0',
512 'rewrite_page_links' => '0',
513 'rewrite_page_links_exclusions' => '',
514 'rewrite_form_actions' => '0',
515 'transliterate_urls' => '0',
516 'omit_prefix_original_language' => '0',
517 'excluded_alias_slugs' => '',
518 'rewrite_default_language_url' => '0',
519 'translate_metadata' => '0',
520 'set_html_lang' => '0',
521 'add_canonical' => '0',
522 'add_alternate' => '0',
523 'translate_placeholders' => '0',
524 'translate_altimages' => '0',
525 'css_selector_classes_translate_altimages_excluded' => '',
526 'translate_srcimages' => '0',
527 'translate_iframe_locale' => '0',
528 'translate_titles' => '0',
529 'translate_values' => '0',
530 'metadata_chosen_engine' => '0',
531 'metadata_words_leafnodes_excluded' => '',
532 'default_language_first' => '0',
533 'css_selector_leafnodes_excluded' => 'a.nturl,.gt-lang-code',
534 'words_leafnodes_excluded' => '',
535 'words_leafnodes_excluded_bylanguage_repeatable' => '[]',
536 'words_min_length' => '',
537 'flatten_inner_formatting_tags' => '0',
538 'flatten_inner_formatting_tags_to_remove' => 'span,b,strong,i,em,u,font',
539 'wrap_excluded_words' => '0',
540 'apply_dictionary_to_aliases' => '0',
541 'crawler_timeout' => '30',
542 'crawler_exclusions' => '',
543 'crawler_skip_translated' => '0',
544 'page_exclusions' => '',
545 'page_inclusions' => '',
546 'chatgpt_gtranslate_request_delay' => '0',
547 'initial_translation_delay' => '0',
548 'realtime_translations' => '0',
549 'css_selector_realtime_translations_retrigger' => '',
550 'realtime_translations_retrigger_events' => ['click'],
551 'realtime_translations_retrigger_events_delay' => '200',
552 'realtime_translations_retrigger_force_google' => '0',
553 'translations_export_format' => '.csv',
554 'ignore_querystring' => '0',
555 'enable_indexer' => '0',
556 'lightweight_ajax_endpoint' => '0',
557 'storage_type' => 'session',
558 'subfolder_installation' => '0',
559 'alt_flags' => [],
560 'languages' => ['en', 'es', 'de', 'it', 'fr'],
561 'excluded_languages' => [],
562 'enable_reader' => '0',
563 'responsivevoice_apikey' => 'PEVOFBma',
564 'responsivevoice_language_gender' => 'auto',
565 'responsivevoice_volume_tts' => '100',
566 'responsivevoice_voice_speed' => 'normal',
567 'mainpage_selector' => '*[name*=main], *[class*=main], *[id*=main], *[id*=container], *[class*=container]',
568 'elements_toexclude_custom' => '',
569 'proxy_responsive_loading_script' => '1',
570 'proxy_responsive_reading_mode' => 'native',
571 'chunksize' => '200',
572 'widget_text_color' => '#000000',
573 'widget_background_color' => '#FFFFFF',
574 'popup_border_radius' => '0',
575 'popup_fontsize' => '20',
576 'popup_iconsize' => '32',
577 'popup_shadow' => '1',
578 'disable_toast_popups' => '0',
579 'widget_opacity' => '1.0',
580 'float_position' => 'bottom-left',
581 'float_switcher_open_direction' => 'top',
582 'flag_style' => '2d',
583 'flag_loading' => 'local',
584 'show_language_titles' => '1',
585 'enable_dropdown' => '1',
586 'enable_modal' => '0',
587 'equal_widths' => '0',
588 'reader_button_position' => 'top',
589 'widget_max_height' => '260',
590 'wrapper_selector' => '.gptranslate_wrapper',
591 'draggable_widget' => '0',
592 'disable_control' => '0',
593 'custom_css' => '',
594 'disable_bootstrap_css' => '0',
595 'lock_translations' => '1'
596 ];
597
598 // Se l'opzione non è ancora presente, la crea
599 if (get_option('gptranslate_options') === false) {
600 add_option('gptranslate_options', $default_options);
601 }
602
603 }
604
605 /**
606 * Function to add admin menu for both settings and translations management
607 *
608 * @access public
609 */
610 public function admin_menu() {
611 add_menu_page('GPTranslate', 'GPTranslate', 'manage_options', 'gptranslate', [$this, 'records_page'], 'dashicons-translation');
612
613 // Add a submenu that matches the main menu to prevent duplication and allow renaming
614 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), 'manage_options', 'gptranslate', [$this, 'records_page']);
615
616 // Now you can safely add a differently named submenu
617 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), 'manage_options', 'gptranslate-settings', [$this, 'settings_page']);
618 }
619
620 /**
621 * Load the configuration settings page held in the settings.php file
622 *
623 * @access public
624 */
625 public function settings_page() {
626 require_once 'settings.php';
627
628 echo '<script>
629 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
630 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
631 </script>';
632 }
633
634 /**
635 * Translation records pages, list and edit
636 *
637 * @access public
638 */
639 public function records_page() {
640 global $wpdb;
641
642 // Edit record
643 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'edit') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
644 $id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
645 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
646 $opts = get_option( 'gptranslate_options', [] );
647
648 if ( ! wp_verify_nonce( $nonce, 'gptranslate_edit_' . $id ) ) {
649 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
650 }
651
652 $record = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
653
654 $translationsArray = json_decode($record->translations, true) ?? [];
655 $altTranslationsArray = json_decode($record->alt_translations, true) ?? [];
656
657 uksort($translationsArray, function($a, $b) {
658 return strlen($b) - strlen($a);
659 });
660 uksort($altTranslationsArray, function($a, $b) {
661 return strlen($b) - strlen($a);
662 });
663
664 // Path relativo o assoluto all'immagine della bandiera
665 $flagUrlOriginal = plugins_url('flags/svg/' . esc_attr($record->languageoriginal) . '.svg', __FILE__);
666 $flagUrlTranslated = plugins_url('flags/svg/' . esc_attr($record->languagetranslated) . '.svg', __FILE__);
667
668 // Alternative flags check for edit view
669 $altFlagsOpts = isset($opts['alt_flags']) && is_array($opts['alt_flags']) ? $opts['alt_flags'] : [];
670 $altFlagMap = [
671 'en' => ['usa' => 'en-us', 'canada' => 'en-ca', 'ireland' => 'en-ie'],
672 'pt' => ['brazil' => 'pt-br'],
673 'es' => ['mexico' => 'es-mx', 'argentina' => 'es-ar', 'colombia' => 'es-co'],
674 'fr' => ['quebec' => 'fr-qc'],
675 'zh' => ['taiwan' => 'zh-TW'],
676 'zt' => ['hongkong' => 'zh-HK'],
677 'de' => ['austria' => 'de-at'],
678 ];
679 foreach ($altFlagMap as $langCode => $variants) {
680 foreach ($variants as $country => $flagFile) {
681 if (in_array($country, $altFlagsOpts)) {
682 if ($record->languageoriginal === $langCode) {
683 $flagUrlOriginal = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
684 }
685 if ($record->languagetranslated === $langCode) {
686 $flagUrlTranslated = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
687 }
688 break;
689 }
690 }
691 }
692
693 $pubIcon = $record->published ? '<img src="' . plugins_url('assets/images/published.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . '">' // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
694 : '<img src="' . plugins_url('assets/images/unpublished.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . '">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
695 $rewriteAliasRow = '';
696 if ($opts ['rewrite_language_alias'] == 1) {
697 $rewriteAliasRow = '<tr>
698 <th scope="row"><label for="translated_alias" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</label></th>
699 <td><input type="text" id="translated_alias" name="translated_alias" value="' . esc_attr($record->translated_alias) . '" class="regular-text code"></td>
700 </tr>';
701 }
702
703 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EDIT_TRANSLATION_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
704 echo '<form method="post" id="edit-translations" action="admin-post.php">
705 <p>
706 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVE')) . '" class="button button-primary" data-action="save_gptranslate_record">
707 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVEANDECLOSE')) . '" class="button button-primary" data-action="save_gptranslate_record_and_close">
708 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CANCEL')) . '" class="button button-primary" data-action="cancel_gptranslate_record">
709 </p>
710 <input type="hidden" name="languagetranslated" id="languagetranslated" value="' . esc_attr($record->languagetranslated) . '">
711 <input type="hidden" name="action" id="form_action" value="save_gptranslate_record">
712 <input type="hidden" name="id" value="' . (int) $record->id . '">
713
714 <table class="form-table">
715 <tr>
716 <th scope="row"><label for="pagelink" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</label></th>
717 <td><input type="text" id="pagelink" name="pagelink" value="' . esc_attr($record->pagelink) . '" class="regular-text code"></td>
718 </tr>' .
719 $rewriteAliasRow .
720 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languageoriginal))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
721 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languagetranslated))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
722 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED')) . '</label></th><td>' . wp_kses_post($pubIcon) . '</td></tr>' .
723 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DATE')) . '</label></th><td>' . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime( get_date_from_gmt($record->translate_date) ) ) ) . '</td></tr>' .
724 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</label></th><td><span class="gpt-label">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper(esc_attr($record->translation_engine)) . '_ENGINE')) . '</span></td></tr>
725
726 <tr>
727 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')) . '</label></th>
728 <td>
729 <div class="gptcard gptcard-default">
730 <div class="gptcard-header">
731 <div class="accordion-toggle">
732 <div class="input-group">
733 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
734 <input type="text" name="search" value="" class="text_area">
735 <button class="btn btn-primary" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
736 <button class="btn btn-primary" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
737 <select class="gpt-sort-select" data-role="sort-translations">
738 <option value="length-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_DESC')) . '</option>
739 <option value="alpha-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_ASC')) . '</option>
740 <option value="alpha-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_DESC')) . '</option>
741 <option value="length-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_ASC')) . '</option>
742 </select>
743 </div>
744 </div>
745 </div>
746 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
747 <button type="button" class="btn btn-success btn-adder" data-addtype="translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_TRANSLATION')) . '</button>
748 <textarea name="translations_json" id="translations_json" hidden>' . esc_textarea(json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
749 <div id="translations-container"></div>
750 </div>
751 </div>
752 </td>
753 </tr>
754
755 <tr class="alt_translations_' . (int)$opts['translate_altimages'] . '">
756 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS')) . '</label></th>
757 <td>
758 <div class="gptcard gptcard-default">
759 <div class="gptcard-header">
760 <div class="accordion-toggle">
761 <div class="input-group">
762 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
763 <input type="text" name="search" value="" class="text_area">
764 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
765 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
766 <select class="gpt-sort-select" data-role="sort-translations">
767 <option value="length-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_DESC')) . '</option>
768 <option value="alpha-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_ASC')) . '</option>
769 <option value="alpha-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_DESC')) . '</option>
770 <option value="length-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_ASC')) . '</option>
771 </select>
772 </div>
773 </div>
774 </div>
775 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
776 <button type="button" class="btn btn-success btn-adder" data-addtype="alt-translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALT_TRANSLATION')) . '</button>
777 <textarea name="alt_translations_json" id="alt_translations_json" hidden>' . esc_textarea(json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
778 <div id="alt-translations-container"></div>
779 </div>
780 </div>
781 </td>
782 </tr>
783 </table>' .
784 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce field is safe
785 wp_nonce_field('gptranslate_save_record_action', '_gptranslate_nonce') .
786 '</form>';
787
788 echo '<script>
789 const initialTranslations = ' . (json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
790 const initialAltTranslations = ' . (json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
791
792 const PLG_GPTRANSLATE_ORIGINAL_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_ORIGINAL_TEXT')) . '";
793 const PLG_GPTRANSLATE_TRANSLATED_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_TEXT')) . '";
794 const PLG_GPTRANSLATE_DELETE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '";
795 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
796 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
797 const PLG_GPTRANSLATE_SYNC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC')) . '";
798 const PLG_GPTRANSLATE_SYNC_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_TITLE')) . '";
799 const PLG_GPTRANSLATE_SYNC_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_DESC')) . '";
800 const PLG_GPTRANSLATE_SYNC_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_COMPLETED')) . '";
801 const PLG_GPTRANSLATE_SYNC_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_ERROR')) . '";
802 const gptServerSideLink = "' . esc_url_raw(rest_url('gptranslate/v1/request')) . '";
803 </script>';
804 } else {
805 // FREE period
806 // echo '<div class="notice notice-info is-dismissible"><p>You’re currently using the full version of GPTranslate – completely FREE during the initial launch period! Enjoy unlimited AI-powered translations and all PRO features at no cost. 🚀</p></div>';
807
808 // UPGRADE period
809 echo '<div class="notice notice-warning is-dismissible"><p>⚠️ GPTranslate runs in FREE Mode with usage limits. To unlock unlimited translations and advanced features, upgrade to the <a href="https://gptranslate.storejextensions.org/" target="_blank"><strong>PRO version</strong></a>. Current FREE Plan: translate up to <strong>2500 words</strong>, read aloud up to <strong>100 words</strong> per page and crawl up to <strong>10 pages</strong>. Don’t lose AI power – <a href="https://gptranslate.storejextensions.org/" target="_blank">Upgrade Now</a>.</p></div>';
810
811 // List records
812 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
813
814 // Filters section
815 $opts = get_option( 'gptranslate_options', [] );
816 $languageFilter = esc_attr( sanitize_text_field( wp_unslash( $_GET['language'] ?? '' ) ) );
817 $languages = $opts['languages'] ?? [];
818
819 // Paginate records
820 $records_per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
821 $valid_per_page_options = [5, 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 9999999];
822 if (!in_array($records_per_page, $valid_per_page_options)) {
823 $records_per_page = 10;
824 }
825
826 $searchFilter = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) );
827 $engineFilter = sanitize_key( wp_unslash( $_GET['engine'] ?? '' ) );
828 $publishedFilter = sanitize_key( wp_unslash( $_GET['published'] ?? '' ) );
829 $exactMatchFilter = isset($_GET['exactmatch']) && $_GET['exactmatch'] == '1' ? true : false;
830
831 echo
832 '<form method="get" class="form-filter-container">' .
833 '<div class="left-filter-container">' .
834 '<input type="text" name="s" id="search-input" value="' . (isset($_GET['s']) ? esc_attr(sanitize_text_field( wp_unslash($_GET['s']))) : '') . '" placeholder="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SEARCH')) . '" />' .
835 '<button type="button" class="button" onclick="this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>' .
836 '<button type="button" class="button" onclick="document.getElementById(\'search-input\').value=\'\'; this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>' .
837 '<label class="button"><input type="checkbox" name="exactmatch" value="1" ' . checked($exactMatchFilter, true, false) . ' title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXACT_MATCH')) . '"> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EXACT_MATCH_LABEL')) . '</label>' .
838 '</div>' .
839 '<div class="right-filter-container">' .
840 '<input type="hidden" name="_gptranslate_nonce" value="' . esc_attr(wp_create_nonce('gptranslate_filter_action')) . '" />' .
841 '<select name="published">' .
842 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_ALL')) . '</option>' .
843 '<option value="1" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '1')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</option>' .
844 '<option value="0" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '0')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '</option>' .
845 '</select>' .
846 '<select name="language">' .
847 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</option>';
848
849
850 // Loop languages
851 foreach ($languages as $lang) {
852 echo "<option value='" . esc_attr($lang) . "'" . esc_html($this->isSelected($languageFilter, $lang)) . ">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($lang))) . "</option>";
853 }
854
855 echo
856 '</select>' .
857 '<select name="engine">' .
858 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</option>' .
859 '<option value="gtranslate" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gtranslate')) . '>Google AI</option>' .
860 '<option value="chatgpt" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'chatgpt')) . '>ChatGPT</option>' .
861 '<option value="gemini" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gemini')) . '>Gemini</option>' .
862 '<option value="deepseek" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'deepseek')) . '>DeepSeek</option>' .
863 '<option value="googlecloud" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'googlecloud')) . '>Google Cloud</option>' .
864 '<option value="claude" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'claude')) . '>Claude</option>' .
865 '<option value="deepl" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'deepl')) . '>DeepL</option>' .
866 '</select>' .
867 '<select name="per_page">' .
868 '<option value="5" ' . esc_html($this->isSelected($records_per_page, 5)) . '>5</option>' .
869 '<option value="10" ' . esc_html($this->isSelected($records_per_page, 10)) . '>10</option>' .
870 '<option value="25" ' . esc_html($this->isSelected($records_per_page, 25)) . '>25</option>' .
871 '<option value="50" ' . esc_html($this->isSelected($records_per_page, 50)) . '>50</option>' .
872 '<option value="100" ' . esc_html($this->isSelected($records_per_page, 100)) . '>100</option>' .
873 '<option value="200" ' . esc_html($this->isSelected($records_per_page, 200)) . '>200</option>' .
874 '<option value="500" ' . esc_html($this->isSelected($records_per_page, 500)) . '>500</option>' .
875 '<option value="1000" ' . esc_html($this->isSelected($records_per_page, 1000)) . '>1000</option>' .
876 '<option value="2000" ' . esc_html($this->isSelected($records_per_page, 2000)) . '>2000</option>' .
877 '<option value="5000" ' . esc_html($this->isSelected($records_per_page, 5000)) . '>5000</option>' .
878 '<option value="10000" ' . esc_html($this->isSelected($records_per_page, 10000)) . '>10000</option>' .
879 '<option value="9999999" ' . esc_html($this->isSelected($records_per_page, 9999999)) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALL')) . '</option>' .
880 '</select>' .
881 '<input type="submit" class="button" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '" />' .
882 '</div>' .
883 '<input type="hidden" name="page" value="gptranslate" />' .
884 '</form>';
885
886 // Bottoni Import/Export
887 echo '<div class="action-buttons-toolbar">';
888 echo '<button class="button button-primary" id="bulk-delete-btn">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '</button>';
889 echo '<button class="button button-primary" id="toggle-migration">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_BTNS')) . '</button>';
890
891 // Export CSV/XLIFF
892 $exportFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_export_translations_xliff' : 'gptranslate_export_translations_csv';
893 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" style="display:inline;margin-right:10px;">';
894 if($exportFormat == 'gptranslate_export_translations_csv') {
895 wp_nonce_field('gptranslate_export_csv', 'gptranslate_export_csv_nonce');
896 } else {
897 wp_nonce_field('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce');
898 }
899 echo '<input type="hidden" name="action" value="' . $exportFormat . '">';
900 echo '<input type="submit" class="button button-primary" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXPORT_TRANSLATIONS')) . '">';
901 echo '</form>';
902
903 // Import CSV/XLIFF
904 $importFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_import_translations_xliff' : 'gptranslate_import_translations_csv';
905 echo '<input type="button" class="button button-primary button-import" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_IMPORT_TRANSLATIONS')) . '">';
906 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" enctype="multipart/form-data" style="display:inline;">';
907 if($importFormat == 'gptranslate_import_translations_csv') {
908 wp_nonce_field('gptranslate_import_csv', 'gptranslate_import_csv_nonce');
909 $acceptFormat = '.csv';
910 } else {
911 wp_nonce_field('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce');
912 $acceptFormat = '.xliff';
913 }
914 echo '<input type="hidden" name="action" value="' . $importFormat . '">';
915 echo '<input type="file" name="import_file" class="toggle-import hidden" accept="' . $acceptFormat . '" required>';
916 echo '<input type="submit" class="button button-primary toggle-import hidden" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '">';
917 echo '</form>';
918
919 echo '</div>';
920
921 echo '<div id="migraterow" class="hidden">
922 <span class="input-group">
923 <label for="migratetranslations_currentdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_PREVIOUS_DOMAIN')) . '</strong></label>
924 <input type="text" class="form-control" id="migratetranslations_currentdomain" name="migratetranslations_currentdomain" value="">
925 </span>
926 <span class="input-group">
927 <label for="migratetranslations_newdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_NEW_DOMAIN')) . '</strong></label>
928 <input type="text" class="form-control" id="migratetranslations_newdomain" name="migratetranslations_newdomain" value="">
929 </span>
930 <button class="button button-primary" id="migrationconfirm">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '</button>
931 <button class="button" id="migrationcancel">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
932 </div>
933 <input type="button" class="button button-warning button-crawler" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '">
934
935 <script>
936 const PLG_GPTRANSLATE_MIGRATION_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_SUCCESS')) . '";
937 const PLG_GPTRANSLATE_MIGRATION_FAILED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_FAILED')) . '";
938 const PLG_GPTRANSLATE_UNKNOWN_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_UNKNOWN_ERROR')) . '";
939 const PLG_GPTRANSLATE_NETWORK_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_NETWORK_ERROR')) . '";
940 const PLG_GPTRANSLATE_BULK_DELETE_CONFIRM = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_CONFIRM')) . '";
941 const PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE')) . '";
942 const PLG_GPTRANSLATE_BULK_DELETE_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SUCCESS')) . '";
943 const PLG_GPTRANSLATE_BULK_DELETE_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_ERROR')) . '";
944 const PLG_GPTRANSLATE_BULK_DELETE_NETWORK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_NETWORK')) . '";
945 const PLG_GPTRANSLATE_CRAWLER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '";
946 const PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE')) . '";
947 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
948 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
949 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START')) . '";
950 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
951 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
952 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
953 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
954 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
955 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
956 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
957 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
958 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
959 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
960 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
961 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
962 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
963 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
964 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
965 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
966 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
967 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
968 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
969 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
970 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
971 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
972 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
973 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
974 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
975 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
976 var gptranslateBaseCrawlerHome = "' . esc_js( trailingslashit( get_site_url() ) ) . '";
977 var gptranslateSitemapUrl = "' . esc_js( rest_url( 'gptranslate/v1/sitemap.xml' ) ) . '";
978 var gptranslateDefaultLanguage = "' . $opts['language'] . '";
979 var gptranslateCrawlerTimeout = "' . (isset($opts['crawler_timeout']) ? $opts['crawler_timeout'] : '30') . '";
980 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($opts['crawler_exclusions']) ? $opts['crawler_exclusions'] : ''))), ',')) . '";
981 var gptranslateCrawlerSkipTranslated = ' . (int)($opts['crawler_skip_translated'] ?? 0) . ';
982 var gptSubfolderInstallation = ' . (int)($opts['subfolder_installation'] ?? 0) . ';
983 var gptranslateRewriteLanguageUrl = ' . (int)$opts['rewrite_language_url'] . ';
984 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($opts['omit_prefix_original_language']) ? (int)$opts['omit_prefix_original_language'] : 0) . ';
985 var gptVersionNumeric = ' . 0 . ';
986 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
987 </script>';
988
989
990 echo '<table class="widefat fixed striped">';
991 echo '<thead><tr>';
992 echo '<th style="width: 1%"><input class="form-check-input" autocomplete="off" type="checkbox" id="checkall"></th>';
993 echo '<th style="width: 2%;;white-space:nowrap">ID</th>';
994 if($opts['rewrite_language_alias'] == 1) {
995 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
996 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</th>';
997 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
998 } else {
999 echo '<th style="width: 25%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
1000 echo '<th style="width: 20%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
1001 }
1002 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</th>';
1003 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</th>';
1004 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</th>';
1005 echo '<th style="width:5%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_ENGINE')) . '</th>';
1006 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DATE')) . '</th>';
1007 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ACTIONS')) . '</th>';
1008 echo '</tr></thead>';
1009
1010 echo '<tbody>';
1011
1012 $current_page = isset($_GET['paged']) && is_numeric($_GET['paged']) ? (int)$_GET['paged'] : 1;
1013 $offset = ($current_page - 1) * $records_per_page;
1014 $sql_count = "SELECT COUNT(*) FROM {$this->table_name} WHERE 1=1";
1015
1016 // Add dynamic filters
1017 if (!empty($searchFilter)) {
1018 if ($exactMatchFilter) {
1019 // Ricerca esatta
1020 $sql_count .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1021 } else {
1022 // Ricerca LIKE (comportamento attuale)
1023 $sql_count .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
1024 }
1025 }
1026 if ($publishedFilter !== '') {
1027 $sql_count .= " AND published = '" . esc_sql($publishedFilter) . "'";
1028 }
1029 if (!empty($languageFilter)) {
1030 $sql_count .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1031 }
1032 if (!empty($engineFilter)) {
1033 $sql_count .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1034 }
1035 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1036 $total_records = $wpdb->get_var($sql_count); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1037 $total_pages = ceil($total_records / $records_per_page);
1038
1039 // Load records count with filtering
1040 $sql_data = "SELECT * FROM {$this->table_name} WHERE 1=1";
1041
1042 // Add dynamic filters
1043 if (!empty($searchFilter)) {
1044 if ($exactMatchFilter) {
1045 // Ricerca esatta
1046 $sql_data .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1047 } else {
1048 // Ricerca LIKE (comportamento attuale)
1049 $sql_data .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
1050 }
1051 }
1052 if ($publishedFilter !== '') {
1053 $sql_data .= " AND published = '" . esc_sql($publishedFilter) . "'";
1054 }
1055 if (!empty($languageFilter)) {
1056 $sql_data .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1057 }
1058 if (!empty($engineFilter)) {
1059 $sql_data .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1060 }
1061 $sql_data .= " ORDER BY translate_date DESC LIMIT $records_per_page OFFSET $offset";
1062
1063 // Load records with filtering and pagination
1064 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1065 $records = $wpdb->get_results($sql_data); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1066
1067 // Message for no translations
1068 if (!count($records) && stripos($sql_data, "AND") === false) {
1069 echo '<div class="notice notice-success is-dismissible" id="notranslations-notice">';
1070 echo '<p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO_TRANSLATIONS')) . '</p>';
1071 echo '</div>';
1072 ?>
1073 <div id="gptranslate-zero-state" class="gptranslate-zero-state">
1074 <div class="gptranslate-zero-icon">
1075 <svg fill="#222222" height="96px" width="96px" version="1.1" id="XMLID_275_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" xml:space="preserve">
1076 <g id="language">
1077 <g>
1078 <path d="M12,24C5.4,24,0,18.6,0,12S5.4,0,12,0s12,5.4,12,12S18.6,24,12,24z M9.5,17c0.6,3.1,1.7,5,2.5,5s1.9-1.9,2.5-5H9.5z
1079 M16.6,17c-0.3,1.7-0.8,3.3-1.4,4.5c2.3-0.8,4.3-2.4,5.5-4.5H16.6z M3.3,17c1.2,2.1,3.2,3.7,5.5,4.5c-0.6-1.2-1.1-2.8-1.4-4.5H3.3
1080 z M16.9,15h4.7c0.2-0.9,0.4-2,0.4-3s-0.2-2.1-0.5-3h-4.7c0.2,1,0.2,2,0.2,3S17,14,16.9,15z M9.2,15h5.7c0.1-0.9,0.2-1.9,0.2-3
1081 S15,9.9,14.9,9H9.2C9.1,9.9,9,10.9,9,12C9,13.1,9.1,14.1,9.2,15z M2.5,15h4.7c-0.1-1-0.1-2-0.1-3s0-2,0.1-3H2.5C2.2,9.9,2,11,2,12
1082 S2.2,14.1,2.5,15z M16.6,7h4.1c-1.2-2.1-3.2-3.7-5.5-4.5C15.8,3.7,16.3,5.3,16.6,7z M9.5,7h5.1c-0.6-3.1-1.7-5-2.5-5
1083 C11.3,2,10.1,3.9,9.5,7z M3.3,7h4.1c0.3-1.7,0.8-3.3,1.4-4.5C6.5,3.3,4.6,4.9,3.3,7z"/>
1084 </g>
1085 </g>
1086 </svg>
1087 </div>
1088
1089 <div class="gptranslate-zero-title">
1090 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_TITLE')); ?>
1091 </div>
1092
1093 <p class="gptranslate-zero-desc">
1094 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_DESC')); ?>
1095 </p>
1096
1097 <button id="gptranslate-start-crawler" class="button button-primary button-hero">
1098 <span class="dashicons-before dashicons-translation" aria-hidden="true"></span>
1099 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_BUTTON')); ?>
1100 </button>
1101
1102 <p class="gptranslate-zero-hint">
1103 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_HINT')); ?>
1104 </p>
1105 </div>
1106 <?php
1107 }
1108
1109 foreach ( $records as $r ) {
1110 // Build a short summary of the translations
1111 $tr = json_decode( $r->translations, true );
1112 if ( is_array( $tr ) ) {
1113 $pairs = array_map(
1114 function( $k, $v ) {
1115 return esc_html( $k ) . '' . esc_html( $v );
1116 },
1117 array_keys( $tr ),
1118 array_values( $tr )
1119 );
1120 $short = implode( ', ', $pairs );
1121 $short = mb_substr( $short, 0, 80 ) . ( mb_strlen( $short ) > 80 ? '' : '' );
1122 } else {
1123 $short = '';
1124 }
1125
1126 // Escape all output
1127 $id = (int) $r->id;
1128 $link = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=edit&edit={$id}" ), 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1129 $deleteLink = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=delete_translation&translation_id={$id}" ), 'gptranslate_delete_' . $id, '_gptranslate_nonce' );
1130 $origLang = esc_html( strtoupper( $r->languageoriginal ) );
1131 $transLang = esc_html( strtoupper( $r->languagetranslated ) );
1132 $pub = $r->published ? esc_html($this->loadTranslations('PLG_GPTRANSLATE_YES')) : esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO'));
1133 $engine = esc_html( $r->translation_engine );
1134 $date = esc_html( $r->translate_date );
1135
1136 $langOriginal = esc_attr($r->languageoriginal);
1137
1138 // Path relativo o assoluto all'immagine della bandiera
1139 $flagUrlOriginal = plugins_url('flags/svg/' . $r->languageoriginal . '.svg', __FILE__);
1140 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1141 $flagUrlTranslated = plugins_url('flags/svg/' . $r->languagetranslated . '.svg', __FILE__);
1142 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1143
1144 // Alternative flags check for list view
1145 $altFlagsOpts = isset($opts['alt_flags']) && is_array($opts['alt_flags']) ? $opts['alt_flags'] : [];
1146 $altFlagMap = [
1147 'en' => ['usa' => 'en-us', 'canada' => 'en-ca', 'ireland' => 'en-ie'],
1148 'pt' => ['brazil' => 'pt-br'],
1149 'es' => ['mexico' => 'es-mx', 'argentina' => 'es-ar', 'colombia' => 'es-co'],
1150 'fr' => ['quebec' => 'fr-qc'],
1151 'zh' => ['taiwan' => 'zh-TW'],
1152 'zt' => ['hongkong' => 'zh-HK'],
1153 'de' => ['austria' => 'de-at'],
1154 ];
1155 foreach ($altFlagMap as $langCode => $variants) {
1156 foreach ($variants as $country => $flagFile) {
1157 if (in_array($country, $altFlagsOpts)) {
1158 if ($r->languageoriginal === $langCode) {
1159 $flagUrlOriginal = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1160 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1161 }
1162 if ($r->languagetranslated === $langCode) {
1163 $flagUrlTranslated = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1164 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1165 }
1166 break;
1167 }
1168 }
1169 }
1170
1171 $togglePublishedUrl = wp_nonce_url(
1172 admin_url("admin.php?page=gptranslate&action=toggle_published&translation_id={$id}"),
1173 'gptranslate_toggle_' . $id,
1174 '_gptranslate_nonce'
1175 );
1176
1177 $pubIcon = $r->published ? '<img src="' . plugins_url('assets/images/published.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . '">' // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1178 : '<img src="' . plugins_url('assets/images/unpublished.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . '">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1179
1180
1181 $pub = $r->published ? "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-published' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . "'>" . $pubIcon . "</a>"
1182 : "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-unpublished' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . "'>" . $pubIcon . "</a>";
1183
1184 $local_date = get_date_from_gmt($date);
1185
1186 echo '<tr>';
1187 echo "<td style='width: 1%'><input class='form-check-input' autocomplete='off' type='checkbox' id='cb0' name='gptid[]' value='" . esc_attr($r->id) . "'></td>";
1188 echo "<td style='width: 2%;white-space:nowrap'>". esc_html($r->id) . "</td>";
1189 echo "<td><a href='" . esc_attr( $link ) . "'><span class='icon-style' aria-hidden='true'>📝</span>" . esc_html( $r->pagelink ) . "</a><a class='doublesize-icon' href='" . esc_attr( $r->pagelink ) . "' target='_blank'>↗</a></td>";
1190 if($opts['rewrite_language_alias'] == 1) {
1191 echo $r->translated_alias ? "<td><a href='" . esc_attr( $r->translated_alias ) . "'>" . esc_html( $r->translated_alias ) . "</a><a class='doublesize-icon' href='" . esc_attr( $r->translated_alias ) . "' target='_blank'>↗</a></td>" : '<td>-</td>';
1192 }
1193 echo "<td>" . esc_html($short) . "</td>";
1194 echo "<td>" . wp_kses_post($flagOriginal) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($langOriginal))) . "</td>";
1195 echo "<td>" . wp_kses_post($flagTranslated) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper(esc_attr($r->languagetranslated)))) . "</td>";
1196 echo "<td>" . wp_kses_post($pub) . "</td>";
1197 echo "<td><span class='gpt-label'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper($engine) . '_ENGINE')) . "</span></td>";
1198 echo "<td>" . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime($local_date)) ) . "</td>";
1199 echo "<td><a href='" . esc_attr($link) . "'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EDIT')) . "</a> | <a href='" . esc_attr($deleteLink) . "' onclick=\"return confirm('" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETE_CONFIRM')) . "');\">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETE')) . "</a></td>";
1200 echo '</tr>';
1201 }
1202 echo '</tbody>';
1203 echo '</table>';
1204
1205 if ($total_pages > 1) {
1206 echo '<div class="tablenav"><div class="tablenav-pages">';
1207 for ($i = 1; $i <= $total_pages; $i++) {
1208 $url = add_query_arg(array_merge($_GET, ['paged' => $i]), admin_url('admin.php'));
1209 $class = ($i == $current_page) ? "class='current-page button'" : "class='button'";
1210 echo "<a " . wp_kses_post($class) . " href='" . esc_url($url) . "'>" . esc_html($i) . "</a> ";
1211 }
1212 echo '</div></div>';
1213 }
1214 }
1215
1216 echo '</div>';
1217 echo '</div>';
1218 }
1219
1220 /**
1221 * Load language file and translations
1222 * @return array
1223 */
1224 public function loadTranslations($key) {
1225 // Text translations
1226 static $adminLanguageStrings = null;
1227
1228 if(!$adminLanguageStrings) {
1229 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1230 if(file_exists($adminLanguageFile)) {
1231 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1232 }
1233 }
1234
1235 if(array_key_exists($key, $adminLanguageStrings)) {
1236 return $adminLanguageStrings[$key];
1237 }
1238
1239 return $key;
1240 }
1241
1242 /**
1243 * Load language file and translations
1244 * @return array
1245 */
1246 public static function loadTranslation($key) {
1247 // Text translations
1248 static $adminLanguageStrings = null;
1249
1250 if(!$adminLanguageStrings) {
1251 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1252 if(file_exists($adminLanguageFile)) {
1253 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1254 }
1255 }
1256
1257 if(array_key_exists($key, $adminLanguageStrings)) {
1258 return $adminLanguageStrings[$key];
1259 }
1260
1261 return $key;
1262 }
1263
1264 /**
1265 * Save translation record
1266 *
1267 * @access public
1268 */
1269 public function save_record() {
1270 global $wpdb;
1271
1272 // Retrieve and sanitize basic inputs
1273 $id = isset ( $_POST ['id'] ) ? intval ( $_POST ['id'] ) : 0;
1274 $formAction = isset ( $_POST ['action'] ) ? sanitize_key ( $_POST ['action'] ) : '';
1275
1276 if ( !isset($_POST['_gptranslate_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_gptranslate_nonce'])), 'gptranslate_save_record_action') ) {
1277 wp_die(esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'gptranslate');
1278 }
1279
1280 // Handle cancel action
1281 if ($formAction === 'cancel_gptranslate_record') {
1282 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1283 exit ();
1284 }
1285
1286 // Sanitize pagelink
1287 $pagelink = isset ( $_POST ['pagelink'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['pagelink'] ) ) : '';
1288
1289 // Sanitize translated_alias
1290 $translatedAlias = isset ( $_POST ['translated_alias'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['translated_alias'] ) ) : '';
1291
1292 // Process and sanitize translations JSON
1293 $raw_translations = filter_input( INPUT_POST, 'translations_json', FILTER_UNSAFE_RAW );
1294 $raw_translations = is_string($raw_translations) ? $raw_translations : '[]';
1295
1296 $decoded_translations = json_decode ( $raw_translations, true );
1297 if (! is_array ( $decoded_translations )) {
1298 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1299 'response' => 400
1300 ] );
1301 }
1302 $clean_translations = $decoded_translations;
1303 $sanitized_translations_json = wp_json_encode ( $clean_translations );
1304
1305 // Process and sanitize alternative translations JSON
1306 $raw_alt = filter_input( INPUT_POST, 'alt_translations_json', FILTER_UNSAFE_RAW );
1307 $raw_alt = is_string($raw_translations) ? $raw_alt : '[]';
1308
1309 $decoded_alt = json_decode ( $raw_alt, true );
1310 if (! is_array ( $decoded_alt )) {
1311 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_ALTTRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1312 'response' => 400
1313 ] );
1314 }
1315 $clean_alt = $decoded_alt;
1316 $sanitized_alt_json = wp_json_encode ( $clean_alt );
1317
1318 // Update database record
1319 $wpdb->update ( $this->table_name, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1320 'pagelink' => $pagelink,
1321 'translated_alias' => $translatedAlias,
1322 'translations' => $sanitized_translations_json,
1323 'alt_translations' => $sanitized_alt_json
1324 ], [
1325 'id' => $id
1326 ], [
1327 '%s',
1328 '%s',
1329 '%s'
1330 ], [
1331 '%d'
1332 ] );
1333
1334 // Redirect based on action
1335 if ($formAction === 'save_gptranslate_record_and_close') {
1336 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1337 } else {
1338 $url = admin_url( 'admin.php?page=gptranslate&action=edit&edit=' . $id );
1339 $url = wp_nonce_url( $url, 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1340 wp_redirect( html_entity_decode( $url ) );
1341 }
1342 exit ();
1343 }
1344
1345 public function gptranslate_handle_deletion() {
1346 // 1) Verify nonce
1347 $id = isset( $_GET['translation_id'] ) ? intval( $_GET['translation_id'] ) : 0;
1348 $nonce = isset( $_GET['_gptranslate_nonce'] ) ? wp_unslash( $_GET['_gptranslate_nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1349
1350 if ( ! wp_verify_nonce( $nonce, 'gptranslate_delete_' . $id ) ) {
1351 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
1352 }
1353
1354 // 2) Delete the row
1355 global $wpdb;
1356 $table = $wpdb->prefix . 'gptranslate';
1357 $deleted = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1358
1359 // 3) Redirect back with a message
1360 $redirect_url = add_query_arg(
1361 [
1362 'page' => 'gptranslate',
1363 'deleted' => $deleted ? '1' : '0',
1364 ],
1365 admin_url( 'admin.php' )
1366 );
1367 wp_redirect( $redirect_url );
1368 exit;
1369 }
1370
1371
1372 /**
1373 * Add main app frontend script
1374 *
1375 * @access public
1376 */
1377 public function enqueue_frontend_scripts() {
1378 add_filter('script_loader_tag', function($tag, $handle) {
1379 if ($handle === 'gptranslate-responsivevoice') {
1380 return str_replace('<script ', '<script defer ', $tag);
1381 }
1382
1383 if ($handle === 'gptranslate-jsonrepair') {
1384 return str_replace('<script ', '<script type="module" ', $tag);
1385 }
1386
1387 if ($handle === 'gptranslate-bstoast') {
1388 return str_replace('<script ', '<script type="module" ', $tag);
1389 }
1390 if ($handle === 'gptranslate-main') {
1391 // 1) prendi i valori raw
1392 $raw_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
1393 $raw_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
1394
1395 // 2) unslash WP
1396 $unslashed_uri = wp_unslash ( $raw_uri );
1397 $unslashed_host = wp_unslash ( $raw_host );
1398
1399 // 3) sanitizza URI come URL
1400 $orig_url = esc_url_raw ( $unslashed_uri );
1401 // - accetta path e query, rimuove caratteri pericolosi
1402
1403 // 4) sanitizza host
1404 // a) rimuovi tag e control chars
1405 $host_clean = sanitize_text_field ( $unslashed_host );
1406 // b) mantieni solo [a-z0-9.-] per sicurezza
1407 $orig_domain = preg_replace ( '/[^a-z0-9.-]/i', '', $host_clean );
1408
1409 // 5) infine escape per attributo HTML
1410 return str_replace ( '<script ', sprintf ( '<script type="module" data-gt-orig-url="%s" data-gt-orig-domain="%s" data-gt-widget-id="1" ', esc_attr ( $orig_url ), esc_attr ( $orig_domain ) ), $tag );
1411 }
1412
1413 return $tag;
1414 }, 10, 2);
1415
1416 $settings = get_option("gptranslate_options");
1417
1418 // Excluded languages check
1419 $excludedLangs = isset($settings['excluded_languages']) ? (array) $settings['excluded_languages'] : [];
1420 if (!empty($excludedLangs) && defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
1421 if (in_array(GPTRANSLATE_CURRENT_LANG, $excludedLangs, true)) {
1422 return;
1423 }
1424 }
1425
1426 // Move default language to the first one in the list
1427 if($settings ['default_language_first']) {
1428 if(!isset($settings ['languages'])) {
1429 $settings ['languages'] = array_map ( 'strtolower', [
1430 'AF',
1431 'SQ',
1432 'AM',
1433 'AR',
1434 'HY',
1435 'AZ',
1436 'EU',
1437 'BE',
1438 'BN',
1439 'BS',
1440 'BG',
1441 'CA',
1442 'CEB',
1443 'NY',
1444 'ZH',
1445 'CO',
1446 'HR',
1447 'CS',
1448 'DA',
1449 'NL',
1450 'EN',
1451 'EO',
1452 'ET',
1453 'TL',
1454 'FI',
1455 'FR',
1456 'FY',
1457 'GL',
1458 'KA',
1459 'DE',
1460 'EL',
1461 'GU',
1462 'HT',
1463 'HA',
1464 'HAW',
1465 'IW',
1466 'HI',
1467 'HMN',
1468 'HU',
1469 'IS',
1470 'IG',
1471 'ID',
1472 'GA',
1473 'IT',
1474 'JA',
1475 'JW',
1476 'KN',
1477 'KK',
1478 'KM',
1479 'KO',
1480 'KU',
1481 'KY',
1482 'LO',
1483 'LA',
1484 'LV',
1485 'LT',
1486 'LB',
1487 'MK',
1488 'MG',
1489 'MS',
1490 'ML',
1491 'MT',
1492 'MI',
1493 'MR',
1494 'MN',
1495 'MY',
1496 'NE',
1497 'NO',
1498 'PS',
1499 'FA',
1500 'PL',
1501 'PT',
1502 'PA',
1503 'RO',
1504 'RU',
1505 'SM',
1506 'GD',
1507 'SR',
1508 'ST',
1509 'SN',
1510 'SD',
1511 'SI',
1512 'SK',
1513 'SL',
1514 'SO',
1515 'ES',
1516 'SU',
1517 'SW',
1518 'SV',
1519 'TG',
1520 'TA',
1521 'TE',
1522 'TH',
1523 'TR',
1524 'UK',
1525 'UR',
1526 'UZ',
1527 'VI',
1528 'CY',
1529 'XH',
1530 'YI',
1531 'YO',
1532 'ZU',
1533 'ZT'
1534 ] );
1535 }
1536 $defaultLanguageKeyIndex = array_search($settings ['language'], $settings ['languages']);
1537 if ($defaultLanguageKeyIndex !== false) {
1538 // Remove the 'de' element from its current position
1539 $defaultLanguage = $settings ['languages'][$defaultLanguageKeyIndex];
1540 unset($settings ['languages'][$defaultLanguageKeyIndex]);
1541
1542 // Re-index the array to maintain numerical indexes
1543 $settings ['languages'] = array_values($settings ['languages']);
1544
1545 // Add 'de' to the beginning of the array
1546 array_unshift($settings ['languages'], $defaultLanguage);
1547 }
1548 }
1549
1550 // build alt_flags array
1551 $alt_flags = array ();
1552 $raw_alt_flags = isset($settings ['alt_flags']) ? $settings ['alt_flags'] : [];
1553 foreach ( $raw_alt_flags as $country ) {
1554 if ($country == 'usa' || $country == 'canada' || $country == 'ireland')
1555 $alt_flags ['en'] = $country;
1556 elseif ($country == 'brazil')
1557 $alt_flags ['pt'] = $country;
1558 elseif ($country == 'mexico' or $country == 'argentina' or $country == 'colombia')
1559 $alt_flags ['es'] = $country;
1560 elseif ($country == 'quebec')
1561 $alt_flags ['fr'] = $country;
1562 elseif ($country == 'taiwan')
1563 $alt_flags ['zh'] = $country;
1564 elseif ($country == 'hongkong')
1565 $alt_flags ['zt'] = $country;
1566 elseif ($country == 'austria')
1567 $alt_flags ['de'] = $country;
1568 }
1569
1570 // Build float position variables
1571 $float_position = $settings ['float_position'];
1572 if($float_position != 'inline'){
1573 list ( $switcher_vertical_position, $switcher_horizontal_position ) = explode ( '-', $float_position );
1574 } else {
1575 list ( $switcher_vertical_position, $switcher_horizontal_position ) = ['inline', 'inline'];
1576 }
1577
1578 // Set local flags path
1579 $flagsPath = trailingslashit(plugins_url('flags', __FILE__));
1580
1581 // Ensure a default array value
1582 if(!isset($settings['realtime_translations_retrigger_events'])) {
1583 $settings['realtime_translations_retrigger_events'] = ['click'];
1584 $settings['realtime_translations_retrigger_events_delay'] = 200;
1585 }
1586 if(!isset ( $settings ['realtime_translations_retrigger_force_google'] )) {
1587 $settings ['realtime_translations_retrigger_force_google'] = 0;
1588 }
1589 if(!isset ( $settings ['translate_srcimages'] )) {
1590 $settings ['translate_srcimages'] = 0;
1591 }
1592 if(!isset ( $settings ['css_selector_realtime_translations_retrigger'] )) {
1593 $settings ['css_selector_realtime_translations_retrigger'] = '';
1594 }
1595 if(!isset($settings ['serverside_translations_language_switching_mode'])) {
1596 $settings ['serverside_translations_language_switching_mode'] = 'url';
1597 }
1598 if(!isset($settings ['excluded_alias_slugs'])) {
1599 $settings ['excluded_alias_slugs'] = '';
1600 }
1601 if(!isset($settings ['popup_shadow'])) {
1602 $settings ['popup_shadow'] = 1;
1603 }
1604 if(!isset($settings ['wrap_excluded_words'])) {
1605 $settings ['wrap_excluded_words'] = 1;
1606 }
1607 if(!isset($settings ['transliterate_urls'])) {
1608 $settings ['transliterate_urls'] = 0;
1609 }
1610 if(!isset($settings ['rewrite_form_actions'])) {
1611 $settings ['rewrite_form_actions'] = 0;
1612 }
1613 if(!isset($settings ['translate_iframe_locale'])) {
1614 $settings ['translate_iframe_locale'] = 0;
1615 }
1616
1617 wp_register_script('gptranslate-main-inline', '', [], $this->version, true);
1618 wp_enqueue_script('gptranslate-main-inline');
1619
1620 // Example: $settings is an array like in Joomla
1621 $base64Encode = 'base' . 64 . '_encode';
1622 $key = $settings['chatgpt_apikey'];
1623 $key = strrev($key);
1624 $secret = 'gptranslate';
1625 $out = '';
1626 for ($i = 0; $i < strlen($key); $i++) {
1627 $out .= chr(ord($key[$i]) ^ ord($secret[$i % strlen($secret)]));
1628 }
1629 $encoded = $base64Encode($out);
1630
1631 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
1632
1633 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";';
1634 if (!empty($settings['lightweight_ajax_endpoint'])) {
1635 $lightweightEndpoint = esc_url_raw(plugin_dir_url(__FILE__) . 'ajax-handler.php');
1636 $inlineScript .= '
1637 var gptServerSideLightLink = "' . esc_js($lightweightEndpoint) . '";';
1638 }
1639 $inlineScript .= '
1640 var gptApiKey = "' . esc_js(hash( 'sha256', get_site_url() )) . '";
1641 var gptAjaxSecret = "' . esc_js(hash('sha256', 'gptranslate')) . '";
1642 var gptLiveSite = "' . esc_js(get_site_url()) . '";
1643 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
1644 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
1645 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
1646 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
1647 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
1648 var gptExcludedAliasSlugs = "' . esc_js(rtrim($settings['excluded_alias_slugs'], ', ')) . '";
1649 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
1650 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
1651 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
1652 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
1653 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
1654 var gptServersideTranslationsMatchquotes = ' . (int)$settings['serverside_translations_matchquotes'] . ';
1655 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
1656 var gptRewritePageLinksExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($settings['rewrite_page_links_exclusions']) ? $settings['rewrite_page_links_exclusions'] : ''))), ',')) . '";
1657 var gptRewriteFormActions = ' . (int)$settings['rewrite_form_actions'] . ';
1658 var gptTransliterateUrls = ' . (int)$settings ['transliterate_urls'] . ';
1659 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
1660 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
1661 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
1662 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
1663 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
1664 var gptTranslateIframeLocale = ' . (int)$settings['translate_iframe_locale'] . ';
1665 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
1666 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
1667 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
1668 var chatgptMetadataWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['metadata_words_leafnodes_excluded'], ', ')) . '";
1669 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
1670 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
1671 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
1672 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
1673 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
1674 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
1675 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
1676 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
1677 var chatgptApiKey = "' . esc_js($encoded) . '";
1678 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
1679 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
1680 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
1681 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
1682 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
1683 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
1684 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
1685 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
1686 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
1687 var chatgptWordsLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['words_leafnodes_excluded'])), ', ')) . '";
1688 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
1689 var chatgptFlattenInnerFormattingTags = ' . (int)($settings['flatten_inner_formatting_tags'] ?? 0) . ';
1690 var chatgptFlattenInnerFormattingTagsToRemove = "' . esc_js(str_ireplace('"', '', trim(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['flatten_inner_formatting_tags_to_remove'] ?? 'strong,em,u,b,i'))), ',')))) . '";
1691 var chatgptWrapExcludedWords = ' . (int)$settings['wrap_excluded_words'] . ';
1692 var gptApplyDictionaryToAliases = ' . (int)($settings['apply_dictionary_to_aliases'] ?? 0) . ';
1693 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
1694 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
1695 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
1696 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
1697 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
1698 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
1699 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
1700 var gTranslateMethod = ' . (int)($settings['google_translate_method'] ?? 0) . ';
1701 var gptRealtimeTranslationsRetriggerForceGoogle = ' . (int)$settings['realtime_translations_retrigger_force_google'] . ';
1702 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
1703 var gptPopupShadow = ' . (int)$settings ['popup_shadow'] . ';
1704 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
1705 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
1706 var gptVersionNumeric = ' . 0 . ';
1707 var svgIconArrow = \'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 285 285"><path d="M282 76.5l-14.2-14.3a9 9 0 0 0-13.1 0L142.5 174.4 30.3 62.2a9 9 0 0 0-13.2 0L3 76.5a9 9 0 0 0 0 13.1l133 133a9 9 0 0 0 13.1 0l133-133a9 9 0 0 0 0-13z" style="fill:%23' . ltrim($settings['widget_text_color'], '#') . '"/></svg>\';';
1708
1709 // Inject it AFTER gptranslate-main-inline
1710 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
1711
1712 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
1713 wp_enqueue_script('gptranslate-js-specs');
1714 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
1715 window.gptranslateSettings["1"] = {
1716 "default_language": "' . $settings['language'] . '",
1717 "languages": ' . json_encode($settings['languages']) . ',
1718 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
1719 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
1720 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
1721 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
1722 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
1723 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
1724 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
1725 "widget_text_color": "' . $settings['widget_text_color'] . '",
1726 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
1727 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
1728 "enable_modal": ' . (int)($settings['enable_modal'] ?? 0) . ',
1729 "equal_widths": ' . (int)$settings['equal_widths'] . ',
1730 "reader_button_position": "' . $settings['reader_button_position'] . '",
1731 "custom_css": "' . addslashes(preg_replace('/\s+/', ' ', str_replace(["\r", "\n"], ' ', (string) $settings['custom_css']))) . '",
1732 "alt_flags": ' . json_encode($alt_flags). ',
1733 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
1734 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
1735 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
1736 "switcher_vertical_position": "' . $switcher_vertical_position . '",
1737 "flags_location": "' . esc_js($flagsPath) . '",
1738 "flag_loading": "' . $settings['flag_loading'] . '",
1739 "flag_style": "' . $settings['flag_style'] . '",
1740 "widget_max_height": ' . (int)$settings['widget_max_height'] . '
1741 };');
1742
1743 $languageStringsScript = '';
1744
1745 // Generic translations
1746 $labels = [
1747 'TRANSLATING',
1748 'TRANSLATING_WAIT',
1749 'TRANSLATING_COMPLETE',
1750 'READING_INPROGRESS',
1751 'READING_END',
1752 'READING_EMPTY',
1753 'CHOOSE_LANGUAGE'
1754 ];
1755
1756 foreach ($labels as $label) {
1757 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
1758 }
1759
1760 $languages = [
1761 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
1762 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
1763 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
1764 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KM', 'KO', 'KU', 'KY', 'LO',
1765 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
1766 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
1767 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
1768 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
1769 ];
1770
1771
1772 foreach ($languages as $lang) {
1773 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
1774 }
1775
1776 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
1777 wp_enqueue_script('gptranslate-js-language-strings');
1778 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
1779
1780
1781 // Dictionary
1782 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
1783 if ($words_leafnodes_excluded_bylanguage_repeatable) {
1784 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
1785 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
1786 }
1787
1788 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
1789 $formatted = [];
1790 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
1791 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
1792 'words_leafnodes_excluded_bylanguage' => $row['word'] ?? '',
1793 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
1794 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
1795 'words_leafnodes_excluded_bylanguage_translation' => $row['optionalTranslation'] ?? ''
1796 ];
1797 }
1798
1799 // Correctly formatted JSON encode
1800 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1801
1802 // Inietto i dati PRIMA che venga eseguito gptranslate.js
1803 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
1804 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
1805 wp_add_inline_script(
1806 'gptranslate-js-word-leafones-excluded-language',
1807 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
1808 );
1809 }
1810
1811 // Local or remote script
1812 if($settings['proxy_responsive_loading_script'] == 1) {
1813 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
1814 } else {
1815 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
1816 }
1817
1818 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
1819 wp_enqueue_script('gptranslate-main', plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js', [], $this->version, true);
1820
1821 // Enqueue Bootstrap component
1822 if(!$settings['disable_bootstrap_css']) {
1823 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
1824 wp_enqueue_style(
1825 'bootstrap-css',
1826 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
1827 [],
1828 '5.3.2'
1829 );
1830 } else {
1831 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
1832 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
1833 wp_enqueue_style('gptranslate-bootstrap-style');
1834 wp_add_inline_style('gptranslate-bootstrap-style', '
1835 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
1836 .progress-gptranslate .toast,.progress-gptranslate-reading .toast{width:350px;max-width:100%;font-size:0.875rem;pointer-events:auto;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15);border-radius:0.25rem}
1837 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
1838 .progress-gptranslate .toast-header,.progress-gptranslate-reading .toast-header{display:flex;align-items:center;padding:0.5rem 0.75rem;color:#6c757d;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05);border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}
1839 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
1840 .progress-gptranslate .progress-bar,.progress-gptranslate-reading .progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width 0.6s ease}
1841 .progress-gptranslate .progress-bar-striped,.progress-gptranslate-reading .progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}
1842 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
1843 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
1844 .progress-gptranslate .btn-close,.progress-gptranslate-reading .btn-close{box-sizing:content-box;width:1em;height:1em;padding:0.25em 0.25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\' fill=\'%23000\'%3e%3cpath d=\'M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z\'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:0.25rem;opacity:0.5;cursor:pointer}
1845 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
1846 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
1847 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
1848 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
1849 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
1850 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
1851 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
1852 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
1853 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
1854 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
1855 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
1856 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
1857 ');
1858
1859 // Closer for the toast element
1860 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
1861 wp_enqueue_script('gptranslate-toast-dismiss');
1862 wp_add_inline_script('gptranslate-toast-dismiss', '
1863 document.addEventListener("DOMContentLoaded", function() {
1864 document.addEventListener("click", function(e) {
1865 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
1866 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
1867 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
1868 const toast = btnClose.closest(".toast");
1869 if (toast) {
1870 toast.classList.remove("show");
1871 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
1872 if (progressContainer) {
1873 progressContainer.remove();
1874 }
1875 }
1876 }
1877 });
1878 });
1879 ');
1880 }
1881
1882 // Registra un handle CSS vuoto se necessario
1883 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
1884 wp_enqueue_style('gptranslate-dynamic-css');
1885
1886 // Prepara lo stile dinamico
1887 $dynamic_css = 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . (!empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF') . '; }' .
1888 'div.gpt_float_switcher, div.gpt_float_switcher div.gt-selected div.gpt-current-lang, div.gpt_float_switcher div.gpt_options a { color: ' . (!empty($settings['widget_text_color']) ? esc_attr($settings['widget_text_color']) : '#000000') . '; font-size: ' . intval($settings['popup_fontsize']) . 'px; }' .
1889 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
1890 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
1891
1892 if (!empty($settings['disable_toast_popups']) && $settings['disable_toast_popups'] == 1) {
1893 $dynamic_css .= '.progress.progress-gptranslate,.progress.progress-gptranslate-reading{ display: none !important; }';
1894 }
1895
1896 // Opacity del background widget (solo se diverso da 1.0)
1897 if (!empty($settings['widget_opacity']) && floatval($settings['widget_opacity']) != 1.0) {
1898 $bgColor = !empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF';
1899 $opacity = floatval($settings['widget_opacity']);
1900 $alphaHex = str_pad(dechex(round($opacity * 255)), 2, '0', STR_PAD_LEFT);
1901 $bgColorWithAlpha = $bgColor . strtoupper($alphaHex);
1902
1903 $dynamic_css .= 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . $bgColorWithAlpha . ' !important; }';
1904 }
1905
1906 // Inietta il CSS inline
1907 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
1908
1909 // --- Load theme RTL stylesheet if available ---
1910 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
1911 // Common file names
1912 $rtl_candidates = array(
1913 get_stylesheet_directory() . '/style-rtl.css',
1914 get_stylesheet_directory() . '/rtl.css'
1915 );
1916
1917 $rtl_file = '';
1918 foreach ( $rtl_candidates as $candidate ) {
1919 if ( file_exists( $candidate ) ) {
1920 $rtl_file = $candidate;
1921 break;
1922 }
1923 }
1924
1925 if ( $rtl_file ) {
1926 $rtl_uri = str_replace(
1927 get_stylesheet_directory(),
1928 get_stylesheet_directory_uri(),
1929 $rtl_file
1930 );
1931
1932 wp_enqueue_style(
1933 'theme-rtl',
1934 $rtl_uri,
1935 array(),
1936 filemtime( $rtl_file )
1937 );
1938 }
1939 }
1940 }
1941 }
1942
1943 // Force WordPress.org update check on plugin activation
1944 register_activation_hook( __FILE__, function() {
1945 if ( function_exists('wp_update_plugins') ) {
1946 wp_update_plugins();
1947 }
1948 });
1949
1950 // Schedule a daily update check
1951 add_action( 'gptranslate_daily_update_check', function() {
1952 if ( function_exists('wp_update_plugins') ) {
1953 wp_update_plugins();
1954 }
1955 });
1956
1957 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
1958 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
1959 }
1960
1961 // 🧹 Cleanup scheduled event on deactivation
1962 register_deactivation_hook( __FILE__, function() {
1963 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
1964 });
1965
1966 /**
1967 * Global function to add links to WP
1968 *
1969 * @access public
1970 */
1971 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) {
1972 $settings_link = '<a href="admin.php?page=gptranslate">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
1973 array_unshift($links, $settings_link);
1974 return $links;
1975 });
1976
1977 /**
1978 * Add main admin scripts for example to manage records add/delete functions
1979 *
1980 * @access public
1981 */
1982 add_action('admin_enqueue_scripts', function() {
1983 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1984 // Enqueue JS
1985 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
1986 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
1987
1988 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1989 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
1990 }
1991
1992 // Enqueue CSS
1993 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
1994 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
1995
1996 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1997 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
1998 }
1999
2000 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
2001 'ajaxurl' => admin_url('admin-ajax.php'),
2002 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
2003 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
2004 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2005 'testApikeyNonce' => wp_create_nonce('gptranslate_test_apikey'),
2006 'gptApiKey' => hash( 'sha256', get_site_url() ),
2007 'i18n_test_apikey' => __('Test API Key', 'gptranslate'),
2008 'i18n_test_apikey_testing' => __('Testing...', 'gptranslate'),
2009 'i18n_test_apikey_success' => __('API Key Valid', 'gptranslate'),
2010 'i18n_test_apikey_error' => __('API Key Error', 'gptranslate'),
2011 'i18n_test_apikey_empty' => __('Please enter an API Key first', 'gptranslate'),
2012 'i18n_apikey_required' => __('API Key is required.', 'gptranslate')
2013 ]);
2014 }
2015 });
2016
2017 // ============================================================================
2018 // AJAX handler for toggling server-side translations during crawler execution
2019 // This prevents conflicts between crawler and server-side translation system
2020 // ============================================================================
2021 add_action('wp_ajax_gptranslate_toggle_serverside', 'gptranslate_toggle_serverside_handler');
2022
2023 /**
2024 * AJAX handler to disable/restore server-side translations during crawler
2025 *
2026 * Actions:
2027 * - 'check': Check current state and disable if enabled
2028 * - 'restore': Restore previous state if it was enabled
2029 *
2030 * This ensures crawler operates without interference from server-side translations
2031 * and automatically restores the original state when crawler stops.
2032 */
2033 function gptranslate_toggle_serverside_handler() {
2034 // Verify nonce for security
2035 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'gptranslate_crawler_nonce')) {
2036 wp_send_json_error(array('message' => 'Invalid security token'));
2037 return;
2038 }
2039
2040 // Verify admin permissions
2041 if (!current_user_can('manage_options')) {
2042 wp_send_json_error(array('message' => 'Unauthorized access'));
2043 return;
2044 }
2045
2046 // Get the action: 'check' or 'restore'
2047 $toggle_action = isset($_POST['toggle_action']) ? sanitize_text_field($_POST['toggle_action']) : '';
2048
2049 if ($toggle_action !== 'check' && $toggle_action !== 'restore') {
2050 wp_send_json_error(array('message' => 'Invalid action parameter'));
2051 return;
2052 }
2053
2054 // Get plugin options from database
2055 // NOTE: Verify this option name matches your actual plugin option name
2056 $option_name = 'gptranslate_options';
2057 $options = get_option($option_name, array());
2058
2059 if (!is_array($options)) {
2060 $options = array();
2061 }
2062
2063 try {
2064 if ($toggle_action === 'check') {
2065 // ACTION CHECK: Check if server-side translations are enabled and disable if necessary
2066
2067 // Get current value
2068 $current_value = isset($options['serverside_translations']) ? $options['serverside_translations'] : '0';
2069
2070 if ($current_value === '1') {
2071 // Server-side translations are enabled, disable them temporarily
2072 $options['serverside_translations'] = '0';
2073
2074 // Update options in database
2075 update_option($option_name, $options);
2076
2077 $message = 'Server-side translations were enabled and have been disabled for crawler';
2078 $action_taken = 'disabled';
2079 } else {
2080 // Already disabled, no action needed
2081 $message = 'Server-side translations were already disabled, no action needed';
2082 $action_taken = 'none';
2083 }
2084
2085 // Return response with original state
2086 wp_send_json_success(array(
2087 'message' => $message,
2088 'were_enabled' => $current_value,
2089 'action_taken' => $action_taken
2090 ));
2091
2092 } else if ($toggle_action === 'restore') {
2093 // ACTION RESTORE: Re-enable only if client tells us they were enabled before
2094
2095 $were_enabled = isset($_POST['were_enabled']) ? sanitize_text_field($_POST['were_enabled']) : '0';
2096
2097 if ($were_enabled === '1') {
2098 // They were enabled before crawler, restore them
2099 $options['serverside_translations'] = '1';
2100
2101 // Update options in database
2102 update_option($option_name, $options);
2103
2104 $message = 'Server-side translations have been restored to enabled';
2105 $action_taken = 'restored';
2106 } else {
2107 // They were not enabled, no action needed
2108 $message = 'Server-side translations were not enabled before, no action needed';
2109 $action_taken = 'none';
2110 }
2111
2112 // Return response
2113 wp_send_json_success(array(
2114 'message' => $message,
2115 'action_taken' => $action_taken
2116 ));
2117 }
2118
2119 } catch (Exception $e) {
2120 wp_send_json_error(array('message' => 'Error: ' . $e->getMessage()));
2121 }
2122 }
2123
2124 // ============================================================================
2125 // AJAX handler for getting already-translated URLs (crawler fast skip)
2126 // ============================================================================
2127 add_action('wp_ajax_gptranslate_get_translated_urls', function () {
2128 check_ajax_referer('gptranslate_crawler_nonce', 'nonce');
2129 if (!current_user_can('manage_options')) {
2130 wp_send_json_error(array('message' => 'Unauthorized'));
2131 return;
2132 }
2133 global $wpdb;
2134 $table = $wpdb->prefix . 'gptranslate';
2135
2136 // Get enabled target languages (excluding the default/source language)
2137 $opts = get_option('gptranslate_options', array());
2138 $defaultLanguage = strtolower($opts['language'] ?? 'en');
2139 $enabledLanguages = array_map('strtolower', $opts['languages'] ?? array());
2140 $targetLanguages = array_values(array_filter($enabledLanguages, function($lang) use ($defaultLanguage) {
2141 return $lang !== $defaultLanguage;
2142 }));
2143 $numTargetLanguages = count($targetLanguages);
2144
2145 if ($numTargetLanguages === 0) {
2146 wp_send_json_success(array('urls' => array()));
2147 return;
2148 }
2149
2150 // Fetch all published rows: pagelink + languagetranslated
2151 // pagelink already contains the language prefix (e.g. http://site/fr/page/)
2152 // so we normalize it in PHP to group by the original URL
2153 $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2154 "SELECT pagelink, languagetranslated FROM {$table} WHERE published = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2155 ARRAY_A
2156 );
2157
2158 // Helper: strip language prefix from URL path
2159 $stripLangPrefix = function($url) {
2160 $cleaned = rtrim($url, '/');
2161 $parsedUrl = parse_url($cleaned);
2162 if ($parsedUrl === false) {
2163 return $cleaned;
2164 }
2165 $pathname = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
2166 if (preg_match('#^/([a-z]{2}(-[a-z]{2})?)(?=/|$)#i', $pathname)) {
2167 $pathname = preg_replace('#^/[a-z]{2}(-[a-z]{2})?#i', '', $pathname);
2168 if (empty($pathname)) {
2169 $pathname = '/';
2170 }
2171 }
2172 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
2173 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
2174 $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
2175 return $scheme . $host . $port . $pathname;
2176 };
2177
2178 // Group by normalized URL → collect translated languages
2179 $urlLangMap = array();
2180 foreach ($rows as $row) {
2181 $normalizedUrl = $stripLangPrefix($row['pagelink']);
2182 $lang = strtolower($row['languagetranslated']);
2183 if (!isset($urlLangMap[$normalizedUrl])) {
2184 $urlLangMap[$normalizedUrl] = array();
2185 }
2186 if (!in_array($lang, $urlLangMap[$normalizedUrl])) {
2187 $urlLangMap[$normalizedUrl][] = $lang;
2188 }
2189 }
2190
2191 // Keep only URLs where ALL target languages are present
2192 $fullyTranslatedUrls = array();
2193 foreach ($urlLangMap as $normalizedUrl => $translatedLangs) {
2194 $missing = array_diff($targetLanguages, $translatedLangs);
2195 if (empty($missing)) {
2196 $fullyTranslatedUrls[] = $normalizedUrl;
2197 }
2198 }
2199
2200 wp_send_json_success(array('urls' => $fullyTranslatedUrls));
2201 });
2202
2203 // ============================================================================
2204 // AJAX handler for testing API key validity
2205 // ============================================================================
2206 add_action('wp_ajax_gptranslate_test_apikey', function () {
2207 // Verify nonce
2208 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gptranslate_test_apikey')) {
2209 wp_send_json_error(array('message' => 'Invalid security token'));
2210 return;
2211 }
2212
2213 // Verify admin permissions
2214 if (!current_user_can('manage_options')) {
2215 wp_send_json_error(array('message' => 'Unauthorized access'));
2216 return;
2217 }
2218
2219 $apiKey = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : '';
2220 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2221
2222 if (empty($apiKey)) {
2223 wp_send_json_error(array('message' => 'API key is empty'));
2224 return;
2225 }
2226
2227 $url = '';
2228 $headers = array('Content-Type' => 'application/json');
2229 $body = '';
2230
2231 try {
2232 if (strpos($model, 'gpt-') === 0) {
2233 // OpenAI / ChatGPT
2234 $url = 'https://api.openai.com/v1/chat/completions';
2235 $headers['Authorization'] = 'Bearer ' . $apiKey;
2236 $useNewTokenParam = (strpos($model, 'gpt-4.1') === 0 || strpos($model, 'gpt-5') === 0);
2237 $tokenParam = $useNewTokenParam ? 'max_completion_tokens' : 'max_tokens';
2238 $body = wp_json_encode(array(
2239 'model' => $model,
2240 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2241 $tokenParam => 5
2242 ));
2243 } elseif (strpos($model, 'deepseek-') === 0) {
2244 // DeepSeek
2245 $url = 'https://api.deepseek.com/v1/chat/completions';
2246 $headers['Authorization'] = 'Bearer ' . $apiKey;
2247 $body = wp_json_encode(array(
2248 'model' => $model,
2249 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2250 'max_tokens' => 5
2251 ));
2252 } elseif (strpos($model, 'gemini-') === 0) {
2253 // Google Gemini
2254 $apiVersion = (strpos($model, '-preview') !== false) ? 'v1beta' : 'v1';
2255 $url = "https://generativelanguage.googleapis.com/{$apiVersion}/models/{$model}:generateContent";
2256 $headers['x-goog-api-key'] = $apiKey;
2257 $body = wp_json_encode(array(
2258 'contents' => array(array('parts' => array(array('text' => 'Hi')))),
2259 'generationConfig' => array('maxOutputTokens' => 5)
2260 ));
2261 } elseif (strpos($model, 'claude-') === 0) {
2262 // Claude / Anthropic
2263 $url = 'https://api.anthropic.com/v1/messages';
2264 $headers['x-api-key'] = $apiKey;
2265 $headers['anthropic-version'] = '2023-06-01';
2266 unset($headers['Authorization']);
2267 $body = wp_json_encode(array(
2268 'model' => $model,
2269 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2270 'max_tokens' => 5
2271 ));
2272 } elseif ($model === 'google-cloud-translation-api') {
2273 // Google Cloud Translation
2274 $url = 'https://translation.googleapis.com/language/translate/v2?key=' . urlencode($apiKey);
2275 $body = wp_json_encode(array(
2276 'q' => array('hello'),
2277 'source' => 'en',
2278 'target' => 'es',
2279 'format' => 'text'
2280 ));
2281 } elseif ($model === 'deepl-api') {
2282 // DeepL - Auto-detect endpoint based on API key type
2283 // Free API keys end with ':fx', paid keys don't have this suffix
2284 $deeplEndpoint = (strpos ( $apiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
2285 $url = $deeplEndpoint . '/v2/translate';
2286 $headers ['Authorization'] = 'DeepL-Auth-Key ' . $apiKey;
2287 $headers ['Content-Type'] = 'application/x-www-form-urlencoded';
2288 // DeepL requires repeated "text" params (not text[0])
2289 $body = 'text=' . urlencode ( 'hello' ) . '&source_lang=EN&target_lang=ES';
2290 } else {
2291 wp_send_json_error(array('message' => 'Unsupported model: ' . $model));
2292 return;
2293 }
2294
2295 $response = wp_remote_post($url, array(
2296 'headers' => $headers,
2297 'body' => $body,
2298 'timeout' => 60
2299 ));
2300
2301 if (is_wp_error($response)) {
2302 wp_send_json_error(array(
2303 'message' => $response->get_error_message(),
2304 'error_code' => 0,
2305 'http_code' => 0
2306 ));
2307 return;
2308 }
2309
2310 $httpCode = wp_remote_retrieve_response_code($response);
2311 $responseBody = wp_remote_retrieve_body($response);
2312
2313 if ($httpCode >= 200 && $httpCode < 300) {
2314 wp_send_json_success(array(
2315 'result' => true,
2316 'http_code' => $httpCode
2317 ));
2318 } else {
2319 $errorMessage = 'HTTP ' . $httpCode;
2320 $decoded = json_decode($responseBody, true);
2321 if ($decoded) {
2322 if (isset($decoded['error']['message'])) {
2323 $errorMessage = $decoded['error']['message'];
2324 } elseif (isset($decoded['error']['status'])) {
2325 $errorMessage = $decoded['error']['status'];
2326 }
2327 // Claude/Anthropic error format: {type: "error", error: {type, message}}
2328 if (isset($decoded['type']) && $decoded['type'] === 'error' && isset($decoded['error']['message'])) {
2329 $errorMessage = $decoded['error']['message'];
2330 }
2331 }
2332 wp_send_json_error(array(
2333 'message' => $errorMessage,
2334 'error_code' => $httpCode,
2335 'http_code' => $httpCode
2336 ));
2337 }
2338 } catch (Exception $e) {
2339 wp_send_json_error(array(
2340 'message' => $e->getMessage(),
2341 'error_code' => 0,
2342 'http_code' => 0
2343 ));
2344 }
2345 });
2346
2347 add_action('wp_ajax_gptranslate_bulk_delete', function () {
2348 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
2349 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2350 }
2351
2352 global $wpdb;
2353 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
2354
2355 if (empty($ids)) {
2356 wp_send_json_error('No records selected');
2357 }
2358
2359 $table = $wpdb->prefix . 'gptranslate';
2360 $in = implode(',', array_fill(0, count($ids), '%d'));
2361 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
2362 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
2363
2364 if ($result === false) {
2365 wp_send_json_error('Database error');
2366 }
2367
2368 wp_send_json_success();
2369 });
2370
2371 // Handle Export CSV
2372 add_action('admin_post_gptranslate_export_translations_csv', function () {
2373 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
2374 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2375 }
2376
2377 global $wpdb;
2378 $table = $wpdb->prefix . 'gptranslate';
2379
2380 $records = $wpdb->get_results("SELECT * FROM $table ORDER BY translate_date DESC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2381
2382 if (!$records) {
2383 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
2384 }
2385
2386 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
2387 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
2388
2389 header('Content-Type: text/csv');
2390 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
2391
2392 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
2393 $output = fopen('php://output', 'w');
2394
2395 // Intestazioni CSV
2396 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
2397
2398 foreach ($records as $record) {
2399 fputcsv($output, $record, ",", '"', "\\");
2400 }
2401
2402 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
2403 fclose($output);
2404 exit;
2405 });
2406
2407 // Handle Import CSV
2408 add_action('admin_post_gptranslate_import_translations_csv', function () {
2409 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
2410 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2411 }
2412
2413 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
2414 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
2415 }
2416
2417 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2418 if (!is_uploaded_file($tmp_name)) {
2419 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
2420 }
2421
2422 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
2423 $file = fopen($tmp_name, 'r');
2424 if (!$file) {
2425 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
2426 }
2427
2428 $headers = fgetcsv($file);
2429 if (!$headers) {
2430 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
2431 }
2432
2433 global $wpdb;
2434 $table = $wpdb->prefix . 'gptranslate';
2435
2436 while (($row = fgetcsv($file)) !== false) {
2437 $countHeaders = count($headers);
2438 $countRow = count($row);
2439 // Invalid combine
2440 if($countHeaders != $countRow) {
2441 continue;
2442 }
2443 $record = array_combine($headers, $row);
2444 if (empty($record['pagelink'])) {
2445 continue; // skip if no primary key
2446 }
2447
2448 $pagelink = sanitize_text_field($record['pagelink']);
2449 $exists = $wpdb->get_var($wpdb->prepare(
2450 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
2451 $pagelink, $record['languageoriginal'], $record['languagetranslated']
2452 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2453
2454 $data = [
2455 'translated_alias' => $record['translated_alias'],
2456 'translations' => $record['translations'],
2457 'alt_translations' => $record['alt_translations'],
2458 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
2459 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
2460 'published' => isset($record['published']) ? (int)$record['published'] : 1,
2461 'translate_date' => $record['translate_date'],
2462 'translation_engine' => sanitize_text_field($record['translation_engine']),
2463 ];
2464
2465 if ($exists) {
2466 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2467 $table,
2468 $data,
2469 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
2470 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
2471 ['%s','%s','%s']
2472 );
2473 } else {
2474 $data['pagelink'] = $pagelink;
2475 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2476 $table,
2477 $data,
2478 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
2479 );
2480 }
2481 }
2482
2483 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
2484 fclose($file);
2485
2486 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
2487 exit;
2488 });
2489
2490 // Handle Export XLIFF
2491 add_action('admin_post_gptranslate_export_translations_xliff', function () {
2492 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
2493 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2494 }
2495
2496 global $wpdb;
2497 $table = $wpdb->prefix . 'gptranslate';
2498
2499 $records = $wpdb->get_results("SELECT * FROM $table ORDER BY translate_date DESC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2500
2501 if (!$records) {
2502 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
2503 }
2504
2505 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
2506 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
2507
2508 header('Content-Type: application/xml; charset=utf-8');
2509 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
2510
2511 $xml = new SimpleXMLElement('<xliff/>');
2512 $xml->addAttribute('version', '1.2');
2513
2514 foreach ($records as $record) {
2515 $file = $xml->addChild('file');
2516 $file->addAttribute('source-language', $record['languageoriginal']);
2517 $file->addAttribute('target-language', $record['languagetranslated']);
2518 $file->addAttribute('datatype', 'html');
2519 $file->addAttribute('original', $record['pagelink']);
2520
2521 $body = $file->addChild('body');
2522
2523 $translations = json_decode($record['translations'], true) ?: [];
2524 foreach ($translations as $source => $target) {
2525 $unit = $body->addChild('trans-unit');
2526 $unit->addAttribute('id', md5($source));
2527 $unit->addChild('source', htmlspecialchars($source));
2528 $unit->addChild('target', htmlspecialchars($target));
2529 }
2530 }
2531
2532 echo $xml->asXML();
2533 exit;
2534 });
2535
2536 // Handle Import XLIFF
2537 add_action('admin_post_gptranslate_import_translations_xliff', function () {
2538 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
2539 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2540 }
2541
2542 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
2543 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
2544 }
2545
2546 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2547 if (!is_uploaded_file($tmp_name)) {
2548 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
2549 }
2550
2551 $xml = simplexml_load_file($tmp_name);
2552 if (!$xml) {
2553 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
2554 }
2555
2556 global $wpdb;
2557 $table = $wpdb->prefix . 'gptranslate';
2558
2559 foreach ($xml->file as $file) {
2560 $sourceLang = (string)$file['source-language'];
2561 $targetLang = (string)$file['target-language'];
2562 $pagelink = (string)$file['original'];
2563
2564 $translations = [];
2565 foreach ($file->body->{'trans-unit'} as $unit) {
2566 $src = (string)$unit->source;
2567 $tgt = (string)$unit->target;
2568 $translations[$src] = $tgt;
2569 }
2570
2571 $json_translations = wp_json_encode($translations);
2572
2573 $exists = $wpdb->get_var($wpdb->prepare(
2574 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
2575 $pagelink, $sourceLang, $targetLang
2576 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2577
2578 if ($exists) {
2579 // �
2580 Update only the translations and date, keep everything else intact
2581 $wpdb->update(
2582 $table,
2583 [
2584 'translations' => $json_translations,
2585 'translate_date' => current_time('mysql'),
2586 ],
2587 ['id' => $exists]
2588 ); // phpcs:ignore
2589 } else {
2590 // �
2591 Insert full record only if it doesn't exist
2592 $data = [
2593 'pagelink' => $pagelink,
2594 'translated_alias' => '',
2595 'translations' => $json_translations,
2596 'alt_translations' => '[]',
2597 'languageoriginal' => $sourceLang,
2598 'languagetranslated'=> $targetLang,
2599 'published' => 1,
2600 'translate_date' => current_time('mysql'),
2601 'translation_engine'=> 'chatgpt',
2602 ];
2603 $wpdb->insert($table, $data); // phpcs:ignore
2604 }
2605 }
2606
2607 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
2608 exit;
2609 });
2610
2611 // Handle Export XML Sitemap
2612 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
2613 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
2614 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2615 }
2616
2617 global $wpdb;
2618 $table = $wpdb->prefix . 'gptranslate';
2619
2620 // Get language filter from POST parameter
2621 $language_filter = isset($_POST['sitemap_language']) ? sanitize_text_field(wp_unslash($_POST['sitemap_language'])) : '';
2622
2623 // Solo record con translated_alias non vuoto
2624 $query = "
2625 SELECT *
2626 FROM $table
2627 WHERE translated_alias IS NOT NULL
2628 AND translated_alias != ''";
2629
2630 // Add language filter if specified and not "all"
2631 if (!empty($language_filter) && $language_filter !== 'all') {
2632 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
2633 }
2634
2635 $query .= " ORDER BY translate_date DESC";
2636
2637 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2638
2639 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
2640 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
2641
2642 // Add language suffix to filename if specific language is selected
2643 if (!empty($language_filter) && $language_filter !== 'all') {
2644 $fileDate .= '-' . sanitize_file_name($language_filter);
2645 }
2646
2647 $dom = new DOMDocument('1.0', 'UTF-8');
2648 $dom->preserveWhiteSpace = false;
2649 $dom->formatOutput = true;
2650
2651 $urlset = $dom->createElement('urlset');
2652 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
2653 $dom->appendChild($urlset);
2654
2655 if (empty($records)) {
2656 $urlset->appendChild($dom->createTextNode(''));
2657 }
2658
2659 foreach ($records as $record) {
2660 $url = $dom->createElement('url');
2661 $loc = $dom->createElement('loc');
2662 $loc->appendChild($dom->createTextNode($record['translated_alias']));
2663 $url->appendChild($loc);
2664
2665 if (!empty($record['translate_date'])) {
2666 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
2667 $url->appendChild($lastmod);
2668 }
2669
2670 $urlset->appendChild($url);
2671 }
2672
2673 header('Content-Type: application/xml; charset=UTF-8');
2674 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
2675 echo $dom->saveXML();
2676 exit;
2677 });
2678
2679 // Register Ajax handler
2680 add_action('wp_ajax_gptranslate_migrate_translations', function() {
2681 if (!current_user_can('manage_options')) {
2682 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2683 }
2684
2685 check_ajax_referer('gptranslate_migrate_translations');
2686
2687 global $wpdb;
2688 $table = $wpdb->prefix . 'gptranslate';
2689 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
2690 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
2691
2692 if (empty($old) || empty($new)) {
2693 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
2694 }
2695
2696 $query = $wpdb->prepare(
2697 "UPDATE $table SET pagelink = REPLACE(pagelink, %s, %s), translated_alias = REPLACE(translated_alias, %s, %s)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2698 $old, $new, $old, $new
2699 );
2700
2701 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
2702 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2703
2704 if ($result === false) {
2705 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
2706 } else {
2707 wp_send_json_success();
2708 }
2709 });
2710
2711 function gptranslate_export_settings() {
2712 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
2713 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
2714 }
2715
2716 $options = get_option( 'gptranslate_options', [] );
2717
2718 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
2719 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
2720
2721 header( 'Content-Type: application/json' );
2722 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
2723 echo wp_json_encode( $options );
2724 exit;
2725 }
2726 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
2727
2728 function gptranslate_import_settings() {
2729 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
2730 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
2731 }
2732
2733 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
2734 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
2735 }
2736
2737 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2738 $decoded = json_decode( $content, true );
2739
2740 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
2741 wp_die( esc_html(GPTranslate::loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
2742 }
2743
2744 // Optional: sanitize known values, or just update if you're confident of source
2745 update_option( 'gptranslate_options', $decoded );
2746
2747 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
2748 exit;
2749 }
2750 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
2751
2752 // ============================================================================
2753 // Shortcode [gptranslate] - Renders the language switcher at the shortcode position
2754 // ============================================================================
2755 add_shortcode('gptranslate', function ($atts) {
2756 $settings = get_option("gptranslate_options");
2757
2758 // Disable interface
2759 if (!empty($settings['disable_control'])) {
2760 return '';
2761 }
2762
2763 $wrapper_class = 'gptranslate_wrapper';
2764 $custom_selector = $settings['wrapper_selector'] ?? '.gptranslate_wrapper';
2765
2766 // If the user has set a custom wrapper selector (not the default), use that class
2767 if ($custom_selector !== '.gptranslate_wrapper' && strpos($custom_selector, '.') === 0) {
2768 $wrapper_class = substr($custom_selector, 1);
2769 }
2770
2771 // Flag that the shortcode was used, to prevent duplicate wrapper in footer
2772 if (!defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
2773 define('GPTRANSLATE_SHORTCODE_RENDERED', true);
2774 }
2775
2776 return '<div class="' . esc_attr($wrapper_class) . '"></div>';
2777 });
2778
2779 add_action('wp_footer', function () {
2780 // Add the default target container if default CSS selector
2781 $settings = get_option("gptranslate_options");
2782
2783 // Disable interface
2784 if($settings ['disable_control']) {
2785 $settings ['wrapper_selector'] = '';
2786 }
2787
2788 // Skip the automatic footer wrapper if shortcode was already used
2789 if (defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
2790 return;
2791 }
2792
2793 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
2794 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
2795 }
2796 });
2797
2798 // Get available (enabled) languages for sitemap filtering (excluding original language)
2799 function gptranslate_get_available_languages() {
2800 $opts = get_option('gptranslate_options', array());
2801
2802 // Get enabled target languages from settings
2803 $enabled_languages = isset($opts['languages']) ? array_map('strtolower', (array)$opts['languages']) : array();
2804
2805 // Exclude the original/source language
2806 $original_language = isset($opts['language']) ? strtolower($opts['language']) : '';
2807 $enabled_languages = array_filter($enabled_languages, function($lang) use ($original_language) {
2808 return $lang !== $original_language;
2809 });
2810
2811 return $enabled_languages ? array_values($enabled_languages) : array();
2812 }
2813
2814 // POST API REST Translations storage
2815 add_action('rest_api_init', function () {
2816 register_rest_route('gptranslate/v1', '/request', [
2817 'methods' => 'POST',
2818 'callback' => 'gpt_handle_request',
2819 'permission_callback' => 'gptranslate_public_permission'
2820 ]);
2821 });
2822
2823 // Real-time XML Sitemap endpoint - public, generates sitemap on-the-fly
2824 add_action('rest_api_init', function () {
2825 register_rest_route('gptranslate/v1', '/sitemap.xml', [
2826 'methods' => 'GET',
2827 'callback' => 'gptranslate_realtime_sitemap',
2828 'permission_callback' => '__return_true'
2829 ]);
2830 });
2831
2832 function gptranslate_realtime_sitemap() {
2833 global $wpdb;
2834 $table = $wpdb->prefix . 'gptranslate';
2835
2836 // Get language filter from query parameter
2837 $language_filter = isset($_GET['language']) ? sanitize_text_field(wp_unslash($_GET['language'])) : '';
2838
2839 $query = "
2840 SELECT translated_alias, translate_date
2841 FROM $table
2842 WHERE translated_alias IS NOT NULL
2843 AND translated_alias != ''";
2844
2845 // Add language filter if specified and not "all"
2846 if (!empty($language_filter) && $language_filter !== 'all') {
2847 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
2848 }
2849
2850 $query .= " ORDER BY translate_date DESC";
2851
2852 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2853
2854 $dom = new DOMDocument('1.0', 'UTF-8');
2855 $dom->preserveWhiteSpace = false;
2856 $dom->formatOutput = true;
2857
2858 $urlset = $dom->createElement('urlset');
2859 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
2860 $dom->appendChild($urlset);
2861
2862 if (empty($records)) {
2863 $urlset->appendChild($dom->createTextNode(''));
2864 }
2865
2866 foreach ($records as $record) {
2867 $url = $dom->createElement('url');
2868 $loc = $dom->createElement('loc');
2869 $loc->appendChild($dom->createTextNode($record['translated_alias']));
2870 $url->appendChild($loc);
2871
2872 if (!empty($record['translate_date'])) {
2873 $lastmod = $dom->createElement('lastmod', gmdate('c', strtotime($record['translate_date'])));
2874 $url->appendChild($lastmod);
2875 }
2876
2877 $urlset->appendChild($url);
2878 }
2879
2880 header('Content-Type: application/xml; charset=UTF-8');
2881 echo $dom->saveXML();
2882 exit;
2883 }
2884
2885 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
2886 // Remove the default 'Settings' item
2887 unset($links[0]);
2888
2889 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
2890 array_unshift($links, $settings_link);
2891
2892 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
2893 array_unshift($links, $translations_link);
2894
2895 return $links;
2896 });
2897
2898 /*
2899 // Remove any WP update for the free version over the paid full one
2900 add_filter('auto_update_plugin', function($update, $item) {
2901 if (isset($item->slug) && $item->slug === 'gptranslate') {
2902 return false;
2903 }
2904 return $update;
2905 }, 10, 2);
2906
2907
2908 add_filter('site_transient_update_plugins', function($transient) {
2909 if (isset($transient->response['gptranslate/gptranslate.php'])) {
2910 unset($transient->response['gptranslate/gptranslate.php']);
2911 }
2912 return $transient;
2913 });
2914 */
2915
2916 /**
2917 * Permission callback public API
2918 * @param WP_REST_Request $request
2919 * @return bool|WP_Error
2920 */
2921 function gptranslate_public_permission( WP_REST_Request $request ) {
2922 // 1) Controllo chiave API inviata via header
2923 $headerApiKey = $request->get_header('x-gptranslate-key');
2924 $restApiKey = hash( 'sha256', get_site_url() );
2925 if ( $headerApiKey != $restApiKey) {
2926 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
2927 }
2928 return true;
2929 }
2930
2931 /**
2932 * Normalize WP URL
2933 * @param string $url
2934 * @return string
2935 */
2936 function gpt_trailingslashit_url($url) {
2937 $parsed = wp_parse_url($url);
2938 if (empty($parsed['path'])) {
2939 $parsed['path'] = '/';
2940 } else {
2941 $parsed['path'] = trailingslashit(untrailingslashit($parsed['path']));
2942 }
2943
2944 $rebuilt = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
2945 $rebuilt .= $parsed['host'] ?? '';
2946 $rebuilt .= $parsed['path'];
2947 if (!empty($parsed['query'])) {
2948 $rebuilt .= '?' . $parsed['query'];
2949 }
2950 if (!empty($parsed['fragment'])) {
2951 $rebuilt .= '#' . $parsed['fragment'];
2952 }
2953
2954 return $rebuilt;
2955 }
2956
2957 /**
2958 * Callback per GET/STORE translations via REST.
2959 * Frontend API
2960 *
2961 * @param WP_REST_Request $request
2962 * @return WP_REST_Response
2963 */
2964 function gpt_handle_request( WP_REST_Request $request ) {
2965 global $wpdb;
2966
2967 $table = $wpdb->prefix . 'gptranslate';
2968
2969 $params = $request->get_json_params();
2970
2971 if(!$params) {
2972 $params = $request->get_body_params();
2973 }
2974
2975 // Sanitize input params
2976 $task = sanitize_text_field( $params['task'] ?? '' );
2977 $pageLink = esc_url_raw( $params['pagelink'] ?? '' );
2978 $translatedAlias = esc_url_raw( $params['translated_alias'] ?? '' );
2979 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
2980 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
2981 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
2982 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
2983
2984 $now = current_time( 'mysql' );
2985
2986 $response = [ 'result' => false ];
2987
2988 if ( $task === 'storetranslations' ) {
2989 // Ensure there is not a mismatching insert with the same languages
2990 if($languageOriginal == $languageTranslated) {
2991 $response['result'] = true;
2992 return rest_ensure_response( $response );
2993 }
2994
2995 // Fetch raw param (could be array or JSON string)
2996 $rawFull = $params['translations'] ?? '[]';
2997 $rawAlt = $params['alt_translations'] ?? '[]';
2998
2999 // If it’s already a string (JSON), use it directly.
3000 // If it’s an array (unlikely with FormData), JSON encode it.
3001 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
3002 $fullTranslations = $rawFull;
3003 } else {
3004 $fullTranslations = wp_json_encode( (array) $rawFull );
3005 }
3006
3007 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
3008 $altTranslations = $rawAlt;
3009 } else {
3010 $altTranslations = wp_json_encode( (array) $rawAlt );
3011 }
3012
3013 // Check if record already exists
3014 $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3015 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3016 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3017 "\n AND languageoriginal = %s" .
3018 "\n AND languagetranslated = %s",
3019 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3020 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3021
3022 $existing_id = $existing ? (int)$existing : null;
3023 $opts = get_option('gptranslate_options', []);
3024
3025 // Only if it is a retrigger then ignore the db processing
3026 if($retriggerTranslation !== 1) {
3027 if ( $existing_id ) {
3028 // If lock_translations is enabled globally, skip the UPDATE silently
3029 if ( !empty($opts['lock_translations']) ) {
3030 $response['result'] = false;
3031 } else {
3032 // UPDATE
3033 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3034 $table,
3035 [
3036 'translations' => $fullTranslations,
3037 'alt_translations' => $altTranslations,
3038 'translated_alias' => $translatedAlias,
3039 'translate_date' => $now,
3040 'translation_engine' => $translationEngine,
3041 ],
3042 [ 'id' => (int) $existing_id ],
3043 [ '%s', '%s', '%s', '%s', '%s' ],
3044 [ '%d' ]
3045 );
3046 $response['result'] = ( $updated !== false );
3047 }
3048 } else {
3049 // INSERT
3050 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3051 $table,
3052 [
3053 'pagelink' => $pageLink,
3054 'translations' => $fullTranslations,
3055 'alt_translations' => $altTranslations,
3056 'translated_alias' => $translatedAlias,
3057 'languageoriginal' => $languageOriginal,
3058 'languagetranslated' => $languageTranslated,
3059 'published' => 1,
3060 'translate_date' => $now,
3061 'translation_engine' => $translationEngine,
3062 ],
3063 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
3064 );
3065 $response['result'] = ( $inserted !== false );
3066 }
3067 } else {
3068 $response['result'] = true;
3069 }
3070 } elseif ($task === 'gettranslations') {
3071 $opts = get_option('gptranslate_options', []);
3072 if (!empty($opts['realtime_translations']) || $retriggerTranslation === 1) {
3073 $response['result'] = false;
3074 } else {
3075 // Prepare decoded version for URL matching
3076 $pageLinkDecoded = urldecode($pageLink);
3077
3078 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
3079 // Check 8 variants (with/without slash + encoded/decoded)
3080 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3081 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3082 "\n 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)" .
3083 "\n AND languageoriginal = %s" .
3084 "\n AND languagetranslated = %s" .
3085 "\n AND published = 1",
3086 rtrim($pageLink, '/'),
3087 rtrim($pageLink, '/') . '/',
3088 rtrim($pageLinkDecoded, '/'),
3089 rtrim($pageLinkDecoded, '/') . '/',
3090 rtrim($pageLink, '/'),
3091 rtrim($pageLink, '/') . '/',
3092 rtrim($pageLinkDecoded, '/'),
3093 rtrim($pageLinkDecoded, '/') . '/',
3094 $languageOriginal,
3095 $languageTranslated
3096 ), ARRAY_A);
3097 } else {
3098 // Check 4 variants (with/without slash + encoded/decoded)
3099 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3100 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3101 "\n WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)" .
3102 "\n AND languageoriginal = %s" .
3103 "\n AND languagetranslated = %s" .
3104 "\n AND published = 1",
3105 rtrim($pageLink, '/'),
3106 rtrim($pageLink, '/') . '/',
3107 rtrim($pageLinkDecoded, '/'),
3108 rtrim($pageLinkDecoded, '/') . '/',
3109 $languageOriginal,
3110 $languageTranslated
3111 ), ARRAY_A);
3112 }
3113
3114 if ($row) {
3115 $response['result'] = true;
3116 $response['translations'] = json_decode($row['translations'], true) ?: [];
3117 $response['alt_translations'] = json_decode($row['alt_translations'], true) ?: [];
3118 $response['translated_alias'] = $row['translated_alias'];
3119 $response['pagelink_alias'] = $row['pagelink'];
3120 } else {
3121 $response['result'] = false;
3122 }
3123 }
3124 } elseif ($task == 'getaliastranslation') {
3125 // Always perform a new realtime translation if the option is enabled
3126 try {
3127 $row = $wpdb->get_row( $wpdb->prepare(
3128 "SELECT translated_alias FROM {$table}" .
3129 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3130 "\n AND languageoriginal = %s" .
3131 "\n AND languagetranslated = %s" .
3132 "\n AND published = 1",
3133 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3134 ), ARRAY_A );
3135
3136 if ( $row ) {
3137 $response['result'] = true;
3138 $response['translated_alias'] = $row['translated_alias'] ?? '';
3139 } else {
3140 $response['result'] = false;
3141 }
3142 } catch ( Exception $e ) {
3143 $response['result'] = false;
3144 $response['exception'] = $e->getMessage();
3145 }
3146 } elseif ( $task === 'syncTranslation' ) {
3147 $original = wp_unslash( $params['original'] ?? '' );
3148 $translated = wp_unslash( $params['translated'] ?? '' );
3149 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3150 $translationType = sanitize_text_field( $params['translation_type'] ?? 'translations' ); // default to 'translations'
3151
3152 // Recupera tutti i record nella lingua target
3153 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3154 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3155 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3156 "\n WHERE languagetranslated = %s",
3157 $languageTranslated
3158 ) );
3159
3160 $updatedCount = 0;
3161
3162 foreach ( $rows as $row ) {
3163 $currentTranslations = json_decode( $row->$translationType, true );
3164
3165 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
3166 // Aggiorna la traduzione e salva
3167 $currentTranslations[ $original ] = $translated;
3168 $jsonUpdated = wp_json_encode( $currentTranslations );
3169
3170 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3171 $table,
3172 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
3173 [ 'id' => $row->id ],
3174 [ '%s', '%s' ],
3175 [ '%d' ]
3176 );
3177
3178 if ( $success !== false ) {
3179 $updatedCount++;
3180 }
3181 }
3182 }
3183
3184 $response['result'] = $updatedCount > 0;
3185 } elseif ($task == 'gettranslatedaliases') {
3186 try {
3187 if ($languageTranslated) {
3188 $rows = $wpdb->get_results(
3189 $wpdb->prepare(
3190 "SELECT pagelink, translated_alias
3191 FROM {$table}
3192 WHERE languagetranslated = %s
3193 AND published = 1",
3194 $languageTranslated
3195 ),
3196 ARRAY_A
3197 );
3198 } elseif ($languageOriginal) {
3199 $rows = $wpdb->get_results(
3200 $wpdb->prepare(
3201 "SELECT translated_alias AS pagelink, pagelink AS translated_alias
3202 FROM {$table}
3203 WHERE languageoriginal = %s
3204 AND published = 1",
3205 $languageOriginal
3206 ),
3207 ARRAY_A
3208 );
3209 } else {
3210 $response['result'] = false;
3211 echo wp_json_encode($response);
3212 exit;
3213 }
3214
3215 if ($rows) {
3216 $encodedResult = [];
3217
3218 foreach ($rows as $row) {
3219
3220 // Normalizza WordPress-style (CON trailing slash)
3221 $pagelink = gpt_trailingslashit_url($row['pagelink']);
3222 $translatedAlias = !empty($row['translated_alias']) ? gpt_trailingslashit_url($row['translated_alias']) : '';
3223
3224 // Encode pagelink (solo path)
3225 $parsedUrl = wp_parse_url($pagelink);
3226 $encodedPagelink = $pagelink;
3227
3228 if (!empty($parsedUrl['path'])) {
3229 $pathParts = explode('/', $parsedUrl['path']);
3230 $encodedParts = array_map('rawurlencode', $pathParts);
3231 $encodedPath = implode('/', $encodedParts);
3232
3233 $encodedPagelink = ($parsedUrl['scheme'] ?? '') . '://' . ($parsedUrl['host'] ?? '');
3234 $encodedPagelink .= $encodedPath;
3235 if (!empty($parsedUrl['query'])) {
3236 $encodedPagelink .= '?' . $parsedUrl['query'];
3237 }
3238 if (!empty($parsedUrl['fragment'])) {
3239 $encodedPagelink .= '#' . $parsedUrl['fragment'];
3240 }
3241 }
3242
3243 // Encode translated alias
3244 $encodedAlias = $translatedAlias;
3245 if (!empty($translatedAlias)) {
3246 $parsedAlias = wp_parse_url($translatedAlias);
3247 if (!empty($parsedAlias['path'])) {
3248 $pathParts = explode('/', $parsedAlias['path']);
3249 $encodedParts = array_map('rawurlencode', $pathParts);
3250 $encodedPath = implode('/', $encodedParts);
3251
3252 $encodedAlias = ($parsedAlias['scheme'] ?? '') . '://' . ($parsedAlias['host'] ?? '');
3253 $encodedAlias .= $encodedPath;
3254 if (!empty($parsedAlias['query'])) {
3255 $encodedAlias .= '?' . $parsedAlias['query'];
3256 }
3257 if (!empty($parsedAlias['fragment'])) {
3258 $encodedAlias .= '#' . $parsedAlias['fragment'];
3259 }
3260 }
3261 }
3262
3263 $encodedResult[$encodedPagelink] = [
3264 'pagelink' => $encodedPagelink,
3265 'translated_alias' => $encodedAlias
3266 ];
3267 }
3268
3269 $response['result'] = true;
3270 $response['translated_aliases'] = $encodedResult;
3271
3272 } else {
3273 $response['result'] = false;
3274 }
3275
3276 } catch (Exception $e) {
3277 $response['result'] = false;
3278 $response['exception'] = $e->getMessage();
3279 }
3280 } elseif ( $task === 'deepseektranslations' ) {
3281 try {
3282 // Read raw JSON payload sent from JS
3283 $rawInput = file_get_contents( 'php://input' );
3284 $requestData = json_decode( $rawInput, true );
3285
3286 if ( ! $requestData || empty( $requestData['messages'] ) ) {
3287 throw new Exception( 'Invalid DeepSeek request payload' );
3288 }
3289
3290 // Get plugin options
3291 $opts = get_option( 'gptranslate_options', [] );
3292
3293 $deepseekApiKey = $opts['chatgpt_apikey'] ?? '';
3294 $deepseekModel = $opts['chatgpt_model'] ?? 'deepseek-chat';
3295
3296 if ( empty( $deepseekApiKey ) ) {
3297 throw new Exception( 'DeepSeek API key not configured' );
3298 }
3299
3300 // Fixed DeepSeek parameters (server controlled)
3301 $payload = [
3302 'model' => $deepseekModel,
3303 'messages' => $requestData['messages'],
3304 'max_tokens' => 4096,
3305 'temperature' => 0.5,
3306 ];
3307
3308 // Call DeepSeek API
3309 $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
3310 curl_setopt_array( $ch, [
3311 CURLOPT_POST => true,
3312 CURLOPT_RETURNTRANSFER => true,
3313 CURLOPT_HTTPHEADER => [
3314 'Content-Type: application/json',
3315 'Authorization: Bearer ' . $deepseekApiKey,
3316 ],
3317 CURLOPT_POSTFIELDS => wp_json_encode( $payload ),
3318 CURLOPT_TIMEOUT => 60,
3319 ] );
3320
3321 $apiResponse = curl_exec( $ch );
3322
3323 if ( $apiResponse === false ) {
3324 throw new Exception( 'DeepSeek cURL error: ' . curl_error( $ch ) );
3325 }
3326
3327 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
3328 curl_close( $ch );
3329
3330 if ( $httpCode >= 400 ) {
3331 throw new Exception( 'DeepSeek API HTTP error: ' . $httpCode );
3332 }
3333
3334 // IMPORTANT:
3335 // Return DeepSeek response EXACTLY as the API returns it
3336 header( 'Content-Type: application/json; charset=utf-8' );
3337 echo $apiResponse;
3338 exit;
3339
3340 } catch ( Exception $e ) {
3341 // FALLBACK SAFE RESPONSE (OpenAI-compatible)
3342 header('Content-Type: application/json; charset=utf-8');
3343 echo wp_json_encode([
3344 'choices' => [[
3345 'finish_reason' => 'error',
3346 'message' => [
3347 'role' => 'assistant',
3348 'content' => ''
3349 ]
3350 ]]
3351 ]);
3352 exit;
3353 }
3354 } elseif ($task === 'deepltranslations') {
3355 try {
3356 // Use $params already parsed by WordPress REST API (php://input is consumed)
3357 if (empty ( $params ['texts'] )) {
3358 throw new Exception ( 'Invalid DeepL request payload' );
3359 }
3360
3361 // Get plugin options
3362 $opts = get_option ( 'gptranslate_options', [ ] );
3363
3364 $deeplApiKey = $opts ['chatgpt_apikey'] ?? '';
3365
3366 if (empty ( $deeplApiKey )) {
3367 throw new Exception ( 'DeepL API key not configured' );
3368 }
3369
3370 $sourceLanguage = sanitize_text_field ( $params ['source_lang'] ?? 'auto');
3371 $targetLanguage = sanitize_text_field ( $params ['target_lang'] ?? 'EN');
3372
3373 // Build query string manually - DeepL requires repeated "text" params (not text[0], text[1])
3374 $queryParts = [ ];
3375 $queryParts [] = 'source_lang=' . urlencode ( strtoupper ( $sourceLanguage ) );
3376 $queryParts [] = 'target_lang=' . urlencode ( strtoupper ( $targetLanguage ) );
3377
3378 foreach ( $params ['texts'] as $text ) {
3379 $queryParts [] = 'text=' . urlencode ( $text );
3380 }
3381
3382 $queryString = implode ( '&', $queryParts );
3383
3384 // Call DeepL API - Auto-detect endpoint based on API key type
3385 // Free API keys end with ':fx', paid keys don't have this suffix
3386 $deeplEndpoint = (strpos ( $deeplApiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
3387 $ch = curl_init ( $deeplEndpoint . '/v2/translate' );
3388 curl_setopt_array ( $ch, [
3389 CURLOPT_POST => true,
3390 CURLOPT_RETURNTRANSFER => true,
3391 CURLOPT_HTTPHEADER => [
3392 'Authorization: DeepL-Auth-Key ' . $deeplApiKey,
3393 'Content-Type: application/x-www-form-urlencoded'
3394 ],
3395 CURLOPT_POSTFIELDS => $queryString,
3396 CURLOPT_TIMEOUT => 60
3397 ] );
3398
3399 $apiResponse = curl_exec ( $ch );
3400
3401 if ($apiResponse === false) {
3402 throw new Exception ( 'DeepL cURL error: ' . curl_error ( $ch ) );
3403 }
3404
3405 $httpCode = curl_getinfo ( $ch, CURLINFO_HTTP_CODE );
3406 curl_close ( $ch );
3407
3408 if ($httpCode >= 400) {
3409 throw new Exception ( 'DeepL API HTTP error: ' . $httpCode . ' - ' . $apiResponse );
3410 }
3411
3412 // Return DeepL response EXACTLY as the API returns it
3413 header ( 'Content-Type: application/json; charset=utf-8' );
3414 echo $apiResponse;
3415 exit ();
3416 } catch ( Exception $e ) {
3417 wp_send_json_error ( [
3418 'message' => $e->getMessage ()
3419 ] );
3420 }
3421 }
3422
3423 return rest_ensure_response( $response );
3424 }
3425
3426 // Instantiate and run the app
3427 GPTranslate::get_instance();
3428
3429 // Optimizer compatibility – Phase 1 (early).
3430 // PHP-side exclusion filters must be registered at plugin load time so they
3431 // are in place before any optimizer (e.g. WP Rocket) reads them during its
3432 // own initialisation. Never runs in wp-admin (handled inside init_early).
3433 GPTranslate_Optimizer_Compat::init_early();
3434
3435 // Optimizer compatibility – Phase 2 (late).
3436 // HTML tag-manipulation filters (script_loader_tag, wp_inline_script_tag)
3437 // are registered here, after GPTranslate has decided whether to enqueue
3438 // scripts on this page. Bails immediately if gptranslate-main is not queued.
3439 add_action( 'wp_enqueue_scripts', [ 'GPTranslate_Optimizer_Compat', 'init_late' ], 999 );