PluginProbe
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.27.10
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.27.10
2.34.5 2.34.4 2.34.3 2.34.2 2.34.1 2.34 2.33.10 2.33.9 2.33.8 2.33.7 2.33.6 2.33.5 2.33.2 2.32.10 2.33 2.33.1 2.32.6 2.32.7 2.32.8 trunk 2.10.3 2.10.4 2.10.5 2.10.6 2.11 All 57 releases
gptranslate / gptranslate.php

gptranslate.php in GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI 2.27.10, at gptranslate.php

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