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