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

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

2,350 lines 164.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: GPTranslate
4 Plugin URI: https://gptranslate.storejextensions.org/
5 Description: GPTranslate for Wordpress is the revolutionary multilanguage solution to automatically translate your Wordpress website thanks to the power of Artificial Intelligence like ChatGPT, Deepseek, Gemini and more. ⚠️GPTranslate FREE Mode active
6 Author: JExtensions Store
7 Version: 2.18.2
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.18.2';
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.18.2';
36
37 $settings = get_option ( 'gptranslate_options', [ ] );
38
39 // Server-side plugin exclusion check for frontend pages
40 if ( !is_admin() ) {
41 $current_url = esc_url_raw( ( ( !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ) ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
42 $page_exclusions_raw = isset($settings['page_exclusions']) ? $settings['page_exclusions'] : '';
43
44 if (!empty($page_exclusions_raw)) {
45 // Normalize input: replace newlines with commas, collapse multiple commas
46 $patterns = explode(',', trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $page_exclusions_raw)), ','));
47
48 foreach ($patterns as $pattern) {
49 $pattern = trim($pattern);
50 if ($pattern === '') continue;
51
52 // Match if current URL contains the exclusion string (case-insensitive)
53 if (stripos($current_url, $pattern) !== false) {
54 // Skip plugin execution completely
55 return;
56 }
57 }
58 }
59 }
60
61 // Include various functions like multilanguage URLs, hreflang tag, HTML lang attribute rewriting
62 require_once plugin_dir_path(__FILE__) . 'multilang-routing.php';
63
64 if ( isset($settings ['serverside_translations']) && $settings ['serverside_translations'] == 1 ) {
65 require_once plugin_dir_path(__FILE__) . 'serverside-translations.php';
66 }
67
68 register_activation_hook ( __FILE__, [
69 $this,
70 'activate_plugin'
71 ] );
72
73 add_action ( 'admin_init', function () {
74 // Disable WordPress emoji script and styles
75 remove_action('wp_head', 'print_emoji_detection_script', 7);
76 remove_action('admin_print_scripts', 'print_emoji_detection_script');
77 remove_action('wp_print_styles', 'print_emoji_styles');
78 remove_action('admin_print_styles', 'print_emoji_styles');
79 remove_filter('the_content_feed', 'wp_staticize_emoji');
80 remove_filter('comment_text_rss', 'wp_staticize_emoji');
81 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
82
83 function gptranslate_sanitize_options( $options ) {
84 $clean = [];
85 $clean = $options;
86 return $clean;
87 }
88 register_setting ( 'gptranslate_settings', 'gptranslate_options', [
89 'sanitize_callback' => 'gptranslate_sanitize_options'
90 ]);
91
92 // Register record deletion
93 $page = sanitize_key($_GET['page'] ?? '');
94 $action = sanitize_key($_GET['action'] ?? '');
95 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
96 $translation_id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
97
98 if ($page === 'gptranslate' &&
99 $action === 'delete_translation' &&
100 $translation_id &&
101 wp_verify_nonce($nonce, 'gptranslate_delete_' . $translation_id)
102 ) {
103 $this->gptranslate_handle_deletion($translation_id);
104 wp_redirect(admin__rewrit('admin.php?page=gptranslate&deleted=1'));
105 exit;
106 }
107
108 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'toggle_published') {
109 // Toggle published state
110 $id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0;
111
112 if (!$id || !isset($_GET['_gptranslate_nonce']) || !wp_verify_nonce(wp_unslash($_GET['_gptranslate_nonce']), 'gptranslate_toggle_' . $id)) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
113 wp_die('Invalid nonce.');
114 }
115
116 global $wpdb;
117 $table = $wpdb->prefix . 'gptranslate';
118
119 // Toggle published flag
120 $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
121 $new = ($current == 1) ? 0 : 1;
122
123 $wpdb->update($table, ['published' => $new], ['id' => $id]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
124
125 wp_redirect(admin_url('admin.php?page=gptranslate&action=state_toggled'));
126 exit;
127 }
128
129 // Check for plugin update
130 function check_for_gptranslate_update($currentVersion) {
131 // Start the session if not already started
132 if (session_status() === PHP_SESSION_NONE) {
133 session_start();
134 }
135
136 // Reset session vars after update
137 if (isset($_SESSION['gptranslate_update_version']) && version_compare($currentVersion, $_SESSION['gptranslate_update_version'], '>=')) {
138 unset($_SESSION['gptranslate_update_version']);
139 unset($_SESSION['gptranslate_update_checked']);
140 }
141
142 // Check if the update check has been done in this session
143 if (!isset($_SESSION['gptranslate_update_checked']) || $_SESSION['gptranslate_update_checked'] !== true) {
144
145 // Perform the remote XML check
146 $remote_url = 'https://storejextensions.org/updates/gptranslatewp_updater.xml';
147 $response = wp_remote_get($remote_url);
148
149 if (!is_wp_error($response)) {
150 $body = wp_remote_retrieve_body($response);
151 if (!empty($body)) {
152 $xml = simplexml_load_string($body);
153 if ($xml && !empty($xml->update->version)) {
154 $updateversion = (string)$xml->update->version;
155 if (version_compare($updateversion, $currentVersion, '>')) {
156 // Store the update info in session (version, and flag that update is available)
157 $_SESSION['gptranslate_update_version'] = $updateversion;
158 }
159 $_SESSION['gptranslate_update_checked'] = true;
160 }
161 }
162 }
163 }
164
165 // If update is available, show the notice
166 if (isset($_SESSION['gptranslate_update_version']) && sanitize_text_field($_SESSION['gptranslate_update_version'])) {
167 add_action('admin_notices', function () {
168 echo '<div class="notice notice-warning is-dismissible">';
169 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(sanitize_text_field($_SESSION['gptranslate_update_version'])) . '</strong> can be downloaded from your reserved area if you have a valid subscription and license for the full version.</p>'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
170 echo '</div>';
171 });
172 }
173 }
174 //check_for_gptranslate_update($this->version);
175 } );
176
177 // Post admin notices after actions
178 add_action( 'admin_notices', function() {
179 if ( isset( $_GET['page'], $_GET['deleted'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
180 if ( (int) $_GET['deleted'] === 1 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
181 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED')) . '</p></div>';
182 } elseif ( (int) $_GET['deleted'] === 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
183 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED_ERROR')) . '</p></div>';
184 }
185 }
186
187 if ( isset( $_GET['page'], $_GET['action'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
188 if ( $_GET['action'] === 'state_toggled' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
189 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_STATE_UPDATED_SUCCESSFULLY')) . '</p></div>';
190 }
191 }
192
193 if (isset($_GET['imported']) && $_GET['imported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
194 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_IMPORTED_SUCCESSFULLY')) . '</p></div>';
195 }
196
197 if (isset($_GET['settingsimported']) && $_GET['settingsimported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
198 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_IMPORTED_SUCCESSFULLY')) . '</p></div>';
199 }
200 });
201
202 // Add hook for admin menu links
203 add_action ( 'admin_menu', [
204 $this,
205 'admin_menu'
206 ] );
207
208 // Add hook for record saving/deleting
209 add_action ( 'admin_post_save_gptranslate_record', [
210 $this,
211 'save_record'
212 ] );
213
214 add_action( 'admin_post_save_gptranslate_record_and_close', [
215 $this,
216 'save_record'
217 ]);
218
219 add_action('admin_post_cancel_gptranslate_record', [
220 $this,
221 'save_record'
222 ]);
223
224 // Add hook for adding main frontend app scripts
225 add_action ( 'wp_enqueue_scripts', [
226 $this,
227 'enqueue_frontend_scripts'
228 ] );
229 }
230
231 /**
232 * Singleton class instance
233 *
234 * @access public
235 */
236 public static function get_instance() {
237 if (null === self::$instance) {
238 self::$instance = new static();
239 }
240 return self::$instance;
241 }
242
243 /**
244 * Activation plugin hook with db table creation
245 *
246 * @access public
247 */
248 public function activate_plugin() {
249 global $wpdb;
250 $charset_collate = $wpdb->get_charset_collate();
251
252 $sql = "CREATE TABLE " . $this->table_name . " (
253 id int UNSIGNED NOT NULL AUTO_INCREMENT,
254 pagelink varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
255 translated_alias varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
256 translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
257 alt_translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
258 languageoriginal char(20) NOT NULL,
259 languagetranslated char(20) NOT NULL,
260 published tinyint NOT NULL DEFAULT '1',
261 translate_date datetime DEFAULT NULL,
262 translation_engine varchar(20) NOT NULL,
263 PRIMARY KEY (id)
264 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
265
266 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
267 dbDelta($sql);
268
269 // Valori di default
270 $default_options = [
271 'google_translate_engine' => '1',
272 'chatgpt_apikey' => '',
273 'chatgpt_model' => 'gpt-3.5-turbo',
274 '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.",
275 'chatgpt_request_conversation_mode' => 'user',
276 'language' => 'en',
277 'max_translations_per_request' => '100',
278 'max_characters_per_request' => '2048',
279 'detect_browser_language' => '0',
280 'autotranslate_detected_language' => '0',
281 'always_detect_autotranslated_language' => '0',
282 'auto_set_language_direction' => '0',
283 'serverside_translations' => '0',
284 'serverside_translations_method' => 'regex',
285 'serverside_translations_caseinsensitive' => '1',
286 'serverside_translations_matchquotes' => '1',
287 'serverside_translations_urldecode' => '1',
288 'serverside_translations_language_switching_mode' => 'url',
289 'serverside_translations_ignore_querystring' => '0',
290 'serverside_translations_urlencode_space' => '0',
291 'css_selector_serverside_leafnodes_excluded' => '',
292 'detect_current_language' => '0',
293 'detect_default_language' => '0',
294 'rewrite_language_url' => '0',
295 'rewrite_language_alias' => '0',
296 'rewrite_language_alias_original_language' => '0',
297 'rewrite_page_links' => '0',
298 'omit_prefix_original_language' => '0',
299 'rewrite_default_language_url' => '0',
300 'translate_metadata' => '0',
301 'set_html_lang' => '0',
302 'add_canonical' => '0',
303 'add_alternate' => '0',
304 'translate_placeholders' => '0',
305 'translate_altimages' => '0',
306 'css_selector_classes_translate_altimages_excluded' => '',
307 'translate_srcimages' => '0',
308 'translate_titles' => '0',
309 'translate_values' => '0',
310 'metadata_chosen_engine' => '0',
311 'default_language_first' => '0',
312 'css_selector_leafnodes_excluded' => 'a.nturl,.gt-lang-code',
313 'words_leafnodes_excluded' => '',
314 'words_leafnodes_excluded_bylanguage_repeatable' => '[]',
315 'words_min_length' => '',
316 'crawler_timeout' => '30',
317 'crawler_exclusions' => '',
318 'page_exclusions' => '',
319 'chatgpt_gtranslate_request_delay' => '0',
320 'initial_translation_delay' => '0',
321 'realtime_translations' => '0',
322 'css_selector_realtime_translations_retrigger' => '',
323 'realtime_translations_retrigger_events' => ['click'],
324 'realtime_translations_retrigger_events_delay' => '200',
325 'translations_export_format' => '.csv',
326 'ignore_querystring' => '0',
327 'enable_indexer' => '0',
328 'storage_type' => 'session',
329 'subfolder_installation' => '0',
330 'alt_flags' => [],
331 'languages' => ['en', 'es', 'de', 'it', 'fr'],
332 'enable_reader' => '0',
333 'responsivevoice_apikey' => 'kWyXm5dC',
334 'responsivevoice_language_gender' => 'auto',
335 'responsivevoice_volume_tts' => '100',
336 'responsivevoice_voice_speed' => 'normal',
337 'mainpage_selector' => '*[name*=main], *[class*=main], *[id*=main], *[id*=container], *[class*=container]',
338 'elements_toexclude_custom' => '',
339 'proxy_responsive_loading_script' => '1',
340 'proxy_responsive_reading_mode' => 'native',
341 'chunksize' => '200',
342 'widget_text_color' => '#000000',
343 'widget_background_color' => '#FFFFFF',
344 'popup_border_radius' => '0',
345 'popup_fontsize' => '20',
346 'popup_iconsize' => '32',
347 'float_position' => 'bottom-left',
348 'float_switcher_open_direction' => 'top',
349 'flag_style' => '2d',
350 'flag_loading' => 'local',
351 'show_language_titles' => '1',
352 'enable_dropdown' => '1',
353 'equal_widths' => '0',
354 'reader_button_position' => 'top',
355 'widget_max_height' => '260',
356 'wrapper_selector' => '.gptranslate_wrapper',
357 'draggable_widget' => '0',
358 'disable_control' => '0',
359 'custom_css' => '',
360 'disable_bootstrap_css' => '0'
361 ];
362
363 // Se l'opzione non è ancora presente, la crea
364 if (get_option('gptranslate_options') === false) {
365 add_option('gptranslate_options', $default_options);
366 }
367
368 }
369
370 /**
371 * Function to add admin menu for both settings and translations management
372 *
373 * @access public
374 */
375 public function admin_menu() {
376 add_menu_page('GPTranslate', 'GPTranslate', 'manage_options', 'gptranslate', [$this, 'records_page'], 'dashicons-translation');
377
378 // Add a submenu that matches the main menu to prevent duplication and allow renaming
379 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), 'manage_options', 'gptranslate', [$this, 'records_page']);
380
381 // Now you can safely add a differently named submenu
382 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']);
383 }
384
385 /**
386 * Load the configuration settings page held in the settings.php file
387 *
388 * @access public
389 */
390 public function settings_page() {
391 require_once 'settings.php';
392
393 echo '<script>
394 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
395 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
396 </script>';
397 }
398
399 /**
400 * Translation records pages, list and edit
401 *
402 * @access public
403 */
404 public function records_page() {
405 global $wpdb;
406
407 // Edit record
408 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'edit') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
409 $id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
410 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
411 $opts = get_option( 'gptranslate_options', [] );
412
413 if ( ! wp_verify_nonce( $nonce, 'gptranslate_edit_' . $id ) ) {
414 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
415 }
416
417 $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
418
419 $translationsArray = json_decode($record->translations, true) ?? [];
420 $altTranslationsArray = json_decode($record->alt_translations, true) ?? [];
421
422 uksort($translationsArray, function($a, $b) {
423 return strlen($b) - strlen($a);
424 });
425 uksort($altTranslationsArray, function($a, $b) {
426 return strlen($b) - strlen($a);
427 });
428
429 // Path relativo o assoluto all'immagine della bandiera
430 $flagUrlOriginal = plugins_url('flags/svg/' . esc_attr($record->languageoriginal) . '.svg', __FILE__);
431 $flagUrlTranslated = plugins_url('flags/svg/' . esc_attr($record->languagetranslated) . '.svg', __FILE__);
432
433 $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
434 : '<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
435 $rewriteAliasRow = '';
436 if ($opts ['rewrite_language_alias'] == 1) {
437 $rewriteAliasRow = '<tr>
438 <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>
439 <td><input type="text" id="translated_alias" name="translated_alias" value="' . esc_attr($record->translated_alias) . '" class="regular-text code"></td>
440 </tr>';
441 }
442
443 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
444 echo '<form method="post" id="edit-translations" action="admin-post.php">
445 <p>
446 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVE')) . '" class="button button-primary" data-action="save_gptranslate_record">
447 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVEANDECLOSE')) . '" class="button button-primary" data-action="save_gptranslate_record_and_close">
448 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CANCEL')) . '" class="button button-primary" data-action="cancel_gptranslate_record">
449 </p>
450 <input type="hidden" name="languagetranslated" id="languagetranslated" value="' . esc_attr($record->languagetranslated) . '">
451 <input type="hidden" name="action" id="form_action" value="save_gptranslate_record">
452 <input type="hidden" name="id" value="' . (int) $record->id . '">
453
454 <table class="form-table">
455 <tr>
456 <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>
457 <td><input type="text" id="pagelink" name="pagelink" value="' . esc_attr($record->pagelink) . '" class="regular-text code"></td>
458 </tr>' .
459 $rewriteAliasRow .
460 '<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
461 '<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
462 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED')) . '</label></th><td>' . wp_kses_post($pubIcon) . '</td></tr>' .
463 '<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>' .
464 '<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>
465
466 <tr>
467 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')) . '</label></th>
468 <td>
469 <div class="gptcard gptcard-default">
470 <div class="gptcard-header">
471 <div class="accordion-toggle">
472 <div class="input-group">
473 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
474 <input type="text" name="search" value="" class="text_area">
475 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
476 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
477 </div>
478 </div>
479 </div>
480 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
481 <button type="button" class="btn btn-success btn-adder" data-addtype="translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_TRANSLATION')) . '</button>
482 <textarea name="translations_json" id="translations_json" hidden>' . esc_textarea(json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
483 <div id="translations-container"></div>
484 </div>
485 </div>
486 </td>
487 </tr>
488
489 <tr class="alt_translations_' . (int)$opts['translate_altimages'] . '">
490 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS')) . '</label></th>
491 <td>
492 <div class="gptcard gptcard-default">
493 <div class="gptcard-header">
494 <div class="accordion-toggle">
495 <div class="input-group">
496 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
497 <input type="text" name="search" value="" class="text_area">
498 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
499 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
500 </div>
501 </div>
502 </div>
503 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
504 <button type="button" class="btn btn-success btn-adder" data-addtype="alt-translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALT_TRANSLATION')) . '</button>
505 <textarea name="alt_translations_json" id="alt_translations_json" hidden>' . esc_textarea(json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
506 <div id="alt-translations-container"></div>
507 </div>
508 </div>
509 </td>
510 </tr>
511 </table>' .
512 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce field is safe
513 wp_nonce_field('gptranslate_save_record_action', '_gptranslate_nonce') .
514 '</form>';
515
516 echo '<script>
517 const initialTranslations = ' . (json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
518 const initialAltTranslations = ' . (json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
519
520 const PLG_GPTRANSLATE_ORIGINAL_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_ORIGINAL_TEXT')) . '";
521 const PLG_GPTRANSLATE_TRANSLATED_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_TEXT')) . '";
522 const PLG_GPTRANSLATE_DELETE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '";
523 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
524 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
525 const PLG_GPTRANSLATE_SYNC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC')) . '";
526 const PLG_GPTRANSLATE_SYNC_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_TITLE')) . '";
527 const PLG_GPTRANSLATE_SYNC_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_DESC')) . '";
528 const PLG_GPTRANSLATE_SYNC_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_COMPLETED')) . '";
529 const PLG_GPTRANSLATE_SYNC_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_ERROR')) . '";
530 const gptServerSideLink = "' . esc_url_raw(rest_url('gptranslate/v1/request')) . '";
531 </script>';
532 } else {
533 // FREE period
534 // 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>';
535
536 // UPGRADE period
537 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>';
538
539 // List records
540 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
541
542 // Filters section
543 $opts = get_option( 'gptranslate_options', [] );
544 $languageFilter = esc_attr( sanitize_text_field( wp_unslash( $_GET['language'] ?? '' ) ) );
545 $languages = $opts['languages'] ?? [];
546
547 // Paginate records
548 $records_per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
549 $valid_per_page_options = [5, 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 9999999];
550 if (!in_array($records_per_page, $valid_per_page_options)) {
551 $records_per_page = 10;
552 }
553
554 $searchFilter = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) );
555 $engineFilter = sanitize_key( wp_unslash( $_GET['engine'] ?? '' ) );
556 $publishedFilter = sanitize_key( wp_unslash( $_GET['published'] ?? '' ) );
557
558 echo
559 '<form method="get" class="form-filter-container">' .
560 '<div class="left-filter-container">' .
561 '<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')) . '" />' .
562 '<button type="button" class="button" onclick="this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>' .
563 '<button type="button" class="button" onclick="document.getElementById(\'search-input\').value=\'\'; this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>' .
564 '</div>' .
565 '<div class="right-filter-container">' .
566 '<input type="hidden" name="_gptranslate_nonce" value="' . esc_attr(wp_create_nonce('gptranslate_filter_action')) . '" />' .
567 '<select name="published">' .
568 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_ALL')) . '</option>' .
569 '<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>' .
570 '<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>' .
571 '</select>' .
572 '<select name="language">' .
573 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</option>';
574
575
576 // Loop languages
577 foreach ($languages as $lang) {
578 echo "<option value='" . esc_attr($lang) . "'" . esc_html($this->isSelected($languageFilter, $lang)) . ">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($lang))) . "</option>";
579 }
580
581 echo
582 '</select>' .
583 '<select name="engine">' .
584 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</option>' .
585 '<option value="gtranslate" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gtranslate')) . '>Google AI</option>' .
586 '<option value="chatgpt" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'chatgpt')) . '>ChatGPT</option>' .
587 '<option value="gemini" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'Gemini')) . '>Gemini</option>' .
588 '<option value="deepseek" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'DeepSeek')) . '>DeepSeek</option>' .
589 '</select>' .
590 '<select name="per_page">' .
591 '<option value="5" ' . esc_html($this->isSelected($records_per_page, 5)) . '>5</option>' .
592 '<option value="10" ' . esc_html($this->isSelected($records_per_page, 10)) . '>10</option>' .
593 '<option value="25" ' . esc_html($this->isSelected($records_per_page, 25)) . '>25</option>' .
594 '<option value="50" ' . esc_html($this->isSelected($records_per_page, 50)) . '>50</option>' .
595 '<option value="100" ' . esc_html($this->isSelected($records_per_page, 100)) . '>100</option>' .
596 '<option value="200" ' . esc_html($this->isSelected($records_per_page, 200)) . '>200</option>' .
597 '<option value="500" ' . esc_html($this->isSelected($records_per_page, 500)) . '>500</option>' .
598 '<option value="1000" ' . esc_html($this->isSelected($records_per_page, 1000)) . '>1000</option>' .
599 '<option value="2000" ' . esc_html($this->isSelected($records_per_page, 2000)) . '>2000</option>' .
600 '<option value="5000" ' . esc_html($this->isSelected($records_per_page, 5000)) . '>5000</option>' .
601 '<option value="10000" ' . esc_html($this->isSelected($records_per_page, 10000)) . '>10000</option>' .
602 '<option value="9999999" ' . esc_html($this->isSelected($records_per_page, 9999999)) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALL')) . '</option>' .
603 '</select>' .
604 '<input type="submit" class="button" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '" />' .
605 '</div>' .
606 '<input type="hidden" name="page" value="gptranslate" />' .
607 '</form>';
608
609 // Bottoni Import/Export
610 echo '<div class="action-buttons-toolbar">';
611 echo '<button class="button button-primary" id="bulk-delete-btn">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '</button>';
612 echo '<button class="button button-primary" id="toggle-migration">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_BTNS')) . '</button>';
613
614 // Export CSV/XLIFF
615 $exportFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_export_translations_xliff' : 'gptranslate_export_translations_csv';
616 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" style="display:inline;margin-right:10px;">';
617 if($exportFormat == 'gptranslate_export_translations_csv') {
618 wp_nonce_field('gptranslate_export_csv', 'gptranslate_export_csv_nonce');
619 } else {
620 wp_nonce_field('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce');
621 }
622 echo '<input type="hidden" name="action" value="' . $exportFormat . '">';
623 echo '<input type="submit" class="button button-primary" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXPORT_TRANSLATIONS')) . '">';
624 echo '</form>';
625
626 // Import CSV/XLIFF
627 $importFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_import_translations_xliff' : 'gptranslate_import_translations_csv';
628 echo '<input type="button" class="button button-primary button-import" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_IMPORT_TRANSLATIONS')) . '">';
629 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" enctype="multipart/form-data" style="display:inline;">';
630 if($importFormat == 'gptranslate_import_translations_csv') {
631 wp_nonce_field('gptranslate_import_csv', 'gptranslate_import_csv_nonce');
632 $acceptFormat = '.csv';
633 } else {
634 wp_nonce_field('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce');
635 $acceptFormat = '.xliff';
636 }
637 echo '<input type="hidden" name="action" value="' . $importFormat . '">';
638 echo '<input type="file" name="import_file" class="toggle-import hidden" accept="' . $acceptFormat . '" required>';
639 echo '<input type="submit" class="button button-primary toggle-import hidden" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '">';
640 echo '</form>';
641
642 echo '</div>';
643
644 echo '<div id="migraterow" class="hidden">
645 <span class="input-group">
646 <label for="migratetranslations_currentdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_PREVIOUS_DOMAIN')) . '</strong></label>
647 <input type="text" class="form-control" id="migratetranslations_currentdomain" name="migratetranslations_currentdomain" value="">
648 </span>
649 <span class="input-group">
650 <label for="migratetranslations_newdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_NEW_DOMAIN')) . '</strong></label>
651 <input type="text" class="form-control" id="migratetranslations_newdomain" name="migratetranslations_newdomain" value="">
652 </span>
653 <button class="button button-primary" id="migrationconfirm">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '</button>
654 <button class="button" id="migrationcancel">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
655 </div>
656 <input type="button" class="button button-warning button-crawler" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '">
657
658 <script>
659 const PLG_GPTRANSLATE_MIGRATION_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_SUCCESS')) . '";
660 const PLG_GPTRANSLATE_MIGRATION_FAILED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_FAILED')) . '";
661 const PLG_GPTRANSLATE_UNKNOWN_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_UNKNOWN_ERROR')) . '";
662 const PLG_GPTRANSLATE_NETWORK_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_NETWORK_ERROR')) . '";
663 const PLG_GPTRANSLATE_BULK_DELETE_CONFIRM = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_CONFIRM')) . '";
664 const PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE')) . '";
665 const PLG_GPTRANSLATE_BULK_DELETE_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SUCCESS')) . '";
666 const PLG_GPTRANSLATE_BULK_DELETE_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_ERROR')) . '";
667 const PLG_GPTRANSLATE_BULK_DELETE_NETWORK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_NETWORK')) . '";
668 const PLG_GPTRANSLATE_CRAWLER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '";
669 const PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE')) . '";
670 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
671 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
672 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START')) . '";
673 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
674 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
675 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
676 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
677 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
678 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
679 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
680 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
681 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
682 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
683 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
684 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
685 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
686 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
687 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
688 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
689 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
690 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
691 var gptranslateBaseCrawlerHome = "' . esc_js( trailingslashit( get_site_url() ) ) . '";
692 var gptranslateDefaultLanguage = "' . $opts['language'] . '";
693 var gptranslateCrawlerTimeout = "' . (isset($opts['crawler_timeout']) ? $opts['crawler_timeout'] : '30') . '";
694 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($opts['crawler_exclusions']) ? $opts['crawler_exclusions'] : ''))), ',')) . '";
695 var gptranslateRewriteLanguageUrl = ' . (int)$opts['rewrite_language_url'] . ';
696 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($opts['omit_prefix_original_language']) ? (int)$opts['omit_prefix_original_language'] : 0) . ';
697 var gptVersionNumeric = ' . 0 . ';
698 </script>';
699
700
701 echo '<table class="widefat fixed striped">';
702 echo '<thead><tr>';
703 echo '<th style="width: 1%"><input class="form-check-input" autocomplete="off" type="checkbox" id="checkall"></th>';
704 echo '<th style="width: 2%;;white-space:nowrap">ID</th>';
705 if($opts['rewrite_language_alias'] == 1) {
706 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
707 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</th>';
708 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
709 } else {
710 echo '<th style="width: 25%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
711 echo '<th style="width: 20%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
712 }
713 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</th>';
714 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</th>';
715 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</th>';
716 echo '<th style="width:5%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_ENGINE')) . '</th>';
717 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DATE')) . '</th>';
718 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ACTIONS')) . '</th>';
719 echo '</tr></thead>';
720
721 echo '<tbody>';
722
723 $current_page = isset($_GET['paged']) && is_numeric($_GET['paged']) ? (int)$_GET['paged'] : 1;
724 $offset = ($current_page - 1) * $records_per_page;
725 $sql_count = "SELECT COUNT(*) FROM {$this->table_name} WHERE 1=1";
726
727 // Add dynamic filters
728 if (!empty($searchFilter)) {
729 $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) . "%'" . ")";
730 }
731 if ($publishedFilter !== '') {
732 $sql_count .= " AND published = '" . esc_sql($publishedFilter) . "'";
733 }
734 if (!empty($languageFilter)) {
735 $sql_count .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
736 }
737 if (!empty($engineFilter)) {
738 $sql_count .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
739 }
740 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
741 $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
742 $total_pages = ceil($total_records / $records_per_page);
743
744 // Load records count with filtering
745 $sql_data = "SELECT * FROM {$this->table_name} WHERE 1=1";
746
747 // Add dynamic filters
748 if (!empty($searchFilter)) {
749 $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) . "%'" . ")";
750 }
751 if ($publishedFilter !== '') {
752 $sql_data .= " AND published = '" . esc_sql($publishedFilter) . "'";
753 }
754 if (!empty($languageFilter)) {
755 $sql_data .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
756 }
757 if (!empty($engineFilter)) {
758 $sql_data .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
759 }
760 $sql_data .= " ORDER BY translate_date DESC LIMIT $records_per_page OFFSET $offset";
761
762 // Load records with filtering and pagination
763 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
764 $records = $wpdb->get_results($sql_data); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
765
766 // Message for no translations
767 if (!count($records) && stripos($sql_data, "AND") === false) {
768 echo '<div class="notice notice-success is-dismissible" id="notranslations-notice">';
769 echo '<p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO_TRANSLATIONS')) . '</p>';
770 echo '</div>';
771 ?>
772 <div id="gptranslate-zero-state" class="gptranslate-zero-state">
773 <div class="gptranslate-zero-icon">
774 <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">
775 <g id="language">
776 <g>
777 <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
778 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
779 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
780 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
781 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
782 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"/>
783 </g>
784 </g>
785 </svg>
786 </div>
787
788 <div class="gptranslate-zero-title">
789 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_TITLE')); ?>
790 </div>
791
792 <p class="gptranslate-zero-desc">
793 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_DESC')); ?>
794 </p>
795
796 <button id="gptranslate-start-crawler" class="button button-primary button-hero">
797 <span class="dashicons-before dashicons-translation" aria-hidden="true"></span>
798 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_BUTTON')); ?>
799 </button>
800
801 <p class="gptranslate-zero-hint">
802 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_HINT')); ?>
803 </p>
804 </div>
805 <?php
806 }
807
808 foreach ( $records as $r ) {
809 // Build a short summary of the translations
810 $tr = json_decode( $r->translations, true );
811 if ( is_array( $tr ) ) {
812 $pairs = array_map(
813 function( $k, $v ) {
814 return esc_html( $k ) . '' . esc_html( $v );
815 },
816 array_keys( $tr ),
817 array_values( $tr )
818 );
819 $short = implode( ', ', $pairs );
820 $short = mb_substr( $short, 0, 80 ) . ( mb_strlen( $short ) > 80 ? '' : '' );
821 } else {
822 $short = '';
823 }
824
825 // Escape all output
826 $id = (int) $r->id;
827 $link = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=edit&edit={$id}" ), 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
828 $deleteLink = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=delete_translation&translation_id={$id}" ), 'gptranslate_delete_' . $id, '_gptranslate_nonce' );
829 $origLang = esc_html( strtoupper( $r->languageoriginal ) );
830 $transLang = esc_html( strtoupper( $r->languagetranslated ) );
831 $pub = $r->published ? esc_html($this->loadTranslations('PLG_GPTRANSLATE_YES')) : esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO'));
832 $engine = esc_html( $r->translation_engine );
833 $date = esc_html( $r->translate_date );
834
835 $langOriginal = esc_attr($r->languageoriginal);
836
837 // Path relativo o assoluto all'immagine della bandiera
838 $flagUrlOriginal = plugins_url('flags/svg/' . $r->languageoriginal . '.svg', __FILE__);
839 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
840 $flagUrlTranslated = plugins_url('flags/svg/' . $r->languagetranslated . '.svg', __FILE__);
841 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
842
843 $togglePublishedUrl = wp_nonce_url(
844 admin_url("admin.php?page=gptranslate&action=toggle_published&translation_id={$id}"),
845 'gptranslate_toggle_' . $id,
846 '_gptranslate_nonce'
847 );
848
849 $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
850 : '<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
851
852
853 $pub = $r->published ? "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-published' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . "'>" . $pubIcon . "</a>"
854 : "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-unpublished' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . "'>" . $pubIcon . "</a>";
855
856 $local_date = get_date_from_gmt($date);
857
858 echo '<tr>';
859 echo "<td style='width: 1%'><input class='form-check-input' autocomplete='off' type='checkbox' id='cb0' name='gptid[]' value='" . esc_attr($r->id) . "'></td>";
860 echo "<td style='width: 2%;white-space:nowrap'>". esc_html($r->id) . "</td>";
861 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>";
862 if($opts['rewrite_language_alias'] == 1) {
863 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>';
864 }
865 echo "<td>" . esc_html($short) . "</td>";
866 echo "<td>" . wp_kses_post($flagOriginal) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($langOriginal))) . "</td>";
867 echo "<td>" . wp_kses_post($flagTranslated) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper(esc_attr($r->languagetranslated)))) . "</td>";
868 echo "<td>" . wp_kses_post($pub) . "</td>";
869 echo "<td><span class='gpt-label'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper($engine) . '_ENGINE')) . "</span></td>";
870 echo "<td>" . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime($local_date)) ) . "</td>";
871 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>";
872 echo '</tr>';
873 }
874 echo '</tbody>';
875 echo '</table>';
876
877 if ($total_pages > 1) {
878 echo '<div class="tablenav"><div class="tablenav-pages">';
879 for ($i = 1; $i <= $total_pages; $i++) {
880 $url = add_query_arg(array_merge($_GET, ['paged' => $i]), admin_url('admin.php'));
881 $class = ($i == $current_page) ? "class='current-page button'" : "class='button'";
882 echo "<a " . wp_kses_post($class) . " href='" . esc_url($url) . "'>" . esc_html($i) . "</a> ";
883 }
884 echo '</div></div>';
885 }
886 }
887
888 echo '</div>';
889 echo '</div>';
890 }
891
892 /**
893 * Load language file and translations
894 * @return array
895 */
896 public function loadTranslations($key) {
897 // Text translations
898 static $adminLanguageStrings = null;
899
900 if(!$adminLanguageStrings) {
901 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
902 if(file_exists($adminLanguageFile)) {
903 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
904 }
905 }
906
907 if(array_key_exists($key, $adminLanguageStrings)) {
908 return $adminLanguageStrings[$key];
909 }
910
911 return $key;
912 }
913
914 /**
915 * Load language file and translations
916 * @return array
917 */
918 public static function loadTranslation($key) {
919 // Text translations
920 static $adminLanguageStrings = null;
921
922 if(!$adminLanguageStrings) {
923 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
924 if(file_exists($adminLanguageFile)) {
925 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
926 }
927 }
928
929 if(array_key_exists($key, $adminLanguageStrings)) {
930 return $adminLanguageStrings[$key];
931 }
932
933 return $key;
934 }
935
936 /**
937 * Save translation record
938 *
939 * @access public
940 */
941 public function save_record() {
942 global $wpdb;
943
944 // Retrieve and sanitize basic inputs
945 $id = isset ( $_POST ['id'] ) ? intval ( $_POST ['id'] ) : 0;
946 $formAction = isset ( $_POST ['action'] ) ? sanitize_key ( $_POST ['action'] ) : '';
947
948 if ( !isset($_POST['_gptranslate_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_gptranslate_nonce'])), 'gptranslate_save_record_action') ) {
949 wp_die(esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'gptranslate');
950 }
951
952 // Handle cancel action
953 if ($formAction === 'cancel_gptranslate_record') {
954 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
955 exit ();
956 }
957
958 // Sanitize pagelink
959 $pagelink = isset ( $_POST ['pagelink'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['pagelink'] ) ) : '';
960
961 // Sanitize translated_alias
962 $translatedAlias = isset ( $_POST ['translated_alias'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['translated_alias'] ) ) : '';
963
964 // Process and sanitize translations JSON
965 $raw_translations = filter_input( INPUT_POST, 'translations_json', FILTER_UNSAFE_RAW );
966 $raw_translations = is_string($raw_translations) ? $raw_translations : '[]';
967
968 $decoded_translations = json_decode ( $raw_translations, true );
969 if (! is_array ( $decoded_translations )) {
970 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
971 'response' => 400
972 ] );
973 }
974 $clean_translations = $decoded_translations;
975 $sanitized_translations_json = wp_json_encode ( $clean_translations );
976
977 // Process and sanitize alternative translations JSON
978 $raw_alt = filter_input( INPUT_POST, 'alt_translations_json', FILTER_UNSAFE_RAW );
979 $raw_alt = is_string($raw_translations) ? $raw_alt : '[]';
980
981 $decoded_alt = json_decode ( $raw_alt, true );
982 if (! is_array ( $decoded_alt )) {
983 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_ALTTRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
984 'response' => 400
985 ] );
986 }
987 $clean_alt = $decoded_alt;
988 $sanitized_alt_json = wp_json_encode ( $clean_alt );
989
990 // Update database record
991 $wpdb->update ( $this->table_name, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
992 'pagelink' => $pagelink,
993 'translated_alias' => $translatedAlias,
994 'translations' => $sanitized_translations_json,
995 'alt_translations' => $sanitized_alt_json
996 ], [
997 'id' => $id
998 ], [
999 '%s',
1000 '%s',
1001 '%s'
1002 ], [
1003 '%d'
1004 ] );
1005
1006 // Redirect based on action
1007 if ($formAction === 'save_gptranslate_record_and_close') {
1008 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1009 } else {
1010 $url = admin_url( 'admin.php?page=gptranslate&action=edit&edit=' . $id );
1011 $url = wp_nonce_url( $url, 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1012 wp_redirect( html_entity_decode( $url ) );
1013 }
1014 exit ();
1015 }
1016
1017 public function gptranslate_handle_deletion() {
1018 // 1) Verify nonce
1019 $id = isset( $_GET['translation_id'] ) ? intval( $_GET['translation_id'] ) : 0;
1020 $nonce = isset( $_GET['_gptranslate_nonce'] ) ? wp_unslash( $_GET['_gptranslate_nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1021
1022 if ( ! wp_verify_nonce( $nonce, 'gptranslate_delete_' . $id ) ) {
1023 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
1024 }
1025
1026 // 2) Delete the row
1027 global $wpdb;
1028 $table = $wpdb->prefix . 'gptranslate';
1029 $deleted = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1030
1031 // 3) Redirect back with a message
1032 $redirect_url = add_query_arg(
1033 [
1034 'page' => 'gptranslate',
1035 'deleted' => $deleted ? '1' : '0',
1036 ],
1037 admin_url( 'admin.php' )
1038 );
1039 wp_redirect( $redirect_url );
1040 exit;
1041 }
1042
1043
1044 /**
1045 * Add main app frontend script
1046 *
1047 * @access public
1048 */
1049 public function enqueue_frontend_scripts() {
1050 add_filter('script_loader_tag', function($tag, $handle) {
1051 if ($handle === 'gptranslate-responsivevoice') {
1052 return str_replace('<script ', '<script defer ', $tag);
1053 }
1054
1055 if ($handle === 'gptranslate-jsonrepair') {
1056 return str_replace('<script ', '<script type="module" ', $tag);
1057 }
1058
1059 if ($handle === 'gptranslate-bstoast') {
1060 return str_replace('<script ', '<script type="module" ', $tag);
1061 }
1062 if ($handle === 'gptranslate-main') {
1063 // 1) prendi i valori raw
1064 $raw_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
1065 $raw_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
1066
1067 // 2) unslash WP
1068 $unslashed_uri = wp_unslash ( $raw_uri );
1069 $unslashed_host = wp_unslash ( $raw_host );
1070
1071 // 3) sanitizza URI come URL
1072 $orig_url = esc_url_raw ( $unslashed_uri );
1073 // - accetta path e query, rimuove caratteri pericolosi
1074
1075 // 4) sanitizza host
1076 // a) rimuovi tag e control chars
1077 $host_clean = sanitize_text_field ( $unslashed_host );
1078 // b) mantieni solo [a-z0-9.-] per sicurezza
1079 $orig_domain = preg_replace ( '/[^a-z0-9.-]/i', '', $host_clean );
1080
1081 // 5) infine escape per attributo HTML
1082 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 );
1083 }
1084
1085 return $tag;
1086 }, 10, 2);
1087
1088 $settings = get_option("gptranslate_options");
1089
1090 // Excluded languages check
1091 $excludedLangs = isset($settings['excluded_languages']) ? (array) $settings['excluded_languages'] : [];
1092 if (!empty($excludedLangs) && defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
1093 if (in_array(GPTRANSLATE_CURRENT_LANG, $excludedLangs, true)) {
1094 return;
1095 }
1096 }
1097
1098 // Move default language to the first one in the list
1099 if($settings ['default_language_first']) {
1100 if(!isset($settings ['languages'])) {
1101 $settings ['languages'] = array_map ( 'strtolower', [
1102 'AF',
1103 'SQ',
1104 'AM',
1105 'AR',
1106 'HY',
1107 'AZ',
1108 'EU',
1109 'BE',
1110 'BN',
1111 'BS',
1112 'BG',
1113 'CA',
1114 'CEB',
1115 'NY',
1116 'ZH',
1117 'CO',
1118 'HR',
1119 'CS',
1120 'DA',
1121 'NL',
1122 'EN',
1123 'EO',
1124 'ET',
1125 'TL',
1126 'FI',
1127 'FR',
1128 'FY',
1129 'GL',
1130 'KA',
1131 'DE',
1132 'EL',
1133 'GU',
1134 'HT',
1135 'HA',
1136 'HAW',
1137 'IW',
1138 'HI',
1139 'HMN',
1140 'HU',
1141 'IS',
1142 'IG',
1143 'ID',
1144 'GA',
1145 'IT',
1146 'JA',
1147 'JW',
1148 'KN',
1149 'KK',
1150 'KM',
1151 'KO',
1152 'KU',
1153 'KY',
1154 'LO',
1155 'LA',
1156 'LV',
1157 'LT',
1158 'LB',
1159 'MK',
1160 'MG',
1161 'MS',
1162 'ML',
1163 'MT',
1164 'MI',
1165 'MR',
1166 'MN',
1167 'MY',
1168 'NE',
1169 'NO',
1170 'PS',
1171 'FA',
1172 'PL',
1173 'PT',
1174 'PA',
1175 'RO',
1176 'RU',
1177 'SM',
1178 'GD',
1179 'SR',
1180 'ST',
1181 'SN',
1182 'SD',
1183 'SI',
1184 'SK',
1185 'SL',
1186 'SO',
1187 'ES',
1188 'SU',
1189 'SW',
1190 'SV',
1191 'TG',
1192 'TA',
1193 'TE',
1194 'TH',
1195 'TR',
1196 'UK',
1197 'UR',
1198 'UZ',
1199 'VI',
1200 'CY',
1201 'XH',
1202 'YI',
1203 'YO',
1204 'ZU',
1205 'ZT'
1206 ] );
1207 }
1208 $defaultLanguageKeyIndex = array_search($settings ['language'], $settings ['languages']);
1209 if ($defaultLanguageKeyIndex !== false) {
1210 // Remove the 'de' element from its current position
1211 $defaultLanguage = $settings ['languages'][$defaultLanguageKeyIndex];
1212 unset($settings ['languages'][$defaultLanguageKeyIndex]);
1213
1214 // Re-index the array to maintain numerical indexes
1215 $settings ['languages'] = array_values($settings ['languages']);
1216
1217 // Add 'de' to the beginning of the array
1218 array_unshift($settings ['languages'], $defaultLanguage);
1219 }
1220 }
1221
1222 // build alt_flags array
1223 $alt_flags = array ();
1224 $raw_alt_flags = isset($settings ['alt_flags']) ? $settings ['alt_flags'] : [];
1225 foreach ( $raw_alt_flags as $country ) {
1226 if ($country == 'usa' || $country == 'canada' || $country == 'ireland')
1227 $alt_flags ['en'] = $country;
1228 elseif ($country == 'brazil')
1229 $alt_flags ['pt'] = $country;
1230 elseif ($country == 'mexico' or $country == 'argentina' or $country == 'colombia')
1231 $alt_flags ['es'] = $country;
1232 elseif ($country == 'quebec')
1233 $alt_flags ['fr'] = $country;
1234 elseif ($country == 'taiwan')
1235 $alt_flags ['zh'] = $country;
1236 elseif ($country == 'hongkong')
1237 $alt_flags ['zt'] = $country;
1238 }
1239
1240 // Build float position variables
1241 $float_position = $settings ['float_position'];
1242 if($float_position != 'inline'){
1243 list ( $switcher_vertical_position, $switcher_horizontal_position ) = explode ( '-', $float_position );
1244 } else {
1245 list ( $switcher_vertical_position, $switcher_horizontal_position ) = ['inline', 'inline'];
1246 }
1247
1248 // Set local flags path
1249 $flagsPath = trailingslashit(plugins_url('flags', __FILE__));
1250
1251 // Ensure a default array value
1252 if(!isset($settings['realtime_translations_retrigger_events'])) {
1253 $settings['realtime_translations_retrigger_events'] = ['click'];
1254 $settings['realtime_translations_retrigger_events_delay'] = 200;
1255 }
1256 if(!isset ( $settings ['translate_srcimages'] )) {
1257 $settings ['translate_srcimages'] = 0;
1258 }
1259 if(!isset ( $settings ['css_selector_realtime_translations_retrigger'] )) {
1260 $settings ['css_selector_realtime_translations_retrigger'] = '';
1261 }
1262 if(!isset($settings ['serverside_translations_language_switching_mode'])) {
1263 $settings ['serverside_translations_language_switching_mode'] = 'url';
1264 }
1265
1266 wp_register_script('gptranslate-main-inline', '', [], $this->version, true);
1267 wp_enqueue_script('gptranslate-main-inline');
1268
1269 // Example: $settings is an array like in Joomla
1270 $base64Encode = 'base' . 64 . '_encode';
1271 $key = $settings['chatgpt_apikey'];
1272 $key = strrev($key);
1273 $secret = 'gptranslate';
1274 $out = '';
1275 for ($i = 0; $i < strlen($key); $i++) {
1276 $out .= chr(ord($key[$i]) ^ ord($secret[$i % strlen($secret)]));
1277 }
1278 $encoded = $base64Encode($out);
1279
1280 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
1281
1282 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";
1283 var gptApiKey = "' . esc_js(hash( 'sha256', get_site_url() )) . '";
1284 var gptLiveSite = "' . esc_js(get_site_url()) . '";
1285 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
1286 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
1287 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
1288 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
1289 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
1290 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
1291 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
1292 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
1293 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
1294 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
1295 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
1296 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
1297 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
1298 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
1299 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
1300 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
1301 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
1302 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
1303 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
1304 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
1305 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
1306 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
1307 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
1308 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
1309 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
1310 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
1311 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
1312 var chatgptApiKey = "' . esc_js($encoded) . '";
1313 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
1314 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
1315 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
1316 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
1317 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
1318 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
1319 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
1320 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
1321 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
1322 var chatgptWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['words_leafnodes_excluded'], ', ')) . '";
1323 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
1324 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
1325 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
1326 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
1327 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
1328 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
1329 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
1330 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
1331 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
1332 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
1333 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
1334 var gptVersionNumeric = ' . 0 . ';
1335 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>\';';
1336
1337 // Inject it AFTER gptranslate-main-inline
1338 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
1339
1340 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
1341 wp_enqueue_script('gptranslate-js-specs');
1342 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
1343 window.gptranslateSettings["1"] = {
1344 "default_language": "' . $settings['language'] . '",
1345 "languages": ' . json_encode($settings['languages']) . ',
1346 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
1347 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
1348 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
1349 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
1350 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
1351 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
1352 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
1353 "widget_text_color": "' . $settings['widget_text_color'] . '",
1354 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
1355 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
1356 "equal_widths": ' . (int)$settings['equal_widths'] . ',
1357 "reader_button_position": "' . $settings['reader_button_position'] . '",
1358 "custom_css": "' . addslashes(preg_replace('/\s+/', ' ', str_replace(["\r", "\n"], ' ', (string) $settings['custom_css']))) . '",
1359 "alt_flags": ' . json_encode($alt_flags). ',
1360 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
1361 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
1362 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
1363 "switcher_vertical_position": "' . $switcher_vertical_position . '",
1364 "flags_location": "' . esc_js($flagsPath) . '",
1365 "flag_loading": "' . $settings['flag_loading'] . '",
1366 "flag_style": "' . $settings['flag_style'] . '",
1367 "widget_max_height": ' . (int)$settings['widget_max_height'] . '
1368 };');
1369
1370 $languageStringsScript = '';
1371
1372 // Generic translations
1373 $labels = [
1374 'TRANSLATING',
1375 'TRANSLATING_WAIT',
1376 'TRANSLATING_COMPLETE',
1377 'READING_INPROGRESS',
1378 'READING_END',
1379 'READING_EMPTY'
1380 ];
1381
1382 foreach ($labels as $label) {
1383 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
1384 }
1385
1386 $languages = [
1387 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
1388 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
1389 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
1390 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KM', 'KO', 'KU', 'KY', 'LO',
1391 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
1392 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
1393 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
1394 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
1395 ];
1396
1397
1398 foreach ($languages as $lang) {
1399 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
1400 }
1401
1402 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
1403 wp_enqueue_script('gptranslate-js-language-strings');
1404 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
1405
1406
1407 // Dictionary
1408 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
1409 if ($words_leafnodes_excluded_bylanguage_repeatable) {
1410 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
1411 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
1412 }
1413
1414 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
1415 $formatted = [];
1416 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
1417 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
1418 'words_leafnodes_excluded_bylanguage' => $row['word'] ?? '',
1419 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
1420 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
1421 'words_leafnodes_excluded_bylanguage_translation' => $row['optionalTranslation'] ?? ''
1422 ];
1423 }
1424
1425 // Correctly formatted JSON encode
1426 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1427
1428 // Inietto i dati PRIMA che venga eseguito gptranslate.js
1429 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
1430 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
1431 wp_add_inline_script(
1432 'gptranslate-js-word-leafones-excluded-language',
1433 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
1434 );
1435 }
1436
1437 // Local or remote script
1438 if($settings['proxy_responsive_loading_script'] == 1) {
1439 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
1440 } else {
1441 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
1442 }
1443
1444 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
1445 wp_enqueue_script('gptranslate-main', plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js', [], $this->version, true);
1446
1447 // Enqueue Bootstrap component
1448 if(!$settings['disable_bootstrap_css']) {
1449 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
1450 wp_enqueue_style(
1451 'bootstrap-css',
1452 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
1453 [],
1454 '5.3.2'
1455 );
1456 } else {
1457 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
1458 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
1459 wp_enqueue_style('gptranslate-bootstrap-style');
1460 wp_add_inline_style('gptranslate-bootstrap-style', '
1461 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
1462 .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}
1463 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
1464 .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)}
1465 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
1466 .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}
1467 .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}
1468 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
1469 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
1470 .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}
1471 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
1472 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
1473 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
1474 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
1475 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
1476 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
1477 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
1478 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
1479 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
1480 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
1481 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
1482 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
1483 ');
1484
1485 // Closer for the toast element
1486 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
1487 wp_enqueue_script('gptranslate-toast-dismiss');
1488 wp_add_inline_script('gptranslate-toast-dismiss', '
1489 document.addEventListener("DOMContentLoaded", function() {
1490 document.addEventListener("click", function(e) {
1491 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
1492 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
1493 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
1494 const toast = btnClose.closest(".toast");
1495 if (toast) {
1496 toast.classList.remove("show");
1497 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
1498 if (progressContainer) {
1499 progressContainer.remove();
1500 }
1501 }
1502 }
1503 });
1504 });
1505 ');
1506 }
1507
1508 // Registra un handle CSS vuoto se necessario
1509 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
1510 wp_enqueue_style('gptranslate-dynamic-css');
1511
1512 // Prepara lo stile dinamico
1513 $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') . '; }' .
1514 '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; }' .
1515 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
1516 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
1517
1518 // Inietta il CSS inline
1519 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
1520
1521 // --- Load theme RTL stylesheet if available ---
1522 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
1523 // Common file names
1524 $rtl_candidates = array(
1525 get_stylesheet_directory() . '/style-rtl.css',
1526 get_stylesheet_directory() . '/rtl.css'
1527 );
1528
1529 $rtl_file = '';
1530 foreach ( $rtl_candidates as $candidate ) {
1531 if ( file_exists( $candidate ) ) {
1532 $rtl_file = $candidate;
1533 break;
1534 }
1535 }
1536
1537 if ( $rtl_file ) {
1538 $rtl_uri = str_replace(
1539 get_stylesheet_directory(),
1540 get_stylesheet_directory_uri(),
1541 $rtl_file
1542 );
1543
1544 wp_enqueue_style(
1545 'theme-rtl',
1546 $rtl_uri,
1547 array(),
1548 filemtime( $rtl_file )
1549 );
1550 }
1551 }
1552 }
1553 }
1554
1555 // Force WordPress.org update check on plugin activation
1556 register_activation_hook( __FILE__, function() {
1557 if ( function_exists('wp_update_plugins') ) {
1558 wp_update_plugins();
1559 }
1560 });
1561
1562 // Schedule a daily update check
1563 add_action( 'gptranslate_daily_update_check', function() {
1564 if ( function_exists('wp_update_plugins') ) {
1565 wp_update_plugins();
1566 }
1567 });
1568
1569 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
1570 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
1571 }
1572
1573 // 🧹 Cleanup scheduled event on deactivation
1574 register_deactivation_hook( __FILE__, function() {
1575 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
1576 });
1577
1578 /**
1579 * Global function to add links to WP
1580 *
1581 * @access public
1582 */
1583 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) {
1584 $settings_link = '<a href="admin.php?page=gptranslate">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
1585 array_unshift($links, $settings_link);
1586 return $links;
1587 });
1588
1589 /**
1590 * Add main admin scripts for example to manage records add/delete functions
1591 *
1592 * @access public
1593 */
1594 add_action('admin_enqueue_scripts', function() {
1595 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1596 // Enqueue JS
1597 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
1598 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
1599
1600 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1601 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
1602 }
1603
1604 // Enqueue CSS
1605 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
1606 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
1607
1608 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1609 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
1610 }
1611
1612 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
1613 'ajaxurl' => admin_url('admin-ajax.php'),
1614 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
1615 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
1616 'gptApiKey' => hash( 'sha256', get_site_url() )
1617 ]);
1618 }
1619 });
1620
1621 add_action('wp_ajax_gptranslate_bulk_delete', function () {
1622 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
1623 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1624 }
1625
1626 global $wpdb;
1627 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
1628
1629 if (empty($ids)) {
1630 wp_send_json_error('No records selected');
1631 }
1632
1633 $table = $wpdb->prefix . 'gptranslate';
1634 $in = implode(',', array_fill(0, count($ids), '%d'));
1635 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
1636 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
1637
1638 if ($result === false) {
1639 wp_send_json_error('Database error');
1640 }
1641
1642 wp_send_json_success();
1643 });
1644
1645 // Handle Export CSV
1646 add_action('admin_post_gptranslate_export_translations_csv', function () {
1647 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
1648 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1649 }
1650
1651 global $wpdb;
1652 $table = $wpdb->prefix . 'gptranslate';
1653
1654 $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
1655
1656 if (!$records) {
1657 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
1658 }
1659
1660 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1661 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
1662
1663 header('Content-Type: text/csv');
1664 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
1665
1666 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1667 $output = fopen('php://output', 'w');
1668
1669 // Intestazioni CSV
1670 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
1671
1672 foreach ($records as $record) {
1673 fputcsv($output, $record, ",", '"', "\\");
1674 }
1675
1676 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1677 fclose($output);
1678 exit;
1679 });
1680
1681 // Handle Import CSV
1682 add_action('admin_post_gptranslate_import_translations_csv', function () {
1683 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
1684 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1685 }
1686
1687 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
1688 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
1689 }
1690
1691 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1692 if (!is_uploaded_file($tmp_name)) {
1693 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1694 }
1695
1696 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1697 $file = fopen($tmp_name, 'r');
1698 if (!$file) {
1699 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1700 }
1701
1702 $headers = fgetcsv($file);
1703 if (!$headers) {
1704 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
1705 }
1706
1707 global $wpdb;
1708 $table = $wpdb->prefix . 'gptranslate';
1709
1710 while (($row = fgetcsv($file)) !== false) {
1711 $countHeaders = count($headers);
1712 $countRow = count($row);
1713 // Invalid combine
1714 if($countHeaders != $countRow) {
1715 continue;
1716 }
1717 $record = array_combine($headers, $row);
1718 if (empty($record['pagelink'])) {
1719 continue; // skip if no primary key
1720 }
1721
1722 $pagelink = sanitize_text_field($record['pagelink']);
1723 $exists = $wpdb->get_var($wpdb->prepare(
1724 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
1725 $pagelink, $record['languageoriginal'], $record['languagetranslated']
1726 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1727
1728 $data = [
1729 'translated_alias' => $record['translated_alias'],
1730 'translations' => $record['translations'],
1731 'alt_translations' => $record['alt_translations'],
1732 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
1733 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
1734 'published' => isset($record['published']) ? (int)$record['published'] : 1,
1735 'translate_date' => $record['translate_date'],
1736 'translation_engine' => sanitize_text_field($record['translation_engine']),
1737 ];
1738
1739 if ($exists) {
1740 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1741 $table,
1742 $data,
1743 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
1744 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
1745 ['%s','%s','%s']
1746 );
1747 } else {
1748 $data['pagelink'] = $pagelink;
1749 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1750 $table,
1751 $data,
1752 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
1753 );
1754 }
1755 }
1756
1757 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1758 fclose($file);
1759
1760 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
1761 exit;
1762 });
1763
1764 // Handle Export XLIFF
1765 add_action('admin_post_gptranslate_export_translations_xliff', function () {
1766 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
1767 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1768 }
1769
1770 global $wpdb;
1771 $table = $wpdb->prefix . 'gptranslate';
1772
1773 $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
1774
1775 if (!$records) {
1776 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
1777 }
1778
1779 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1780 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
1781
1782 header('Content-Type: application/xml; charset=utf-8');
1783 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
1784
1785 $xml = new SimpleXMLElement('<xliff/>');
1786 $xml->addAttribute('version', '1.2');
1787
1788 foreach ($records as $record) {
1789 $file = $xml->addChild('file');
1790 $file->addAttribute('source-language', $record['languageoriginal']);
1791 $file->addAttribute('target-language', $record['languagetranslated']);
1792 $file->addAttribute('datatype', 'html');
1793 $file->addAttribute('original', $record['pagelink']);
1794
1795 $body = $file->addChild('body');
1796
1797 $translations = json_decode($record['translations'], true) ?: [];
1798 foreach ($translations as $source => $target) {
1799 $unit = $body->addChild('trans-unit');
1800 $unit->addAttribute('id', md5($source));
1801 $unit->addChild('source', htmlspecialchars($source));
1802 $unit->addChild('target', htmlspecialchars($target));
1803 }
1804 }
1805
1806 echo $xml->asXML();
1807 exit;
1808 });
1809
1810 // Handle Import XLIFF
1811 add_action('admin_post_gptranslate_import_translations_xliff', function () {
1812 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
1813 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1814 }
1815
1816 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
1817 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
1818 }
1819
1820 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1821 if (!is_uploaded_file($tmp_name)) {
1822 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1823 }
1824
1825 $xml = simplexml_load_file($tmp_name);
1826 if (!$xml) {
1827 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
1828 }
1829
1830 global $wpdb;
1831 $table = $wpdb->prefix . 'gptranslate';
1832
1833 foreach ($xml->file as $file) {
1834 $sourceLang = (string)$file['source-language'];
1835 $targetLang = (string)$file['target-language'];
1836 $pagelink = (string)$file['original'];
1837
1838 $translations = [];
1839 foreach ($file->body->{'trans-unit'} as $unit) {
1840 $src = (string)$unit->source;
1841 $tgt = (string)$unit->target;
1842 $translations[$src] = $tgt;
1843 }
1844
1845 $json_translations = wp_json_encode($translations);
1846
1847 $exists = $wpdb->get_var($wpdb->prepare(
1848 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
1849 $pagelink, $sourceLang, $targetLang
1850 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1851
1852 if ($exists) {
1853 // �
1854 Update only the translations and date, keep everything else intact
1855 $wpdb->update(
1856 $table,
1857 [
1858 'translations' => $json_translations,
1859 'translate_date' => current_time('mysql'),
1860 ],
1861 ['id' => $exists]
1862 ); // phpcs:ignore
1863 } else {
1864 // �
1865 Insert full record only if it doesn't exist
1866 $data = [
1867 'pagelink' => $pagelink,
1868 'translated_alias' => '',
1869 'translations' => $json_translations,
1870 'alt_translations' => '[]',
1871 'languageoriginal' => $sourceLang,
1872 'languagetranslated'=> $targetLang,
1873 'published' => 1,
1874 'translate_date' => current_time('mysql'),
1875 'translation_engine'=> 'chatgpt',
1876 ];
1877 $wpdb->insert($table, $data); // phpcs:ignore
1878 }
1879 }
1880
1881 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
1882 exit;
1883 });
1884
1885 // Handle Export XML Sitemap
1886 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
1887 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
1888 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1889 }
1890
1891 global $wpdb;
1892 $table = $wpdb->prefix . 'gptranslate';
1893
1894 // Solo record con translated_alias non vuoto
1895 $records = $wpdb->get_results("
1896 SELECT *
1897 FROM $table
1898 WHERE translated_alias IS NOT NULL
1899 AND translated_alias != ''
1900 ORDER BY translate_date DESC
1901 ", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1902
1903 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1904 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
1905
1906 $dom = new DOMDocument('1.0', 'UTF-8');
1907 $dom->formatOutput = true;
1908
1909 $urlset = $dom->createElement('urlset');
1910 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
1911 $dom->appendChild($urlset);
1912
1913 foreach ($records as $record) {
1914 $url = $dom->createElement('url');
1915 $loc = $dom->createElement('loc');
1916 $loc->appendChild($dom->createTextNode($record['translated_alias']));
1917 $url->appendChild($loc);
1918
1919 if (!empty($record['translate_date'])) {
1920 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
1921 $url->appendChild($lastmod);
1922 }
1923
1924 $urlset->appendChild($url);
1925 }
1926
1927 header('Content-Type: application/xml; charset=UTF-8');
1928 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
1929 echo $dom->saveXML();
1930 exit;
1931 });
1932
1933 // Register Ajax handler
1934 add_action('wp_ajax_gptranslate_migrate_translations', function() {
1935 if (!current_user_can('manage_options')) {
1936 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1937 }
1938
1939 check_ajax_referer('gptranslate_migrate_translations');
1940
1941 global $wpdb;
1942 $table = $wpdb->prefix . 'gptranslate';
1943 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
1944 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
1945
1946 if (empty($old) || empty($new)) {
1947 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
1948 }
1949
1950 $query = $wpdb->prepare(
1951 "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
1952 $old, $new, $old, $new
1953 );
1954
1955 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1956 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1957
1958 if ($result === false) {
1959 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
1960 } else {
1961 wp_send_json_success();
1962 }
1963 });
1964
1965 function gptranslate_export_settings() {
1966 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
1967 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
1968 }
1969
1970 $options = get_option( 'gptranslate_options', [] );
1971
1972 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1973 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
1974
1975 header( 'Content-Type: application/json' );
1976 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
1977 echo wp_json_encode( $options );
1978 exit;
1979 }
1980 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
1981
1982 function gptranslate_import_settings() {
1983 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
1984 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
1985 }
1986
1987 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
1988 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
1989 }
1990
1991 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1992 $decoded = json_decode( $content, true );
1993
1994 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
1995 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
1996 }
1997
1998 // Optional: sanitize known values, or just update if you're confident of source
1999 update_option( 'gptranslate_options', $decoded );
2000
2001 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
2002 exit;
2003 }
2004 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
2005
2006 add_action('wp_footer', function () {
2007 // Add the default target container if default CSS selector
2008 $settings = get_option("gptranslate_options");
2009
2010 // Disable interface
2011 if($settings ['disable_control']) {
2012 $settings ['wrapper_selector'] = '';
2013 }
2014
2015 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
2016 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
2017 }
2018 });
2019
2020 // POST API REST Translations storage
2021 add_action('rest_api_init', function () {
2022 register_rest_route('gptranslate/v1', '/request', [
2023 'methods' => 'POST',
2024 'callback' => 'gpt_handle_request',
2025 'permission_callback' => 'gptranslate_public_permission'
2026 ]);
2027 });
2028
2029 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
2030 // Remove the default 'Settings' item
2031 unset($links[0]);
2032
2033 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
2034 array_unshift($links, $settings_link);
2035
2036 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
2037 array_unshift($links, $translations_link);
2038
2039 return $links;
2040 });
2041
2042 /*
2043 // Remove any WP update for the free version over the paid full one
2044 add_filter('auto_update_plugin', function($update, $item) {
2045 if (isset($item->slug) && $item->slug === 'gptranslate') {
2046 return false;
2047 }
2048 return $update;
2049 }, 10, 2);
2050
2051
2052 add_filter('site_transient_update_plugins', function($transient) {
2053 if (isset($transient->response['gptranslate/gptranslate.php'])) {
2054 unset($transient->response['gptranslate/gptranslate.php']);
2055 }
2056 return $transient;
2057 });
2058 */
2059
2060 /**
2061 * Permission callback public API
2062 * @param WP_REST_Request $request
2063 * @return bool|WP_Error
2064 */
2065 function gptranslate_public_permission( WP_REST_Request $request ) {
2066 // 1) Controllo chiave API inviata via header
2067 $headerApiKey = $request->get_header('x-gptranslate-key');
2068 $restApiKey = hash( 'sha256', get_site_url() );
2069 if ( $headerApiKey != $restApiKey) {
2070 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
2071 }
2072 return true;
2073 }
2074
2075 /**
2076 * Callback per GET/STORE translations via REST.
2077 * Frontend API
2078 *
2079 * @param WP_REST_Request $request
2080 * @return WP_REST_Response
2081 */
2082 function gpt_handle_request( WP_REST_Request $request ) {
2083 global $wpdb;
2084
2085 $table = $wpdb->prefix . 'gptranslate';
2086
2087 $params = $request->get_json_params();
2088
2089 if(!$params) {
2090 $params = $request->get_body_params();
2091 }
2092
2093 // Sanitize input params
2094 $task = sanitize_text_field( $params['task'] ?? '' );
2095 $pageLink = sanitize_text_field( $params['pagelink'] ?? '' );
2096 $translatedAlias = sanitize_text_field( $params['translated_alias'] ?? '' );
2097 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
2098 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
2099 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
2100 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
2101
2102 $now = current_time( 'mysql' );
2103
2104 $response = [ 'result' => false ];
2105
2106 if ( $task === 'storetranslations' ) {
2107 // Fetch raw param (could be array or JSON string)
2108 $rawFull = $params['translations'] ?? '[]';
2109 $rawAlt = $params['alt_translations'] ?? '[]';
2110
2111 // If it’s already a string (JSON), use it directly.
2112 // If it’s an array (unlikely with FormData), JSON encode it.
2113 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
2114 $fullTranslations = $rawFull;
2115 } else {
2116 $fullTranslations = wp_json_encode( (array) $rawFull );
2117 }
2118
2119 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
2120 $altTranslations = $rawAlt;
2121 } else {
2122 $altTranslations = wp_json_encode( (array) $rawAlt );
2123 }
2124
2125 // Verifica se esiste già
2126 $existing_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2127 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2128 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2129 "\n AND languageoriginal = %s" .
2130 "\n AND languagetranslated = %s",
2131 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2132 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
2133
2134 // Only if it is a retrigger then ignore the db processing
2135 if($retriggerTranslation !== 1) {
2136 if ( $existing_id ) {
2137 // UPDATE
2138 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2139 $table,
2140 [
2141 'translations' => $fullTranslations,
2142 'alt_translations' => $altTranslations,
2143 'translated_alias' => $translatedAlias,
2144 'translate_date' => $now,
2145 'translation_engine' => $translationEngine,
2146 ],
2147 [ 'id' => (int) $existing_id ],
2148 [ '%s', '%s', '%s', '%s', '%s' ],
2149 [ '%d' ]
2150 );
2151 $response['result'] = ( $updated !== false );
2152 } else {
2153 // INSERT
2154 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2155 $table,
2156 [
2157 'pagelink' => $pageLink,
2158 'translations' => $fullTranslations,
2159 'alt_translations' => $altTranslations,
2160 'translated_alias' => $translatedAlias,
2161 'languageoriginal' => $languageOriginal,
2162 'languagetranslated' => $languageTranslated,
2163 'published' => 1,
2164 'translate_date' => $now,
2165 'translation_engine' => $translationEngine,
2166 ],
2167 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
2168 );
2169 $response['result'] = ( $inserted !== false );
2170 }
2171 } else {
2172 $response['result'] = true;
2173 }
2174 } elseif ( $task === 'gettranslations' ) {
2175 $opts = get_option( 'gptranslate_options', [] );
2176 if ( ! empty( $opts['realtime_translations'] ) || $retriggerTranslation === 1) {
2177 $response['result'] = false;
2178 } else {
2179 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
2180 $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2181 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2182 "\n WHERE (pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s)" .
2183 "\n AND languageoriginal = %s" .
2184 "\n AND languagetranslated = %s" .
2185 "\n AND published = 1",
2186 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2187 ), ARRAY_A );
2188 } else {
2189 $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2190 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2191 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2192 "\n AND languageoriginal = %s" .
2193 "\n AND languagetranslated = %s" .
2194 "\n AND published = 1",
2195 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2196 ), ARRAY_A );
2197 }
2198
2199 if ( $row ) {
2200 $response['result'] = true;
2201 $response['translations'] = json_decode( $row['translations'], true ) ?: [];
2202 $response['alt_translations'] = json_decode( $row['alt_translations'], true ) ?: [];
2203 $response['translated_alias'] = $row['translated_alias'];
2204 $response['pagelink_alias'] = $row['pagelink'];
2205 } else {
2206 $response['result'] = false;
2207 }
2208 }
2209 } elseif ($task == 'getaliastranslation') {
2210 // Always perform a new realtime translation if the option is enabled
2211 try {
2212 $row = $wpdb->get_row( $wpdb->prepare(
2213 "SELECT translated_alias FROM {$table}" .
2214 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2215 "\n AND languageoriginal = %s" .
2216 "\n AND languagetranslated = %s" .
2217 "\n AND published = 1",
2218 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2219 ), ARRAY_A );
2220
2221 if ( $row ) {
2222 $response['result'] = true;
2223 $response['translated_alias'] = $row['translated_alias'] ?? '';
2224 } else {
2225 $response['result'] = false;
2226 }
2227 } catch ( Exception $e ) {
2228 $response['result'] = false;
2229 $response['exception'] = $e->getMessage();
2230 }
2231 } elseif ( $task === 'syncTranslation' ) {
2232 $original = wp_unslash( $params['original'] ?? '' );
2233 $translated = wp_unslash( $params['translated'] ?? '' );
2234 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
2235 $translationType = sanitize_text_field( $params['translation_type'] ?? 'translations' ); // default to 'translations'
2236
2237 // Recupera tutti i record nella lingua target
2238 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2239 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2240 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2241 "\n WHERE languagetranslated = %s",
2242 $languageTranslated
2243 ) );
2244
2245 $updatedCount = 0;
2246
2247 foreach ( $rows as $row ) {
2248 $currentTranslations = json_decode( $row->$translationType, true );
2249
2250 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
2251 // Aggiorna la traduzione e salva
2252 $currentTranslations[ $original ] = $translated;
2253 $jsonUpdated = wp_json_encode( $currentTranslations );
2254
2255 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2256 $table,
2257 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
2258 [ 'id' => $row->id ],
2259 [ '%s', '%s' ],
2260 [ '%d' ]
2261 );
2262
2263 if ( $success !== false ) {
2264 $updatedCount++;
2265 }
2266 }
2267 }
2268
2269 $response['result'] = $updatedCount > 0;
2270 } elseif ( $task === 'deepseektranslations' ) {
2271 try {
2272 // Read raw JSON payload sent from JS
2273 $rawInput = file_get_contents( 'php://input' );
2274 $requestData = json_decode( $rawInput, true );
2275
2276 if ( ! $requestData || empty( $requestData['messages'] ) ) {
2277 throw new Exception( 'Invalid DeepSeek request payload' );
2278 }
2279
2280 // Get plugin options
2281 $opts = get_option( 'gptranslate_options', [] );
2282
2283 $deepseekApiKey = $opts['chatgpt_apikey'] ?? '';
2284 $deepseekModel = $opts['chatgpt_model'] ?? 'deepseek-chat';
2285
2286 if ( empty( $deepseekApiKey ) ) {
2287 throw new Exception( 'DeepSeek API key not configured' );
2288 }
2289
2290 // Fixed DeepSeek parameters (server controlled)
2291 $payload = [
2292 'model' => $deepseekModel,
2293 'messages' => $requestData['messages'],
2294 'max_tokens' => 4096,
2295 'temperature' => 0.5,
2296 ];
2297
2298 // Call DeepSeek API
2299 $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
2300 curl_setopt_array( $ch, [
2301 CURLOPT_POST => true,
2302 CURLOPT_RETURNTRANSFER => true,
2303 CURLOPT_HTTPHEADER => [
2304 'Content-Type: application/json',
2305 'Authorization: Bearer ' . $deepseekApiKey,
2306 ],
2307 CURLOPT_POSTFIELDS => wp_json_encode( $payload ),
2308 CURLOPT_TIMEOUT => 60,
2309 ] );
2310
2311 $apiResponse = curl_exec( $ch );
2312
2313 if ( $apiResponse === false ) {
2314 throw new Exception( 'DeepSeek cURL error: ' . curl_error( $ch ) );
2315 }
2316
2317 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
2318 curl_close( $ch );
2319
2320 if ( $httpCode >= 400 ) {
2321 throw new Exception( 'DeepSeek API HTTP error: ' . $httpCode );
2322 }
2323
2324 // IMPORTANT:
2325 // Return DeepSeek response EXACTLY as the API returns it
2326 header( 'Content-Type: application/json; charset=utf-8' );
2327 echo $apiResponse;
2328 exit;
2329
2330 } catch ( Exception $e ) {
2331 // FALLBACK SAFE RESPONSE (OpenAI-compatible)
2332 header('Content-Type: application/json; charset=utf-8');
2333 echo wp_json_encode([
2334 'choices' => [[
2335 'finish_reason' => 'error',
2336 'message' => [
2337 'role' => 'assistant',
2338 'content' => ''
2339 ]
2340 ]]
2341 ]);
2342 exit;
2343 }
2344 }
2345
2346 return rest_ensure_response( $response );
2347 }
2348
2349 // Instantiate and run the app
2350 GPTranslate::get_instance();