PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.18
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.18
2.34.3 2.34.2 2.34.1 2.34 2.33.10 2.33.9 2.33.8 2.33.7 2.33.6 2.33.5 2.33.2 2.32.10 2.33 2.33.1 2.32.6 2.32.7 2.32.8 trunk 2.10.3 2.10.4 2.10.5 2.10.6 2.11 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 8 months ago flags 8 months ago language 8 months ago gptranslate.php 8 months ago multilang-routing.php 8 months ago readme.txt 8 months ago serverside-translations.php 8 months ago settings.php 8 months ago simplehtmldom.php 8 months ago uninstall.php 8 months ago
gptranslate.php
2241 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 and more. ⚠️GPTranslate FREE Mode active
6 Author: JExtensions Store
7 Version: 2.18
8 Author URI: https://storejextensions.org
9 License: GPLv2 or later
10 License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13 if (!defined('ABSPATH')) exit;
14
15 class GPTranslate {
16 private static $instance = null;
17 private $table_name;
18 private $version;
19
20 private function isSelected($current, $value) {
21 return selected($current, $value, false);
22 }
23
24 public static $pluginVersion = '2.18';
25
26 /**
27 * Class constructor and settings inizializer with register_setting
28 *
29 * @access public
30 */
31 public function __construct() {
32 global $wpdb;
33 $this->table_name = $wpdb->prefix . 'gptranslate';
34
35 $this->version = '2.18';
36
37 $settings = get_option ( 'gptranslate_options', [ ] );
38
39 // Server-side plugin exclusion check for frontend pages
40 if ( !is_admin() ) {
41 $current_url = esc_url_raw( ( ( !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ) ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
42 $page_exclusions_raw = isset($settings['page_exclusions']) ? $settings['page_exclusions'] : '';
43
44 if (!empty($page_exclusions_raw)) {
45 // Normalize input: replace newlines with commas, collapse multiple commas
46 $patterns = explode(',', trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $page_exclusions_raw)), ','));
47
48 foreach ($patterns as $pattern) {
49 $pattern = trim($pattern);
50 if ($pattern === '') continue;
51
52 // Match if current URL contains the exclusion string (case-insensitive)
53 if (stripos($current_url, $pattern) !== false) {
54 // Skip plugin execution completely
55 return;
56 }
57 }
58 }
59 }
60
61 // Include various functions like multilanguage URLs, hreflang tag, HTML lang attribute rewriting
62 require_once plugin_dir_path(__FILE__) . 'multilang-routing.php';
63
64 if ( isset($settings ['serverside_translations']) && $settings ['serverside_translations'] == 1 ) {
65 require_once plugin_dir_path(__FILE__) . 'serverside-translations.php';
66 }
67
68 register_activation_hook ( __FILE__, [
69 $this,
70 'activate_plugin'
71 ] );
72
73 add_action ( 'admin_init', function () {
74 // Disable WordPress emoji script and styles
75 remove_action('wp_head', 'print_emoji_detection_script', 7);
76 remove_action('admin_print_scripts', 'print_emoji_detection_script');
77 remove_action('wp_print_styles', 'print_emoji_styles');
78 remove_action('admin_print_styles', 'print_emoji_styles');
79 remove_filter('the_content_feed', 'wp_staticize_emoji');
80 remove_filter('comment_text_rss', 'wp_staticize_emoji');
81 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
82
83 function gptranslate_sanitize_options( $options ) {
84 $clean = [];
85 $clean = $options;
86 return $clean;
87 }
88 register_setting ( 'gptranslate_settings', 'gptranslate_options', [
89 'sanitize_callback' => 'gptranslate_sanitize_options'
90 ]);
91
92 // Register record deletion
93 $page = sanitize_key($_GET['page'] ?? '');
94 $action = sanitize_key($_GET['action'] ?? '');
95 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
96 $translation_id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
97
98 if ($page === 'gptranslate' &&
99 $action === 'delete_translation' &&
100 $translation_id &&
101 wp_verify_nonce($nonce, 'gptranslate_delete_' . $translation_id)
102 ) {
103 $this->gptranslate_handle_deletion($translation_id);
104 wp_redirect(admin__rewrit('admin.php?page=gptranslate&deleted=1'));
105 exit;
106 }
107
108 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'toggle_published') {
109 // Toggle published state
110 $id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0;
111
112 if (!$id || !isset($_GET['_gptranslate_nonce']) || !wp_verify_nonce(wp_unslash($_GET['_gptranslate_nonce']), 'gptranslate_toggle_' . $id)) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
113 wp_die('Invalid nonce.');
114 }
115
116 global $wpdb;
117 $table = $wpdb->prefix . 'gptranslate';
118
119 // Toggle published flag
120 $current = $wpdb->get_var($wpdb->prepare("SELECT published FROM $table WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
121 $new = ($current == 1) ? 0 : 1;
122
123 $wpdb->update($table, ['published' => $new], ['id' => $id]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
124
125 wp_redirect(admin_url('admin.php?page=gptranslate&action=state_toggled'));
126 exit;
127 }
128
129 // Check for plugin update
130 function check_for_gptranslate_update($currentVersion) {
131 // Start the session if not already started
132 if (session_status() === PHP_SESSION_NONE) {
133 session_start();
134 }
135
136 // Reset session vars after update
137 if (isset($_SESSION['gptranslate_update_version']) && version_compare($currentVersion, $_SESSION['gptranslate_update_version'], '>=')) {
138 unset($_SESSION['gptranslate_update_version']);
139 unset($_SESSION['gptranslate_update_checked']);
140 }
141
142 // Check if the update check has been done in this session
143 if (!isset($_SESSION['gptranslate_update_checked']) || $_SESSION['gptranslate_update_checked'] !== true) {
144
145 // Perform the remote XML check
146 $remote_url = 'https://storejextensions.org/updates/gptranslatewp_updater.xml';
147 $response = wp_remote_get($remote_url);
148
149 if (!is_wp_error($response)) {
150 $body = wp_remote_retrieve_body($response);
151 if (!empty($body)) {
152 $xml = simplexml_load_string($body);
153 if ($xml && !empty($xml->update->version)) {
154 $updateversion = (string)$xml->update->version;
155 if (version_compare($updateversion, $currentVersion, '>')) {
156 // Store the update info in session (version, and flag that update is available)
157 $_SESSION['gptranslate_update_version'] = $updateversion;
158 }
159 $_SESSION['gptranslate_update_checked'] = true;
160 }
161 }
162 }
163 }
164
165 // If update is available, show the notice
166 if (isset($_SESSION['gptranslate_update_version']) && sanitize_text_field($_SESSION['gptranslate_update_version'])) {
167 add_action('admin_notices', function () {
168 echo '<div class="notice notice-warning is-dismissible">';
169 echo '<p>An update for <strong><a href="https://storejextensions.org/extensions/gptranslate.html" target="_blank">GPTranslate</a></strong> is available. The new version <strong>' . esc_html(sanitize_text_field($_SESSION['gptranslate_update_version'])) . '</strong> can be downloaded from your reserved area if you have a valid subscription and license for the full version.</p>'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
170 echo '</div>';
171 });
172 }
173 }
174 //check_for_gptranslate_update($this->version);
175 } );
176
177 // Post admin notices after actions
178 add_action( 'admin_notices', function() {
179 if ( isset( $_GET['page'], $_GET['deleted'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
180 if ( (int) $_GET['deleted'] === 1 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
181 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED')) . '</p></div>';
182 } elseif ( (int) $_GET['deleted'] === 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
183 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED_ERROR')) . '</p></div>';
184 }
185 }
186
187 if ( isset( $_GET['page'], $_GET['action'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
188 if ( $_GET['action'] === 'state_toggled' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
189 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_STATE_UPDATED_SUCCESSFULLY')) . '</p></div>';
190 }
191 }
192
193 if (isset($_GET['imported']) && $_GET['imported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
194 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_IMPORTED_SUCCESSFULLY')) . '</p></div>';
195 }
196
197 if (isset($_GET['settingsimported']) && $_GET['settingsimported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
198 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_IMPORTED_SUCCESSFULLY')) . '</p></div>';
199 }
200 });
201
202 // Add hook for admin menu links
203 add_action ( 'admin_menu', [
204 $this,
205 'admin_menu'
206 ] );
207
208 // Add hook for record saving/deleting
209 add_action ( 'admin_post_save_gptranslate_record', [
210 $this,
211 'save_record'
212 ] );
213
214 add_action( 'admin_post_save_gptranslate_record_and_close', [
215 $this,
216 'save_record'
217 ]);
218
219 add_action('admin_post_cancel_gptranslate_record', [
220 $this,
221 'save_record'
222 ]);
223
224 // Add hook for adding main frontend app scripts
225 add_action ( 'wp_enqueue_scripts', [
226 $this,
227 'enqueue_frontend_scripts'
228 ] );
229 }
230
231 /**
232 * Singleton class instance
233 *
234 * @access public
235 */
236 public static function get_instance() {
237 if (null === self::$instance) {
238 self::$instance = new static();
239 }
240 return self::$instance;
241 }
242
243 /**
244 * Activation plugin hook with db table creation
245 *
246 * @access public
247 */
248 public function activate_plugin() {
249 global $wpdb;
250 $charset_collate = $wpdb->get_charset_collate();
251
252 $sql = "CREATE TABLE " . $this->table_name . " (
253 id int UNSIGNED NOT NULL AUTO_INCREMENT,
254 pagelink varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
255 translated_alias varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
256 translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
257 alt_translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
258 languageoriginal char(20) NOT NULL,
259 languagetranslated char(20) NOT NULL,
260 published tinyint NOT NULL DEFAULT '1',
261 translate_date datetime DEFAULT NULL,
262 translation_engine varchar(20) NOT NULL,
263 PRIMARY KEY (id)
264 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
265
266 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
267 dbDelta($sql);
268
269 // Valori di default
270 $default_options = [
271 'google_translate_engine' => '1',
272 'chatgpt_apikey' => '',
273 'chatgpt_model' => 'gpt-3.5-turbo',
274 'chatgpt_request_message' => "Compile this JSON object key-value pairs adding the translation into '{{target}}' language to the empty value from the original '{{source}}' language of the key and return me only a parsable JSON object without any surrounding characters, preserve and return in the JSON object the key in the original '{{source}}' language within double quotes: '{{translations}}'. Pay attention to not skip any key and translate all keys. Return only a parsable JSON object with no surrounding text, explanations, or markdown formatting. Ensure the response is valid JSON and can be parsed directly.",
275 'chatgpt_request_conversation_mode' => 'user',
276 'language' => 'en',
277 'max_translations_per_request' => '100',
278 'max_characters_per_request' => '2048',
279 'detect_browser_language' => '0',
280 'autotranslate_detected_language' => '0',
281 'always_detect_autotranslated_language' => '0',
282 'auto_set_language_direction' => '0',
283 'serverside_translations' => '0',
284 'serverside_translations_method' => 'regex',
285 'serverside_translations_caseinsensitive' => '1',
286 'serverside_translations_matchquotes' => '1',
287 'serverside_translations_urldecode' => '1',
288 'serverside_translations_language_switching_mode' => 'url',
289 'serverside_translations_ignore_querystring' => '0',
290 'serverside_translations_urlencode_space' => '0',
291 'css_selector_serverside_leafnodes_excluded' => '',
292 'detect_current_language' => '0',
293 'detect_default_language' => '0',
294 'rewrite_language_url' => '0',
295 'rewrite_language_alias' => '0',
296 'rewrite_language_alias_original_language' => '0',
297 'rewrite_page_links' => '0',
298 'omit_prefix_original_language' => '0',
299 'rewrite_default_language_url' => '0',
300 'translate_metadata' => '0',
301 'set_html_lang' => '0',
302 'add_canonical' => '0',
303 'add_alternate' => '0',
304 'translate_placeholders' => '0',
305 'translate_altimages' => '0',
306 'css_selector_classes_translate_altimages_excluded' => '',
307 'translate_srcimages' => '0',
308 'translate_titles' => '0',
309 'translate_values' => '0',
310 'metadata_chosen_engine' => '0',
311 'default_language_first' => '0',
312 'css_selector_leafnodes_excluded' => 'a.nturl,.gt-lang-code',
313 'words_leafnodes_excluded' => '',
314 'words_leafnodes_excluded_bylanguage_repeatable' => '[]',
315 'words_min_length' => '',
316 'crawler_timeout' => '30',
317 'crawler_exclusions' => '',
318 'page_exclusions' => '',
319 'chatgpt_gtranslate_request_delay' => '0',
320 'initial_translation_delay' => '0',
321 'realtime_translations' => '0',
322 'css_selector_realtime_translations_retrigger' => '',
323 'realtime_translations_retrigger_events' => ['click'],
324 'realtime_translations_retrigger_events_delay' => '200',
325 'translations_export_format' => '.csv',
326 'ignore_querystring' => '0',
327 'enable_indexer' => '0',
328 'storage_type' => 'session',
329 'subfolder_installation' => '0',
330 'alt_flags' => [],
331 'languages' => ['en', 'es', 'de', 'it', 'fr'],
332 'enable_reader' => '0',
333 'responsivevoice_apikey' => 'kWyXm5dC',
334 'responsivevoice_language_gender' => 'auto',
335 'responsivevoice_volume_tts' => '100',
336 'responsivevoice_voice_speed' => 'normal',
337 'mainpage_selector' => '*[name*=main], *[class*=main], *[id*=main], *[id*=container], *[class*=container]',
338 'elements_toexclude_custom' => '',
339 'proxy_responsive_loading_script' => '1',
340 'proxy_responsive_reading_mode' => 'native',
341 'chunksize' => '200',
342 'widget_text_color' => '#000000',
343 'widget_background_color' => '#FFFFFF',
344 'popup_border_radius' => '0',
345 'popup_fontsize' => '20',
346 'popup_iconsize' => '32',
347 'float_position' => 'bottom-left',
348 'float_switcher_open_direction' => 'top',
349 'flag_style' => '2d',
350 'flag_loading' => 'local',
351 'show_language_titles' => '1',
352 'enable_dropdown' => '1',
353 'equal_widths' => '0',
354 'reader_button_position' => 'top',
355 'widget_max_height' => '260',
356 'wrapper_selector' => '.gptranslate_wrapper',
357 'draggable_widget' => '0',
358 'disable_control' => '0',
359 'custom_css' => '',
360 'disable_bootstrap_css' => '0'
361 ];
362
363 // Se l'opzione non è ancora presente, la crea
364 if (get_option('gptranslate_options') === false) {
365 add_option('gptranslate_options', $default_options);
366 }
367
368 }
369
370 /**
371 * Function to add admin menu for both settings and translations management
372 *
373 * @access public
374 */
375 public function admin_menu() {
376 add_menu_page('GPTranslate', 'GPTranslate', 'manage_options', 'gptranslate', [$this, 'records_page'], 'dashicons-translation');
377
378 // Add a submenu that matches the main menu to prevent duplication and allow renaming
379 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), 'manage_options', 'gptranslate', [$this, 'records_page']);
380
381 // Now you can safely add a differently named submenu
382 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), 'manage_options', 'gptranslate-settings', [$this, 'settings_page']);
383 }
384
385 /**
386 * Load the configuration settings page held in the settings.php file
387 *
388 * @access public
389 */
390 public function settings_page() {
391 require_once 'settings.php';
392
393 echo '<script>
394 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
395 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
396 </script>';
397 }
398
399 /**
400 * Translation records pages, list and edit
401 *
402 * @access public
403 */
404 public function records_page() {
405 global $wpdb;
406
407 // Edit record
408 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'edit') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
409 $id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
410 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
411 $opts = get_option( 'gptranslate_options', [] );
412
413 if ( ! wp_verify_nonce( $nonce, 'gptranslate_edit_' . $id ) ) {
414 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
415 }
416
417 $record = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
418
419 $translationsArray = json_decode($record->translations, true) ?? [];
420 $altTranslationsArray = json_decode($record->alt_translations, true) ?? [];
421
422 uksort($translationsArray, function($a, $b) {
423 return strlen($b) - strlen($a);
424 });
425 uksort($altTranslationsArray, function($a, $b) {
426 return strlen($b) - strlen($a);
427 });
428
429 // Path relativo o assoluto all'immagine della bandiera
430 $flagUrlOriginal = plugins_url('flags/svg/' . esc_attr($record->languageoriginal) . '.svg', __FILE__);
431 $flagUrlTranslated = plugins_url('flags/svg/' . esc_attr($record->languagetranslated) . '.svg', __FILE__);
432
433 $pubIcon = $record->published ? '<img src="' . plugins_url('assets/images/published.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . '">' // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
434 : '<img src="' . plugins_url('assets/images/unpublished.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . '">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
435 $rewriteAliasRow = '';
436 if ($opts ['rewrite_language_alias'] == 1) {
437 $rewriteAliasRow = '<tr>
438 <th scope="row"><label for="translated_alias" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</label></th>
439 <td><input type="text" id="translated_alias" name="translated_alias" value="' . esc_attr($record->translated_alias) . '" class="regular-text code"></td>
440 </tr>';
441 }
442
443 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAABAI0lEQVR4nNW9d7xdV3nn/V1l79PuuU1Xvdqy5KpiS64YU1wAm2pCYAiQOgmBSUgmk2QmbxIyqZNJMm+mQEgjCUkgDgZTTLMhrrKRbdnYcpMlS5Zldd2rW0/Ze6/1vH+stc+9VzZYJsznk3d9Plc6Z5/d1rPWesrvKUsBGlDxr/ys371CX5U7+aGucLmD1aDQCImRcAYKL2B0/ApoDUj5WXBeYXT47jxYI2gVzlYiOFGoeE3hQStITLxZvKl4UCr85n08Hp+hlCCiEMI5IrP/ewGtFFqVJ0u4WMJ7+nhvEfBeoTUIgpQEUL3H4EQhEo5p5VGxD+F5CgU4P3tt4RWC4EWByKTx6hE8X2k7vvqVo/4o4fEeEAWYSHgN6PeuUFdPOP5rFy4qgESBtQqlFamBvoonTSB3irwAaxSJAScea8KAiICxgWCCwpTEMUKSgPYKY4ROFqhptCJ34VhqFUoJSgkahYpE0YBHxbEXFIG4hVM4L1jN7EDE55aERINBemOXGCgkvFNeaJwI1oRnOAfagFaCFxBRdItwpdWKVAs+PkcRBgCBrADRgBM6ORQF5EV4t+mu4JxCC52kkD/654Py38pBUEAa+2fet1L/x31t+W2dKJY1hNRCaqFR1ShraGcajcdoT15A14VB0VpRiJCaMMu9By+KxAhOykEQtFEYHWaGUeEllQrX5F5ReEVqBKvDVNZxJUk5FWNnJRIzzHaFc4LW5WDNznKFx3sFGvScma/jepe48hThvUQUrhC0DhPJA84pCkccAB36J2FyKRWu0wSie6UwSmENDDSgqj2TU55j44HSk10YmxEange3j/Gm/S1pKaAG2BuXqveOi/pEmggDVUV/RRgZBKxh/2HF1LTQzQSXC1oE0YrCQ2oCMZworAajAsHEK2wcAOfBRAIZ1SPTvH8zCVwiMXOWow7XlitAACVhhfh5XCVSk5L1xD/AxfNUyScJ15csxM/53RMmjp3zu5vDEbVSeJF4bhjo8tyuDxPGKEG0ptlU1OvC8iWavkQxPe2ZagvjM8JE16MyteOfDsjrFdA8u09tPrtf3U0qjFTD1Fq1TDHVVTz9rDA14WgkUK1ALVXUkkDYzCusgooNM9gqiUszzP6sUGQO6knogQeSONMUsSM+dCZ3kMQVQjzPKIUTesQt+S0CHokcVMqFAQKu5P2U7DCyKxPuUw6El0DsMIkir/dhRVhDWE2iwirwc+RCHGAfiZ0oyETRdYqqEcRDqwvdrjA+A0WiWbbCsHmtIesUHBvzTHWglcEL4/IpC9ihCv/dJDBUh0qiGBk0nJiGxx4vGEqFMxdDo65waBILzbpHaWGqrTFGUUs83RxSq5E4K2o1aHWC7KhXhMJBUSiSBBBPqjVFJKxWULjAVEVprFJY5UBrxAcCigqsTokg3gM6SDGn0SqwjVJIKx2mtjU6EjwIf62DmPVOobUic+G96hUoCkEwSFi+KB36UjgQNM4LSgeBaU1QLrz32Hif3GlSK4hTdHKgELodYWxc2PNMzkwHrr7IhJEfD4rMmQN8wC5I2XRmn7qMCthUsWBIMZMrHn0iY1m/YsmIJq0JSapIEkW1Cv0NTZ5rhpyQVATlFV5Cp6zxQRtCs8hImJaicF6hjWAtKKdIU830jCeJQlyU0C3C6qtVQKN7GkqpSaHCKvDeoBHQQpFplNIo5REfTtNGo1TU2HRQ7BITVmbu4mw20M1BudC3vCj5OhitUSoI0MJpxHsyr0iUYLTGJoQ+oamnQicTikJjE0+RCbmzuBw67YJGn2KgT9i5v+CBpxIuPlsz1XI4H1i2Ha6oKyupIqkKxihsqnl2j2NRHZYv0QwNQb0PKlWDTTTGevqrAniUCpI+SYIgKgSqlcB2XAbN/iBYuy2FTSGtCtprguLo6WsoRGuSRGGsxnlDkWdUEkhTjTgB5SM/1ogSlIA4C9pjTUHRFbyKQrPQiDiMVYAlTYogjG2YGC5XeBX0KW0gzzUqCli0Js8lCGw0aeIRpSlyRadt8BoqiaBESKsCTuNFqNcUnY6mm0OSKIpCUeQe7xJaLUNazaiknk1K8ei+nGUjlmULFS8cE/Kuwo5UuaBe9XS9Zumw4vgEtKc9569W9A0IA0OKRp+i0fCBBWlFoxaIJoVQFIZ6Xx54uVekFSEvgqCr1YTcG7oJGOuxNRVYgQ9swySWogARD16wVU+RJyTWY4wPuqJSGKMp8qBFIR5fgLYGqx2FUxSZwlqNiAStR4IsSatBNhgrGG1QWnDi8YVHYSkKjXcOZRRGa7oZpLXA/zUaZSDrarKOwolQqwaWmVaD8tHpaJIK1D10O0FDKlxgPVYVTM4kmMRQMwpRwvIpz57nPEsu0jRSYaoQbGJkiTUKrGATxeHDwmBDqPcbRhYq+vo8VatJ06CX1aqe4WGhqh3HD4LVOdZBXkCCI/Vg4xJXOahORtINE7laDcd9HjQPIw4j0G0HtmAUUEASP5cCjwx0AUZAGSjEoYrAXbQHFX8XFQSoFA5tINVhJVrApoSBc9FOUBnKg8vDd23A5JAmwYYRH1QoV+RUVZAFiQNtIVWgXPjfuPC+/X1AUuHklIbUkFY9PsmDnuc1fbmwaoXi8b2OsUnNYL/Q6jqsiupjrc8zNm3Iu44VSxVpHfoGFM1a0HTSiqJRg7ovOH4I9ncUJ7OVpI06quNxRdDn0jRagCjwghdBfBCwaRY65p0DH1QPFVmDCCQpPQFpEoIAtgqKIFO0DsLLR91UmUBUMBjjezxc4gdzMiivRiuUjr+jCUpssCFUqTkZjXeCmQgWdjD2LM55kKAM6EQwUbBLtMaUCoK5VhxgdWWK5UsUeW2Ibt4mtRqXetoVxYIRTTfzNNKCdtuxaCgoFdZHW9gohXioVaG/CfU6WC0kiaLZB5WqI80LHtoLB4fezZor382m5auoVWs476JmoiLBfRSWPqiBWqEMzPgOB6dPoIxjJuvgpLRXQ2tzem1Wpz/d9t0vEMJqa1RqpLrCiuZimrYP5XXQeqJAllLtLc3pUiVWGu8dk+PH2f30LTy/+w/YunKMenOAmQySpEu9rhCjqTUVjSq0WqCUxhqw07koazUCVBLQRmETqFeCDa8IQrZaZNz1hGL8vN/lHf/uP7C4mZN1pijyHCGJ6ht4CcQP/NjjjOLR4wd47PDzdIocozWFc6Q2Cezl30DzAoemxrDG8OTxfVRtysWrzuTiZeuoJykQNKiS+IJEzSvwSaUV6bqldC6/mHu/eTU77nonl62aolJp0skM1Yqj3clQKqFaNxBljjEemwkkFrBhyXsREqswqUGnQqVWUEk8z+yByTM/yHt+4pfp8y8wdnwK78ulLL3/y885nt3TY9x3YA+5K+ir1KglKQqoGNsDuv4tNK0gSSsAiIHcOW7f9Rh37nmCq844j82LV1BPKr2+zYJxs99npidJrOENb7maO4v/za4HPsCmc1tMTFdQDrRojA54ElowVmG1RosPi0nbgM0kRqOtIi+ExLowS7vCsXQxm1/3kwzaMcYnpnoPnkv08lgLzz8+/SDbnt9NLUkZbjRJTOD50TL4N9fmvldiDMONJrWkwl17n+Tj27/FeNbuER540WAopSmcMH3kKOsveS9u+etpTeWkNUVtuEqlnlKpKIwN9BLvSFIT0GFXBB1aGaLBIrhMsEZjrWFyGioLN7JixWomJ0/2XkBEAjwQB0IpRdfA3z9xH8YY+qs1jNbzBuj/L00kGF391RrGaD52/zeYyDu9vgLzBqRs3W6bgQGDam7gxNGouXmHVgWFmADBq2CPdAtBax1nsfMICofvoY/driZNA/KpTJNqJaHIi3msx/tZfPxQa5J/fPzbVJMqFZv8m5zpr7QJULEJ1aTKX2z/Fs9PjmGMQX/XiaXQChwJWQGJzfF5uwcSQsCxPCaAjC7qxVp59Jz7JTZCtD7gKgGu9fP4fdm894jRfG73I2itqSUJ/gc8670I3TxnJuswnXWZybp0izygkz/QJ730s2tJgtaav9lxFwWzK/+lmgggRRDYrkDQKK+pkaNEyAqNRqhaQUtAeCMm7vGFQrzHRsPMe4VoUNqHwWB25pcrQSnFw6MHSbShmqQ/MOIrpejkGccmxzk5M0ViDQsa/Szs62dBvQ+rDGMz0xydGqebZ9+VID+I5kWoJimJNtz53NPBFxBXwqnvXNoJSoMnoZ0leBesY41DieBdjipybAD/Iqada1xwWeA8OF/gvcZa8EUw4+0pD9Na4xLDtv27WNg38APh91opWnmXkzPTLBkY5oZ153PO0pUs6R9ioFZHKY0Xz0SrxaGJUZ489DwPPbebw+OjDDf6qf5fWIEQJl5/rc5dex7nVavX0zBJT/a9mDM4ihwEi1I5oqArhkKi79UbRHmsUgEPD1i4RnBhyWjwXqO0Q2lwLliVpfAVkd7obz+0D6vNvBf5fptWimNTEzTSCu+95LW89pyN1KOKeGqrDqQsHhjkwlVredvmy7jtyYf5xhMPM511GGk0573LD2o4lFJYbbhr75PcsH7zi1ZdoA0orUGDlYKqETpaRxmgcN5QSRXjBJQB78C7AOG6QuNc0IwSHWABmeMMn6sBaK3xRvH40QMM1Or/auIrpTg0PsZZi5bxodfdwMLmwIvOKdmfMWbe8b5qjRsvehWXnnEOH7/jVvaPHqWaVMh98GkZraiYhGqakhjD9/uqIsJArc4jL+zj2rM2kKhZFjTLkgP+owBjBO8tVikqthtco9HPHXwnCE50HIngWJcY8aC1oFUF8S2skYjPz/L9rjhOzLSCd0ibf9UAaKU5PDHKxhVr+KXrbpzHW2e6HR7c9wzPHD3IielJsiKnUaky0tfP2UtWsGXNOio2AWD50AJ+5U0/xCfvuY3cOxY2B0FBJ+tyfGqCwxMnOTE1SbNao69Sg0iMV9KMNnhg//gJlvcPUTuFFQlBafEK2j6h8B2y3NAtUlAOqwsKB0ZsYOniA8hllCLRnkoSYIgs0zjvgnZUzrySYFoz2p5i/9Qo9l+p62ulGZ2Z5MyRJXzkmrfPI/6du3byhe/cz4nJCawxVJIErTTF+BiPH9zPHbt2MvTQPbx54yVce96FAAzUGvzide94yWc9P3qM7xzYy7f3Ps2BsVGGGw1qSYp7Be8vEhz0e0aPYrVhzcCCedZx6eluZ6BxJIkhLxSZN+TKI6LBGzQOC8FtqPDxf0gTj8aSZQrvc0Qg6wRbca712/IFE3n3Xy3wukVGojU/esU1pHZWzH/qvm/xpUe/zYK+fpYPLeh1MMwwTe4cx6bGOTYxzrFoIL5cW7VgEasWLOJNG7Zy+xMPc8sj99PKMhb0NXFxdZ9O8yKcaE1xsjPDGYMjveOlFpQXmj4N9cSRdQzaWLTPAcFH167SGls+U2uF76qIUEKSSIgHQsX4HkFFZLBsY60pprttqnH5fz/NKMWhqUmuO+8izli4pHf809vv4EuPbmf1gkVYbXqDXAJgRyfHcd5z5drzuPGiK1jYP/iie4+3pjk+NQECw81+FjSavd8SY7l+4yWcu2wVf/KNWzg2OcGi/oHTHoSqTZjqtDgxPdF7r5ILeB/80jUToiq6RYooh9WelJwOCXkG4LEBZhXyQvdGr3AKmxQYY1A6OBDNXAe31mityZ0PzurvQ//WStHOM45PTWC14fXnbur99tBzu/nSd7azanhh4LdSxgEpxtstpjotzlu6knduuZJzl66cd1/nPXc/s5MHntvN0YmTdLIuKEUlSVjQaLJ55Vpef+6mnmZ1xsgSfuPN7+EPvvbPjM1MMVRv4uXlByHA7dKLD5qPh4GRnMJDliUgOUZ5sBUyURQu+JqLosAaY8A7nAsODm2CdM5yg/PBYRHcsuUM1HM+vzQe8nKE7+Q5Y60pRvr6ee36jWxdcxYrh2eX8Vd3PkQtrZAYixffG6yx6UmWD43w/stex5Xrzn/RvR/Zv4fPP3Ifu48eopZWaFZrVNM0Dozw3Ogxdh7czzef+g4/dsU1bF51JgCLB4b44Guu53/cfgudPJvHBl9uEMpAgbm4WBk3VGhAGxJT4F2CywuUuOALsC5gbVor5SWoS84JLg/2QJYHB7fSQuHBFyp4iXro33c3xb878TUnpidQCt666VLecP4W+mv1eefsPX6EgydPMNzoi04QODx5kkqScOOWK7lhw1YqEaMv23MnjvK5HffyyIG9VGzKmpHFiAhuDk5lNFSSJiN9/YxOT/GHX/9nfvxV13Ld+VsAOGfpSq47fwu3PHIfKwZHTmsVwGx44ly6hGbxXfBeEJPiyfHOocXj0eRicC7DEvwJWBui01DgXAjxCHPdgwlBTFrpeQbYKyX+4Ykxlg8O88HX3sDqBYte8rznThwlcwUmPmuy22LL6rW8++LXsGRgaN65k+0Wn3/4Pu5+ZidOPEsGhkiMpdXtMtlp4b0ntQmD9UZvljoRhhtNqknCJ++9naFGPxevWQfAG86/iAf37WK62/6uxt9LtbmIMJS+bI8xoJxFChfDKDVCiKYQF+KTbDlkSsIghHiY4MQ2WiFF8KKWVnD5oFcyCEZpDk+OcfbiZfzyG981b4m3sy4CvQ6fnJkKHVGKsekpLjljPT/zmjfNu5+IcNsTD/Olx7ZzcmaKxc0hqmlK4QoOj4+RGstbNl7CqgWLuHPXYzzy/LP01xoMVOt48Xjx1NMKfdUaNz14F5tWrCG1Cc1qjcvOOIdbvnMfjUr19FTrl7D+Q8SfYFIw9QTfzgNgSfBDWz03CA1UNAOCg9oHbUdrCWwHDREFnYt39MIEX6ZppRidmWLpwAJ+4dp3zCP+A/t28Zntd/KBy6/mwtVnxbef/a/wjuWDw/Pu9/D+PXzxO/fzzNFDDDX6WL1gMc47TkxN0sq6XLR6Le/eehUrokzZumYd9zzzOJ9/5H72jx1nYTM4WrwICxr9HBg7zl3PPN6zITasWMM3nnyYwjnMaU6yuTKgHABBIxGO1sagtcdoFxHlcF9BAhiHgDHgcyErgqmsPRTO9wJZlQ4C2BiDc643S19uELpFAQg/esXraVSqveOfffAebnronuCinMPTT5Urfs7k+odv38HndmxjuK/JquFFaAXjrRkm2zOsXbSUt22+jC2r173oHV69/gIuXL2WWx99gLt2Pc54+yQjjX6M0VSThCcO7u8NwJqRRSzpH+To1AR9c9735QZgniY0pwtKWyoNhZ0ApbI4OgqlAu5mlQraj3OQZyHm3loBNF4M4gqUC+zo1Ae+XNNKMTYzxdXnbuLcpat6x7/48P3800N3s3xwAc657zmIc4GCAydP0KzWWNwcoJV1OTY1wUijnw9cfjXXnn8heg4us/OFfQzVm72V0Fep8Z5LXsPrzt7E5x6+l/uffZrUWuqVKqMzk7S6HeqVKtZYhhtNDpw8Aa9gAE6li8LQ7cQACutIEhchbIfyConZDgGOFsgdOK+oxHicoghJC70gb1UaGadv9ebOUa9UuGr9ht6xx17Yx2cf3sbKoRGsNkwXxWnfb7BWx2jNofExlILrL9jK2zZfTrNW651zbHKcz+3Yxr17nmSo3uBtmy/n6nM39WTW4oFBPvS6N/Oa9Rv47I572PnC8+SFYzoLAwCQmFcGZ5/qE0fC5FMGVN7BdRRae6rWhfB17xGnyQuFFQJtxQfwTamwErKOjQGvQl4E4WGMRhUAqmeRfq820W6xceUazhhZ3Dt2y8P3Y7WhYlNyV7wib9bJ9gxHJ07yunM28UNbr5x3X4C/3XY7X9n5ENUkYdngMK0842+23c5dz+zknVtexYWr1vbOPX/5as5fvppbH3uAu3ftnDexPP60VnjZTmVBCvA40jrgDUoEbUEnCUo5rAZlFF65AIKKD+ZzSI4oYzBVYEvdMlki8HytdWAbL8OGlFIU3rF24bLesScPPc/zY8dY0Hd61uapbWn/EB+55m284YItL/n7koFh1ixYxOGJk7SyLv3VOn1phcMTY/zxbZ/n0jVn884tV7B8aNboe/PGS7hq3QVUklk4pZN1MeqVq9q9FomqHOSugzJgTRJiYHFYawmKkMf6GPBViXZAMAWESi0HQkSyMYDMSnrdixb77s17T6I1q4YX9I7tHz1K7oqYJfPKmgAfuPzqeZrJA3t3AXDJmWcD8MYLtnDd+Rdy2+OP8OVHt/P82DEWNQdZ3D9IVhQ8tH83Ow89x2vXb+Ctmy+lWQ1G4Fxj8MjkSV4YH31FdsCpamjQTxRkQRb4IkfyAlcI3ms6BeR5hHyIKiiakB9F6R9WiAM8SOmwAZxzGGN6foHv1rwIqbW9TgKMTk8FY+60uzbbFPSIf3xqgs8+dA/b9jwJwKv2n8e7tr6ahc0BtNK8ccMWLjvrHD730Da27XmCk61pRpoDLB9aQKvb5Ss7H+SBfbt4y6ZLuebcC+dpLTOdDuMz0/TXGvM1m+/xXi91njWCaEJ4e9cyM2PIsoJuEZJN0MEzaVGBxZQ3KxPcEBXDEiGHWbVT655H6uX4ZITQet/nQgPfT+vmGV/fuYOvPvEQ050OSwfD6tq25ykefWEf15+/lTdu2EIlSRmsNfjJV1/H1edu4nMPb+OR558NQWJ9/axesIiJ1gx/cffX2fbsU/zsa65nUURT1y5ayls3X8bnHt7GssEF3+Ntyj6+eAX0AoiBQgwUOYVLyXwe4R2LOEU1ibRXKkT4EsL0Q2CWd8E/7IN7UmsfB2jOw77H7AjWoCfL896x/lr9ZVdOO88ovAsRY6fc/q/vvZ2/vPc2EmNZOTzSG96VwyMkxvKX997GX997+7xr1ows5peuu5GPXP02hhpN9o8eY7LdYqDWYM3IYnYfPcjv3PppDowd713zjouu4JwlKzg5M/V9TRilwIuhKMDg8F4D3ZCPrGJUuJKQh6CIMfs+ZLuUUTai9Kxr0oKxGpTu8f/ek75LM0qTuZxjU+O9Y8uGFqBPichVSpHMsY5HGk1a3S4nZqZCTticZ8xkXRY2+6mnlXm4vfMBWljY7Gcm6/aOT3Xa5C74hC8+Yz2/+/YP8N5LXoN44djUBEopVgyNMN3t8L+++UVa3U7v2tees4ncFacFR7zkIEmwrYo8DIb3gsJjYlYpShClYyyW0Msodw7EaXBEGRDkwit2OUZBu+f44d6hDcvXMNzoZ7rTDpmLUVOaaLd657zhgq385pv/HUv7hzgwdpyZOUSpJsl35culglCdo83c+th2/vvXbma8NQ1Aai1v3XwZv3fjB1i/eBkTrRmc9yzuH+LAyRPcvGNb79qtq89i5fCiec9/yW7Ooc1cKMJ7BQXkeYg69IUOeQ/GIy4m+tmQzYn3xGTkOKmVB+VDLpcvcX9mywyol7cDRIS+So2nDh1goj0DBMDt1evOZ3R6qne/1Fg+t2MbTx16vnft5tVr+a9vex/vv/xqaukcmOI0rIa55yQm4c5nHuM3vvApvvLoA+TR6BtqNLlh48UhYzLC1gubAzy4fw9jM2GwqknKOUtX0sq635MNlRQ4NVBZJEZYKU1RCB6PTkDEYDRY7TDKoY0OGSJZrnr5BwDigyUnqBhqJ/P49+nAEfW0wpGJMR7Y+0zv2Fs3X8aGFas5cPIESmn6qnVGpyf5w6/fzCfu+mpwIcb7/+gVV/OWjZf2rp3stiicmwc5lE2rkHcw2Z1dTYkxrBxaiKD4xF1f5cuPbe/9VrFJD/IuCT7ZnubJQ/t755w5shh9OprQXCNMqZjy5DAV0FZjbQh2U6LIvUISjU4MzhNZskRBrIIP03uFFETQKMQNlctAzfl72SZCvVrlX55+tLeUtVL8x+tuZN2ipewbPUJe5CzoazLUaHLv7if59Vv+jlsevq83W+fq/auHFjLVaXGyNYVRuieEjdKcbE0x1WmxemjhvFdw4ulLqwz3NXvyAKAoAcVT2pGJWef+SF8/afTKvZImAsYELceqLjjQyuK9QnmH85D7EHPV04K0ClaYjZxFtOBj4G4QGmF5zzW7X04qCDBQrfPCyRN8evudveONSpVfu/49XHvuhRyePMmRiZNhtg6PkNqEzz50L7/+hU9x/56n5t3vRy57Hb9+w3sYbjTZP3aMbpHTLXL2jx1juNHk1294Dz9y2ete4j1CKlRySjDXqU0rzVR3NlHKGttL2P6e/TwFC1JxUksBWptoZxUoo0g0aO+QIkShWx9Ltmgd87skDoTWsRaD9DIGlZ7NC7Cn6TcVEUaa/dz1zOO8OsZ4AlSShH9/1Rt51Vnn8fmHt/HU4Rfoq1QZavTRqFQZm5niY3d8mX95+lF++OJXs27xcgC2rFnHppVncutjD/CVxx4A4N0XX8WbN16CfRkCn867zo3wkBKrf4X3gOAP8B60yknrkOQZ2pcJsLEOhkQwzkdEVCTWUICeSxJC3GgIXVTz4uJPhw0JYbkvH1rwIpciwHnLVnHeslXc9fRj3PrYA+wfPcbC5gAjff3k1Qa7jx3i9796E1etu4C3X3gFQ40+rDG8/cLL2bwyONXXRFBORJiJsLI+HRY5p6mYYDjSN9A7NtGeISsKGpXa97jylP72YkMtxgYCCBqtDdYWKC34wmDEY3XwuwSnR7SAc6cQFwSylxAVrQhxLiXhX6lPuJ3nLOkfZLDeB4RQw+dHj8475zXnbOR33v4B3nnRq2hlXZ4fO44gLB0cZqje5I5dO/m5z/wZn7r/m71r1ows7hG/m+d88p7b+Kt7vjFL/Ijylv1TpxiRas5fUeRUjGXt4qW9U547fgRXFBiYd2748ygvJUrfa73w9DgQxlSjAatQYnDO94BNESIWFNVMceUqUBRFyOSQIoyENkQvDshLBMd+r+a9p2Jn1cnJTovf/8o/s3n1Wt576Wvpj3hRNU35oa1XcsW687j5oXvZsX83qUmoJikSnekL+vpfdP9Hx57j89/ZxiMHnuXN581qTbWBKo2FVQb6Uk7UmnSGB2cJ1d/PzNIRvLEYrRltTfO6tVs4c2RJOXY8Im2KFYuZrtY5tYkx5DNtWuYlsCAF3kko/pRmIEUvV1rQQdkRTe6LEBvqI+sRH4seeXBFkMZKx8EoolsyDPMrMtHL+kBlS7QhtQnffPI7PHFoP2/ZeCnXnHdhb+YuGxjm569+K08c3M9nHriT41OTvHPLq7hh48UkxvLcviP8y7ceYfnyEW574mEeO/IcFWWx1rBn/AC3HryfwWYfdz76MCenJzjYHmeB7TKw/w7G1xxFgKmTx1m76zFSE4TkQldwyWSDqb3T5K5g78njqF3fYYNN5vVV6VDDYvzppxhcdxZLt24NfZzjLw8J4SESwuU6prj6EPZPrMBAgVHEfAs/B0KNB0yiCPXYgu8yAhzznM+n24zSdItZTKhZrTPU6MMjOOf52/u+yX3PPsWNF17BxpVn9M47f/lqfvVN76LV7bB4YNY5f9M/38HXv/ogT+16nssuPY+J/RMsW7IA8QV3n3iEp1fsY8eO3Vx/7SU8/M3H2fLON1H50j+hnvsKn433SFFswPRcnhrN0zgeCqWYUMBGTK9QVNk8Dgdcsflqtv7q+9BnrGI8m9WcekG6BOPWO0MoDpLEEmYttI3sRwetM14ZVVEVvF/iw5+xobII/hRfQCm1T6NVkoQT0xO0sy61tEIlSVjcHGT/6DEW9w/RV6lx8OQJ/vi2z7N55Zn80JYrWbVgYW+wSkg7KwruOPgYDxx8hmalxg3XX0ZROEa29PdynIeWhiDbq6++kE6R8arXb+DQ83tZfvkWLvkv72Btx/cimPUcv4QiQOhzv5e/q1iMyHe75DMtqiuXc8aPvQdtDFmWwZwBCBM0qu9JMMgQF+SqDlHVhQ8FC7WOK0BFHNrouHxMYEHOhTh3E+sMzWU9qhQcp9GqScKRyXH2njjC+ctWA3Dh6rVse/apEKYBDDcC4b5z4FkeP7ifa8/bzNsvvJxadIw89Nwz3LxjG0f9BGjF7l0H2XzpWdx33xOce+5qjh8fB2BwqI9n9xzioovWce+2nVz9+i184y++zNYPvo0FP/0BXh5gPv32mc88SDfvcsObzu7lzPVYkAorKcuFPAtlePAGrUMdIlAoT1BDRUKRDNHBGrZaQtlJCULXx4Qz4PvShIw25EXBI/uf7Q3A5WvP5WuP7+C5E0dZNjjc8xUsG1hAu8i49bEH2XnwOS5few4vjI1y/96nqdiEVSsX8bib4F0//BpuueVerrvuYnbs2MXKlQspCmHP7kNcsGENX//6g7zjxiv59D98k3d9+N3M3P0vPHTlPzBpq6el2QvBiGofPYIe6eeiP/oDFl9yEQCTJ1v81M/dxC03beeaa87iTW9Y16NNqf1oJZDHHGztUcaGTCQ8jlDAKuQZQA9uCA8N+WCpDblj3iu8m7WGy/ZyuP68zogwUG/w4HPPcN35F/WcH++/7HX87q03MTYzzXCjD+c9TjwVY1kxtICJdoubd2wjMQkLmwNUrKWddXBa+ORffY2f/sW38NWvbueyS8/lhRcCnn/WWcvY8eAzvOUtl/O5z97NB95/LR/74n28cfow4898m9Gk77TeWQHdfJzmuvO4/Pc+2iP+l774HX70Zz/D+OFRkvXL0KsWvuTFzsXqLFojrsA5hfcFzivwOmiaQogL0ioQvlChKkhRBCOskjqyTAVMO+Y8wSwQ90pEcS2pcGh8lC8/up2ffPUbAFi3eDn/8bq387++9SUOjo+xtH+QMgNSRKinFRpppZeMceDkCVb1LWJxfZA3v+0y7rv3cbZctJ79+48xMFBDPBw/McGGTWt56MFdXPeGrdx5906uPHOI1edfyDW/9cXTetfsxBh5N0MpqCxcgEkSJifa/NQv3MRn/3YbDPfD+at7aDHM14LKuqXaluCcRXxIoxUHSofybSJgXZSlwd8bPDWCkHuNQaN8cNr7aM3MDcP2Jf86jebFs6g5wD3PPMF5S1dx+VnnArBp5Zn8ztvez6fu/xY7D+5Ha0UtqZBE9TBzBa2si9GGS844m597/Vv4x0Pf5C/v+wrLlo0wNdWi0UgRCY6ZNLVMTbXoH2hw/PgEQ8NNxqdaFNMzMHqcmYnWS65ebS3dY8dpnzjOoqteTWMkaF0e+O0//AYf/bXPgB8DtQxOdmDsED7VuNUDc1iP9DAyERsxoQylNWlqyT04ySNbtzhMiIroDaQPGpATiYaECqk0AqFg0hx9uASdXoFKao2hXq3wyW230VetsWHFGiB4yv7z9T/Mjv17eOi53RydHGem20EhNKo1Vg4t5PIzz+HspSsAyFzO4zv3ceWVG9i+/SnWr1/B6OgkIjA42GDvviNs2ngm27/9FK++8gK+8PALHPjCrXzxTz7MyVP81CGS3yIkVIcWs/G/f5TlfbOG14ETHaaqdT76Zx9i+bIhcEVvzikFi4bSHuF7gtiD9w6XEYr+SYFSNqwY5XFigrNLhaqL0UILsz8YEQoRTTcrK15Fa1nriA4KzjmsMbGYqe/5Or9X8yL0V+uMt2b4H7ffws9c9SYuW3tO7/ctq89iy+qzcN4z1W6hFDRrjRfhOt1uwQ+/+7V88YvbuO66rezYsZuVKxfinGfvs4e54II13H77Dt7xjiv5zKe/yXU/fC2VgS4b3rGZSbGz7KIosAuGUY0a7T27WX3Dm1h8/RsojhyhaHcAxfJE80cf3hxCx3nxyul2Ck6MTvc0IJFQWVepIIBDAqSQZQ6cDvnWxoeaqL7MEwZw4HWQ3lZHOMJBnkl0MMT6nTKbpD3S6OdEe5qx1jQ2PT1ownnPYK3BTNbh43feys4X9vH2iy4P6aSxGa0ZbHx3YXlw6gR/+9ff4Fd+7T187avbueSSczh48ASgOOOMJTzw4NNcf/0lfPazd/K+91/HH/zeP/CL/+V9nPlb73/J+41+7M8Ze/AJTt79CId//BfDwbmD/hKrXAn4xJK++lIG/+Sj87xhMAfWKTQaS15A0Sv1EBJilKgyTTVEQnin8E6hJCwlr0wM8vLRzzkLKYgIg5UaC6oNnn+FlrGL8fmptdy75ym+c2Avm1aeycaVZ7B20VKGan09aHm620FEaFZnEcmqSnnt1Zt5eMduLrjgTA4dHqPRV0O8MDY+zTnnrGLnzn28+qpN3Hff47zxfddz8r7t7Prx+xh3GqmkuFqVwUoV/S93MP7wPaR6WZzlpwk/iyBpgm915glg51wUwi5UZHeCy3OUMjEaLqYvOUErN1v6QatQYNsL5E73EEAnEgrjlVVp5wBPixv9KKX49vN7qKWVeTPg5ZoXQSvN4oFBukXOtr1Pcd/epxiq9THc16SSpCiB0elJAD70uhtYFbNqhuoNZmbaHD82Tppant9/lKGhPsQLk5NtZMUC9u49TH9/g2d3v8C6FauYfnY3u26/jfaCc/BZl8GpUQwdEjtEY9FmRLnTViggroDUouo1nHO4Od42EVA2AmfK4bzB56EQuJdg8CpCefkQmBXDUrwK9S0hOhQKkFwjPoymnpMuCiGqa2lzkNTaV5TQMLeJCKmxLO4bwCPkznFwfBTvHIIitZZ2nvGHX7+Zn7zyWi5avY4f+5m3cdkbL2dkuMn0TIdqNaXIQwS3NYZON6fRqAZtqL/ByemMRb/7Xpqjhzn8j59l+m9uJvUG39dPd+oo08ceRpG+/MvOf3PEJtTGlvb60ZMDwZMeAh20xYuLZZXLsvygjccT4Wgt4FS0eCNI5F3YiMBoImARLWClgmM8YkJa4IrV6/nW7p0s6Ot/5eErve4E4Z8aS2rme9ua1RqTnRZ/+q0vcfEZ5/CejVu5aHkf5BkM6uD7q5av6KCmQTIYToCMFd0j6CVrcPVVVGemabVaZCbBnzxO35VbaV71H3CTU+A9qpKiGw0QoZhuUdVCvVEJ5SvnNAV4o8mWL6d1SgEr74IjQnlwWfweCR+8ZMHbKJRQRJQBIhKyIlEoLSQ2hFLgysoppbY0n+dvXX4m/7LncZz3r9gTdTrNeU+zUqOWVNiWTzD2E/+ejV/6MjO82DdQNqHApAPoRh8Lr9jCpp/7KUbecDUr/+cfMXHX/eSP7iXnBEuvvZJl//k/zV5XFEhWoOshV2DUwREHa1NehIyKCK12m+nDhyliEIEOGxcgTmJgg8M7RZHrmJ8mGIQiC3nGVoiREIVCVIiMU9Eixilc4aPDZtYVOTc4V2tNRWuuXb+R2595jAWN5v+VWj1BZigGvbBiy1bOkmGmmy/OYBHvUcZQXbeW9uEjjP/5J1l0912MXHEW4xddiBnoR2c5IRhTYbwwBdDOaNZSpFvw3C/9Z478+f/hzBvfhf8/H+c3P/Uk//TfbmFkWT8LFzZxeaiGZRPN1k2L+bUPXzyviEnIA6NXzd1UQFqhjrbSKiC3BO5ig7ymh/er0FvyXKGqRB9Aie7p6EqTnm+4/Lt42RncsfsJZrpd+ipV3PcR/386rX3oKKve/TY2/Oam73lefuAFnr767aygzarf/VX+unIxf/lLt3Hzb7+GRi2lFSMXFi8f5BPfep5P/D//wDdu+TCLlw5w5if+lOqyZTz90V9l8Te/wsf/4lMs+W/v509/7dOceHIXDC8D8RirWDJ8SqUUCWXeJDIC5TXWFoDD5xoTjQRRAqKwCgWeWIM/4N/BCQ9YUAGbxhUhjgUdUux7MiAKXoPi51/1Rj6+/XYmOi36Y0roD7pVRoY59q17eP7vvs5EpQfRAooisVSGhkm2P8LxT/1PLnr1ZXDzg/zEHR3+5oOf4IyNaxlecD3HTnEfLh5p8uj2h1l+3m/xd3/2I/zIe7ay7Dd/hUU/+xM8ccO72P2ed/D/fuRn+bn7f4P3//FD3PeXt8KiAfSSJmag1mPJPYhDRRDTg7EONWMD74+bGGWFiuCnn8WCtI6RD+J6vF4KKHIdASTb62zpDz5V4DZsws9f8Ub+9/1fp513ezV8fpCtNrKIg39/C3ff/CW6REGm65h0IZXOFH2Msd4arvjY/+Jrl/4QP/PLX+fAbdugXidZPowqTpkUIqEUQ205Tine9+8+zj994XL+5n++m5HFI2x64A6O/tknuftDP8n6f7iJbTd9mj+6/pf5lV+4Cf/EC7iVfT3tpxwE8YJ4g4qGbZ4J2iTYxOGCxQVImPQ+akAihPB0Qk6YdyF0LsdTScDmoxQSeVhcasaYeWwIoKoNH770OirWMtPt0MnzH6hgntq3l3M++EHet3MXN+54hB9+4RA37LifszefzRmMcf0738rSBx7kp9yruP6qP+HA3Tvg3JWhPLD7HiuycLCwCWtXcOtND7B2w2/z6ZseBGDxz/4El71wkKMrz+PBa97IL2/7Ux7/8ge49KdvoLl+9XwkVDy5KEwxSl1DnivE5mgyJFbvElUGwwUNNMTiEpaEF0XuJYh8ZfCFpm8Ipp/ZzvGDzzKwcDneFb3RflFer/dUteFDl1zD2y+4mMRaRmemaOdZ3ADnlcHYpzbX7VJdugQuWM/QRZupezjy7h9j0bdv4+JP/Tm3/erHWfefdvDXP/9xWFiB1SMhBfR0FINys5hzVzDp4Efe82dc/64/5+SJadLly9j0yD2s/tgnufuPP8bgNRdx+w0JH/vxc2cxIFdQH1jE6JH9uP33MDwUtkZRYgM04TxaxULpEoRy3OBjNhwlREYoHGEzAhGPqykGp6Z49NO/QVZP6B9eFKul0xNAc5tSocrK+sFF/Nxl1/KujZeztDmEILS6HWay77/IU9+Za9j1sb/iW4s38O2zr+Tbq5YxmDoWPbqTnxrbwhuu+h8cvv9ROGdl2Eyg8JGf0tMhlYSdoEqPcPTIzv5lDkYacNYKvnbzDlae/VH+8Z/Calj0oR/n8iPHGV19Lk+++Y2c/MhHAPCuoNoYIK8vYd83fpsF7UPopsU7TRmrqAAlHu2DqeZ8NMQKD8YH5M6VdpwESLVwiulpw/JVnr3bPssXfuOnedN/+QuWLBikNdEi73ZQ2vQ6Mjd5rxycTY0hNq06h/HONPvHjjLemmZsZjIUh5oTRHU6S6Mz2M/ilWew5PwX8LRY9u//hNbPfoSf/J3bOfLAY7z2xnPRafIiduMn2qw4exk5Crn0YuzIalQxTrZoCQNVuOp161CLm/OygARBXrUGGWvxiY9/nX0HR/mlD78e6W+y7u5vcuTvP02n3SXpW4BpLGSi5Xj0kx9i4TN/z9LlismuxzuLCbO7t8tIAI5D7K1aP5Dc/5qF+WW2qnBK89xhz9plioUDloE6DDUE4xV9fYp+nXHwOSE/53KWvubHWHL2Vqr1PlxMQwp+4xKenmO2RLZTpj+V/k1VnlESX0pfc9wWMEaY9aK0Zf5+AyCzA94LZwgmZmkwzgUPeyMcn1XybCjxrdKbNUegRh+vF0+Jy0t0RIl40JZup8X4czuYfuxmlpzcxqplMJlXyHNPtwj5B8dHoTUDj+2GWtOyaNgzM1UGZnnoZGEHpUZVM90SlgxBt4C2E+omQNMn8worzugy9fz97P8/93N8WYqpDmJ1gTYOLwZwYZe83OANJNYhTuOcAqfQSYG1Pqi1JuzAFPbVUcxGwQt520TtyyFi8AWY1IUg4oJwr1oIF9G4WTdfhFOUCa4/m3q81+QdIbHglZDnNuyTkBYBajFhIH0R0oYgGKbGCCYJ55uKYHA4Z7BpKDeTFwrvUrRMYqe6rO+D5go4MWlxTiNe4XF0MoMXR5ErJlrQP+TIuqFEqBWJuUwailwY7tccnwiZ23kBrQJS7aOXTDM6k1AbEdYsFLTPyLNjJAasD/4ERZDfnbhdSa0RFAxXxE0rXZD+eTtsWVLuhucjqy4dRFkeMHWrA0sWgYoBihhAIGBiSE4lhbQe1GZfgE3C3jIQFAyfzT5fQvYVkkNFQ1ILpXnydsjnsqGSJRlhD5paDVwS2XSsqWEETAJdF3ZXqVaBhqZQmpPT0O0qrM4o8BSFodvWVDQ8fUxQiWbpsHBsDFwu2MLjsiIk4k21PANNxeFxzXPHPOeuMGQdmIwYj/eaXBUUHY2IppKmeO3JBGzMfwphGNBVITNEjNBxGlGaipZoRwguhcKGvRvFhy2jAssIVdyplPkJJmz0mUJuHJKHUfIeDJ7cWDLx1JKQi+VQmJiIq1So9uK8Qpm4n4wRvA5VIAsBU4QdkTIMTimsuOCMwtBxQl44JKYO+TywUKuD3dRyBq/CfmTSMthqRjer4ASM7eByTTv35F6QNhwYhRXLdXBr+gBL27ZoN5NDIw2R0VkmrFxieHq/Y6TPMTyomeloUuPxuVBJNOhQijHLFdaGCiAuC9ufOBdiH3PnSbXCdYR2NxAtQ7BKsITgVeWFQhQuC/uYaSXk3cCDlQTEMMs93mm0F0yhovsuDICO0ca5F3yLXlS3AUzqkVzQPsgkV2gQhfaOIg9GkhKwXYLarULoSNHVeAWuG5dkiCzEGE+no7FGUVEBSu46jxYdtsZyUHSh6wpwiqlck8d3tSLc/rjQ6LMs6XccHycGEYAR7I83xK0ergf4OfeK/jo0qpZ9RzzeCyNNjdJC5gPfEhFmcsgjZJE7jVeKbtz6Nc8UXR82P3NeaOcGHy1Ap8KudVmhKCg/G5wOu6K2O/G4aJxXtLvhf0mCFlHEeJquEwqgUwQ/q7ZhO0GHxmnB+eibNZqiUHTjdrNlepCLZRgk7nucZ5pOZsPmTgKdrkJMACK7RQhcy4sQsOAKyDJFnptQT8OH/nQdAYjLPU5CFPTJcbhnp6CtYcMaGJ/0dPIQ8Dw+E+Ho/TOwuC/UClWp4uS0sLgpNFcZXjgB2UFPsy4MNgz1VCGFxqHIc4VLhEQHr5m4kJqvjFCIp+gG1pWL64XhaePDfpBh0uEduMJjJYCCPg/QfhBaYXlXEiCnlzLlCCzARsQx84o0D3s9uoxYXCrsCSaFp9OJWmkRgmG98hRxNflK2IeyyBVZIVgvaCNkmSGJmpJDkYvErVmhG+upFnH7LaVAlMFoT7edkHeFyVbBsQnNZBuazYT1yzyTLc9kR9HJhaoS9o6BrRvUaA57ZjTnDno6eVh3o21hpN+zYW3KZCfl5FTBzETYRc/o2FkPVnsqadhvycSe6yQsPY0OMISOexAQalAYQzD4tCJsAxU0GCcKKpCbCK7FAGFvok1B4Bsh9C+wtDKYuIjhlMQtQlCBpSkEV40alkjYII4QPduVgMUYpcgBp4OB5vOoEBRC6akU8eCCMeVV0OuzPAJwEnZe1UrT6mqyHNodQ6MGFyyHVBUcG4fpmRhp4oQXupp9Y2E7ANUA9kwLlVrKGSanU3jSqqGVG8Qqhvs1SxZWyFxItU8rCUnFYpM6A/Uq9bqhWktIbIKxCZU0Ja0YbJKSJpZKVYNJMSbFVAwqsegkAWPQNkEZG6xWY1BG440GZVDGgNZBCJpSvyoty1MNuAhmiYveKI/E4EydO+iGiiW+yGL1kgzxORI2SsAXLrg1nUMKR5b7oFZJTlF48tzhncPnBbnLybKcyckC7zK8y+l2PM7lZN5T5J6qcVSsMDOdc2QyoVs4pjKHyz1jznD/YY1xGRbIEwDxPDrqcarKmqbgnWem0CG4SAkVp6nXEmyaUGtUqNZTkrROvV6lUbNUqxZlLNYkVCsGYy2JtRhrsWmonWytoZIYTKIxiULH4Hhjwy51cT8PVKLABP4c670jlujtDhrTbJxkOQjhNxxhpvqwUwVFGBhXSPDNiqdwHq+jkaV9YHfKYYynEI/THqXLmS0I8XuQ/OHRRmEqBuPDzqm5FEjhMJmjK56xGYV3HnGGmVyRZ45OrjhSVNl+RJhpdVkPWNH6SAH0KZh2OU8eF0bzlDMHKwx5T9VB3WuqPqFVaGxFU82g2oak4qlM51SqQq0S9mBMEk2lorCJUElAWSFNFYnVmERTTQ3WGhJrsIlFKUuaaIwNg6R0/M2qsDIwaBu9dEoHdVIzuwK0jtBB9O05D04H468o8EWQVd22Ii88Lhe6mQ8CtAiC3bmQqO5cEYRqDs5JDEzWoZitCyqzK4L86HRVqP/sFUVhyHJPp60oumHn8bwI+WOu0Mx0HROdlD0nFc8dz2hlHRZGBNqK0oeIiztRYduS42MFk+2UvlRRTw3V1JDYPHj0VYbVCms02pjA07UiTQzWBOJaE/6Mjb8bHb7ruHmZjnUTYqSdVqGWZikvjA7yREerzOjSDx1+Q8XoPTWH+5RpUGWJTV+GCgaFIHc+hI94jys8Pm6/6CRAAYWXUAtaPLmb3bDIu7B1i/PB3VnE8MOi8GTRt+AlZPpkuaPwLt4ncMSicEx3cqa6MN3KseKpq7ABaAZYp9S+MuhBqbCCUwXSyZhsw0Sca4b5GHYyqxSQqMidI4odXaEo6AUelQErSbxX+d2Xgz/nOj3neh/vT+RAOnCnecT3xLwGNSsSfAn7SLBWy+/Ez2WfSgjJxfvEGlW9dyk/ewl1k3QkXjsSkJLL0TNRcPGZ5a5UZcRQNV5TnV2/2KlW65HhxB72ebHUAoWCXOJWrbFDJdEgdNKo2QgBHTuexs/lANg5g2YigYj3Tcp7xGcZBRU1hzDxnkrCMVum9Mvs/cpnR6Ax5FudQrhyZWQR8VWROBIHuQxMFsJ7SCSci/csJyRRtHTigKbxGSZe4+KjnMz2Scvse+ZxEHIJtOyjp05gvfeZNOrf0vn0+4Rw80wRkwdmZZynB2sHFjAXWIwvq+Issmp25tu4KkxJ6PhZS2DfOg5UGRYl8fe01K8BcUJVz96vDCYud0x3cbalkcDla5aroexTmZCumCWuin3V5cDE+xWxX5aeXO9NqnLQYHYiCjG2Snpo9jwXQ04oID4Q+2AEOkly2AJ+pnCf70+T68nyYaXCEilUeAnD/M5Ib2bFh6tZdlGylZLFlKyhJHI5862OxIuDlc65R3l9Ze4gxxUy994Sj/feiVlW5X2wessJoUrWGAeunPmJzK6YGJvQ65eJhCeyHlGzk8nHh1rpif4AKsosS1XxOQXQjau9XwW/S3ndaK36KQu4Trt9rD44+CnJxn+BuIwqsUNFOeIq8K+SJ5b8rcdH43VzebiOM6kMrkviyrAyy9J02cGyE3HQ8jnuBBuXNXMGqVzCwnzZEKM9ekR2QHcOj3ZziR6vzUs5Ed+l5PmocK+OhIqSas594o4uFHPuW7IiX/YnBjc0VFjRiZ+dpNON+kMnJqe+ZiONivHJya8O9TcvcJNT1xDZRSJxJurQwULCPokVNTs4hlDWOI0zWYgzurRGJWyHlcTBSTW9zyrmySoV7pvEQTYK6jqgqr2BUpDE/Qt8fL/S8pU4o9K447YSFXm94DxUnQpEkdkBC5M4aE6dIrIOFN0YB1XycxR0vNBxs9cBtCPoWIj05EpBOBbYkPTknpXZ1WSBmWrl6JHC/TFQWMKg5977znQ3++tmf39DJicvVzIr8JCShQQks0qIay/ZUoJQKwUpswK5J4SVUFXluVEO6LCUg8ANgUwVHfgohM9Wh9+LSPCwrX2IsUwjcUrfsghUA+DZC4wSiUqFllk2QRDIlnAuAm0dhbNIL9SlDKwywAxBhgjSkzFtFTShCDFBXBUFIegkp2dXEkNFAehW0hPHbPL72fT0MSA3BLmgAeOcKwqRx6vNvpS8WGe8qERmialVIG7K7Mws2UpKWDGmXBXxs5VAdBtfKO4djSkd45EXEwWhijyuFFRlaHwaPYLeBaeOEQJOU/LEIqrHEY2IVdfCTrBzrhUfFQAf7+/iPeMSKX9XPsqseI6R8M6KwMd78VSxX6VCUMqCUmCXHjoBJhqNx44o/cftmZldQBdoG6DBLEvVzrm8k+VPm2bzoE6SpvZuoXaiTHwpLbNqqZpD8ITZ3+bx+TnHlITZX6pxSPhs44snJZ+Pn02cbamaJVx5rySeZ8tBi+cnKhCzJIr3s2yg1FBqzBI5YmxhQJgzqPEdrMTAipLg8bwiXlcS10dVtbxniaJ0FUxXqwdHG/Uvneh0/ybrdg8RNNo20P7/AEq4cA9VgzqWAAAAAElFTkSuQmCC"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EDIT_TRANSLATION_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
444 echo '<form method="post" id="edit-translations" action="admin-post.php">
445 <p>
446 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVE')) . '" class="button button-primary" data-action="save_gptranslate_record">
447 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVEANDECLOSE')) . '" class="button button-primary" data-action="save_gptranslate_record_and_close">
448 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CANCEL')) . '" class="button button-primary" data-action="cancel_gptranslate_record">
449 </p>
450 <input type="hidden" name="languagetranslated" id="languagetranslated" value="' . esc_attr($record->languagetranslated) . '">
451 <input type="hidden" name="action" id="form_action" value="save_gptranslate_record">
452 <input type="hidden" name="id" value="' . (int) $record->id . '">
453
454 <table class="form-table">
455 <tr>
456 <th scope="row"><label for="pagelink" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</label></th>
457 <td><input type="text" id="pagelink" name="pagelink" value="' . esc_attr($record->pagelink) . '" class="regular-text code"></td>
458 </tr>' .
459 $rewriteAliasRow .
460 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languageoriginal))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
461 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languagetranslated))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
462 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED')) . '</label></th><td>' . wp_kses_post($pubIcon) . '</td></tr>' .
463 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DATE')) . '</label></th><td>' . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime( get_date_from_gmt($record->translate_date) ) ) ) . '</td></tr>' .
464 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</label></th><td><span class="gpt-label">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper(esc_attr($record->translation_engine)) . '_ENGINE')) . '</span></td></tr>
465
466 <tr>
467 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')) . '</label></th>
468 <td>
469 <div class="gptcard gptcard-default">
470 <div class="gptcard-header">
471 <div class="accordion-toggle">
472 <div class="input-group">
473 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
474 <input type="text" name="search" value="" class="text_area">
475 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
476 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
477 </div>
478 </div>
479 </div>
480 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
481 <button type="button" class="btn btn-success btn-adder" data-addtype="translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_TRANSLATION')) . '</button>
482 <textarea name="translations_json" id="translations_json" hidden>' . esc_textarea(json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
483 <div id="translations-container"></div>
484 </div>
485 </div>
486 </td>
487 </tr>
488
489 <tr class="alt_translations_' . (int)$opts['translate_altimages'] . '">
490 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS')) . '</label></th>
491 <td>
492 <div class="gptcard gptcard-default">
493 <div class="gptcard-header">
494 <div class="accordion-toggle">
495 <div class="input-group">
496 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
497 <input type="text" name="search" value="" class="text_area">
498 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
499 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
500 </div>
501 </div>
502 </div>
503 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
504 <button type="button" class="btn btn-success btn-adder" data-addtype="alt-translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALT_TRANSLATION')) . '</button>
505 <textarea name="alt_translations_json" id="alt_translations_json" hidden>' . esc_textarea(json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
506 <div id="alt-translations-container"></div>
507 </div>
508 </div>
509 </td>
510 </tr>
511 </table>' .
512 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce field is safe
513 wp_nonce_field('gptranslate_save_record_action', '_gptranslate_nonce') .
514 '</form>';
515
516 echo '<script>
517 const initialTranslations = ' . (json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
518 const initialAltTranslations = ' . (json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') . ';
519
520 const PLG_GPTRANSLATE_ORIGINAL_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_ORIGINAL_TEXT')) . '";
521 const PLG_GPTRANSLATE_TRANSLATED_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_TEXT')) . '";
522 const PLG_GPTRANSLATE_DELETE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '";
523 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
524 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
525 const PLG_GPTRANSLATE_SYNC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC')) . '";
526 const PLG_GPTRANSLATE_SYNC_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_TITLE')) . '";
527 const PLG_GPTRANSLATE_SYNC_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_DESC')) . '";
528 const PLG_GPTRANSLATE_SYNC_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_COMPLETED')) . '";
529 const PLG_GPTRANSLATE_SYNC_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_ERROR')) . '";
530 const gptServerSideLink = "' . esc_url_raw(rest_url('gptranslate/v1/request')) . '";
531 </script>';
532 } else {
533 // FREE period
534 // echo '<div class="notice notice-info is-dismissible"><p>You’re currently using the full version of GPTranslate – completely FREE during the initial launch period! Enjoy unlimited AI-powered translations and all PRO features at no cost. 🚀</p></div>';
535
536 // UPGRADE period
537 echo '<div class="notice notice-warning is-dismissible"><p>⚠️ GPTranslate runs in FREE Mode with usage limits. To unlock unlimited translations and advanced features, upgrade to the <a href="https://gptranslate.storejextensions.org/" target="_blank"><strong>PRO version</strong></a>. Current FREE Plan: translate up to <strong>500 words</strong>, read aloud up to <strong>100 words</strong> per page and crawl up to <strong>10 pages</strong>. Don’t lose AI power – <a href="https://gptranslate.storejextensions.org/" target="_blank">Upgrade Now</a>.</p></div>';
538
539 // List records
540 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
541
542 // Filters section
543 $opts = get_option( 'gptranslate_options', [] );
544 $languageFilter = esc_attr( sanitize_text_field( wp_unslash( $_GET['language'] ?? '' ) ) );
545 $languages = $opts['languages'] ?? [];
546
547 // Paginate records
548 $records_per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
549 $valid_per_page_options = [5, 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 9999999];
550 if (!in_array($records_per_page, $valid_per_page_options)) {
551 $records_per_page = 10;
552 }
553
554 $searchFilter = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) );
555 $engineFilter = sanitize_key( wp_unslash( $_GET['engine'] ?? '' ) );
556 $publishedFilter = sanitize_key( wp_unslash( $_GET['published'] ?? '' ) );
557
558 echo
559 '<form method="get" class="form-filter-container">' .
560 '<div class="left-filter-container">' .
561 '<input type="text" name="s" id="search-input" value="' . (isset($_GET['s']) ? esc_attr(sanitize_text_field( wp_unslash($_GET['s']))) : '') . '" placeholder="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SEARCH')) . '" />' .
562 '<button type="button" class="button" onclick="this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>' .
563 '<button type="button" class="button" onclick="document.getElementById(\'search-input\').value=\'\'; this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>' .
564 '</div>' .
565 '<div class="right-filter-container">' .
566 '<input type="hidden" name="_gptranslate_nonce" value="' . esc_attr(wp_create_nonce('gptranslate_filter_action')) . '" />' .
567 '<select name="published">' .
568 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_ALL')) . '</option>' .
569 '<option value="1" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '1')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</option>' .
570 '<option value="0" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '0')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '</option>' .
571 '</select>' .
572 '<select name="language">' .
573 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</option>';
574
575
576 // Loop languages
577 foreach ($languages as $lang) {
578 echo "<option value='" . esc_attr($lang) . "'" . esc_html($this->isSelected($languageFilter, $lang)) . ">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($lang))) . "</option>";
579 }
580
581 echo
582 '</select>' .
583 '<select name="engine">' .
584 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</option>' .
585 '<option value="gtranslate" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gtranslate')) . '>Google AI</option>' .
586 '<option value="chatgpt" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'chatgpt')) . '>ChatGPT</option>' .
587 '<option value="gemini" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'Gemini')) . '>Gemini</option>' .
588 '<option value="deepseek" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'DeepSeek')) . '>DeepSeek</option>' .
589 '</select>' .
590 '<select name="per_page">' .
591 '<option value="5" ' . esc_html($this->isSelected($records_per_page, 5)) . '>5</option>' .
592 '<option value="10" ' . esc_html($this->isSelected($records_per_page, 10)) . '>10</option>' .
593 '<option value="25" ' . esc_html($this->isSelected($records_per_page, 25)) . '>25</option>' .
594 '<option value="50" ' . esc_html($this->isSelected($records_per_page, 50)) . '>50</option>' .
595 '<option value="100" ' . esc_html($this->isSelected($records_per_page, 100)) . '>100</option>' .
596 '<option value="200" ' . esc_html($this->isSelected($records_per_page, 200)) . '>200</option>' .
597 '<option value="500" ' . esc_html($this->isSelected($records_per_page, 500)) . '>500</option>' .
598 '<option value="1000" ' . esc_html($this->isSelected($records_per_page, 1000)) . '>1000</option>' .
599 '<option value="2000" ' . esc_html($this->isSelected($records_per_page, 2000)) . '>2000</option>' .
600 '<option value="5000" ' . esc_html($this->isSelected($records_per_page, 5000)) . '>5000</option>' .
601 '<option value="10000" ' . esc_html($this->isSelected($records_per_page, 10000)) . '>10000</option>' .
602 '<option value="9999999" ' . esc_html($this->isSelected($records_per_page, 9999999)) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALL')) . '</option>' .
603 '</select>' .
604 '<input type="submit" class="button" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '" />' .
605 '</div>' .
606 '<input type="hidden" name="page" value="gptranslate" />' .
607 '</form>';
608
609 // Bottoni Import/Export
610 echo '<div class="action-buttons-toolbar">';
611 echo '<button class="button button-primary" id="bulk-delete-btn">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '</button>';
612 echo '<button class="button button-primary" id="toggle-migration">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_BTNS')) . '</button>';
613
614 // Export CSV/XLIFF
615 $exportFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_export_translations_xliff' : 'gptranslate_export_translations_csv';
616 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" style="display:inline;margin-right:10px;">';
617 if($exportFormat == 'gptranslate_export_translations_csv') {
618 wp_nonce_field('gptranslate_export_csv', 'gptranslate_export_csv_nonce');
619 } else {
620 wp_nonce_field('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce');
621 }
622 echo '<input type="hidden" name="action" value="' . $exportFormat . '">';
623 echo '<input type="submit" class="button button-primary" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXPORT_TRANSLATIONS')) . '">';
624 echo '</form>';
625
626 // Import CSV/XLIFF
627 $importFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_import_translations_xliff' : 'gptranslate_import_translations_csv';
628 echo '<input type="button" class="button button-primary button-import" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_IMPORT_TRANSLATIONS')) . '">';
629 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" enctype="multipart/form-data" style="display:inline;">';
630 if($importFormat == 'gptranslate_import_translations_csv') {
631 wp_nonce_field('gptranslate_import_csv', 'gptranslate_import_csv_nonce');
632 $acceptFormat = '.csv';
633 } else {
634 wp_nonce_field('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce');
635 $acceptFormat = '.xliff';
636 }
637 echo '<input type="hidden" name="action" value="' . $importFormat . '">';
638 echo '<input type="file" name="import_file" class="toggle-import hidden" accept="' . $acceptFormat . '" required>';
639 echo '<input type="submit" class="button button-primary toggle-import hidden" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '">';
640 echo '</form>';
641
642 echo '</div>';
643
644 echo '<div id="migraterow" class="hidden">
645 <span class="input-group">
646 <label for="migratetranslations_currentdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_PREVIOUS_DOMAIN')) . '</strong></label>
647 <input type="text" class="form-control" id="migratetranslations_currentdomain" name="migratetranslations_currentdomain" value="">
648 </span>
649 <span class="input-group">
650 <label for="migratetranslations_newdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_NEW_DOMAIN')) . '</strong></label>
651 <input type="text" class="form-control" id="migratetranslations_newdomain" name="migratetranslations_newdomain" value="">
652 </span>
653 <button class="button button-primary" id="migrationconfirm">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '</button>
654 <button class="button" id="migrationcancel">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
655 </div>
656 <input type="button" class="button button-warning button-crawler" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '">
657
658 <script>
659 const PLG_GPTRANSLATE_MIGRATION_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_SUCCESS')) . '";
660 const PLG_GPTRANSLATE_MIGRATION_FAILED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_FAILED')) . '";
661 const PLG_GPTRANSLATE_UNKNOWN_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_UNKNOWN_ERROR')) . '";
662 const PLG_GPTRANSLATE_NETWORK_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_NETWORK_ERROR')) . '";
663 const PLG_GPTRANSLATE_BULK_DELETE_CONFIRM = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_CONFIRM')) . '";
664 const PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE')) . '";
665 const PLG_GPTRANSLATE_BULK_DELETE_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SUCCESS')) . '";
666 const PLG_GPTRANSLATE_BULK_DELETE_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_ERROR')) . '";
667 const PLG_GPTRANSLATE_BULK_DELETE_NETWORK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_NETWORK')) . '";
668 const PLG_GPTRANSLATE_CRAWLER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '";
669 const PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE')) . '";
670 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
671 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
672 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START')) . '";
673 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
674 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
675 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
676 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
677 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
678 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
679 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
680 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
681 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
682 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
683 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
684 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
685 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
686 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
687 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
688 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
689 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
690 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
691 var gptranslateBaseCrawlerHome = "' . esc_js( trailingslashit( get_site_url() ) ) . '";
692 var gptranslateDefaultLanguage = "' . $opts['language'] . '";
693 var gptranslateCrawlerTimeout = "' . (isset($opts['crawler_timeout']) ? $opts['crawler_timeout'] : '30') . '";
694 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($opts['crawler_exclusions']) ? $opts['crawler_exclusions'] : ''))), ',')) . '";
695 var gptranslateRewriteLanguageUrl = ' . (int)$opts['rewrite_language_url'] . ';
696 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($opts['omit_prefix_original_language']) ? (int)$opts['omit_prefix_original_language'] : 0) . ';
697 var gptVersionNumeric = ' . 0 . ';
698 </script>';
699
700
701 echo '<table class="widefat fixed striped">';
702 echo '<thead><tr>';
703 echo '<th style="width: 1%"><input class="form-check-input" autocomplete="off" type="checkbox" id="checkall"></th>';
704 echo '<th style="width: 2%;;white-space:nowrap">ID</th>';
705 if($opts['rewrite_language_alias'] == 1) {
706 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
707 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</th>';
708 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
709 } else {
710 echo '<th style="width: 25%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
711 echo '<th style="width: 20%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
712 }
713 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</th>';
714 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</th>';
715 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</th>';
716 echo '<th style="width:5%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_ENGINE')) . '</th>';
717 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DATE')) . '</th>';
718 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ACTIONS')) . '</th>';
719 echo '</tr></thead>';
720
721 echo '<tbody>';
722
723 $current_page = isset($_GET['paged']) && is_numeric($_GET['paged']) ? (int)$_GET['paged'] : 1;
724 $offset = ($current_page - 1) * $records_per_page;
725 $sql_count = "SELECT COUNT(*) FROM {$this->table_name} WHERE 1=1";
726
727 // Add dynamic filters
728 if (!empty($searchFilter)) {
729 $sql_count .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
730 }
731 if ($publishedFilter !== '') {
732 $sql_count .= " AND published = '" . esc_sql($publishedFilter) . "'";
733 }
734 if (!empty($languageFilter)) {
735 $sql_count .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
736 }
737 if (!empty($engineFilter)) {
738 $sql_count .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
739 }
740 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
741 $total_records = $wpdb->get_var($sql_count); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
742 $total_pages = ceil($total_records / $records_per_page);
743
744 // Load records count with filtering
745 $sql_data = "SELECT * FROM {$this->table_name} WHERE 1=1";
746
747 // Add dynamic filters
748 if (!empty($searchFilter)) {
749 $sql_data .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
750 }
751 if ($publishedFilter !== '') {
752 $sql_data .= " AND published = '" . esc_sql($publishedFilter) . "'";
753 }
754 if (!empty($languageFilter)) {
755 $sql_data .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
756 }
757 if (!empty($engineFilter)) {
758 $sql_data .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
759 }
760 $sql_data .= " ORDER BY translate_date DESC LIMIT $records_per_page OFFSET $offset";
761
762 // Load records with filtering and pagination
763 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
764 $records = $wpdb->get_results($sql_data); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
765
766 // Message for no translations
767 if (!count($records) && stripos($sql_data, "AND") === false) {
768 echo '<div class="notice notice-success is-dismissible" id="notranslations-notice">';
769 echo '<p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO_TRANSLATIONS')) . '</p>';
770 echo '</div>';
771 }
772
773 foreach ( $records as $r ) {
774 // Build a short summary of the translations
775 $tr = json_decode( $r->translations, true );
776 if ( is_array( $tr ) ) {
777 $pairs = array_map(
778 function( $k, $v ) {
779 return esc_html( $k ) . '' . esc_html( $v );
780 },
781 array_keys( $tr ),
782 array_values( $tr )
783 );
784 $short = implode( ', ', $pairs );
785 $short = mb_substr( $short, 0, 80 ) . ( mb_strlen( $short ) > 80 ? '' : '' );
786 } else {
787 $short = '';
788 }
789
790 // Escape all output
791 $id = (int) $r->id;
792 $link = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=edit&edit={$id}" ), 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
793 $deleteLink = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=delete_translation&translation_id={$id}" ), 'gptranslate_delete_' . $id, '_gptranslate_nonce' );
794 $origLang = esc_html( strtoupper( $r->languageoriginal ) );
795 $transLang = esc_html( strtoupper( $r->languagetranslated ) );
796 $pub = $r->published ? esc_html($this->loadTranslations('PLG_GPTRANSLATE_YES')) : esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO'));
797 $engine = esc_html( $r->translation_engine );
798 $date = esc_html( $r->translate_date );
799
800 $langOriginal = esc_attr($r->languageoriginal);
801
802 // Path relativo o assoluto all'immagine della bandiera
803 $flagUrlOriginal = plugins_url('flags/svg/' . $r->languageoriginal . '.svg', __FILE__);
804 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
805 $flagUrlTranslated = plugins_url('flags/svg/' . $r->languagetranslated . '.svg', __FILE__);
806 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
807
808 $togglePublishedUrl = wp_nonce_url(
809 admin_url("admin.php?page=gptranslate&action=toggle_published&translation_id={$id}"),
810 'gptranslate_toggle_' . $id,
811 '_gptranslate_nonce'
812 );
813
814 $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
815 : '<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
816
817
818 $pub = $r->published ? "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-published' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . "'>" . $pubIcon . "</a>"
819 : "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-unpublished' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . "'>" . $pubIcon . "</a>";
820
821 $local_date = get_date_from_gmt($date);
822
823 echo '<tr>';
824 echo "<td style='width: 1%'><input class='form-check-input' autocomplete='off' type='checkbox' id='cb0' name='gptid[]' value='" . esc_attr($r->id) . "'></td>";
825 echo "<td style='width: 2%;white-space:nowrap'>". esc_html($r->id) . "</td>";
826 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>";
827 if($opts['rewrite_language_alias'] == 1) {
828 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>';
829 }
830 echo "<td>" . esc_html($short) . "</td>";
831 echo "<td>" . wp_kses_post($flagOriginal) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($langOriginal))) . "</td>";
832 echo "<td>" . wp_kses_post($flagTranslated) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper(esc_attr($r->languagetranslated)))) . "</td>";
833 echo "<td>" . wp_kses_post($pub) . "</td>";
834 echo "<td><span class='gpt-label'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper($engine) . '_ENGINE')) . "</span></td>";
835 echo "<td>" . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime($local_date)) ) . "</td>";
836 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>";
837 echo '</tr>';
838 }
839 echo '</tbody>';
840 echo '</table>';
841
842 if ($total_pages > 1) {
843 echo '<div class="tablenav"><div class="tablenav-pages">';
844 for ($i = 1; $i <= $total_pages; $i++) {
845 $url = add_query_arg(array_merge($_GET, ['paged' => $i]), admin_url('admin.php'));
846 $class = ($i == $current_page) ? "class='current-page button'" : "class='button'";
847 echo "<a " . wp_kses_post($class) . " href='" . esc_url($url) . "'>" . esc_html($i) . "</a> ";
848 }
849 echo '</div></div>';
850 }
851 }
852
853 echo '</div>';
854 echo '</div>';
855 }
856
857 /**
858 * Load language file and translations
859 * @return array
860 */
861 public function loadTranslations($key) {
862 // Text translations
863 static $adminLanguageStrings = null;
864
865 if(!$adminLanguageStrings) {
866 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
867 if(file_exists($adminLanguageFile)) {
868 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
869 }
870 }
871
872 if(array_key_exists($key, $adminLanguageStrings)) {
873 return $adminLanguageStrings[$key];
874 }
875
876 return $key;
877 }
878
879 /**
880 * Load language file and translations
881 * @return array
882 */
883 public static function loadTranslation($key) {
884 // Text translations
885 static $adminLanguageStrings = null;
886
887 if(!$adminLanguageStrings) {
888 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
889 if(file_exists($adminLanguageFile)) {
890 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
891 }
892 }
893
894 if(array_key_exists($key, $adminLanguageStrings)) {
895 return $adminLanguageStrings[$key];
896 }
897
898 return $key;
899 }
900
901 /**
902 * Save translation record
903 *
904 * @access public
905 */
906 public function save_record() {
907 global $wpdb;
908
909 // Retrieve and sanitize basic inputs
910 $id = isset ( $_POST ['id'] ) ? intval ( $_POST ['id'] ) : 0;
911 $formAction = isset ( $_POST ['action'] ) ? sanitize_key ( $_POST ['action'] ) : '';
912
913 if ( !isset($_POST['_gptranslate_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_gptranslate_nonce'])), 'gptranslate_save_record_action') ) {
914 wp_die(esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'gptranslate');
915 }
916
917 // Handle cancel action
918 if ($formAction === 'cancel_gptranslate_record') {
919 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
920 exit ();
921 }
922
923 // Sanitize pagelink
924 $pagelink = isset ( $_POST ['pagelink'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['pagelink'] ) ) : '';
925
926 // Sanitize translated_alias
927 $translatedAlias = isset ( $_POST ['translated_alias'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['translated_alias'] ) ) : '';
928
929 // Process and sanitize translations JSON
930 $raw_translations = filter_input( INPUT_POST, 'translations_json', FILTER_UNSAFE_RAW );
931 $raw_translations = is_string($raw_translations) ? $raw_translations : '[]';
932
933 $decoded_translations = json_decode ( $raw_translations, true );
934 if (! is_array ( $decoded_translations )) {
935 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
936 'response' => 400
937 ] );
938 }
939 $clean_translations = $decoded_translations;
940 $sanitized_translations_json = wp_json_encode ( $clean_translations );
941
942 // Process and sanitize alternative translations JSON
943 $raw_alt = filter_input( INPUT_POST, 'alt_translations_json', FILTER_UNSAFE_RAW );
944 $raw_alt = is_string($raw_translations) ? $raw_alt : '[]';
945
946 $decoded_alt = json_decode ( $raw_alt, true );
947 if (! is_array ( $decoded_alt )) {
948 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_ALTTRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
949 'response' => 400
950 ] );
951 }
952 $clean_alt = $decoded_alt;
953 $sanitized_alt_json = wp_json_encode ( $clean_alt );
954
955 // Update database record
956 $wpdb->update ( $this->table_name, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
957 'pagelink' => $pagelink,
958 'translated_alias' => $translatedAlias,
959 'translations' => $sanitized_translations_json,
960 'alt_translations' => $sanitized_alt_json
961 ], [
962 'id' => $id
963 ], [
964 '%s',
965 '%s',
966 '%s'
967 ], [
968 '%d'
969 ] );
970
971 // Redirect based on action
972 if ($formAction === 'save_gptranslate_record_and_close') {
973 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
974 } else {
975 $url = admin_url( 'admin.php?page=gptranslate&action=edit&edit=' . $id );
976 $url = wp_nonce_url( $url, 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
977 wp_redirect( html_entity_decode( $url ) );
978 }
979 exit ();
980 }
981
982 public function gptranslate_handle_deletion() {
983 // 1) Verify nonce
984 $id = isset( $_GET['translation_id'] ) ? intval( $_GET['translation_id'] ) : 0;
985 $nonce = isset( $_GET['_gptranslate_nonce'] ) ? wp_unslash( $_GET['_gptranslate_nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
986
987 if ( ! wp_verify_nonce( $nonce, 'gptranslate_delete_' . $id ) ) {
988 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
989 }
990
991 // 2) Delete the row
992 global $wpdb;
993 $table = $wpdb->prefix . 'gptranslate';
994 $deleted = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
995
996 // 3) Redirect back with a message
997 $redirect_url = add_query_arg(
998 [
999 'page' => 'gptranslate',
1000 'deleted' => $deleted ? '1' : '0',
1001 ],
1002 admin_url( 'admin.php' )
1003 );
1004 wp_redirect( $redirect_url );
1005 exit;
1006 }
1007
1008
1009 /**
1010 * Add main app frontend script
1011 *
1012 * @access public
1013 */
1014 public function enqueue_frontend_scripts() {
1015 add_filter('script_loader_tag', function($tag, $handle) {
1016 if ($handle === 'gptranslate-responsivevoice') {
1017 return str_replace('<script ', '<script defer ', $tag);
1018 }
1019
1020 if ($handle === 'gptranslate-jsonrepair') {
1021 return str_replace('<script ', '<script type="module" ', $tag);
1022 }
1023
1024 if ($handle === 'gptranslate-bstoast') {
1025 return str_replace('<script ', '<script type="module" ', $tag);
1026 }
1027 if ($handle === 'gptranslate-main') {
1028 // 1) prendi i valori raw
1029 $raw_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
1030 $raw_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
1031
1032 // 2) unslash WP
1033 $unslashed_uri = wp_unslash ( $raw_uri );
1034 $unslashed_host = wp_unslash ( $raw_host );
1035
1036 // 3) sanitizza URI come URL
1037 $orig_url = esc_url_raw ( $unslashed_uri );
1038 // - accetta path e query, rimuove caratteri pericolosi
1039
1040 // 4) sanitizza host
1041 // a) rimuovi tag e control chars
1042 $host_clean = sanitize_text_field ( $unslashed_host );
1043 // b) mantieni solo [a-z0-9.-] per sicurezza
1044 $orig_domain = preg_replace ( '/[^a-z0-9.-]/i', '', $host_clean );
1045
1046 // 5) infine escape per attributo HTML
1047 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 );
1048 }
1049
1050 return $tag;
1051 }, 10, 2);
1052
1053 $settings = get_option("gptranslate_options");
1054
1055 // Excluded languages check
1056 $excludedLangs = isset($settings['excluded_languages']) ? (array) $settings['excluded_languages'] : [];
1057 if (!empty($excludedLangs) && defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
1058 if (in_array(GPTRANSLATE_CURRENT_LANG, $excludedLangs, true)) {
1059 return;
1060 }
1061 }
1062
1063 // Move default language to the first one in the list
1064 if($settings ['default_language_first']) {
1065 if(!isset($settings ['languages'])) {
1066 $settings ['languages'] = array_map ( 'strtolower', [
1067 'AF',
1068 'SQ',
1069 'AM',
1070 'AR',
1071 'HY',
1072 'AZ',
1073 'EU',
1074 'BE',
1075 'BN',
1076 'BS',
1077 'BG',
1078 'CA',
1079 'CEB',
1080 'NY',
1081 'ZH',
1082 'CO',
1083 'HR',
1084 'CS',
1085 'DA',
1086 'NL',
1087 'EN',
1088 'EO',
1089 'ET',
1090 'TL',
1091 'FI',
1092 'FR',
1093 'FY',
1094 'GL',
1095 'KA',
1096 'DE',
1097 'EL',
1098 'GU',
1099 'HT',
1100 'HA',
1101 'HAW',
1102 'IW',
1103 'HI',
1104 'HMN',
1105 'HU',
1106 'IS',
1107 'IG',
1108 'ID',
1109 'GA',
1110 'IT',
1111 'JA',
1112 'JW',
1113 'KN',
1114 'KK',
1115 'KM',
1116 'KO',
1117 'KU',
1118 'KY',
1119 'LO',
1120 'LA',
1121 'LV',
1122 'LT',
1123 'LB',
1124 'MK',
1125 'MG',
1126 'MS',
1127 'ML',
1128 'MT',
1129 'MI',
1130 'MR',
1131 'MN',
1132 'MY',
1133 'NE',
1134 'NO',
1135 'PS',
1136 'FA',
1137 'PL',
1138 'PT',
1139 'PA',
1140 'RO',
1141 'RU',
1142 'SM',
1143 'GD',
1144 'SR',
1145 'ST',
1146 'SN',
1147 'SD',
1148 'SI',
1149 'SK',
1150 'SL',
1151 'SO',
1152 'ES',
1153 'SU',
1154 'SW',
1155 'SV',
1156 'TG',
1157 'TA',
1158 'TE',
1159 'TH',
1160 'TR',
1161 'UK',
1162 'UR',
1163 'UZ',
1164 'VI',
1165 'CY',
1166 'XH',
1167 'YI',
1168 'YO',
1169 'ZU',
1170 'ZT'
1171 ] );
1172 }
1173 $defaultLanguageKeyIndex = array_search($settings ['language'], $settings ['languages']);
1174 if ($defaultLanguageKeyIndex !== false) {
1175 // Remove the 'de' element from its current position
1176 $defaultLanguage = $settings ['languages'][$defaultLanguageKeyIndex];
1177 unset($settings ['languages'][$defaultLanguageKeyIndex]);
1178
1179 // Re-index the array to maintain numerical indexes
1180 $settings ['languages'] = array_values($settings ['languages']);
1181
1182 // Add 'de' to the beginning of the array
1183 array_unshift($settings ['languages'], $defaultLanguage);
1184 }
1185 }
1186
1187 // build alt_flags array
1188 $alt_flags = array ();
1189 $raw_alt_flags = isset($settings ['alt_flags']) ? $settings ['alt_flags'] : [];
1190 foreach ( $raw_alt_flags as $country ) {
1191 if ($country == 'usa' || $country == 'canada' || $country == 'ireland')
1192 $alt_flags ['en'] = $country;
1193 elseif ($country == 'brazil')
1194 $alt_flags ['pt'] = $country;
1195 elseif ($country == 'mexico' or $country == 'argentina' or $country == 'colombia')
1196 $alt_flags ['es'] = $country;
1197 elseif ($country == 'quebec')
1198 $alt_flags ['fr'] = $country;
1199 elseif ($country == 'taiwan')
1200 $alt_flags ['zh'] = $country;
1201 elseif ($country == 'hongkong')
1202 $alt_flags ['zt'] = $country;
1203 }
1204
1205 // Build float position variables
1206 $float_position = $settings ['float_position'];
1207 if($float_position != 'inline'){
1208 list ( $switcher_vertical_position, $switcher_horizontal_position ) = explode ( '-', $float_position );
1209 } else {
1210 list ( $switcher_vertical_position, $switcher_horizontal_position ) = ['inline', 'inline'];
1211 }
1212
1213 // Set local flags path
1214 $flagsPath = trailingslashit(plugins_url('flags', __FILE__));
1215
1216 // Ensure a default array value
1217 if(!isset($settings['realtime_translations_retrigger_events'])) {
1218 $settings['realtime_translations_retrigger_events'] = ['click'];
1219 $settings['realtime_translations_retrigger_events_delay'] = 200;
1220 }
1221 if(!isset ( $settings ['translate_srcimages'] )) {
1222 $settings ['translate_srcimages'] = 0;
1223 }
1224 if(!isset ( $settings ['css_selector_realtime_translations_retrigger'] )) {
1225 $settings ['css_selector_realtime_translations_retrigger'] = '';
1226 }
1227 if(!isset($settings ['serverside_translations_language_switching_mode'])) {
1228 $settings ['serverside_translations_language_switching_mode'] = 'url';
1229 }
1230
1231 wp_register_script('gptranslate-main-inline', '', [], $this->version, true);
1232 wp_enqueue_script('gptranslate-main-inline');
1233
1234 // Example: $settings is an array like in Joomla
1235 $base64Encode = 'base' . 64 . '_encode';
1236 $key = $settings['chatgpt_apikey'];
1237 $key = strrev($key);
1238 $secret = 'gptranslate';
1239 $out = '';
1240 for ($i = 0; $i < strlen($key); $i++) {
1241 $out .= chr(ord($key[$i]) ^ ord($secret[$i % strlen($secret)]));
1242 }
1243 $encoded = $base64Encode($out);
1244
1245 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
1246
1247 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";
1248 var gptApiKey = "' . esc_js(hash( 'sha256', get_site_url() )) . '";
1249 var gptLiveSite = "' . esc_js(get_site_url()) . '";
1250 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
1251 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
1252 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
1253 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
1254 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
1255 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
1256 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
1257 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
1258 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
1259 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
1260 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
1261 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
1262 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
1263 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
1264 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
1265 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
1266 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
1267 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
1268 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
1269 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
1270 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
1271 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
1272 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
1273 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
1274 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
1275 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
1276 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
1277 var chatgptApiKey = "' . esc_js($encoded) . '";
1278 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
1279 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
1280 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
1281 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
1282 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
1283 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
1284 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
1285 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
1286 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
1287 var chatgptWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['words_leafnodes_excluded'], ', ')) . '";
1288 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
1289 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
1290 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
1291 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
1292 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
1293 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
1294 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
1295 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
1296 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
1297 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
1298 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
1299 var gptVersionNumeric = ' . 0 . ';
1300 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>\';';
1301
1302 // Inject it AFTER gptranslate-main-inline
1303 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
1304
1305 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
1306 wp_enqueue_script('gptranslate-js-specs');
1307 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
1308 window.gptranslateSettings["1"] = {
1309 "default_language": "' . $settings['language'] . '",
1310 "languages": ' . json_encode($settings['languages']) . ',
1311 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
1312 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
1313 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
1314 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
1315 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
1316 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
1317 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
1318 "widget_text_color": "' . $settings['widget_text_color'] . '",
1319 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
1320 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
1321 "equal_widths": ' . (int)$settings['equal_widths'] . ',
1322 "reader_button_position": "' . $settings['reader_button_position'] . '",
1323 "custom_css": "' . $settings['custom_css'] . '",
1324 "alt_flags": ' . json_encode($alt_flags). ',
1325 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
1326 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
1327 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
1328 "switcher_vertical_position": "' . $switcher_vertical_position . '",
1329 "flags_location": "' . esc_js($flagsPath) . '",
1330 "flag_loading": "' . $settings['flag_loading'] . '",
1331 "flag_style": "' . $settings['flag_style'] . '",
1332 "widget_max_height": ' . (int)$settings['widget_max_height'] . '
1333 };');
1334
1335 $languageStringsScript = '';
1336
1337 // Generic translations
1338 $labels = [
1339 'TRANSLATING',
1340 'TRANSLATING_WAIT',
1341 'TRANSLATING_COMPLETE',
1342 'READING_INPROGRESS',
1343 'READING_END',
1344 'READING_EMPTY'
1345 ];
1346
1347 foreach ($labels as $label) {
1348 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
1349 }
1350
1351 $languages = [
1352 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
1353 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
1354 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
1355 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KM', 'KO', 'KU', 'KY', 'LO',
1356 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
1357 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
1358 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
1359 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
1360 ];
1361
1362
1363 foreach ($languages as $lang) {
1364 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
1365 }
1366
1367 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
1368 wp_enqueue_script('gptranslate-js-language-strings');
1369 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
1370
1371
1372 // Dictionary
1373 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
1374 if ($words_leafnodes_excluded_bylanguage_repeatable) {
1375 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
1376 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
1377 }
1378
1379 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
1380 $formatted = [];
1381 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
1382 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
1383 'words_leafnodes_excluded_bylanguage' => $row['word'] ?? '',
1384 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
1385 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
1386 'words_leafnodes_excluded_bylanguage_translation' => $row['optionalTranslation'] ?? ''
1387 ];
1388 }
1389
1390 // Correctly formatted JSON encode
1391 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1392
1393 // Inietto i dati PRIMA che venga eseguito gptranslate.js
1394 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
1395 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
1396 wp_add_inline_script(
1397 'gptranslate-js-word-leafones-excluded-language',
1398 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
1399 );
1400 }
1401
1402 // Local or remote script
1403 if($settings['proxy_responsive_loading_script'] == 1) {
1404 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
1405 } else {
1406 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
1407 }
1408
1409 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
1410 wp_enqueue_script('gptranslate-main', plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js', [], $this->version, true);
1411
1412 // Enqueue Bootstrap component
1413 if(!$settings['disable_bootstrap_css']) {
1414 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
1415 wp_enqueue_style(
1416 'bootstrap-css',
1417 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
1418 [],
1419 '5.3.2'
1420 );
1421 } else {
1422 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
1423 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
1424 wp_enqueue_style('gptranslate-bootstrap-style');
1425 wp_add_inline_style('gptranslate-bootstrap-style', '
1426 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
1427 .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}
1428 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
1429 .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)}
1430 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
1431 .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}
1432 .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}
1433 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
1434 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
1435 .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}
1436 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
1437 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
1438 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
1439 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
1440 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
1441 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
1442 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
1443 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
1444 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
1445 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
1446 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
1447 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
1448 ');
1449
1450 // Closer for the toast element
1451 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
1452 wp_enqueue_script('gptranslate-toast-dismiss');
1453 wp_add_inline_script('gptranslate-toast-dismiss', '
1454 document.addEventListener("DOMContentLoaded", function() {
1455 document.addEventListener("click", function(e) {
1456 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
1457 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
1458 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
1459 const toast = btnClose.closest(".toast");
1460 if (toast) {
1461 toast.classList.remove("show");
1462 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
1463 if (progressContainer) {
1464 progressContainer.remove();
1465 }
1466 }
1467 }
1468 });
1469 });
1470 ');
1471 }
1472
1473 // Registra un handle CSS vuoto se necessario
1474 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
1475 wp_enqueue_style('gptranslate-dynamic-css');
1476
1477 // Prepara lo stile dinamico
1478 $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') . '; }' .
1479 '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; }' .
1480 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
1481 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
1482
1483 // Inietta il CSS inline
1484 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
1485
1486 // --- Load theme RTL stylesheet if available ---
1487 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
1488 // Common file names
1489 $rtl_candidates = array(
1490 get_stylesheet_directory() . '/style-rtl.css',
1491 get_stylesheet_directory() . '/rtl.css'
1492 );
1493
1494 $rtl_file = '';
1495 foreach ( $rtl_candidates as $candidate ) {
1496 if ( file_exists( $candidate ) ) {
1497 $rtl_file = $candidate;
1498 break;
1499 }
1500 }
1501
1502 if ( $rtl_file ) {
1503 $rtl_uri = str_replace(
1504 get_stylesheet_directory(),
1505 get_stylesheet_directory_uri(),
1506 $rtl_file
1507 );
1508
1509 wp_enqueue_style(
1510 'theme-rtl',
1511 $rtl_uri,
1512 array(),
1513 filemtime( $rtl_file )
1514 );
1515 }
1516 }
1517 }
1518 }
1519
1520 // Force WordPress.org update check on plugin activation
1521 register_activation_hook( __FILE__, function() {
1522 if ( function_exists('wp_update_plugins') ) {
1523 wp_update_plugins();
1524 }
1525 });
1526
1527 // Schedule a daily update check
1528 add_action( 'gptranslate_daily_update_check', function() {
1529 if ( function_exists('wp_update_plugins') ) {
1530 wp_update_plugins();
1531 }
1532 });
1533
1534 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
1535 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
1536 }
1537
1538 // 🧹 Cleanup scheduled event on deactivation
1539 register_deactivation_hook( __FILE__, function() {
1540 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
1541 });
1542
1543 /**
1544 * Global function to add links to WP
1545 *
1546 * @access public
1547 */
1548 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) {
1549 $settings_link = '<a href="admin.php?page=gptranslate">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
1550 array_unshift($links, $settings_link);
1551 return $links;
1552 });
1553
1554 /**
1555 * Add main admin scripts for example to manage records add/delete functions
1556 *
1557 * @access public
1558 */
1559 add_action('admin_enqueue_scripts', function() {
1560 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1561 // Enqueue JS
1562 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
1563 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
1564
1565 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1566 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
1567 }
1568
1569 // Enqueue CSS
1570 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
1571 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
1572
1573 if(sanitize_key($_GET['page']) != 'gptranslate-settings' && !isset($_GET['action'])) {
1574 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
1575 }
1576
1577 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
1578 'ajaxurl' => admin_url('admin-ajax.php'),
1579 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
1580 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
1581 'gptApiKey' => hash( 'sha256', get_site_url() )
1582 ]);
1583 }
1584 });
1585
1586 add_action('wp_ajax_gptranslate_bulk_delete', function () {
1587 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
1588 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1589 }
1590
1591 global $wpdb;
1592 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
1593
1594 if (empty($ids)) {
1595 wp_send_json_error('No records selected');
1596 }
1597
1598 $table = $wpdb->prefix . 'gptranslate';
1599 $in = implode(',', array_fill(0, count($ids), '%d'));
1600 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
1601 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
1602
1603 if ($result === false) {
1604 wp_send_json_error('Database error');
1605 }
1606
1607 wp_send_json_success();
1608 });
1609
1610 // Handle Export CSV
1611 add_action('admin_post_gptranslate_export_translations_csv', function () {
1612 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
1613 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1614 }
1615
1616 global $wpdb;
1617 $table = $wpdb->prefix . 'gptranslate';
1618
1619 $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
1620
1621 if (!$records) {
1622 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
1623 }
1624
1625 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1626 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
1627
1628 header('Content-Type: text/csv');
1629 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
1630
1631 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1632 $output = fopen('php://output', 'w');
1633
1634 // Intestazioni CSV
1635 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
1636
1637 foreach ($records as $record) {
1638 fputcsv($output, $record, ",", '"', "\\");
1639 }
1640
1641 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1642 fclose($output);
1643 exit;
1644 });
1645
1646 // Handle Import CSV
1647 add_action('admin_post_gptranslate_import_translations_csv', function () {
1648 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
1649 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1650 }
1651
1652 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
1653 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
1654 }
1655
1656 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1657 if (!is_uploaded_file($tmp_name)) {
1658 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1659 }
1660
1661 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1662 $file = fopen($tmp_name, 'r');
1663 if (!$file) {
1664 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1665 }
1666
1667 $headers = fgetcsv($file);
1668 if (!$headers) {
1669 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
1670 }
1671
1672 global $wpdb;
1673 $table = $wpdb->prefix . 'gptranslate';
1674
1675 while (($row = fgetcsv($file)) !== false) {
1676 $countHeaders = count($headers);
1677 $countRow = count($row);
1678 // Invalid combine
1679 if($countHeaders != $countRow) {
1680 continue;
1681 }
1682 $record = array_combine($headers, $row);
1683 if (empty($record['pagelink'])) {
1684 continue; // skip if no primary key
1685 }
1686
1687 $pagelink = sanitize_text_field($record['pagelink']);
1688 $exists = $wpdb->get_var($wpdb->prepare(
1689 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
1690 $pagelink, $record['languageoriginal'], $record['languagetranslated']
1691 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1692
1693 $data = [
1694 'translated_alias' => $record['translated_alias'],
1695 'translations' => $record['translations'],
1696 'alt_translations' => $record['alt_translations'],
1697 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
1698 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
1699 'published' => isset($record['published']) ? (int)$record['published'] : 1,
1700 'translate_date' => $record['translate_date'],
1701 'translation_engine' => sanitize_text_field($record['translation_engine']),
1702 ];
1703
1704 if ($exists) {
1705 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1706 $table,
1707 $data,
1708 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
1709 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
1710 ['%s','%s','%s']
1711 );
1712 } else {
1713 $data['pagelink'] = $pagelink;
1714 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1715 $table,
1716 $data,
1717 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
1718 );
1719 }
1720 }
1721
1722 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1723 fclose($file);
1724
1725 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
1726 exit;
1727 });
1728
1729 // Handle Export XLIFF
1730 add_action('admin_post_gptranslate_export_translations_xliff', function () {
1731 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
1732 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1733 }
1734
1735 global $wpdb;
1736 $table = $wpdb->prefix . 'gptranslate';
1737
1738 $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
1739
1740 if (!$records) {
1741 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
1742 }
1743
1744 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1745 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
1746
1747 header('Content-Type: application/xml; charset=utf-8');
1748 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
1749
1750 $xml = new SimpleXMLElement('<xliff/>');
1751 $xml->addAttribute('version', '1.2');
1752
1753 foreach ($records as $record) {
1754 $file = $xml->addChild('file');
1755 $file->addAttribute('source-language', $record['languageoriginal']);
1756 $file->addAttribute('target-language', $record['languagetranslated']);
1757 $file->addAttribute('datatype', 'html');
1758 $file->addAttribute('original', $record['pagelink']);
1759
1760 $body = $file->addChild('body');
1761
1762 $translations = json_decode($record['translations'], true) ?: [];
1763 foreach ($translations as $source => $target) {
1764 $unit = $body->addChild('trans-unit');
1765 $unit->addAttribute('id', md5($source));
1766 $unit->addChild('source', htmlspecialchars($source));
1767 $unit->addChild('target', htmlspecialchars($target));
1768 }
1769 }
1770
1771 echo $xml->asXML();
1772 exit;
1773 });
1774
1775 // Handle Import XLIFF
1776 add_action('admin_post_gptranslate_import_translations_xliff', function () {
1777 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
1778 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1779 }
1780
1781 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
1782 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
1783 }
1784
1785 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1786 if (!is_uploaded_file($tmp_name)) {
1787 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
1788 }
1789
1790 $xml = simplexml_load_file($tmp_name);
1791 if (!$xml) {
1792 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
1793 }
1794
1795 global $wpdb;
1796 $table = $wpdb->prefix . 'gptranslate';
1797
1798 foreach ($xml->file as $file) {
1799 $sourceLang = (string)$file['source-language'];
1800 $targetLang = (string)$file['target-language'];
1801 $pagelink = (string)$file['original'];
1802
1803 $translations = [];
1804 foreach ($file->body->{'trans-unit'} as $unit) {
1805 $src = (string)$unit->source;
1806 $tgt = (string)$unit->target;
1807 $translations[$src] = $tgt;
1808 }
1809
1810 $json_translations = wp_json_encode($translations);
1811
1812 $exists = $wpdb->get_var($wpdb->prepare(
1813 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
1814 $pagelink, $sourceLang, $targetLang
1815 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1816
1817 if ($exists) {
1818 // �
1819 Update only the translations and date, keep everything else intact
1820 $wpdb->update(
1821 $table,
1822 [
1823 'translations' => $json_translations,
1824 'translate_date' => current_time('mysql'),
1825 ],
1826 ['id' => $exists]
1827 ); // phpcs:ignore
1828 } else {
1829 // �
1830 Insert full record only if it doesn't exist
1831 $data = [
1832 'pagelink' => $pagelink,
1833 'translated_alias' => '',
1834 'translations' => $json_translations,
1835 'alt_translations' => '[]',
1836 'languageoriginal' => $sourceLang,
1837 'languagetranslated'=> $targetLang,
1838 'published' => 1,
1839 'translate_date' => current_time('mysql'),
1840 'translation_engine'=> 'chatgpt',
1841 ];
1842 $wpdb->insert($table, $data); // phpcs:ignore
1843 }
1844 }
1845
1846 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
1847 exit;
1848 });
1849
1850 // Handle Export XML Sitemap
1851 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
1852 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
1853 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1854 }
1855
1856 global $wpdb;
1857 $table = $wpdb->prefix . 'gptranslate';
1858
1859 // Solo record con translated_alias non vuoto
1860 $records = $wpdb->get_results("
1861 SELECT *
1862 FROM $table
1863 WHERE translated_alias IS NOT NULL
1864 AND translated_alias != ''
1865 ORDER BY translate_date DESC
1866 ", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1867
1868 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1869 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
1870
1871 $dom = new DOMDocument('1.0', 'UTF-8');
1872 $dom->formatOutput = true;
1873
1874 $urlset = $dom->createElement('urlset');
1875 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
1876 $dom->appendChild($urlset);
1877
1878 foreach ($records as $record) {
1879 $url = $dom->createElement('url');
1880 $loc = $dom->createElement('loc');
1881 $loc->appendChild($dom->createTextNode($record['translated_alias']));
1882 $url->appendChild($loc);
1883
1884 if (!empty($record['translate_date'])) {
1885 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
1886 $url->appendChild($lastmod);
1887 }
1888
1889 $urlset->appendChild($url);
1890 }
1891
1892 header('Content-Type: application/xml; charset=UTF-8');
1893 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
1894 echo $dom->saveXML();
1895 exit;
1896 });
1897
1898 // Register Ajax handler
1899 add_action('wp_ajax_gptranslate_migrate_translations', function() {
1900 if (!current_user_can('manage_options')) {
1901 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
1902 }
1903
1904 check_ajax_referer('gptranslate_migrate_translations');
1905
1906 global $wpdb;
1907 $table = $wpdb->prefix . 'gptranslate';
1908 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
1909 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
1910
1911 if (empty($old) || empty($new)) {
1912 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
1913 }
1914
1915 $query = $wpdb->prepare(
1916 "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
1917 $old, $new, $old, $new
1918 );
1919
1920 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1921 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1922
1923 if ($result === false) {
1924 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
1925 } else {
1926 wp_send_json_success();
1927 }
1928 });
1929
1930 function gptranslate_export_settings() {
1931 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
1932 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
1933 }
1934
1935 $options = get_option( 'gptranslate_options', [] );
1936
1937 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
1938 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
1939
1940 header( 'Content-Type: application/json' );
1941 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
1942 echo wp_json_encode( $options );
1943 exit;
1944 }
1945 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
1946
1947 function gptranslate_import_settings() {
1948 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
1949 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
1950 }
1951
1952 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
1953 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
1954 }
1955
1956 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1957 $decoded = json_decode( $content, true );
1958
1959 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
1960 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
1961 }
1962
1963 // Optional: sanitize known values, or just update if you're confident of source
1964 update_option( 'gptranslate_options', $decoded );
1965
1966 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
1967 exit;
1968 }
1969 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
1970
1971 add_action('wp_footer', function () {
1972 // Add the default target container if default CSS selector
1973 $settings = get_option("gptranslate_options");
1974
1975 // Disable interface
1976 if($settings ['disable_control']) {
1977 $settings ['wrapper_selector'] = '';
1978 }
1979
1980 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
1981 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
1982 }
1983 });
1984
1985 // POST API REST Translations storage
1986 add_action('rest_api_init', function () {
1987 register_rest_route('gptranslate/v1', '/request', [
1988 'methods' => 'POST',
1989 'callback' => 'gpt_handle_request',
1990 'permission_callback' => 'gptranslate_public_permission'
1991 ]);
1992 });
1993
1994 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
1995 // Remove the default 'Settings' item
1996 unset($links[0]);
1997
1998 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
1999 array_unshift($links, $settings_link);
2000
2001 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
2002 array_unshift($links, $translations_link);
2003
2004 return $links;
2005 });
2006
2007 /*
2008 // Remove any WP update for the free version over the paid full one
2009 add_filter('auto_update_plugin', function($update, $item) {
2010 if (isset($item->slug) && $item->slug === 'gptranslate') {
2011 return false;
2012 }
2013 return $update;
2014 }, 10, 2);
2015
2016
2017 add_filter('site_transient_update_plugins', function($transient) {
2018 if (isset($transient->response['gptranslate/gptranslate.php'])) {
2019 unset($transient->response['gptranslate/gptranslate.php']);
2020 }
2021 return $transient;
2022 });
2023 */
2024
2025 /**
2026 * Permission callback public API
2027 * @param WP_REST_Request $request
2028 * @return bool|WP_Error
2029 */
2030 function gptranslate_public_permission( WP_REST_Request $request ) {
2031 // 1) Controllo chiave API inviata via header
2032 $headerApiKey = $request->get_header('x-gptranslate-key');
2033 $restApiKey = hash( 'sha256', get_site_url() );
2034 if ( $headerApiKey != $restApiKey) {
2035 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
2036 }
2037 return true;
2038 }
2039
2040 /**
2041 * Callback per GET/STORE translations via REST.
2042 * Frontend API
2043 *
2044 * @param WP_REST_Request $request
2045 * @return WP_REST_Response
2046 */
2047 function gpt_handle_request( WP_REST_Request $request ) {
2048 global $wpdb;
2049
2050 $table = $wpdb->prefix . 'gptranslate';
2051
2052 $params = $request->get_json_params();
2053
2054 if(!$params) {
2055 $params = $request->get_body_params();
2056 }
2057
2058 // Sanitize input params
2059 $task = sanitize_text_field( $params['task'] ?? '' );
2060 $pageLink = sanitize_text_field( $params['pagelink'] ?? '' );
2061 $translatedAlias = sanitize_text_field( $params['translated_alias'] ?? '' );
2062 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
2063 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
2064 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
2065 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
2066
2067 $now = current_time( 'mysql' );
2068
2069 $response = [ 'result' => false ];
2070
2071 if ( $task === 'storetranslations' ) {
2072 // Fetch raw param (could be array or JSON string)
2073 $rawFull = $params['translations'] ?? '[]';
2074 $rawAlt = $params['alt_translations'] ?? '[]';
2075
2076 // If it’s already a string (JSON), use it directly.
2077 // If it’s an array (unlikely with FormData), JSON encode it.
2078 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
2079 $fullTranslations = $rawFull;
2080 } else {
2081 $fullTranslations = wp_json_encode( (array) $rawFull );
2082 }
2083
2084 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
2085 $altTranslations = $rawAlt;
2086 } else {
2087 $altTranslations = wp_json_encode( (array) $rawAlt );
2088 }
2089
2090 // Verifica se esiste già
2091 $existing_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2092 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2093 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2094 "\n AND languageoriginal = %s" .
2095 "\n AND languagetranslated = %s",
2096 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2097 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
2098
2099 // Only if it is a retrigger then ignore the db processing
2100 if($retriggerTranslation !== 1) {
2101 if ( $existing_id ) {
2102 // UPDATE
2103 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2104 $table,
2105 [
2106 'translations' => $fullTranslations,
2107 'alt_translations' => $altTranslations,
2108 'translated_alias' => $translatedAlias,
2109 'translate_date' => $now,
2110 'translation_engine' => $translationEngine,
2111 ],
2112 [ 'id' => (int) $existing_id ],
2113 [ '%s', '%s', '%s', '%s', '%s' ],
2114 [ '%d' ]
2115 );
2116 $response['result'] = ( $updated !== false );
2117 } else {
2118 // INSERT
2119 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2120 $table,
2121 [
2122 'pagelink' => $pageLink,
2123 'translations' => $fullTranslations,
2124 'alt_translations' => $altTranslations,
2125 'translated_alias' => $translatedAlias,
2126 'languageoriginal' => $languageOriginal,
2127 'languagetranslated' => $languageTranslated,
2128 'published' => 1,
2129 'translate_date' => $now,
2130 'translation_engine' => $translationEngine,
2131 ],
2132 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
2133 );
2134 $response['result'] = ( $inserted !== false );
2135 }
2136 } else {
2137 $response['result'] = true;
2138 }
2139 } elseif ( $task === 'gettranslations' ) {
2140 $opts = get_option( 'gptranslate_options', [] );
2141 if ( ! empty( $opts['realtime_translations'] ) || $retriggerTranslation === 1) {
2142 $response['result'] = false;
2143 } else {
2144 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
2145 $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2146 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2147 "\n WHERE (pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s)" .
2148 "\n AND languageoriginal = %s" .
2149 "\n AND languagetranslated = %s" .
2150 "\n AND published = 1",
2151 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2152 ), ARRAY_A );
2153 } else {
2154 $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2155 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2156 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2157 "\n AND languageoriginal = %s" .
2158 "\n AND languagetranslated = %s" .
2159 "\n AND published = 1",
2160 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2161 ), ARRAY_A );
2162 }
2163
2164 if ( $row ) {
2165 $response['result'] = true;
2166 $response['translations'] = json_decode( $row['translations'], true ) ?: [];
2167 $response['alt_translations'] = json_decode( $row['alt_translations'], true ) ?: [];
2168 $response['translated_alias'] = $row['translated_alias'];
2169 $response['pagelink_alias'] = $row['pagelink'];
2170 } else {
2171 $response['result'] = false;
2172 }
2173 }
2174 } elseif ($task == 'getaliastranslation') {
2175 // Always perform a new realtime translation if the option is enabled
2176 try {
2177 $row = $wpdb->get_row( $wpdb->prepare(
2178 "SELECT translated_alias FROM {$table}" .
2179 "\n WHERE (pagelink = %s OR pagelink = %s)" .
2180 "\n AND languageoriginal = %s" .
2181 "\n AND languagetranslated = %s" .
2182 "\n AND published = 1",
2183 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
2184 ), ARRAY_A );
2185
2186 if ( $row ) {
2187 $response['result'] = true;
2188 $response['translated_alias'] = $row['translated_alias'] ?? '';
2189 } else {
2190 $response['result'] = false;
2191 }
2192 } catch ( Exception $e ) {
2193 $response['result'] = false;
2194 $response['exception'] = $e->getMessage();
2195 }
2196 } elseif ( $task === 'syncTranslation' ) {
2197 $original = wp_unslash( $params['original'] ?? '' );
2198 $translated = wp_unslash( $params['translated'] ?? '' );
2199 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
2200 $translationType = sanitize_text_field( $params['translation_type'] ?? 'translations' ); // default to 'translations'
2201
2202 // Recupera tutti i record nella lingua target
2203 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2204 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2205 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
2206 "\n WHERE languagetranslated = %s",
2207 $languageTranslated
2208 ) );
2209
2210 $updatedCount = 0;
2211
2212 foreach ( $rows as $row ) {
2213 $currentTranslations = json_decode( $row->$translationType, true );
2214
2215 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
2216 // Aggiorna la traduzione e salva
2217 $currentTranslations[ $original ] = $translated;
2218 $jsonUpdated = wp_json_encode( $currentTranslations );
2219
2220 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2221 $table,
2222 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
2223 [ 'id' => $row->id ],
2224 [ '%s', '%s' ],
2225 [ '%d' ]
2226 );
2227
2228 if ( $success !== false ) {
2229 $updatedCount++;
2230 }
2231 }
2232 }
2233
2234 $response['result'] = $updatedCount > 0;
2235 }
2236
2237 return rest_ensure_response( $response );
2238 }
2239
2240 // Instantiate and run the app
2241 GPTranslate::get_instance();