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