Changeset 3452753
- Timestamp:
- 02/03/2026 10:21:20 AM (2 months ago)
- Location:
- clonable
- Files:
-
- 142 added
- 21 edited
-
tags/2.9.0 (added)
-
tags/2.9.0/Bootstrap.php (added)
-
tags/2.9.0/clonable-wp.php (added)
-
tags/2.9.0/controllers (added)
-
tags/2.9.0/controllers/Controller.php (added)
-
tags/2.9.0/controllers/DashboardController.php (added)
-
tags/2.9.0/controllers/EmailTranslationsController.php (added)
-
tags/2.9.0/controllers/LandingPageController.php (added)
-
tags/2.9.0/controllers/LanguageSwitcherController.php (added)
-
tags/2.9.0/controllers/LanguageTagController.php (added)
-
tags/2.9.0/controllers/SettingController.php (added)
-
tags/2.9.0/controllers/WoocommerceController.php (added)
-
tags/2.9.0/controllers/index.php (added)
-
tags/2.9.0/exceptions (added)
-
tags/2.9.0/exceptions/ApiException.php (added)
-
tags/2.9.0/exceptions/CurlException.php (added)
-
tags/2.9.0/exceptions/ValidationException.php (added)
-
tags/2.9.0/helpers (added)
-
tags/2.9.0/helpers/Functions.php (added)
-
tags/2.9.0/helpers/Html.php (added)
-
tags/2.9.0/helpers/Json.php (added)
-
tags/2.9.0/helpers/Locales.php (added)
-
tags/2.9.0/helpers/MultiCurrency.php (added)
-
tags/2.9.0/helpers/Session.php (added)
-
tags/2.9.0/helpers/TextTranslator.php (added)
-
tags/2.9.0/helpers/UrlTranslator.php (added)
-
tags/2.9.0/helpers/clonable-locales-list.php (added)
-
tags/2.9.0/helpers/index.php (added)
-
tags/2.9.0/images (added)
-
tags/2.9.0/images/Logo_Icon_white.png (added)
-
tags/2.9.0/images/Logo_Icon_white.svg (added)
-
tags/2.9.0/images/clonable-full-logo.png (added)
-
tags/2.9.0/images/clonable.png (added)
-
tags/2.9.0/images/configure-wordpress-thumbnail.png (added)
-
tags/2.9.0/images/flags.png (added)
-
tags/2.9.0/images/index.php (added)
-
tags/2.9.0/images/language-switcher-thumbnail.png (added)
-
tags/2.9.0/index.php (added)
-
tags/2.9.0/middleware (added)
-
tags/2.9.0/middleware/Auth.php (added)
-
tags/2.9.0/middleware/ClonedSite.php (added)
-
tags/2.9.0/middleware/MiddlewareHandler.php (added)
-
tags/2.9.0/middleware/MiddlewareInterface.php (added)
-
tags/2.9.0/middleware/WooCommerce.php (added)
-
tags/2.9.0/middleware/index.php (added)
-
tags/2.9.0/models (added)
-
tags/2.9.0/models/ApiKey.php (added)
-
tags/2.9.0/models/ClonableWooCommerce.php (added)
-
tags/2.9.0/models/ClonedSite.php (added)
-
tags/2.9.0/models/EmailTranslations.php (added)
-
tags/2.9.0/models/LanguageSwitcher.php (added)
-
tags/2.9.0/models/LanguageTag.php (added)
-
tags/2.9.0/models/Settings.php (added)
-
tags/2.9.0/models/Site.php (added)
-
tags/2.9.0/models/index.php (added)
-
tags/2.9.0/objects (added)
-
tags/2.9.0/objects/ApiResponse.php (added)
-
tags/2.9.0/objects/CircuitBreaker.php (added)
-
tags/2.9.0/objects/ClonableConfig.php (added)
-
tags/2.9.0/objects/ClonableResponse.php (added)
-
tags/2.9.0/objects/CurlBuilder.php (added)
-
tags/2.9.0/objects/ExcludedProductTerm.php (added)
-
tags/2.9.0/objects/Notification.php (added)
-
tags/2.9.0/objects/index.php (added)
-
tags/2.9.0/readme-da_DK.txt (added)
-
tags/2.9.0/readme-de_DE.txt (added)
-
tags/2.9.0/readme-es_ES.txt (added)
-
tags/2.9.0/readme-fr_FR.txt (added)
-
tags/2.9.0/readme-it_IT.txt (added)
-
tags/2.9.0/readme-nb_NO.txt (added)
-
tags/2.9.0/readme-nl_NL.txt (added)
-
tags/2.9.0/readme-sv_SE.txt (added)
-
tags/2.9.0/readme.txt (added)
-
tags/2.9.0/routes (added)
-
tags/2.9.0/routes/Router.php (added)
-
tags/2.9.0/routes/index.php (added)
-
tags/2.9.0/services (added)
-
tags/2.9.0/services/AdminUIService.php (added)
-
tags/2.9.0/services/AllowedHostsService.php (added)
-
tags/2.9.0/services/ApiService.php (added)
-
tags/2.9.0/services/CacheService.php (added)
-
tags/2.9.0/services/ClonableWooCommerceService.php (added)
-
tags/2.9.0/services/EmailTranslationsService.php (added)
-
tags/2.9.0/services/LanguageSwitcherService.php (added)
-
tags/2.9.0/services/LanguageTagService.php (added)
-
tags/2.9.0/services/LocaleService.php (added)
-
tags/2.9.0/services/MultiCurrencyService.php (added)
-
tags/2.9.0/services/ShortCodeService.php (added)
-
tags/2.9.0/services/SubfolderService.php (added)
-
tags/2.9.0/services/SyncService.php (added)
-
tags/2.9.0/services/index.php (added)
-
tags/2.9.0/services/modules (added)
-
tags/2.9.0/services/modules/DataPanelModule.php (added)
-
tags/2.9.0/services/modules/ExclusionModule.php (added)
-
tags/2.9.0/services/modules/ProductImporterModule.php (added)
-
tags/2.9.0/services/modules/TaxonomyModule.php (added)
-
tags/2.9.0/services/modules/index.php (added)
-
tags/2.9.0/traits (added)
-
tags/2.9.0/traits/Forms.php (added)
-
tags/2.9.0/traits/PluginCheck.php (added)
-
tags/2.9.0/traits/Validation.php (added)
-
tags/2.9.0/traits/index.php (added)
-
tags/2.9.0/uninstall.php (added)
-
tags/2.9.0/views (added)
-
tags/2.9.0/views/DashboardView.php (added)
-
tags/2.9.0/views/EmailTranslationsView.php (added)
-
tags/2.9.0/views/LanguageSwitcherView.php (added)
-
tags/2.9.0/views/LanguageTagView.php (added)
-
tags/2.9.0/views/Layout.php (added)
-
tags/2.9.0/views/OnboardingView.php (added)
-
tags/2.9.0/views/SettingsView.php (added)
-
tags/2.9.0/views/ViewInterface.php (added)
-
tags/2.9.0/views/WoocommerceView.php (added)
-
tags/2.9.0/views/css (added)
-
tags/2.9.0/views/css/clonable-admin-bar.css (added)
-
tags/2.9.0/views/css/clonable-extra-button.css (added)
-
tags/2.9.0/views/css/clonable-global.css (added)
-
tags/2.9.0/views/css/clonable-language-switcher.css (added)
-
tags/2.9.0/views/css/clonable-thumbnails.css (added)
-
tags/2.9.0/views/css/clonable-woocommerce.css (added)
-
tags/2.9.0/views/css/clonable.css (added)
-
tags/2.9.0/views/css/fomantic-dropdown.css (added)
-
tags/2.9.0/views/css/fomantic-transition.css (added)
-
tags/2.9.0/views/css/freakflags.css (added)
-
tags/2.9.0/views/css/index.php (added)
-
tags/2.9.0/views/index.php (added)
-
tags/2.9.0/views/scripts (added)
-
tags/2.9.0/views/scripts/alpine.js (added)
-
tags/2.9.0/views/scripts/dohjs.js (added)
-
tags/2.9.0/views/scripts/email-translations.js (added)
-
tags/2.9.0/views/scripts/fomantic-dropdown.js (added)
-
tags/2.9.0/views/scripts/fomantic-transition.js (added)
-
tags/2.9.0/views/scripts/index.php (added)
-
tags/2.9.0/views/scripts/language-switcher.js (added)
-
tags/2.9.0/views/scripts/onload.js (added)
-
tags/2.9.0/views/scripts/woocommerce-clone-field.js (added)
-
trunk/clonable-wp.php (modified) (13 diffs)
-
trunk/controllers/EmailTranslationsController.php (added)
-
trunk/controllers/WoocommerceController.php (modified) (3 diffs)
-
trunk/helpers/Functions.php (modified) (3 diffs)
-
trunk/helpers/Html.php (modified) (1 diff)
-
trunk/helpers/TextTranslator.php (added)
-
trunk/helpers/UrlTranslator.php (modified) (1 diff)
-
trunk/models/EmailTranslations.php (added)
-
trunk/objects/ClonableConfig.php (modified) (6 diffs)
-
trunk/readme-da_DK.txt (modified) (2 diffs)
-
trunk/readme-de_DE.txt (modified) (2 diffs)
-
trunk/readme-es_ES.txt (modified) (2 diffs)
-
trunk/readme-fr_FR.txt (modified) (2 diffs)
-
trunk/readme-it_IT.txt (modified) (2 diffs)
-
trunk/readme-nb_NO.txt (modified) (2 diffs)
-
trunk/readme-nl_NL.txt (modified) (2 diffs)
-
trunk/readme-sv_SE.txt (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/routes/Router.php (modified) (3 diffs)
-
trunk/services/ClonableWooCommerceService.php (modified) (11 diffs)
-
trunk/services/EmailTranslationsService.php (added)
-
trunk/traits/PluginCheck.php (modified) (1 diff)
-
trunk/traits/Validation.php (modified) (1 diff)
-
trunk/views/EmailTranslationsView.php (added)
-
trunk/views/WoocommerceView.php (modified) (3 diffs)
-
trunk/views/css/clonable-language-switcher.css (modified) (1 diff)
-
trunk/views/scripts/email-translations.js (added)
Legend:
- Unmodified
- Added
- Removed
-
clonable/trunk/clonable-wp.php
r3434441 r3452753 5 5 Description: Official plugin for improving your clones made with Clonable. 6 6 Plugin URI: https://kb.clonable.net/en/introduction/getting-started/wordpress#de-clonable-plug-in-downloaden 7 Version: 2. 8.27 Version: 2.9.0 8 8 Author: Clonable BV 9 9 Author URI: https://www.clonable.net … … 22 22 use Clonable\Services\AllowedHostsService; 23 23 use Clonable\Services\ClonableWooCommerceService; 24 use Clonable\Services\EmailTranslationsService; 24 25 use Clonable\Services\LanguageSwitcherService; 25 26 use Clonable\Helpers\UrlTranslator; … … 44 45 include_once "models/ClonedSite.php"; 45 46 include_once "models/Settings.php"; 47 include_once "models/EmailTranslations.php"; 46 48 47 49 // custom objects … … 53 55 include_once "objects/Notification.php"; 54 56 include_once "objects/CircuitBreaker.php"; 57 55 58 56 59 // include views … … 63 66 include_once "views/SettingsView.php"; 64 67 include_once "views/OnboardingView.php"; 68 include_once "views/EmailTranslationsView.php"; 65 69 66 70 // include controllers … … 72 76 include_once "controllers/SettingController.php"; 73 77 include_once "controllers/LandingPageController.php"; 78 include_once "controllers/EmailTranslationsController.php"; 74 79 75 80 // include helper classes … … 81 86 include_once "helpers/MultiCurrency.php"; 82 87 include_once "helpers/UrlTranslator.php"; 88 include_once "helpers/TextTranslator.php"; 83 89 84 90 // middleware … … 95 101 // include services 96 102 include_once "services/LanguageSwitcherService.php"; 97 //include_once "services/WPLanguageSwitcherService.php";98 103 include_once "services/ApiService.php"; 99 104 include_once "services/SubfolderService.php"; … … 107 112 include_once "services/CacheService.php"; 108 113 include_once "services/AdminUIService.php"; // general admin UI service for additions, notices, etc. 109 114 include_once "services/EmailTranslationsService.php"; 110 115 111 116 // include service modules … … 121 126 122 127 define('CLONABLE_NAME', 'Clonable'); 123 define('CLONABLE_VERSION', '2. 8.2');128 define('CLONABLE_VERSION', '2.9.0'); 124 129 125 130 if (defined('WP_CLI') && WP_CLI) { … … 148 153 $language_tag_service = new LanguageTagService(); 149 154 add_action('wp_head', array($language_tag_service, 'clonable_echo_language_tags'), 5); 150 155 151 156 $urlTranslator = new UrlTranslator(); 152 157 add_action('clonable_public_key_cron_hook', array($urlTranslator, 'clonable_get_public_key'), 10, 1); … … 158 163 error_log("[Clonable] Error while setting up language tags: {$exception->getMessage()}"); 159 164 } 160 161 165 162 166 try { … … 184 188 } catch (Exception $exception) { 185 189 error_log("[Clonable] Error while setting up WooCommerce modules: {$exception->getMessage()}"); 190 } 191 192 try { 193 $email_translations_service = new EmailTranslationsService(); 194 } catch (Exception $exception) { 195 error_log("[Clonable] Error setting up email translations: {$exception->getMessage()}"); 186 196 } 187 197 -
clonable/trunk/controllers/WoocommerceController.php
r3422714 r3452753 91 91 } 92 92 93 public function clonable_woocommerce_enable_pdf_invoice_translation_validate($input) { 94 return $this->validate_checkbox($input, 'clonable_woocommerce_enable_pdf_invoice_translation'); 95 } 96 93 97 public function clonable_curcy_currency_override_validate($input) 94 98 { … … 128 132 return $old_input; 129 133 } 134 130 135 foreach ($rows as $row) { 131 132 136 if (empty($row->domain)) { 133 137 add_settings_error('clonable_default_country_by_domain', 'err_empty_domain', 'Domain cannot be empty.'); … … 158 162 159 163 } 164 160 165 return Json::handle_output(json_encode($rows)); 161 166 } -
clonable/trunk/helpers/Functions.php
r3422714 r3452753 4 4 5 5 use Clonable\Objects\ClonableConfig; 6 use Clonable\Helpers\TextTranslator; 6 7 7 8 class Functions … … 182 183 // Build result rows for each domain 183 184 $rows = []; 184 185 185 foreach ($clone_urls as $domain) { 186 186 $rows[] = $saved_by_domain[$domain] ?? [ … … 194 194 return $rows; 195 195 } 196 197 public static function minify_html($html = '', $active = true) 198 { 199 if (empty($html)) { 200 return $html; 201 } 202 // Remove comments 203 $html = preg_replace('/<!--(?!<!)[^\[>].*?-->/', '', $html); 204 // Remove whitespace between tags 205 $html = preg_replace('/>\s+</', '><', $html); 206 // Remove leading and trailing whitespace 207 $html = trim($html); 208 // remove multiple spaces 209 $html = preg_replace('/\s+/', ' ', $html); 210 211 // remove inline js and css comments 212 $html = preg_replace_callback('/(<script\b[^>]*>)([\s\S]*?)(<\/script>)/', function ($matches) { 213 $script_open = $matches[1]; 214 $script_content = $matches[2]; 215 $script_close = $matches[3]; 216 217 // Remove JS comments 218 $script_content = preg_replace('/\/\/.*(?=[\n\r])|\/\*[\s\S]*?\*\//', '', $script_content); 219 220 return $script_open . $script_content . $script_close; 221 }, $html); 222 223 224 return $html; 225 } 226 227 public static function lower(string $value): string { 228 return function_exists('mb_strtolower') ? mb_strtolower($value) : strtolower($value); 229 } 230 231 public static function sanitize_text(string $text) { 232 if (empty($text)) { 233 return ""; 234 } 235 $value = trim($text); 236 if (function_exists('sanitize_text_field')) { 237 return sanitize_text_field($value); 238 } 239 240 // Fallback for non-WP contexts (static analysis / early load). 241 $value = strip_tags($value); 242 $value = preg_replace('/[\r\n\t\0\x0B]+/u' ,' ' ,$value); 243 244 return trim((string) $value); 245 } 246 247 public static function sanitize_email(string $email) { 248 if (empty($text)) { 249 return ""; 250 } 251 $value = trim($email); 252 if (function_exists('sanitize_email')) { 253 return sanitize_email($value); 254 } 255 256 $sanitized = (string) filter_var($value ,FILTER_SANITIZE_EMAIL); 257 return is_string($sanitized) ? $sanitized : ''; 258 } 196 259 } -
clonable/trunk/helpers/Html.php
r3422714 r3452753 127 127 self::include_cdn("Alpine-Tooltip.js", "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js", false, true); 128 128 self::include_cdn("Tippy.css", "https://unpkg.com/tippy.js@6/dist/tippy.css", true); 129 self::include_cdn("Collapse", "https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js", false, true); 129 130 self::include_cdn("Alpine.js", "https://unpkg.com/alpinejs", false, true); 130 131 } -
clonable/trunk/helpers/UrlTranslator.php
r3422714 r3452753 160 160 public static function clonable_get_full_url($site) 161 161 { 162 $domain = $site->domain; 162 163 163 if ($site && is_array($site)) {164 $domain = $site['domain'];165 }166 if ($site && is_object($site)) {167 $domain = $site->domain;168 }169 170 if (empty($domain)) {171 return '';172 }173 174 164 // Use fallback '/' for original site data, since there is no notion of subfolders there 175 165 $original_subfolder = ($site->original_subfolder ?? '/'); -
clonable/trunk/objects/ClonableConfig.php
r3378896 r3452753 3 3 namespace Clonable\Objects; 4 4 5 use Clonable\Helpers\Functions;6 5 use Clonable\Models\Site; 7 6 8 defined( 'ABSPATH') || exit;7 defined('ABSPATH') || exit; 9 8 10 9 /** … … 12 11 * the entire plugin. 13 12 */ 14 class ClonableConfig { 13 class ClonableConfig 14 { 15 15 const WOOCOMMERCE_QUERY_ID = "clonable-excluded-%s"; 16 16 const WOOCOMMERCE_TAXONOMY = 'clonable_excluded_products'; 17 17 const UT_API_ENDPOINT = 'ut.api.clonable.net'; 18 const TRANSLATIONS_API_ENDPOINT = 'translations.api.clonable.net'; 18 19 const SERVER_IP = '89.41.171.180'; 19 20 const SERVER_IPV6 = '2a01:7c8:e001:d9:0:0:0:e778'; … … 26 27 * @return array 27 28 */ 28 public static function get_clones() { 29 public static function get_clones() 30 { 29 31 $site = self::get_site(); 30 32 if (empty($site)) { … … 38 40 * @return Site|null 39 41 */ 40 public static function get_site() { 42 public static function get_site() 43 { 41 44 $response = get_option("clonable_site"); 42 45 if (empty($response)) { … … 52 55 * or return the domain + subfolder for the clone. 53 56 */ 54 public static function current_clonable_domain() { 57 public static function current_clonable_domain() 58 { 55 59 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 56 60 $server_data = $_SERVER; … … 67 71 return $domain . $subfolder; 68 72 } 73 74 /** 75 * Get the clone ID based on the current URL or refererrer URL. 76 * @return string|null 77 */ 78 public static function get_clone_id_by_current_url(): ?string { 79 $clones = self::get_clones(); 80 if (empty($clones)) { 81 return null; 82 } 83 84 $host_header = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? ($_SERVER['HTTP_HOST'] ?? ($_SERVER['SERVER_NAME'] ?? '')); 85 $sanitized_host = str_replace('www.', '', $host_header); 86 87 // Get all clones for this domain. 88 $domain_matches = []; 89 foreach ($clones as $clone) { 90 if ($clone['domain'] === $sanitized_host) { 91 $domain_matches[] = $clone; 92 } 93 } 94 95 if (empty($domain_matches)) { 96 return null; 97 } 98 99 if (count($domain_matches) === 1) { 100 return $domain_matches[0]['id']; 101 } 102 103 // More than 1 domain match basically means that there are multiple subfolder clones. 104 foreach ($domain_matches as $clone) { 105 if ($clone['is_subfolder_clone']) { 106 $subfolder = trim($clone['subfolder_clone'], '/'); 107 $request_uri = $_SERVER['HTTP_REFERER'] ?? '/'; 108 $request_path = parse_url($request_uri, PHP_URL_PATH); 109 if (strpos($request_path, $subfolder) !== false) { 110 return $clone['id']; 111 } 112 } 113 } 114 115 return null; 116 } 69 117 } -
clonable/trunk/readme-da_DK.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-de_DE.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-es_ES.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-fr_FR.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-it_IT.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-nb_NO.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme-nl_NL.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 Nieuwe WooCommerce email vertalingen. 36 Configureer vertaalde email voor je clones, inclusief the facturen en pakbonnen 37 34 38 v2.8.2 35 39 Probleem met default land opgelost -
clonable/trunk/readme-sv_SE.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/readme.txt
r3434441 r3452753 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2. 8.27 Stable tag: 2.9.0 8 8 License: GPL v2 or later 9 9 … … 32 32 33 33 == Changelog == 34 v2.9.0 35 New WooCommerce email translations. 36 Configure translated emails for your WooCommerce store, including the invoices. 37 34 38 v2.8.2 35 39 Fixed bug with default country -
clonable/trunk/routes/Router.php
r3280987 r3452753 9 9 use Clonable\Controllers\WooCommerceController; 10 10 use Clonable\Controllers\DashboardController; 11 use Clonable\Controllers\EmailTranslationsController; 11 12 use Clonable\MiddlewareHandler; 12 13 use Clonable\Traits\PluginCheck; … … 74 75 'middleware' => ['auth', 'cloned_site'], 75 76 ]; 77 76 78 $routes['language-switcher'] = [ 77 79 'name' => 'Language Switcher', … … 87 89 'type' => $this::PAGE_TYPE_TAB, 88 90 'middleware' => ['auth', 'cloned_site', 'woocommerce'], 91 ]; 92 93 // email translations tab 94 $routes['email-translations'] = [ 95 'name' => 'Email Translations', 96 'controller' => EmailTranslationsController::class, 97 'type' => $this::PAGE_TYPE_TAB, 98 'middleware' => ['auth', 'cloned_site'], 89 99 ]; 90 100 -
clonable/trunk/services/ClonableWooCommerceService.php
r3434441 r3452753 5 5 use Clonable\Helpers\Functions; 6 6 use Clonable\Helpers\Json; 7 use Clonable\Helpers\UrlTranslator;8 7 use Clonable\Objects\ClonableConfig; 9 8 use Clonable\Services\Modules\DataPanelModule; … … 12 11 use Clonable\Services\Modules\TaxonomyModule; 13 12 use Clonable\Traits\PluginCheck; 13 use Clonable\Helpers\TextTranslator; 14 14 15 15 use WC_Order; … … 20 20 { 21 21 use PluginCheck; 22 23 private const PDF_REPLACEMENTS_BY_DOMAIN = 'clonable_wpo_replacements_by_domain'; 24 private const OPT_HEADERS_BY_DOMAIN = 'clonable_email_headers_by_domain'; 22 25 23 26 public function __construct() … … 62 65 } 63 66 64 add_filter('default_checkout_billing_country', array($this, 'get_default_checkout_country')); 65 add_filter('default_checkout_shipping_country', function () { 66 return $this->get_default_checkout_country('shipping'); 67 // Set default checkout country filters (only when user not logged in so we don't interfere with existing user data) 68 add_action('plugins_loaded', function () { 69 if (!is_user_logged_in()) { 70 add_filter('default_checkout_billing_country', array($this, 'get_default_checkout_country')); 71 add_filter('default_checkout_shipping_country', function () { 72 return $this->get_default_checkout_country('shipping'); 73 }); 74 } 67 75 }); 68 } 69 76 77 // Add a hidden field containing the current clone ID for use in woocommerce email translations 78 add_action('woocommerce_after_order_notes', array($this, 'add_clone_id_field'), 10, 2); 79 // Add the clone ID to the order meta when saving the order 80 add_action('woocommerce_checkout_update_order_meta', array($this, 'save_order_clone_id')); 81 82 // Support for WooCommerce PDF Invoices & Packing Slips plugin translation 83 $pdf_translations_enabled = get_option('clonable_woocommerce_enable_pdf_invoice_translation', 'off') === 'on'; 84 if ($this->pdf_invoices_installed() && $pdf_translations_enabled) { 85 add_action('plugins_loaded', [$this, 'register_pdf_translation_hooks'], 20, 0); 86 } 87 } 88 89 public function register_pdf_translation_hooks() { 90 $filter_function_map = [ 91 "wpo_wcpdf_shop_phone_number_settings_text" => "shop_phone_number", 92 "wpo_wcpdf_shop_email_address_settings_text" => "shop_email_address", 93 "wpo_wcpdf_shop_name_settings_text" => "shop_name", 94 "wpo_wcpdf_vat_number_settings_text" => "vat_number", 95 "wpo_wcpdf_coc_number_settings_text" => "coc_number", 96 "wpo_wcpdf_shop_address_line_1_settings_text" => "shop_address_line_1", 97 "wpo_wcpdf_shop_address_line_2_settings_text" => "shop_address_line_2", 98 "wpo_wcpdf_shop_address_country_settings_text" => "shop_address_country", 99 "wpo_wcpdf_shop_address_state_settings_text" => "shop_address_state", 100 "wpo_wcpdf_shop_address_city_settings_text" => "shop_address_city", 101 "wpo_wcpdf_shop_address_postcode_settings_text" => "shop_address_postcode", 102 ]; 103 104 add_filter('wpo_wcpdf_get_html', function ($html, $document) { 105 return $this->get_pdf_template_translation($html, $document); 106 }, 10, 2); 107 108 foreach ($filter_function_map as $hook => $field_name) { 109 add_filter($hook, function ($text, $document) use ($field_name) { 110 return $this->get_pdf_field_replacement($field_name, $text, $document); 111 }, 10, 2); 112 } 113 114 // Support different logos per language in PDF documents (use multiple filter names for compatibility) 115 add_filter('wpo_wcpdf_logo', [$this, 'pdf_logo_url'], 10, 2); 116 add_filter('wpo_wcpdf_document_logo', [$this, 'pdf_logo_url'], 10, 2); 117 add_filter('wpo_wcpdf_header_logo', [$this, 'pdf_logo_url'], 10, 2); 118 add_filter('wpo_wcpdf_template_logo', [$this, 'pdf_logo_url'], 10, 2); 119 120 // logo ID filter 121 add_filter('wpo_wcpdf_header_logo_id', [$this, 'pdf_logo_id'], 10, 2); 122 } 123 124 /** 125 * Get PDF replacements for a specific clone 126 * 127 * @param string $clone_id 128 * @param string $field_name 129 * @param string $default 130 * 131 * @return string field replacement value or default when not found 132 */ 133 public function get_pdf_field_replacement(string $field_name, string $text, $document): string 134 { 135 $order = $document->order ?? null; 136 $clone_id = $order ? $this->get_clone_id_from_order($order) : ''; 137 138 if (!$clone_id) { 139 // check if we can get clone ID from the $_POST request 140 $clone_id = $clone_id ?? $this->get_clone_id_from_request(); 141 if (!$clone_id) { 142 error_log('No clone ID found for PDF translation'); 143 return $text; 144 } 145 } 146 147 $pdf_replacements = $this->get_pdf_replacements_for_clone($clone_id); 148 149 if ($pdf_replacements === null) { 150 return $text; 151 } 152 153 if (isset($pdf_replacements[$field_name]) && is_string($pdf_replacements[$field_name]) && trim($pdf_replacements[$field_name]) !== '') { 154 return $pdf_replacements[$field_name]; 155 } 156 157 return $text; 158 } 159 160 public function get_pdf_template_translation($html, $document) 161 { 162 $order = $document->order ?? null; 163 $clone_id = $order ? $this->get_clone_id_from_order($order) : ''; 164 165 if (!$clone_id) { 166 // check if we can get clone ID from the $_POST request 167 $clone_id = $clone_id ?? $this->get_clone_id_from_request(); 168 if (!$clone_id) { 169 error_log('No clone ID found for PDF translation'); 170 return $html; 171 } 172 } 173 174 // make sure element with class shop-info and addresses are not translated by adding notranslate class 175 $html = str_replace('class="shop-info', 'class="shop-info notranslate', $html); 176 $html = str_replace('class="address billing-address', 'class="address billing-address notranslate', $html); 177 $html = str_replace('class="address shipping-address', 'class="address shipping-address notranslate', $html); 178 179 // Call translation API 180 $translated_html = TextTranslator::translateText($html, (string) $clone_id, false) ?? $html; 181 return $translated_html ?: $html; 182 } 183 184 private function get_clone_id_from_order($orderObject) 185 { 186 if (!$orderObject || !($orderObject instanceof WC_Order)) { 187 return ''; 188 } 189 $clone_id = $orderObject->get_meta('clonable_clone_id'); 190 return $clone_id ? $clone_id : ''; 191 } 192 193 /** 194 * Filter PDF logo URL based on Clone 195 * 196 * @param string $logo_url Original logo URL 197 * @param mixed $document PDF document object 198 * @return string Custom logo URL or original 199 */ 200 public function pdf_logo_url($logo_url, $document) 201 { 202 $order = $document->order ?? null; 203 $clone_id = $order ? $this->get_clone_id_from_order($order) : ''; 204 if (!$clone_id) { 205 $clone_id = $this->get_clone_id_from_request(); 206 } 207 if (!$clone_id) { 208 return $logo_url; 209 } 210 211 // get the logo URL for the current clone ID 212 $headerInfo = $this->getHeaderInfoForClone($clone_id); 213 if ($headerInfo === null) { 214 return $logo_url; 215 } 216 217 $custom_logo_url = isset($headerInfo['email_logo']) && is_string($headerInfo['email_logo']) 218 ? trim($headerInfo['email_logo']) 219 : ''; 220 221 if ($custom_logo_url !== '') { 222 return $custom_logo_url; 223 } 224 225 return $logo_url; 226 } 227 228 /** 229 * Filter PDF logo ID based on Clone 230 * 231 * @param int $logo_id Original logo attachment ID 232 * @param mixed $document PDF document object 233 * @return int Custom logo attachment ID or original 234 */ 235 public function pdf_logo_id($logo_id, $document) 236 { 237 $order = $document->order ?? null; 238 239 $clone_id = $order ? $this->get_clone_id_from_order($order) : ''; 240 if (!$clone_id) { 241 $clone_id = $this->get_clone_id_from_request(); 242 } 243 if (!$clone_id) { 244 return $logo_id; 245 } 246 247 // get the logo URL for the current clone ID 248 $headerInfo = $this->getHeaderInfoForClone($clone_id); 249 if ($headerInfo === null) { 250 return $logo_id; 251 } 252 253 $custom_logo_url = isset($headerInfo['email_logo']) && is_string($headerInfo['email_logo']) 254 ? trim($headerInfo['email_logo']) 255 : ''; 256 257 $custom_logo_id = attachment_url_to_postid($custom_logo_url); 258 if ($custom_logo_id) { 259 return $custom_logo_id; 260 } 261 262 return $logo_id; 263 } 264 265 public function add_clone_id_field() 266 { 267 $clone_id = ClonableConfig::get_clone_id_by_current_url(); 268 echo '<input type="hidden" name="clonable_clone_id" value="' . $clone_id . '" />'; 269 } 70 270 71 271 /** … … 73 273 * 74 274 * @param string $type 'billing' or 'shipping' 75 *76 275 * @return string 77 276 */ … … 101 300 return ''; 102 301 } 103 104 public function add_origin_field($checkout) { 105 $value = parse_url( get_site_url(), PHP_URL_HOST ); 302 303 public function add_origin_field($checkout) 304 { 305 $value = parse_url(get_site_url(), PHP_URL_HOST); 106 306 echo "<input type=\"hidden\" name=\"clonable_origin\" value=\"" . esc_html($value) . "\" />"; 107 307 } … … 109 309 public function save_origin_field($order_id) 110 310 { 111 // phpcs:disable WordPress.Security.NonceVerification.Missing112 311 $post_data = wp_unslash($_POST); 113 312 if (!empty($post_data['clonable_origin'])) { … … 118 317 } 119 318 } 120 // phpcs:enable WordPress.Security.NonceVerification.Missing 319 } 320 321 public function save_order_clone_id($order_id) 322 { 323 $post_data = wp_unslash($_POST); 324 if (!empty($post_data['clonable_clone_id'])) { 325 $order = wc_get_order($order_id); 326 $order->update_meta_data('clonable_clone_id', sanitize_text_field($post_data['clonable_clone_id'])); 327 $order->save(); 328 } 121 329 } 122 330 … … 132 340 } 133 341 134 135 342 /** Change the return url to point to the clone site */ 136 343 public function filter_return_url($return_url, $order) … … 143 350 // Check if order origin was recorded. No validation is needed here, as it is done when saving the attribute. 144 351 $order_origin = get_post_meta($order->get_id(), 'clonable_origin', true); 145 146 // phpcs:disable WordPress.Security.NonceVerification.Missing147 352 $server_data = $_SERVER; 148 // phpcs:enable WordPress.Security.NonceVerification.Missing149 353 $host = $server_data['HTTP_HOST'] ?? null; 150 354 … … 230 434 } 231 435 } 436 437 private function get_clone_id_from_request(): string 438 { 439 return isset($_POST['clonable_clone_id']) ? (string) $_POST['clonable_clone_id'] : ''; 440 } 441 442 private function get_pdf_replacements_for_clone(string $clone_id): ?array 443 { 444 foreach ($this->get_pdf_replacements_by_domain() as $domain_pdf_replacements) { 445 if (!is_array($domain_pdf_replacements)) { 446 continue; 447 } 448 449 $db_clone_id = isset($domain_pdf_replacements['clone_id']) ? \trim((string) $domain_pdf_replacements['clone_id']) : ''; 450 if ($db_clone_id && $clone_id === $db_clone_id) { 451 return $domain_pdf_replacements; 452 } 453 } 454 455 return null; 456 } 457 458 private function get_pdf_replacements_by_domain(): array 459 { 460 $raw = get_option(self::PDF_REPLACEMENTS_BY_DOMAIN, '[]'); 461 $decoded = json_decode((string) $raw, true); 462 return is_array($decoded) ? $decoded : []; 463 } 464 465 466 private function getHeadersByDomain(): array 467 { 468 $raw = get_option(self::OPT_HEADERS_BY_DOMAIN, '[]'); 469 $decoded = json_decode((string) $raw, true); 470 return is_array($decoded) ? $decoded : []; 471 } 472 473 private function getHeaderInfoForClone(string $clone_id): ?array 474 { 475 foreach ($this->getHeadersByDomain() as $header_info) { 476 if (!is_array($header_info)) { 477 continue; 478 } 479 480 $header_clone_id = isset($header_info['clone_id']) ? trim((string) $header_info['clone_id']) : ''; 481 if ($header_clone_id !== '' && $header_clone_id === $clone_id) { 482 return $header_info; 483 } 484 } 485 486 return null; 487 } 232 488 } -
clonable/trunk/traits/PluginCheck.php
r3280987 r3452753 24 24 } 25 25 26 /** 27 * Check if Select PDF Invoices & Packing Slips for WooCommerce is installed 28 * @return bool 29 */ 30 public function pdf_invoices_installed() { 31 return $this->check_plugin_by_file('woocommerce-pdf-invoices-packing-slips/woocommerce-pdf-invoices-packingslips.php'); 32 } 33 26 34 /** 27 35 * Check if the WooCommerce Multi Currency plugin is installed -
clonable/trunk/traits/Validation.php
r3234826 r3452753 122 122 return $input; 123 123 } 124 125 public function validate_email_strict($email): bool { 126 if ($email === '') { 127 return false; 128 } 129 130 // WP's is_email() may not be available in some contexts (e.g. static analysis / early load). 131 if (function_exists('is_email')) { 132 return is_email($email) !== false; 133 } else { 134 return filter_var($email ,FILTER_VALIDATE_EMAIL) !== false; 135 } 136 } 124 137 } -
clonable/trunk/views/WoocommerceView.php
r3422714 r3452753 10 10 use Clonable\Models\ClonableWooCommerce; 11 11 use Clonable\Traits\Forms; 12 use Clonable\Objects\ClonableConfig;13 12 14 13 class WoocommerceView implements ViewInterface … … 206 205 $default_countries_per_domain = Functions::get_default_countries_per_domain(); 207 206 208 if ($default_countries_per_domain) 207 if ($default_countries_per_domain) { 209 208 $output = esc_js(wp_json_encode($default_countries_per_domain)); 209 } 210 210 211 211 // Build country list … … 216 216 } 217 217 218 ?> 218 ?> 219 219 <p style="max-width: 75%"> 220 220 Configure per domain which default billing and shipping country should be preselected during WooCommerce checkout. -
clonable/trunk/views/css/clonable-language-switcher.css
r3422714 r3452753 65 65 width: var(--clonable-language-switcher-flag-size); 66 66 height: var(--clonable-language-switcher-flag-size); 67 border-radius: var(--clonable-language-switcher-flag-radius); 67 68 } 68 69 69 70 .cl-language-switcher-lan-name { 70 font-size: var(--clonable-language-switcher-font-size);71 font-size: inherit; 71 72 white-space: nowrap; 73 } 74 75 .clonable-language-switcher i.fflag.ff-md { 76 margin-right: var(--clonable-language-switcher-fflag-margin-right); 77 width: var(--clonable-language-switcher-fflag-width); 78 height: var(--clonable-language-switcher-fflag-height); 72 79 } 73 80
Note: See TracChangeset
for help on using the changeset viewer.