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