├── .gitignore
├── CHANGELOG.md
├── README.md
├── composer.json
├── composer.lock
├── inc
├── class-mltools-custom-fields-translation.php
├── class-mltools-elementor-config-generator.php
├── class-mltools-gutenberg-config-generator.php
├── class-mltools-shortcode-attribute-filter.php
├── class-mltools-shortcode-config.php
├── class-mltools-shortcode-wpml-config-parser.php
├── class-mltools-xml-helper.php
├── wpml-compatibility-test-tools-base.class.php
├── wpml-compatibility-test-tools-messages.class.php
├── wpml-compatibility-test-tools.class.php
├── wpml-compatibility-test-tools.functions.php
└── wpml-modify-duplicate-strings.class.php
├── menus
└── settings
│ ├── auto-translate-duplicate.php
│ ├── auto-translate-strings.php
│ ├── custom-fields-translation.php
│ ├── generator.php
│ ├── overview.php
│ ├── settings.php
│ └── shortcode-helper.php
├── multilingual-tools.php
└── res
├── css
└── wctt-style.css
├── img
├── spinner.gif
├── wctt-icon.png
└── wctt-multiselect-bg.png
└── js
└── mt-script.js
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2.2.6
2 |
3 | ## Improvements
4 |
5 | - Added XML config generator for Gutenberg
6 |
7 |
8 | # 2.2.5
9 |
10 | ## Bugfix
11 |
12 | - Fixed fatal error when Elementor widget settings are empty
13 |
14 | # 2.2.4
15 |
16 | ## Improvements
17 |
18 | - Added XML config generator for Elementor
19 |
20 | # 2.2.3
21 |
22 | ## Improvements
23 |
24 | - Update jQuery changes.
25 |
26 | # 2.2.2
27 |
28 | ## Improvements
29 |
30 | - Update deprecated filter.
31 |
32 | # 2.2.1
33 |
34 | ## Improvements
35 |
36 | - Added strings and contexts count.
37 |
38 | # 2.2.0
39 |
40 | ## Improvements
41 |
42 | - Added order options by name.
43 | - Added support for "display-as-translated"
44 |
45 | ## Bugfix
46 |
47 | - Fixed auto translate strings issue with large number of strings.
48 |
49 | # 2.1.0
50 |
51 | ## Features
52 |
53 | - Add shortcode helper.
54 |
55 | # 2.0.0
56 |
57 | ## Improvements
58 |
59 | - Add a Overview screen which displays loaded configuration and validation errors.
60 |
61 | # 1.4.0
62 |
63 | ## Improvements
64 |
65 | - Add "Copy once" option for custom fields.
66 |
67 | # 1.3.1
68 |
69 | ## Bugfix
70 |
71 | - Fix attribute parent node.
72 |
73 | # 1.3.0
74 |
75 | ## Features
76 |
77 | - Add option for generating shortcodes.
78 |
79 | ## Improvements
80 |
81 | - Refactor XML generator code.
82 |
83 | # 1.2.3
84 |
85 | ## Bugfix
86 |
87 | - Fix "Translation management" notice
88 |
89 | # 1.2.2
90 |
91 | ## Improvements
92 |
93 | - Update composer file
94 |
95 | # 1.2.1
96 |
97 | ## Improvements
98 |
99 | - Add composer file.
100 | - Changelog style updated.
101 | - Minor content duplication code refactoring
102 |
103 | ## Bugfix
104 |
105 | - Fix automatic child selection in generator.
106 | - Fix JS object parsing.
107 | - Fix maximum execution time fatal error.
108 | - Fix duplicate post content special chars conversion.
109 | - Remove alt and title for images in content option.
110 |
111 | # 1.2
112 |
113 | ## Improvements
114 |
115 | - Major code improvements.
116 | - Add notice for successfully saved file in theme folder.
117 | - Add responsive layout.
118 | - Add dropdown for context selection.
119 | - Add dropdown option count.
120 | - Add flags for active languages.
121 | - Add new icon.
122 |
123 | # 1.1.2
124 |
125 | ## Improvements
126 |
127 | - Add checkboxes instead of option list when selecting contexts for translation.
128 | - Add "Toggle All" for admin text checkboxes.
129 | - Some code style improvements.
130 | - Checkboxes are now automatically checked on radio button change.
131 |
132 | ## Bugfix
133 |
134 | - WP Options with 'autoload' set as 'no' were not listed on configuration generator screen.
135 | - Escaped special characters in mt-script.js
136 | - Remove notice, checked strings inputs are remembered.
137 |
138 | # 1.1.1
139 |
140 | ## Improvements
141 |
142 | - Add necessary sanitization & validation.
143 |
144 | # 1.1
145 |
146 | ## Features
147 |
148 | - Add option for generating wpml-config.xml files.
149 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multilingual Tools
2 |
3 | 
4 | 
5 |
6 | Multilingual Tools are set of tools related to [WPML](https://wpml.org) plugin bundle. Created with tendency to ease multilingual testing process, and help out WordPress developers who applied to [GoGlobal Program](https://wpml.org/documentation/theme-compatibility/go-global-program/).
7 |
8 | ## Key features
9 |
10 | - Auto generate strings for translations
11 | - Add language information to post duplicate
12 | - Generate WPML config file ( wpml-config.xml )
13 | - Assist with Custom Fields translation preferences
14 |
15 | ## Requirements
16 |
17 | For this plugin to work you will need:
18 |
19 | - WPML Multilingual CMS
20 | - WPML String Translation
21 |
22 | All plugins can be downloaded from [WPML.org](https://wpml.org)
23 |
24 | ## Contribute
25 |
26 | If you spot any bug or have idea for useful feature feel free to contribute via [GitHub](https://github.com/OnTheGoSystems/multilingual-tools).
27 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "otgs/multilingual-tools",
3 | "type": "wordpress-plugin",
4 | "description": "Set of tools related to WPML plugin bundle. Created with tendency to ease WPML compatibility testing process.",
5 | "homepage": "https://wpml.org/documentation/related-projects/wpml-compatibility-test-tools-plugin/",
6 | "license": "GPL-2.0",
7 | "require": {
8 | "php": ">=5.6"
9 | }
10 | }
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "b40cca925aed46cf527bdd48ba810e71",
8 | "packages": [],
9 | "packages-dev": [],
10 | "aliases": [],
11 | "minimum-stability": "stable",
12 | "stability-flags": [],
13 | "prefer-stable": false,
14 | "prefer-lowest": false,
15 | "platform": {
16 | "php": ">=5.6"
17 | },
18 | "platform-dev": [],
19 | "plugin-api-version": "1.1.0"
20 | }
21 |
--------------------------------------------------------------------------------
/inc/class-mltools-custom-fields-translation.php:
--------------------------------------------------------------------------------
1 | get_results( "SELECT DISTINCT meta_key FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%' ORDER BY meta_key ASC" );
14 |
15 | $custom_fields = array();
16 |
17 | foreach ( $meta_keys as $meta_key ) {
18 | $custom_fields[] = $meta_key->meta_key;
19 | }
20 |
21 | // We need to exclude the fields with defined translation preference in WPML
22 |
23 | $excluded_custom_fields = array();
24 |
25 | $settings = get_option( 'icl_sitepress_settings' );
26 |
27 | if ( ! empty( $settings['translation-management']['custom_fields_translation'] ) ) {
28 | foreach ( $settings['translation-management']['custom_fields_translation'] as $custom_field => $value ) {
29 | $excluded_custom_fields[] = $custom_field;
30 | }
31 | }
32 |
33 | // Providing a filter to add more fields to be excluded
34 |
35 | /**
36 | * Example
37 | *
38 | * function my_custom_excluded_fields($excluded_fields) {
39 | * $excluded_fields[] = 'my_custom_field_1';
40 | * $excluded_fields[] = 'my_custom_field_2';
41 | * return $excluded_fields;
42 | * }
43 | * add_filter('wpml_custom_fields_helper_excluded_custom_fields', 'my_custom_excluded_fields');
44 | */
45 |
46 | $excluded_custom_fields = apply_filters( 'wpml_custom_fields_helper_excluded_custom_fields', $excluded_custom_fields );
47 |
48 | $custom_fields = array_diff( $custom_fields, $excluded_custom_fields );
49 |
50 | // We don't need these fields wpml_, attribute_pa-, acfml, etc..
51 |
52 | $excluded_prefixes = [ 'acfml', 'attribute_pa', 'wpml', 'wpform' ];
53 |
54 | $custom_fields = array_filter( $custom_fields, function ( $field ) use ( $excluded_prefixes ) {
55 | foreach ( $excluded_prefixes as $prefix ) {
56 | if ( strpos( $field, $prefix ) === 0 ) {
57 | return false;
58 | }
59 | }
60 |
61 | return true;
62 | } );
63 |
64 |
65 | return $custom_fields;
66 | }
67 |
68 | public function determine_translation_preference() {
69 |
70 | global $wpdb;
71 |
72 | $custom_fields = $this->get_custom_fields();
73 | $translation_preferences = array();
74 |
75 | foreach ( $custom_fields as $custom_field ) {
76 |
77 | $value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s LIMIT 1", $custom_field ) );
78 |
79 | // Check if value is numeric, a date string, or specific strings
80 |
81 | if ( $value ) {
82 | $date = DateTime::createFromFormat( 'd-m-Y', $value );
83 | $date_errors = DateTime::getLastErrors();
84 | }
85 |
86 | // These values should be copied to translations
87 | $copy_values = [ 'yes', 'no', 'on', 'off', 'true', 'false', 'default' ];
88 |
89 | // Is it a hash-like string? Something like ffd4rf34d should be set to copy
90 | $isHashString = $value && strlen( $value ) > 5 && preg_match( '/\d/', $value ) && preg_match( '/[a-zA-Z]/', $value ) && strpos( $value, ' ' ) === false;
91 |
92 |
93 | if ( is_numeric( $value ) ||
94 | ( $date && $date_errors['warning_count'] == 0 && $date_errors['error_count'] == 0 ) ||
95 | in_array( $value, $copy_values ) ||
96 | is_serialized( $value ) ||
97 | null ||
98 | empty( $value ) ||
99 | // Check if the value is an email or a URL.
100 | filter_var( $value, FILTER_VALIDATE_EMAIL ) ||
101 | strpos( $value, 'http' ) !== false
102 | || $isHashString
103 | ) {
104 | $translation_preferences[ $custom_field ] = 'copy';
105 | } else {
106 | $translation_preferences[ $custom_field ] = 'translate';
107 | }
108 | }
109 |
110 | return $translation_preferences;
111 | }
112 |
113 | public function wpml_cf_generate_xml() {
114 |
115 | check_ajax_referer( 'wpml_cf_nonce', 'wpml_cf_nonce' );
116 |
117 | // Prepare the base of your XML
118 | $wpml_config = '';
119 | foreach ( $_POST['cf'] as $custom_field => $preference ) {
120 | $custom_field = sanitize_text_field( $custom_field );
121 | $preference = sanitize_text_field( $preference );
122 |
123 | $wpml_config .= "$custom_field ";
124 | }
125 |
126 | $wpml_config .= ' ';
127 |
128 | // Create the XML file
129 | $formatted_xml = $this->format_xml( $wpml_config );
130 |
131 | echo $formatted_xml;
132 |
133 | wp_die();
134 | }
135 |
136 |
137 | public function format_xml( $xml_string ) {
138 | $dom = new DOMDocument;
139 | $dom->preserveWhiteSpace = false;
140 | $dom->loadXML( $xml_string );
141 | $dom->formatOutput = true;
142 |
143 | return htmlentities( $dom->saveXML( $dom->documentElement ) );
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/inc/class-mltools-elementor-config-generator.php:
--------------------------------------------------------------------------------
1 | ID, '_elementor_data', true );
20 |
21 | if ( ! empty( $elementor_data ) ) {
22 | foreach ( $screens as $screen ) {
23 | add_meta_box(
24 | 'wpmlpb_config_generator_box',
25 | 'WPML - Config Generator for Elementor',
26 | array( $this, 'meta_box_html' ),
27 | $screen
28 | );
29 | }
30 | }
31 | }
32 |
33 | public function get_widgets_blacklist() {
34 | $widgets_blacklist = array();
35 |
36 | if ( class_exists( 'WPML_Elementor_Translatable_Nodes' ) ) {
37 | $wpml_elementor_translatable_nodes = new WPML_Elementor_Translatable_Nodes();
38 | $default_widgets = $wpml_elementor_translatable_nodes->get_nodes_to_translate();
39 | $default_widgets = apply_filters( 'wpml_elementor_widgets_to_translate', $default_widgets );
40 |
41 | foreach ( $default_widgets as $key => $value ) {
42 | $widgets_blacklist[] = $key;
43 | }
44 | }
45 |
46 | return $widgets_blacklist;
47 | }
48 |
49 | public function get_settings_blacklist() {
50 | $blacklist = array();
51 |
52 | $blacklist[] = 'is_external';
53 | $blacklist[] = 'nofollow';
54 | $blacklist[] = 'custom_attributes';
55 |
56 | return $blacklist;
57 | }
58 |
59 | public function get_widgets_list( $elements ) {
60 | $widgets = array();
61 |
62 | foreach ( $elements as $element ) {
63 | if ( $element->elType === 'widget' && isset( $element->settings ) && is_object( $element->settings ) ) {
64 | $widgetType = $element->widgetType;
65 |
66 | $settings = $element->settings;
67 |
68 | if ( is_object( $settings ) ) {
69 | $settings = (array) get_object_vars( $settings );
70 | }
71 |
72 | foreach ( $settings as $field_key => $field_value ) {
73 | $settings[ $field_key ] = $this->get_field_from_widget( $field_key, $field_value, $widgetType );
74 | }
75 |
76 | $widgets[ $widgetType ] = array();
77 | $widgets[ $widgetType ]['widgetType'] = $widgetType;
78 | $widgets[ $widgetType ]['settings'] = $settings;
79 | }
80 |
81 | if ( ! empty( $element->elements ) ) {
82 | $widgets = array_merge( $widgets, $this->get_widgets_list( $element->elements ) );
83 | }
84 | }
85 |
86 | return $widgets;
87 | }
88 |
89 | public function get_field_from_widget( $field_key, $field_value, $parent = '' ) {
90 | // Repeater Fields
91 | if ( is_array( $field_value ) ) {
92 | $field['fieldKey'] = $field_key;
93 | $field['fieldType'] = 'repeater_field';
94 | $field['parent'] = $parent;
95 | $field['subFields'] = array();
96 |
97 | if ( is_array( $field_value ) && array_key_exists( '0', $field_value ) ) {
98 | $field_value = $field_value['0'];
99 | }
100 |
101 | foreach ( $field_value as $subfield_key => $subfield_value ) {
102 | $field['subFields'][ $subfield_key ] = $this->get_field_from_widget( $subfield_key, $subfield_value, $field_key );
103 | }
104 | }
105 |
106 | // Composite Fields
107 | elseif ( is_object( $field_value ) ) {
108 | $field['fieldKey'] = $field_key;
109 | $field['fieldType'] = 'parent_field';
110 | $field['parent'] = $parent;
111 | $field['subFields'] = array();
112 | $field_value = (array) $field_value;
113 |
114 | foreach ( $field_value as $subfield_key => $subfield_value ) {
115 | $field['subFields'][ $subfield_key ] = $this->get_field_from_widget( $subfield_key, $subfield_value, $field_key );
116 | }
117 | }
118 |
119 | // Regular Fields
120 | else {
121 | $field['fieldKey'] = $field_key;
122 | $field['fieldType'] = 'simple_field';
123 | $field['parent'] = $parent;
124 | $field['subFields'] = '';
125 | $field['fieldContent'] = $field_value;
126 | }
127 |
128 | return $field;
129 | }
130 |
131 | public function generate_wpml_config_xml( $widgets ) {
132 | if ( empty( $widgets ) ) {
133 | return __( 'All widgets on this page are already registered.', 'wpml-compatibility-test-tools' );
134 | }
135 |
136 | $settings_blacklist = $this->get_settings_blacklist();
137 | $xml = new SimpleXMLElement( ' ' );
138 | $elementor_widgets = $xml->addChild( 'elementor-widgets' );
139 | $registered_widgets = array();
140 |
141 | foreach ( $widgets as $widget ) {
142 | if ( in_array( $widget['widgetType'], $registered_widgets ) ) {
143 | continue;
144 | }
145 |
146 | $registered_widgets[] = $widget['widgetType'];
147 | $widget_xml = $elementor_widgets->addChild( 'widget' );
148 | $widget_xml->addAttribute( 'name', $widget['widgetType'] );
149 | $fields = $widget_xml->addChild( 'fields' );
150 |
151 | foreach ( $widget['settings'] as $key => $value ) {
152 | if ( in_array( $key, $settings_blacklist ) ) {
153 | continue;
154 | }
155 |
156 | if ( $value['fieldType'] == 'simple_field' ) {
157 | $field = $fields->addChild( 'field', $key );
158 | }
159 | // Repeater Fields
160 | elseif ( $value['fieldType'] == 'repeater_field' ) {
161 | $field = $fields->addChild( 'field', $key );
162 | $fields_in_item = $widget_xml->addChild( 'fields-in-item' );
163 | $fields_in_item->addAttribute( 'items_of', $key );
164 |
165 | $repeater_registered_keys = array();
166 |
167 | foreach ( $value['subFields'] as $sub_key => $sub_value ) {
168 | // Parent fields inside repeater fields
169 | if ( $sub_value['fieldType'] == 'parent_field' ) {
170 | $repeater_subfield_registered_keys = array();
171 |
172 | foreach ( $sub_value['subFields'] as $sub_key_child => $sub_value_child ) {
173 | if ( in_array( $sub_key_child, $settings_blacklist ) || in_array( $sub_key_child, $repeater_subfield_registered_keys ) ) {
174 | continue;
175 | }
176 |
177 | $field_in_item = $fields_in_item->addChild( 'field', $sub_value_child['parent'] . '>' . $sub_key_child );
178 | $repeater_subfield_registered_keys[] = $sub_key_child;
179 | }
180 | }
181 | // Simple fields inside repeater fields
182 | else {
183 | if ( in_array( $sub_key, $settings_blacklist ) || in_array( $sub_key, $repeater_registered_keys ) ) {
184 | continue;
185 | }
186 | $field_in_item = $fields_in_item->addChild( 'field', $sub_key );
187 | $repeater_registered_keys[] = $sub_key;
188 | }
189 | }
190 | }
191 | // Parent Field
192 | elseif ( $value['fieldType'] == 'parent_field' ) {
193 | foreach ( $value['subFields'] as $sub_key => $sub_value ) {
194 | if ( in_array( $sub_key, $settings_blacklist ) ) {
195 | continue;
196 | }
197 | $field = $fields->addChild( 'field', $sub_value['parent'] . '>' . $sub_key );
198 | }
199 | }
200 | }
201 | }
202 |
203 | $dom = dom_import_simplexml( $xml )->ownerDocument;
204 | $dom->formatOutput = true;
205 |
206 | return $dom->saveXML( $dom->documentElement, LIBXML_NOXMLDECL );
207 | }
208 |
209 | public function generate_xml_for_all( $widgets ) {
210 | return $this->generate_wpml_config_xml( $widgets );
211 | }
212 |
213 | public function generate_xml_for_missing_widgets( $widgets ) {
214 | $widgets_blacklist = $this->get_widgets_blacklist();
215 |
216 | foreach ( $widgets as $key => $widget ) {
217 | if ( in_array( $widget['widgetType'], $widgets_blacklist ) ) {
218 | unset( $widgets[ $key ] );
219 | }
220 | }
221 |
222 | return $this->generate_wpml_config_xml( $widgets );
223 | }
224 |
225 | public function meta_box_html( $post ) {
226 | // Variables
227 | $elementor_data = get_post_meta( $post->ID, '_elementor_data', true );
228 | $elementor_data_array = json_decode( $elementor_data );
229 | $widgets = $this->get_widgets_list( $elementor_data_array );
230 |
231 | // XML Config only for missing widgets
232 | echo '
' . __( 'WPML: Elementor Widgets', 'wpml-compatibility-test-tools' ) . ' ';
233 | echo '' . __( 'XML generated for widgets from this page that does not have translation settings.', 'wpml-compatibility-test-tools' ) . '
';
234 | echo '';
237 |
238 | // Debug Information
239 | $sections = array(
240 | array(
241 | 'title' => __( 'WPML Config XML (generated for all widgets in the page)', 'wpml-compatibility-test-tools' ),
242 | 'description' => __( 'WARNING: Using this may overwrite existing settings (including default elementor widgets). Please check it before and use with caution.', 'wpml-compatibility-test-tools' ),
243 | 'content' => htmlspecialchars_decode( $this->generate_xml_for_all( $widgets ) ),
244 | ),
245 | array(
246 | 'title' => __( 'RAW value from _elementor_data (JSON)', 'wpml-compatibility-test-tools' ),
247 | 'description' => __( 'This is the raw value stored in the _elementor_data meta field.', 'wpml-compatibility-test-tools' ),
248 | 'content' => print_r( $elementor_data, true ),
249 | ),
250 | array(
251 | 'title' => __( 'Array generated from _elementor_data', 'wpml-compatibility-test-tools' ),
252 | 'description' => __( 'This is the _elementor_data converted into a PHP array.', 'wpml-compatibility-test-tools' ),
253 | 'content' => print_r( $elementor_data_array, true ),
254 | ),
255 | array(
256 | 'title' => __( 'Extracted Widgets from _elementor_data', 'wpml-compatibility-test-tools' ),
257 | 'description' => __( 'These are the widgets that have been extracted from the _elementor_data array.', 'wpml-compatibility-test-tools' ),
258 | 'content' => print_r( $widgets, true ),
259 | ),
260 | );
261 |
262 | echo '' . __( 'WPML: Elementor Debug Information', 'wpml-compatibility-test-tools' ) . ' ';
263 |
264 | foreach ( $sections as $section ) {
265 | echo '';
266 | echo '' . __( $section['title'], 'wpml-compatibility-test-tools' ) . ' ';
267 | echo '' . __( $section['description'], 'wpml-compatibility-test-tools' ) . '
';
268 | echo '';
271 | echo ' ';
272 | }
273 | }
274 | }
275 |
276 | new MLTools_Elementor_Config_Generator();
277 |
--------------------------------------------------------------------------------
/inc/class-mltools-gutenberg-config-generator.php:
--------------------------------------------------------------------------------
1 | ID );
39 | $xmlContent = $this->generateXmlFromContent( $content );
40 |
41 | $this->renderMetaBoxHeader();
42 | $this->renderXmlTextarea( $xmlContent );
43 | $this->renderDebugInformation( $content );
44 | }
45 |
46 | /**
47 | * Renders the header for the meta box.
48 | */
49 | private function renderMetaBoxHeader() {
50 | echo '' . esc_html__( 'WPML: Gutenberg Blocks', 'wpml-compatibility-test-tools' ) . ' ';
51 | echo '' . wp_kses_post( __( 'XML automatically generated for the blocks and block attributes from this page. Please review before using it. ', 'wpml-compatibility-test-tools' ) ) . '
';
52 | echo '' . wp_kses_post( __( 'For instructions on how to use and implement it, please check the following links:', 'wpml-compatibility-test-tools' ) ) . '
';
53 | echo '- ' . esc_html__( 'WPML Language Configuration File', 'wpml-compatibility-test-tools' ) . '
';
54 | echo '- ' . esc_html__( 'Make Custom Gutenberg Blocks Translatable', 'wpml-compatibility-test-tools' ) . '
';
55 | }
56 |
57 | /**
58 | * Renders the XML content in a textarea.
59 | *
60 | * @param string $xmlContent The XML content to display.
61 | */
62 | private function renderXmlTextarea( $xmlContent ) {
63 | echo '';
66 | }
67 |
68 | /**
69 | * Renders debug information for the Gutenberg blocks.
70 | *
71 | * @param string $content The post content.
72 | */
73 | private function renderDebugInformation( $content ) {
74 | $sections = $this->getDebugSections( $content );
75 |
76 | echo '' . esc_html__( 'Gutenberg Blocks - Debug Information', 'wpml-compatibility-test-tools' ) . ' ';
77 |
78 | foreach ( $sections as $section ) {
79 | $this->renderDebugSection( $section );
80 | }
81 | }
82 |
83 | /**
84 | * Gets the debug sections for the Gutenberg blocks.
85 | *
86 | * @param string $content The post content.
87 | *
88 | * @return array The debug sections.
89 | */
90 | private function getDebugSections( $content ) {
91 | return [
92 | [
93 | 'title' => esc_html__( 'Post Content', 'wpml-compatibility-test-tools' ),
94 | 'description' => esc_html__( 'RAW content from the post_content column of the current post.', 'wpml-compatibility-test-tools' ),
95 | 'content' => htmlspecialchars_decode( $content ),
96 | ],
97 | [
98 | 'title' => esc_html__( 'Parse Blocks from post_content', 'wpml-compatibility-test-tools' ),
99 | 'description' => esc_html__( 'Result of parse_blocks() applied to the page content (JSON format).', 'wpml-compatibility-test-tools' ),
100 | 'content' => htmlspecialchars( json_encode( parse_blocks( $content ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ) ),
101 | ],
102 | ];
103 | }
104 |
105 | /**
106 | * Renders a single debug section.
107 | *
108 | * @param array $section The section data.
109 | */
110 | private function renderDebugSection( $section ) {
111 | $html = '';
112 | $html .= '' . esc_html__( $section['title'], 'wpml-compatibility-test-tools' ) . ' ';
113 | $html .= '' . esc_html__( $section['description'], 'wpml-compatibility-test-tools' ) . '
';
114 | $html .= '';
117 | $html .= ' ';
118 |
119 | echo $html;
120 | }
121 |
122 | /**
123 | * Returns a list of non-translatable Gutenberg blocks.
124 | *
125 | * @return array
126 | */
127 | public function getNonTranslatableBlocksList() {
128 | return apply_filters( 'mltools_gutenberg_non_translatable_blocks_list', self::NON_TRANSLATABLE_BLOCKS );
129 | }
130 |
131 | /**
132 | * Returns a list of wildcard attributes.
133 | *
134 | * @return array
135 | */
136 | public function getWildcardAttributesList() {
137 | return apply_filters( 'mltools_gutenberg_wildcart_attributes_list', self::WILDCARD_ATTRIBUTES );
138 | }
139 |
140 | /**
141 | * Parses the content into an array of Gutenberg blocks.
142 | *
143 | * @param string $content The post content.
144 | *
145 | * @return array
146 | */
147 | public function parseContentToBlocks( $content ) {
148 | return parse_blocks( $content );
149 | }
150 |
151 | /**
152 | * Filters and returns an array of Gutenberg blocks.
153 | *
154 | * @param string $content The post content.
155 | *
156 | * @return array
157 | */
158 | public function getFilteredBlocksArray( $content ) {
159 | $blocks = $this->parseContentToBlocks( $content );
160 | $filteredBlocks = $this->filterBlocksArray( $blocks );
161 |
162 | return (array) $filteredBlocks;
163 | }
164 |
165 | /**
166 | * Filters the blocks array to remove duplicates and organize blocks.
167 | *
168 | * @param array $blocks The array of blocks.
169 | * @param array $filteredBlocks The array of filtered blocks.
170 | *
171 | * @return array
172 | */
173 | public function filterBlocksArray( $blocks, $filteredBlocks = [] ) {
174 |
175 | foreach ( $blocks as $block ) {
176 | if ( isset( $block['blockName'] ) ) {
177 |
178 | if ( ! array_key_exists( $block['blockName'], $filteredBlocks ) ) {
179 | $filteredBlocks[ $block['blockName'] ] = $block;
180 | } elseif ( array_key_exists( $block['blockName'], $filteredBlocks ) ) {
181 | $filteredBlocks[ $block['blockName'] ] = array_merge(
182 | $filteredBlocks[ $block['blockName'] ],
183 | $block
184 | );
185 | }
186 | if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
187 |
188 | $innerBlocks = $this->filterBlocksArray( $block['innerBlocks'], $filteredBlocks );
189 |
190 | $filteredBlocks = array_merge(
191 | $filteredBlocks,
192 | $innerBlocks
193 | );
194 |
195 | unset( $filteredBlocks[ $block['blockName'] ]['innerBlocks'] );
196 |
197 | }
198 | }
199 | }
200 |
201 | return (array) $filteredBlocks;
202 | }
203 |
204 | /**
205 | * Generates XML from the post content.
206 | *
207 | * @param string $content The post content.
208 | *
209 | * @return string
210 | */
211 | private function generateXmlFromContent( $content ) {
212 | try {
213 | $blocks = $this->getFilteredBlocksArray( $content );
214 | $nonTranslatableBlocks = $this->getNonTranslatableBlocksList();
215 |
216 | $xml = new SimpleXMLElement( ' ' );
217 | $gutenbergBlocks = $xml->addChild( 'gutenberg-blocks' );
218 |
219 | foreach ( $blocks as $block ) {
220 | $this->generateXmlForBlock( $gutenbergBlocks, $block, $nonTranslatableBlocks );
221 | }
222 |
223 | $dom = dom_import_simplexml( $xml )->ownerDocument;
224 | $dom->formatOutput = true;
225 | return $dom->saveXML( $dom->documentElement, LIBXML_NOXMLDECL );
226 | } catch ( Exception $e ) {
227 | error_log( 'Error generating XML: ' . $e->getMessage() );
228 | return '';
229 | }
230 | }
231 |
232 | /**
233 | * Generates XML for a single Gutenberg block.
234 | *
235 | * @param SimpleXMLElement $xmlElement The XML element to append to.
236 | * @param array $block The block data.
237 | * @param array $nonTranslatableBlocks The list of non-translatable blocks.
238 | */
239 | private function generateXmlForBlock( $xmlElement, $block, $nonTranslatableBlocks ) {
240 | if ( ! isset( $block['blockName'] ) || empty( $block['blockName'] ) ) {
241 | return;
242 | }
243 |
244 | $blockType = $block['blockName'];
245 | $translate = in_array( $block['blockName'], $nonTranslatableBlocks ) ? '0' : '1';
246 |
247 | $blockElement = $xmlElement->addChild( 'gutenberg-block' );
248 | $blockElement->addAttribute( 'type', $blockType );
249 | $blockElement->addAttribute( 'translate', $translate );
250 |
251 | if ( $translate === '1' && ! empty( $block['innerHTML'] ) ) {
252 | $this->addXpathElements( $blockElement, $block );
253 | }
254 |
255 | if ( ! empty( $block['attrs'] && ! in_array( $block['blockName'], $nonTranslatableBlocks ) ) ) {
256 | $this->generateXmlForAttributes( $blockElement, $block['attrs'] );
257 | }
258 |
259 | if ( ! empty( $block['innerBlocks'] ) ) {
260 | foreach ( $block['innerBlocks'] as $innerBlock ) {
261 | $this->generateXmlForBlock( $xmlElement, $innerBlock, $nonTranslatableBlocks );
262 | }
263 | }
264 | }
265 |
266 | /**
267 | * Generates XML for block attributes.
268 | *
269 | * @param SimpleXMLElement $xmlElement The XML element to append to.
270 | * @param array $attrs The block attributes.
271 | */
272 | private function generateXmlForAttributes( $xmlElement, $attrs ) {
273 | $wildcardAttributesList = $this->getWildcardAttributesList();
274 |
275 | foreach ( $attrs as $key => $value ) {
276 | if ( is_array( $value ) ) {
277 | if ( in_array( $key, $wildcardAttributesList ) ) {
278 | $key = '*';
279 | }
280 | $keyElement = $xmlElement->addChild( 'key' );
281 | $keyElement->addAttribute( 'name', $key );
282 | $this->generateXmlForAttributes( $keyElement, $value );
283 | } else {
284 | $keyElement = $xmlElement->addChild( 'key' );
285 | $keyElement->addAttribute( 'name', $key );
286 |
287 | if ( strpos( $key, 'url' ) !== false ||
288 | strpos( $key, 'link' ) !== false ||
289 | strpos( $key, 'href' ) !== false ) {
290 | $keyElement->addAttribute( 'type', 'link' );
291 | }
292 | }
293 | }
294 | }
295 |
296 | /**
297 | * Adds xpath elements to the block element based on the HTML content.
298 | *
299 | * @param SimpleXMLElement $blockElement The block XML element.
300 | * @param array $block The block data.
301 | */
302 | private function addXpathElements( $blockElement, $block ) {
303 | if ( empty( $block['innerHTML'] ) ) {
304 | return;
305 | }
306 |
307 | $xpaths = $this->analyzeHtmlForXpaths( $block['innerHTML'] );
308 |
309 | foreach ( $xpaths as $xpath ) {
310 | $child = $blockElement->addChild( 'xpath', $xpath );
311 |
312 | if ( strpos( $xpath, '@href' ) !== false ) {
313 | $child->addAttribute( 'type', 'link' );
314 | }
315 | }
316 | }
317 |
318 | /**
319 | * Analyzes HTML content to determine appropriate xpaths.
320 | *
321 | * @param string $html The HTML content.
322 | *
323 | * @return array Array of xpath expressions.
324 | */
325 | private function analyzeHtmlForXpaths( $html ) {
326 | $xpaths = [];
327 |
328 | if ( empty( trim( $html ) ) ) {
329 | return $xpaths;
330 | }
331 |
332 | $doc = new DOMDocument();
333 | @$doc->loadHTML( '' . $html . '
', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
334 |
335 | $xpath = new DOMXPath( $doc );
336 |
337 | $textNodes = $this->findTextNodes( $doc->documentElement );
338 |
339 | foreach ( $textNodes as $node ) {
340 | if ( $node->nodeType === XML_TEXT_NODE ) {
341 | $text = trim( $node->nodeValue );
342 | if ( $this->isTranslatableText( $text ) ) {
343 | $parent = $node->parentNode;
344 | if ( $parent !== null ) {
345 | $xpathExpression = $this->generateUniqueXPath( $parent );
346 | if ( ! empty( $xpathExpression ) ) {
347 | $xpaths[] = $xpathExpression;
348 | }
349 | }
350 | }
351 | }
352 | }
353 |
354 | $links = $xpath->query( '//a[@href]' );
355 | foreach ( $links as $link ) {
356 | $xpaths[] = $this->generateUniqueXPath( $link ) . '/@href';
357 | }
358 |
359 | $attrMap = [
360 | 'img' => [ 'alt', 'title' ],
361 | 'input' => [ 'placeholder', 'value' ],
362 | 'textarea' => [ 'placeholder' ],
363 | 'button' => [ 'value' ],
364 | 'meta' => [ 'content' ],
365 | ];
366 |
367 | foreach ( $attrMap as $tag => $attributes ) {
368 | $elements = $doc->getElementsByTagName( $tag );
369 | foreach ( $elements as $element ) {
370 | foreach ( $attributes as $attribute ) {
371 | if ( $element->hasAttribute( $attribute ) && $this->isTranslatableText( $element->getAttribute( $attribute ) ) ) {
372 | $xpaths[] = $this->generateUniqueXPath( $element ) . '/@' . $attribute;
373 | }
374 | }
375 | }
376 | }
377 |
378 | return array_unique( $xpaths );
379 | }
380 |
381 | /**
382 | * Recursively finds text nodes within an element.
383 | *
384 | * @param DOMNode $node The node to check.
385 | *
386 | * @return array Array of text nodes.
387 | */
388 | private function findTextNodes( $node ) {
389 | $textNodes = [];
390 |
391 | if ( $node->nodeType === XML_TEXT_NODE ) {
392 | $text = trim( $node->nodeValue );
393 | if ( $this->isTranslatableText( $text ) ) {
394 | $textNodes[] = $node;
395 | }
396 | }
397 |
398 | if ( $node->hasChildNodes() ) {
399 | foreach ( $node->childNodes as $child ) {
400 | $textNodes = array_merge( $textNodes, $this->findTextNodes( $child ) );
401 | }
402 | }
403 |
404 | return $textNodes;
405 | }
406 |
407 | /**
408 | * Generates a unique XPath for a DOM element.
409 | *
410 | * @param DOMElement $element The DOM element.
411 | *
412 | * @return string The XPath expression.
413 | */
414 | private function generateUniqueXPath( $element ) {
415 | if ( $element->nodeType !== XML_ELEMENT_NODE ) {
416 | return '';
417 | }
418 |
419 | $nodePath = $element->nodeName;
420 |
421 | if ( $element->hasAttribute( 'class' ) ) {
422 | $classes = explode( ' ', $element->getAttribute( 'class' ) );
423 | $filteredClasses = array_filter( $classes, 'trim' );
424 |
425 | if ( ! empty( $filteredClasses ) ) {
426 | $selectedClass = $this->selectMostUniqueClass( $filteredClasses );
427 | $nodePath .= '[contains(@class, "' . $selectedClass . '")]';
428 | }
429 | } else {
430 | $parent = $element->parentNode;
431 | if ( $parent && $parent->nodeType === XML_ELEMENT_NODE ) {
432 | $siblings = 0;
433 | $position = 0;
434 |
435 | foreach ( $parent->childNodes as $i => $sibling ) {
436 | if ( $sibling->nodeType === XML_ELEMENT_NODE && $sibling->nodeName === $element->nodeName ) {
437 | $siblings++;
438 | if ( $sibling === $element ) {
439 | $position = $siblings;
440 | }
441 | }
442 | }
443 |
444 | if ( $siblings > 1 ) {
445 | $nodePath .= '[' . $position . ']';
446 | }
447 | }
448 | }
449 |
450 | if ( $element->childNodes->length === 1 && $element->firstChild->nodeType === XML_TEXT_NODE ) {
451 | return '//' . $nodePath;
452 | }
453 |
454 | return '//' . $nodePath;
455 | }
456 |
457 | /**
458 | * Selects the most unique class from a list of classes.
459 | *
460 | * @param array $classes Array of class names.
461 | *
462 | * @return string The selected class name.
463 | */
464 | private function selectMostUniqueClass( $classes ) {
465 | foreach ( $classes as $class ) {
466 | if ( strpos( $class, 'id' ) !== false ||
467 | strpos( $class, 'ID' ) !== false ||
468 | preg_match( '/[A-Za-z0-9]{5,}/', $class ) ) {
469 | return $class;
470 | }
471 | }
472 |
473 | return $this->getLongestClass( $classes );
474 | }
475 |
476 | /**
477 | * Gets the longest class name from a list of classes.
478 | *
479 | * This method sorts the array of class names by length (descending)
480 | * and returns the first (longest) element. This is used as a fallback
481 | * strategy when we can't find classes with specific patterns.
482 | *
483 | * @param array $classes Array of class names to analyze.
484 | *
485 | * @return string The class name with the longest string length or empty string if array is empty.
486 | */
487 | private function getLongestClass( $classes ) {
488 | if ( empty( $classes ) ) {
489 | return '';
490 | }
491 |
492 | usort(
493 | $classes,
494 | function( $a, $b ) {
495 | return strlen( $b ) - strlen( $a );
496 | }
497 | );
498 |
499 | return reset( $classes );
500 | }
501 |
502 | /**
503 | * @param string $text The text to check.
504 | *
505 | * @return bool Whether the text is translatable.
506 | */
507 | private function isTranslatableText( $text ) {
508 | $text = trim( $text );
509 | return ! empty( $text ) && ! is_numeric( $text );
510 | }
511 | }
512 |
513 | new MLTools_Gutenberg_Config_Generator();
514 |
--------------------------------------------------------------------------------
/inc/class-mltools-shortcode-attribute-filter.php:
--------------------------------------------------------------------------------
1 | captured_tags = get_option( self::OPTION_NAME, array() );
14 | $this->captured_values = get_option( SELF::OPTION_NAME_VALUES, array() );
15 | $this->ignored_tags = $ignored_tags;
16 | }
17 |
18 | public function add_hooks() {
19 | if ( ! is_admin() ) {
20 | add_action( 'wp_head', array( $this, 'add_shortcode_filters' ) );
21 | add_filter( 'do_shortcode_tag', array( $this, 'do_shortcode_tag_filter' ), 10, 3 );
22 | add_action( 'shutdown', array( $this, 'save_tags' ) );
23 | }
24 | }
25 |
26 | public function add_shortcode_filters() {
27 |
28 | global $shortcode_tags;
29 |
30 | foreach ( $shortcode_tags as $tag => $callback ) {
31 | if ( ! in_array( $tag, $this->ignored_tags ) ) {
32 | add_filter( "shortcode_atts_{$tag}", array( $this, 'shortcode_atts_filter' ), 10, 4 );
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Shortcode attribute filter.
39 | *
40 | * Notice: shortcode_atts() must be called with 3rd parameter $shortcode,
41 | * otherwise this filter will be not be applied.
42 | *
43 | * Example:
44 | * extract( shortcode_atts( $default_atts, $atts, 'My_Widget' ) )
45 | *
46 | * @param string $out Output.
47 | * @param array $pairs Default attributes.
48 | * @param array $atts Shortcode attributes.
49 | * @param string $tag Shortcode tag.
50 | *
51 | * @return mixed
52 | */
53 | public function shortcode_atts_filter( $out, $pairs, $atts, $tag ) {
54 |
55 | if ( is_array( $pairs ) && is_array( $atts ) ) {
56 | $all_attributes = array_merge( $pairs, $atts );
57 | $this->add_tag( $tag, $all_attributes );
58 | }
59 |
60 | return $out;
61 | }
62 |
63 | public function do_shortcode_tag_filter( $output, $tag, $attr ) {
64 |
65 | if ( ! in_array( $tag, $this->ignored_tags ) && is_array( $attr ) ) {
66 | $this->add_tag( $tag, $attr );
67 | }
68 |
69 | return $output;
70 | }
71 |
72 | private function add_tag( $tag, $attributes ) {
73 |
74 | $props = array();
75 | if ( isset( $this->captured_tags[ $tag ] ) ) {
76 | $props = $this->captured_tags[ $tag ];
77 | }
78 |
79 | $config = new MLTools_Shortcode_Config( $tag, $props );
80 |
81 | foreach ( $attributes as $attr_name => $attr_value ) {
82 | $config->add_attribute( $attr_name );
83 | $this->captured_values[ $tag ]['attributes'][ $attr_name ] = $attr_value;
84 | }
85 |
86 | ksort( $this->captured_values[ $tag ]['attributes'] );
87 |
88 | $props = $config->get_props();
89 | ksort( $props['attributes'] );
90 |
91 | $this->captured_tags[ $tag ] = $props;
92 | }
93 |
94 | public function save_tags() {
95 | update_option( self::OPTION_NAME, $this->get_tags() );
96 | update_option( self::OPTION_NAME_VALUES, $this->get_captured_values() );
97 | }
98 |
99 | private function get_tags() {
100 |
101 | foreach ( $this->ignored_tags as $tag ) {
102 | unset( $this->captured_tags[ $tag ] );
103 | }
104 |
105 | ksort( $this->captured_tags );
106 |
107 | return $this->captured_tags;
108 | }
109 |
110 | private function get_captured_values() {
111 |
112 | foreach ( $this->ignored_tags as $tag ) {
113 | unset( $this->captured_values[ $tag ] );
114 | }
115 |
116 | ksort( $this->captured_values );
117 |
118 | return $this->captured_values;
119 | }
120 |
121 | }
--------------------------------------------------------------------------------
/inc/class-mltools-shortcode-config.php:
--------------------------------------------------------------------------------
1 | false,
7 | 'attributes' => array(),
8 | );
9 |
10 | function __construct( $tag, $props = array() ) {
11 | if ( ! empty( $props ) ) {
12 | foreach ( $props as $prop => $value ) {
13 | $this->props[ $prop ] = $value;
14 | }
15 | }
16 | $this->props['tag'] = $tag;
17 | }
18 |
19 | public function set( $name, $value ) {
20 | if ( isset( $this->props[ $name ] ) ) {
21 | $this->props[ $name ] = $value;
22 | }
23 | }
24 |
25 | public function get_props() {
26 | return $this->props;
27 | }
28 |
29 | public function __get( $name ) {
30 | return isset( $this->props[ $name ] ) ? $this->props[ $name ] : null;
31 | }
32 |
33 | public function __set( $name, $value ) {
34 | user_error( 'Use set' );
35 | }
36 |
37 | public function add_attribute( $attr_name ) {
38 | if ( ! isset( $this->props['attributes'][ $attr_name ] ) ) {
39 | $this->props['attributes'][ $attr_name ] = array();
40 | }
41 | }
42 |
43 | public function set_attribute_property( $attr_name, $prop, $value ) {
44 | if ( isset( $this->props['attributes'][ $attr_name ] ) ) {
45 | $this->props['attributes'][ $attr_name ][ $prop ] = $value;
46 | }
47 | }
48 |
49 | public function get_attribute_property( $attr_name, $prop ) {
50 | return isset( $this->props['attributes'][ $attr_name ][ $prop ] ) ? $this->props['attributes'][ $attr_name ][ $prop ] : null;
51 | }
52 | }
--------------------------------------------------------------------------------
/inc/class-mltools-shortcode-wpml-config-parser.php:
--------------------------------------------------------------------------------
1 | set( 'encoding', $shortcode['tag']['attr']['encoding'] );
34 | }
35 |
36 | if ( isset( $shortcode['tag']['attr']['type'] ) ) {
37 | $config->set( 'type', $shortcode['tag']['attr']['type'] );
38 | }
39 |
40 | if ( isset( $shortcode['attributes']['attribute'] ) && is_array( $shortcode['attributes']['attribute'] ) ) {
41 |
42 | if ( isset( $shortcode['attributes']['attribute']['value'] ) ) {
43 |
44 | $attr_name = $shortcode['attributes']['attribute']['value'];
45 | $config->add_attribute( $attr_name );
46 |
47 | if ( isset( $shortcode['attributes']['attribute']['attr']['encoding'] ) ) {
48 | $config->set_attribute_property( $attr_name, 'encoding', $shortcode['attributes']['attribute']['attr']['encoding'] );
49 | }
50 |
51 | if ( isset( $shortcode['attributes']['attribute']['attr']['type'] ) ) {
52 | $config->set_attribute_property( $attr_name, 'type', $shortcode['attributes']['attribute']['attr']['type'] );
53 | }
54 | } else {
55 | foreach ( $shortcode['attributes']['attribute'] as $attr ) {
56 |
57 | $attr_name = $attr['value'];
58 | $config->add_attribute( $attr_name );
59 |
60 | if ( isset( $attr['attr']['encoding'] ) ) {
61 | $config->set_attribute_property( $attr_name, 'encoding', $attr['attr']['encoding'] );
62 | }
63 |
64 | if ( isset( $attr['attr']['type'] ) ) {
65 | $config->set_attribute_property( $attr_name, 'type', $attr['attr']['type'] );
66 | }
67 | }
68 | }
69 | }
70 | $wpml_shortcodes[ $tag ] = $config->get_props();
71 | }
72 | }
73 |
74 | return $wpml_shortcodes;
75 | }
76 |
77 | public static function get_config() {
78 |
79 | if ( self::$config === null ) {
80 |
81 | // @todo Fix dependencies
82 | $array_utility_file = WPML_PLUGIN_PATH . '/inc/utilities/xml2array.php';
83 |
84 | if ( file_exists( $array_utility_file ) ) {
85 | require_once $array_utility_file;
86 | WPML_Config::load_config_run();
87 | } else {
88 | return false;
89 | }
90 | }
91 |
92 | return self::$config;
93 | }
94 |
95 | }
--------------------------------------------------------------------------------
/inc/class-mltools-xml-helper.php:
--------------------------------------------------------------------------------
1 | preserveWhiteSpace = false;
9 | $xml->formatOutput = true;
10 | $xml_shortcodes = $xml->createElement( 'shortcodes' );
11 | foreach ( $shortcodes as $tag => $config ) {
12 | $xml_shortcodes->appendChild( $this->append_shortcode( $config, $xml ) );
13 | }
14 | $xml->appendChild( $xml_shortcodes );
15 |
16 | return $xml->saveXML();
17 | }
18 |
19 | public function get_dom_single_shortcode( array $config ) {
20 |
21 | $xml = new DOMDocument( "1.0", "UTF-8" );
22 | $xml->preserveWhiteSpace = false;
23 | $xml->formatOutput = true;
24 | $xml->appendChild( $this->append_shortcode( $config, $xml ) );
25 |
26 | return $xml->saveXML();
27 | }
28 |
29 | private function append_shortcode( array $config, DOMDocument $xml ) {
30 |
31 | $xml_shortcode = $xml->createElement( 'shortcode' );
32 | $xml_tag = $xml->createElement( 'tag', $config['tag'] );
33 | $xml_shortcode->appendChild( $xml_tag );
34 |
35 | $attributes = $config['attributes'];
36 |
37 | if ( ! empty( $attributes ) ) {
38 | $xml_attributes = $xml->createElement( 'attributes' );
39 | foreach ( $attributes as $attr_name => $props ) {
40 | $xml_attribute = $xml->createElement( 'attribute', $attr_name );
41 | foreach ( $props as $prop_name => $prop_value ) {
42 | $xml_attribute->setAttribute( $prop_name, $prop_value );
43 | }
44 | $xml_attributes->appendChild( $xml_attribute );
45 | }
46 | $xml_shortcode->appendChild( $xml_attributes );
47 | }
48 |
49 | return $xml_shortcode;
50 | }
51 | }
--------------------------------------------------------------------------------
/inc/wpml-compatibility-test-tools-base.class.php:
--------------------------------------------------------------------------------
1 | messages = new WPML_Compatibility_Test_Tools_Messages();
11 | }
12 |
13 | /**
14 | * Save initial configuration to database
15 | */
16 | public static function install() {
17 | if ( get_option( self::OPTIONS_NAME ) === false ) {
18 | $options[ 'string_auto_translate_template' ] = '[%language_name%] %original_string%';
19 | $options[ 'duplicate_strings_template' ] = '[%language_name%] %original_string%';
20 | $options[ 'shortcode_enable_debug' ] = false;
21 | $options[ 'shortcode_enable_debug_value' ] = false;
22 | $options[ 'shortcode_ignored_tags' ] = false;
23 |
24 | add_option( self::OPTIONS_NAME, $options );
25 | }
26 |
27 | return true;
28 | }
29 |
30 | /**
31 | * Return plugin option
32 | *
33 | * @param $option_name
34 | * @param null $default
35 | *
36 | * @return null
37 | */
38 | public static function get_option( $option_name, $default = null ) {
39 | if ( empty( self::$options ) ) {
40 | self::$options = get_option( self::OPTIONS_NAME );
41 | }
42 |
43 | if ( isset( self::$options[$option_name] ) ) {
44 | return self::$options[$option_name];
45 | }
46 |
47 | return $default;
48 | }
49 |
50 | /**
51 | * Update plugin option
52 | *
53 | * @param $option_name
54 | * @param $option_value
55 | * @return bool
56 | */
57 | public static function update_option($option_name, $option_value ) {
58 | $options = get_option( self::OPTIONS_NAME );
59 | $options[$option_name] = $option_value;
60 | $result = update_option( self::OPTIONS_NAME, $options );
61 |
62 | if ( $result ) {
63 | self::refresh_options();
64 | }
65 |
66 | return $result;
67 | }
68 |
69 | /**
70 | * Refresh options
71 | */
72 | public static function refresh_options() {
73 | self::$options = get_option( self::OPTIONS_NAME );
74 | }
75 | }
--------------------------------------------------------------------------------
/inc/wpml-compatibility-test-tools-messages.class.php:
--------------------------------------------------------------------------------
1 | ' . sprintf( $message, 'WPML ' ) . '
';
10 | break;
11 |
12 | case 'not_finished_wpml_setup' :
13 | $message = __( 'Multilingual Tools plugin is enabled but not effective. You have to finish WPML setup.', 'wpml-compatibility-test-tools' );
14 | echo '' . sprintf( $message, 'WPML ' ) . '
';
15 | break;
16 |
17 | case 'no_tm_notice' :
18 | $message = __( 'Multilingual Tools plugin is enabled but not effective. It requires WPML Translation Management plugin in order to work.', 'wpml-compatibility-test-tools' );
19 | echo '' . sprintf( $message, 'WPML ' ) . '
';
20 | break;
21 |
22 | case 'no_st_notice' :
23 | $message = __( 'Multilingual Tools plugin is enabled but not effective. It requires WPML String Translation plugin in order to work.', 'wpml-compatibility-test-tools' );
24 | echo '' . sprintf( $message, 'WPML ' ) . '
';
25 | break;
26 |
27 | case 'no_selected_language_notice' :
28 | echo '' . __( 'At least one language should be selected in order to translate strings.', 'wpml-compatibility-test-tools' ) . '
';
29 | break;
30 |
31 | case 'no_selected_language_for_pages_notice' :
32 | echo '' . __( 'At least one language should be selected in order to create pages with dummy content.', 'wpml-compatibility-test-tools' ) . '
';
33 | break;
34 |
35 | case 'no_context_notice' :
36 | echo '' . __( 'Please select the context.', 'wpml-compatibility-test-tools' ) . '
';
37 | break;
38 |
39 | case 'no_template_notice' :
40 | echo '' . __( 'Template is required.', 'wpml-compatibility-test-tools' ) . '
';
41 | break;
42 |
43 | case 'settings_updated_notice' :
44 | echo '' . __( 'Settings updated.', 'wpml-compatibility-test-tools' ) . '
';
45 | break;
46 |
47 | case 'file_save_success' :
48 | echo '' . __( 'File successfully saved in active theme folder.', 'wpml-compatibility-test-tools' ) . '
';
49 | break;
50 |
51 | case 'duplicate_strings_available' :
52 | $message = sprintf(
53 | __( "Your settings have been updated. Now, continue to the %s screen, select all the site's content, select Duplicate all and click on Send documents . %s.", 'wpml-compatibility-test-tools' ),
54 | "" .
55 | __( 'Translation Dashboard','wpml-compatibility-test-tools' ) . " ", "Help " );
56 |
57 | echo '';
58 | break;
59 |
60 | case 'wctt_in_action_notice' :
61 | // Get current settings for string duplication
62 | $duplicate_strings = WPML_Compatibility_Test_Tools::get_option( 'duplicate_strings' );
63 | // Prepare a message
64 | $message =
65 | __( "WPML Compatibility Tester plugin is running and will automatically add language information to all new duplicates for your site. Right now, it will add language information for the following post fields:", 'wpml-compatibility-test-tools' ) . " " .
66 | ( isset( $duplicate_strings['post']['title'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post title' , 'wpml-compatibility-test-tools' ) . " " .
67 | ( isset( $duplicate_strings['post']['content'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post content' , 'wpml-compatibility-test-tools' ) . " " .
68 | ( isset( $duplicate_strings['post']['excerpt'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post excerpt' , 'wpml-compatibility-test-tools' ) . " " .
69 | ( isset( $duplicate_strings['custom_field']['value'] ) ? '[✔] ' : '[ ] ' ) . __( 'Custom fields', 'wpml-compatibility-test-tools' ) . " " .
70 | ( isset( $duplicate_strings['taxonomy']['all'] ) ? '[✔] ' : '[ ] ' ) . __( 'Term name ' , 'wpml-compatibility-test-tools' ) . " " .
71 | ( isset( $duplicate_strings['taxonomy_slug']['all'] ) ? '[✔] ' : '[ ] ' ) . __( 'Term slug' , 'wpml-compatibility-test-tools' ) . " " .
72 | sprintf( "" . __( "Click here to change fields to duplicate", 'wpml-compatibility-test-tools') . " ", admin_url( 'admin.php?page=wctt' ) ) . " " .
73 | __( "To proceed, select all the site's content, scroll down and select Duplicate content and then click on Duplicate .", 'wpml-compatibility-test-tools' ) . " " .
74 | "" . __( "Please note that any existing translations for selected posts will be overwritten!", 'wpml-compatibility-test-tools' ) . "
";
75 |
76 | echo '';
77 | break;
78 |
79 | case 'shortcode_debug_action_reset' :
80 | echo '' . __( 'Cleared shortcode debug data.', 'wpml-compatibility-test-tools' ) . '
';
81 | break;
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/inc/wpml-compatibility-test-tools.class.php:
--------------------------------------------------------------------------------
1 | messages, 'no_wpml_notice' ) );
26 | }
27 |
28 | return false;
29 | }
30 |
31 | // Check for Translation Management
32 | if ( ! defined( 'WPML_TM_VERSION' ) ) {
33 | add_action( 'admin_notices', array( $this->messages, 'no_tm_notice' ) );
34 |
35 | return false;
36 | }
37 |
38 | // Check for String Translation
39 | if ( ! defined( 'WPML_ST_VERSION' ) ) {
40 | add_action( 'admin_notices', array( $this->messages, 'no_st_notice' ) );
41 |
42 | return false;
43 | }
44 |
45 | // WPML setup has to be finished
46 | global $sitepress;
47 | if ( ! isset( $sitepress ) ) {
48 | add_action( 'admin_notices', array( $this->messages, 'no_wpml_notice' ) );
49 |
50 | return false;
51 | }
52 |
53 | if ( method_exists( $sitepress, 'get_setting' ) && ! $sitepress->get_setting( 'setup_complete' ) ) {
54 | add_action( 'admin_notices', array( $this->messages, 'not_finished_wpml_setup' ) );
55 |
56 | return false;
57 | }
58 |
59 | self::install();
60 |
61 | add_action( 'admin_menu', array( $this, 'register_administration_page' ) );
62 | add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts' ) );
63 | add_action( 'admin_enqueue_scripts', array( $this, 'add_styles' ) );
64 | add_action( 'wp_ajax_generate_strings_translations_action', array( $this, 'generate_strings_translations' ) );
65 |
66 | // Handle admin settings page
67 | $this->process_request();
68 |
69 | // Change WPML behaviour based on selected settings
70 | $this->modify_wpml_behaviour();
71 |
72 | do_action( 'mltools_loaded' );
73 |
74 | return true;
75 | }
76 |
77 | /**
78 | * Process admin settings page requests
79 | *
80 | * @return bool
81 | */
82 | public function process_request() {
83 | $this->process_strings_auto_translate_action_translate();
84 | $this->process_save_duplicate_strings_to_translate();
85 | $this->process_save_shortcode_helper_settings();
86 |
87 | return true;
88 | }
89 |
90 | /**
91 | * Process action strings_auto_translate_action_translate
92 | *
93 | * @return bool
94 | */
95 | private function process_strings_auto_translate_action_translate() {
96 | if ( isset( $_POST['strings_auto_translate_action_save'] ) || isset( $_POST['strings_auto_translate_action_translate'] ) ) {
97 |
98 | $error = false;
99 |
100 | $contexts = ( isset( $_POST['strings_auto_translate_context'] ) ) ? $_POST['strings_auto_translate_context'] : '';
101 | $languages = ( isset( $_POST['active_languages'] ) ) ? $_POST['active_languages'] : array();
102 | $template = ( isset( $_POST['strings_auto_translate_template'] ) ) ? $_POST['strings_auto_translate_template'] : '';
103 |
104 | if ( empty( $template ) ) {
105 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) );
106 | $error = true;
107 | }
108 |
109 | if ( $error ) {
110 | return false;
111 | }
112 |
113 | self::update_option( 'string_auto_translate_context', $contexts );
114 | self::update_option( 'string_auto_translate_languages', $languages );
115 | self::update_option( 'string_auto_translate_template', $template );
116 |
117 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) );
118 |
119 | $contexts = self::get_option( 'string_auto_translate_context' );
120 | $languages = self::get_option( 'string_auto_translate_languages' );
121 | $template = self::get_option( 'string_auto_translate_template' );
122 |
123 | if ( empty( $languages ) ) {
124 | add_action( 'admin_notices', array( $this->messages, 'no_selected_language_notice' ) );
125 | $error = true;
126 | }
127 |
128 | if ( empty( $template ) ) {
129 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) );
130 | $error = true;
131 | }
132 |
133 | if ( empty( $contexts ) ) {
134 | add_action( 'admin_notices', array( $this->messages, 'no_context_notice' ) );
135 | $error = true;
136 | }
137 |
138 | if ( $error ) {
139 | return false;
140 | }
141 | }
142 |
143 | return true;
144 | }
145 |
146 | /**
147 | * Auto translate strings with given context
148 | *
149 | * @param $context
150 | * @param $languages
151 | * @param $template
152 | */
153 | private function translate_strings( $strings, $languages, $template ) {
154 | // For each string add information
155 | foreach ( $strings as $v ) {
156 | foreach ( $languages as $lang ) {
157 | icl_add_string_translation( $v->id, $lang, wpml_ctt_prepare_string( $template, $v->value, $lang ), ICL_STRING_TRANSLATION_COMPLETE );
158 | icl_update_string_status( $v->id );
159 | }
160 | }
161 | }
162 |
163 | public function generate_strings_translations() {
164 | check_ajax_referer( 'mt_generate_strings_translations', '_mt_mighty_nonce' );
165 |
166 | $contexts = isset( $_POST['contexts'] ) ? (array) $_POST['contexts'] : false;
167 | $languages = isset( $_POST['languages'] ) ? $_POST['languages'] : false;
168 | $template = isset( $_POST['template'] ) ? $_POST['template'] : false;
169 | $count = isset( $_POST['count'] ) ? $_POST['count'] : false;
170 | $offset = isset( $_POST['offset'] ) ? $_POST['offset'] : 0;
171 |
172 | // Check in case JS fail.
173 | if ( ! $contexts || ! $languages || ! $template ) {
174 | wp_send_json( 0 );
175 | }
176 |
177 | // Strings batch threshold
178 | $limit = 100;
179 |
180 | global $wpdb;
181 |
182 | $esc_contexts = array_map( function ( $context ) {
183 | return "'" . esc_sql( $context ) . "'";
184 | }, $contexts );
185 | $esc_contexts = implode( ",", $esc_contexts );
186 |
187 | // Skip count if process started.
188 | if ( $count === false ) {
189 | $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}icl_strings WHERE context IN ({$esc_contexts})" );
190 |
191 | // Update settings only on first run.
192 | self::update_option( 'string_auto_translate_context', $contexts );
193 | self::update_option( 'string_auto_translate_languages', $languages );
194 | self::update_option( 'string_auto_translate_template', $template );
195 | }
196 |
197 | if ( $offset <= $count ) {
198 | $strings = $wpdb->get_results( "SELECT id, language, context, value FROM {$wpdb->prefix}icl_strings WHERE context IN ({$esc_contexts}) LIMIT {$offset}, {$limit}" );
199 | $this->translate_strings( $strings, $languages, $template );
200 |
201 | // Update offset.
202 | $offset += $limit;
203 |
204 | // Calculate progress percentage.
205 | $strings_left_count = max( $count - $offset, 0 );
206 | $progress = floor( 100 - $strings_left_count * 100 / $count );
207 |
208 | wp_send_json( array(
209 | 'offset' => $offset,
210 | 'count' => $count,
211 | 'progress' => $progress
212 | ) );
213 | } else {
214 | wp_send_json( 1 );
215 | }
216 | }
217 |
218 | /**
219 | * Process action save_duplicate_strings_to_translate
220 | */
221 | private function process_save_duplicate_strings_to_translate() {
222 | if ( isset( $_POST['save_duplicate_strings_to_translate'] ) ) {
223 |
224 | $error = false;
225 |
226 | $strings = ( isset( $_POST['duplicate_strings_to_translate'] ) ) ? $_POST['duplicate_strings_to_translate'] : array();
227 | $template = ( isset( $_POST['duplicate_strings_template'] ) ) ? $_POST['duplicate_strings_template'] : '';
228 |
229 | if ( empty( $template ) ) {
230 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) );
231 | $error = true;
232 | }
233 |
234 | if ( $error ) {
235 | return false;
236 | }
237 |
238 | self::update_option( 'duplicate_strings', $strings );
239 | self::update_option( 'duplicate_strings_template', $template );
240 |
241 | if ( ! empty( $strings ) ) {
242 | add_action( 'admin_notices', array( $this->messages, 'duplicate_strings_available' ) );
243 | } else {
244 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) );
245 | }
246 | }
247 |
248 | return true;
249 | }
250 |
251 | private function process_save_shortcode_helper_settings() {
252 |
253 | if ( isset( $_POST['_mltools_shortcode_helper_nonce'] )
254 | && wp_verify_nonce( $_POST['_mltools_shortcode_helper_nonce'], 'mltools_shortcode_helper_settings_save' ) ) {
255 |
256 | if ( isset( $_POST['shortcode_debug_action_save'] ) ) {
257 |
258 | $enable = isset( $_POST['shortcode_enable_debug'] );
259 | self::update_option( 'shortcode_enable_debug', $enable );
260 |
261 | $enable_debug_value = isset( $_POST['shortcode_enable_debug_value'] );
262 | self::update_option( 'shortcode_enable_debug_value', $enable_debug_value );
263 |
264 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) );
265 | }
266 | if ( isset( $_POST['shortcode_debug_action_reset'] )
267 | && class_exists( 'MLTools_Shortcode_Attribute_Filter' ) ) {
268 |
269 | delete_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME );
270 | delete_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME_VALUES );
271 |
272 | add_action( 'admin_notices', array( $this->messages, 'shortcode_debug_action_reset' ) );
273 | }
274 | if ( isset( $_POST['shortcode_ignored_tags'] ) ) {
275 | self::update_option( 'shortcode_ignored_tags', sanitize_text_field( $_POST['shortcode_ignored_tags'] ) );
276 | }
277 |
278 | if ( isset( $_POST['_wp_http_referer'] ) ) {
279 | wp_redirect( $_POST['_wp_http_referer'] );
280 | die();
281 | }
282 | }
283 | }
284 |
285 | /**
286 | * Modify WPML behaviour based on selected settings
287 | */
288 | public function modify_wpml_behaviour() {
289 |
290 | // Enable adding language information for duplicated posts.
291 | $duplicate_strings = self::get_option( 'duplicate_strings' );
292 | $duplicate_strings_template = self::get_option( 'duplicate_strings_template' );
293 |
294 | if ( ! empty( $duplicate_strings ) && ! empty( $duplicate_strings_template ) ) {
295 | new Modify_Duplicate_Strings( $duplicate_strings, $duplicate_strings_template );
296 |
297 | // Add information about the plugin settings to Translation Dashboard.
298 | if ( isset( $_GET['page'] ) && ( in_array( $_GET['page'], array( basename( WPML_TM_PATH ) . '/menu/main.php' ) ) ) ) {
299 | add_action( 'admin_notices', array( $this->messages, 'wctt_in_action_notice' ) );
300 | }
301 | }
302 | }
303 |
304 |
305 | /**
306 | * Register settings page
307 | */
308 | public function register_administration_page() {
309 | add_menu_page( __( 'Dashboard', 'wpml-compatibility-test-tools' ), __( 'Multilingual Tools', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt', array(
310 | $this,
311 | 'load_template'
312 | ), WPML_CTT_PLUGIN_URL . '/res/img/wctt-icon.png' );
313 | add_submenu_page( 'mt', __( 'Overview', 'wpml-compatibility-test-tools' ), __( 'Overview', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt', array(
314 | $this,
315 | 'load_template'
316 | ) );
317 | add_submenu_page( 'mt', __( 'Settings', 'wpml-compatibility-test-tools' ), __( 'Settings', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt-settings', array(
318 | $this,
319 | 'load_template'
320 | ) );
321 | add_submenu_page( 'mt', __( 'Configuration Generator', 'wpml-compatibility-test-tools' ), __( 'Configuration Generator', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt-generator', array(
322 | $this,
323 | 'load_template'
324 | ) );
325 | add_submenu_page( 'mt', __( 'Custom Field Settings Helper', 'wpml-compatibility-test-tools' ), __( 'Custom Field Settings Helper', 'wpml-compatibility-test-tools' ), 'manage_options', 'cf-translations', array(
326 | $this,
327 | 'load_template'
328 | ) );
329 | }
330 |
331 | /**
332 | * Load page template
333 | */
334 | public function load_template() {
335 | $screen = get_current_screen();
336 |
337 | switch ( $screen->id ) {
338 | case 'toplevel_page_mt' :
339 | add_filter( 'wpml_config_array', array( $this, 'save_configuration_for_debug' ) );
340 | add_filter( 'wpml_parse_config_file', array( $this, 'display_configuration_for_debug' ) );
341 |
342 | require WPML_CTT_ABS_PATH . 'menus/settings/overview.php';
343 | break;
344 |
345 | case 'multilingual-tools_page_mt-settings' :
346 | require WPML_CTT_ABS_PATH . 'menus/settings/settings.php';
347 | break;
348 |
349 | case 'multilingual-tools_page_mt-generator' :
350 | require WPML_CTT_ABS_PATH . 'menus/settings/generator.php';
351 | break;
352 |
353 | case 'multilingual-tools_page_cf-translations' :
354 | require WPML_CTT_ABS_PATH . 'menus/settings/custom-fields-translation.php';
355 | break;
356 | }
357 | }
358 |
359 | public function js_labels() {
360 | return array(
361 | 'question' => __( "All existing strings translations will be replaced with new values.\n Are you sure you want to do this?", 'multilingual-tools' ),
362 | 'no_context_notice' => __( "* Please select the context.", 'multilingual-tools' ),
363 | 'no_selected_language_notice' => __( "* At least one language should be selected in order to translate strings.", 'multilingual-tools' ),
364 | 'no_template_notice' => __( "* Template is required.", 'multilingual-tools' )
365 | );
366 | }
367 |
368 | /**
369 | * Add scripts only for plugin pages
370 | */
371 | public function add_scripts( $hook ) {
372 | if ( in_array( $hook, array(
373 | 'toplevel_page_mt',
374 | 'multilingual-tools_page_mt-settings',
375 | 'multilingual-tools_page_mt-generator'
376 | ) ) ) {
377 | wp_enqueue_script( 'mt-scripts', WPML_CTT_PLUGIN_URL . '/res/js/mt-script.js', array( 'jquery' ), WPML_CTT_VERSION );
378 | wp_localize_script( 'mt-scripts', 'mt_data', array(
379 | 'ajax_url' => admin_url( 'admin-ajax.php' ),
380 | 'labels' => $this->js_labels()
381 | ) );
382 |
383 | }
384 |
385 | elseif ($hook == 'multilingual-tools_page_cf-translations') {
386 |
387 | wp_enqueue_script(
388 | 'wpml_custom_fields_helper_script',
389 | WPML_CTT_PLUGIN_URL . '/res/js/mt-script.js',
390 | array( 'jquery' ),
391 | false,
392 | true
393 | );
394 |
395 | wp_localize_script(
396 | 'wpml_custom_fields_helper_script',
397 | 'wpmlData',
398 | array(
399 | 'ajax_url' => admin_url( 'admin-ajax.php' ),
400 | )
401 | );
402 |
403 | }
404 | }
405 |
406 | /**
407 | * Add styles only for plugin pages
408 | */
409 | public function add_styles( $hook ) {
410 | if ( in_array( $hook, array(
411 | 'toplevel_page_mt',
412 | 'multilingual-tools_page_mt-settings',
413 | 'multilingual-tools_page_mt-generator'
414 | ) ) ) {
415 | wp_register_style( 'wctt-generator-style', WPML_CTT_PLUGIN_URL . '/res/css/wctt-style.css', WPML_CTT_VERSION );
416 | wp_enqueue_style( 'wctt-generator-style' );
417 | }
418 | }
419 |
420 | /**
421 | * Generate XML file
422 | *
423 | * Generation wpml-config.xml file.
424 | * Used as configuration file for WPML plugin.
425 | *
426 | * @url https://wpml.org/documentation/support/language-configuration-files/
427 | */
428 | public function generate_xml() {
429 | $dom = new DOMDocument();
430 | $dom->preserveWhiteSpace = false;
431 | $dom->formatOutput = true;
432 |
433 | $root = $dom->createElement( 'wpml-config' );
434 | $root = $dom->appendChild( $root );
435 |
436 | $args = array(
437 | '_builtin' => false
438 | );
439 |
440 | $post_types = get_post_types( $args, 'names' );
441 | $checkbox_cpt = isset( $_POST['_cpt'] ) && is_array( $_POST['_cpt'] ) ? $_POST['_cpt'] : null;
442 | $radio_cpt = isset( $_POST['cpt'] ) && is_array( $_POST['cpt'] ) ? $_POST['cpt'] : null;
443 |
444 | $taxonomies = get_taxonomies( $args );
445 | $checkbox_tax = isset( $_POST['_tax'] ) && is_array( $_POST['_tax'] ) ? $_POST['_tax'] : null;
446 | $radio_tax = isset( $_POST['tax'] ) && is_array( $_POST['tax'] ) ? $_POST['tax'] : null;
447 |
448 | $custom_fields = wpml_get_custom_fields();
449 | $checkbox_cf = isset( $_POST['_cf'] ) && is_array( $_POST['_cf'] ) ? $_POST['_cf'] : null;
450 | $radio_cf = isset( $_POST['cf'] ) && is_array( $_POST['cf'] ) ? $_POST['cf'] : null;
451 |
452 | if ( $checkbox_cpt ) {
453 | $this->generate_basic_content_types(
454 | $dom,
455 | $root,
456 | $post_types,
457 | $checkbox_cpt,
458 | $radio_cpt,
459 | 'custom-types',
460 | 'custom-type',
461 | 'translate'
462 | );
463 | }
464 |
465 | if ( $checkbox_tax ) {
466 | $this->generate_basic_content_types(
467 | $dom,
468 | $root,
469 | $taxonomies,
470 | $checkbox_tax,
471 | $radio_tax,
472 | 'taxonomies',
473 | 'taxonomy',
474 | 'translate'
475 | );
476 | }
477 |
478 | if ( $checkbox_cf ) {
479 | $this->generate_basic_content_types(
480 | $dom,
481 | $root,
482 | $custom_fields,
483 | $checkbox_cf,
484 | $radio_cf,
485 | 'custom-fields',
486 | 'custom-field',
487 | 'action'
488 | );
489 | }
490 |
491 | if ( isset( $_POST['at'] ) ) {
492 | $this->generate_admin_texts( $dom, $root );
493 | }
494 |
495 | if ( isset( $_POST['shc'] ) && is_array( $_POST['shc'] ) ) {
496 | $this->generate_shortcodes( $dom, $root );
497 | }
498 |
499 | $xml = $dom->saveXML( $root );
500 |
501 | // Save options
502 | switch ( wpml_ctt_validate_radio( $_POST['save'] ) ) {
503 | case 'file' :
504 | header( "Content-Description: File Transfer" );
505 | header( 'Content-Disposition: attachment; filename="wpml-config.xml"' );
506 | header( "Content-Type: application/xml" );
507 | echo $xml;
508 | die();
509 | break;
510 |
511 | case 'dir' :
512 | if ( file_put_contents( get_template_directory() . '/wpml-config.xml', $xml ) ) {
513 | add_action( 'admin_notices', array( $this->messages, 'file_save_success' ) );
514 | }
515 | break;
516 | }
517 | }
518 |
519 | /**
520 | * Generate XML from option array
521 | *
522 | * @param $options
523 | * @param $node
524 | * @param $dom
525 | *
526 | * @since 1.3.0
527 | *
528 | */
529 | public function option2xml( $options, $node, $dom ) {
530 | if ( is_array( $options ) ) {
531 |
532 | foreach ( $options as $option => $value ) {
533 |
534 | // Only if parent option is selected, both parent and child will be generated
535 | if ( isset( $_POST['at'][ $option ] ) ) {
536 | $at = $node->appendChild( $dom->createElement( 'key' ) );
537 | $atatr = $dom->createAttribute( 'name' );
538 | $atatr->value = $option;
539 | $at->appendChild( $atatr );
540 |
541 | if ( is_array( $value ) ) {
542 | $this->option2xml( $value, $at, $dom );
543 | }
544 | }
545 | }
546 | }
547 | }
548 |
549 | /**
550 | * Generate DOM nodes for basic content types.
551 | *
552 | * Basic content types in this case are: custom post types, taxonomies, custom fields.
553 | *
554 | * @param $dom
555 | * @param $root
556 | * @param $content
557 | * @param $checkbox
558 | * @param $radio
559 | * @param $parent
560 | * @param $child
561 | * @param $attribute
562 | *
563 | * @since 1.3.0
564 | *
565 | */
566 | public function generate_basic_content_types( $dom, $root, $content, $checkbox, $radio, $parent, $child, $attribute ) {
567 | $parent_node = $dom->createElement( $parent );
568 | $parent_node = $root->appendChild( $parent_node );
569 |
570 | foreach ( $content as $c ) {
571 |
572 | if ( $parent === 'custom-fields' ) {
573 | $c = $c->meta_key;
574 | }
575 |
576 | if ( isset( $checkbox[ $c ] ) ) {
577 | $child_node = $dom->createElement( $child, sanitize_key( $c ) );
578 | $child_node = $parent_node->appendChild( $child_node );
579 | $child_node_attr = $dom->createAttribute( $attribute );
580 | $child_node_attr->value = wpml_ctt_validate_radio( $radio[ $c ] );
581 | $child_node->appendChild( $child_node_attr );
582 |
583 | // When set to display as translated.
584 | if ( $radio[ $c ] === '2' ) {
585 | $child_node_attr = $dom->createAttribute( 'display-as-translated' );
586 | $child_node_attr->value = '1';
587 | $child_node->appendChild( $child_node_attr );
588 | }
589 | }
590 | }
591 | }
592 |
593 | /**
594 | * Generate DOM nodes for admin texts
595 | *
596 | * @param $dom
597 | * @param $root
598 | *
599 | * @since 1.3.0
600 | *
601 | */
602 | public function generate_admin_texts( $dom, $root ) {
603 | $ats = $dom->createElement( 'admin-texts' );
604 | $ats = $root->appendChild( $ats );
605 |
606 | $options = wpml_ctt_options_list();
607 |
608 | foreach ( $options as $name => $value ) {
609 | $options[ $name ] = maybe_unserialize( maybe_unserialize( $value ) );
610 | }
611 |
612 | $this->option2xml( $options, $ats, $dom );
613 | }
614 |
615 | /**
616 | * Generate DOM nodes for shortcodes
617 | *
618 | * @param $dom
619 | * @param $root
620 | *
621 | * @since 1.3.0
622 | *
623 | */
624 | public function generate_shortcodes( $dom, $root ) {
625 | $shortcodes = array_unique( $_POST['shc'] );
626 | $shortcode_attr = isset( $_POST['shc-attr'] ) && is_array( $_POST['shc-attr'] ) ? (array) $_POST['shc-attr'] : null;
627 |
628 | // Create xml node
629 | $shortcodes_node = $dom->createElement( 'shortcodes' );
630 | $shortcodes_node = $root->appendChild( $shortcodes_node );
631 |
632 | foreach ( $shortcodes as $shortcode ) {
633 |
634 | $shortcode_index = array_search( $shortcode, $shortcodes, true );
635 | $shortcode = str_replace( ' ', '', sanitize_html_class( $shortcode, "Invalid_shortcode" ) );
636 |
637 | $shortcode_node = $dom->createElement( 'shortcode' );
638 | $shortcode_node = $shortcodes_node->appendChild( $shortcode_node );
639 |
640 | $tag_node = $dom->createElement( 'tag', $shortcode );
641 | $shortcode_node->appendChild( $tag_node );
642 |
643 | if ( ! is_null( $shortcode_attr ) && $shortcode_attr[ $shortcode_index ] !== "" ) {
644 |
645 | $attribute = str_replace( ' ', '', $shortcode_attr[ $shortcode_index ] );
646 | $attributes_array = explode( ",", $attribute );
647 |
648 | // Dealing with shortcode attribute if available.
649 | $attributes_node = $dom->createElement( 'attributes' );
650 | $attributes_node = $shortcode_node->appendChild( $attributes_node );
651 |
652 | if ( ! empty( $attributes_array ) ) {
653 |
654 | foreach ( $attributes_array as $a ) {
655 | $attribute_node = $dom->createElement( 'attribute', sanitize_html_class( $a, "Invalid_attribute" ) );
656 | $attributes_node->appendChild( $attribute_node );
657 | }
658 |
659 | } else {
660 | $attribute_node = $dom->createElement( 'attribute', sanitize_html_class( $attribute, "Invalid_attribute" ) );
661 | $attributes_node->appendChild( $attribute_node );
662 | }
663 | }
664 | }
665 | }
666 |
667 | /**
668 | * Save current configuration in a global variable to display later.
669 | *
670 | * @param array $config
671 | *
672 | * @return array
673 | * @global array $wpml_config_debug
674 | */
675 | function save_configuration_for_debug( $config ) {
676 | global $wpml_config_debug;
677 |
678 | // Check which sections have content and assign a title for each section.
679 | $wpml_config_debug = array();
680 | if ( ! empty( $config['wpml-config']['custom-types']['custom-type'] ) ) {
681 | $wpml_config_debug['Custom posts'] = $config['wpml-config']['custom-types']['custom-type'];
682 | }
683 | if ( ! empty( $config['wpml-config']['taxonomies']['taxonomy'] ) ) {
684 | $wpml_config_debug['Custom taxonomies'] = $config['wpml-config']['taxonomies']['taxonomy'];
685 | }
686 | if ( ! empty( $config['wpml-config']['custom-fields']['custom-field'] ) ) {
687 | $wpml_config_debug['Custom fields translation'] = $config['wpml-config']['custom-fields']['custom-field'];
688 | }
689 | if ( ! empty( $config['wpml-config']['custom-term-fields']['custom-term-field'] ) ) {
690 | $wpml_config_debug['Custom Term Meta Translation'] = $config['wpml-config']['custom-term-fields']['custom-term-field'];
691 | }
692 | if ( ! empty( $config['wpml-config']['shortcodes']['shortcode'] ) ) {
693 | $wpml_config_debug['Shortcodes'] = $config['wpml-config']['shortcodes']['shortcode'];
694 | }
695 | if ( ! empty( $config['wpml-config']['admin-texts']['key'] ) ) {
696 | $wpml_config_debug['Admin Strings to Translate'] = $config['wpml-config']['admin-texts']['key'];
697 | }
698 | if ( ! empty( $config['wpml-config']['language-switcher-settings']['key'] ) ) {
699 | $wpml_config_debug['Language Switcher Settings'] = $config['wpml-config']['language-switcher-settings']['key'];
700 | }
701 |
702 | return $config;
703 | }
704 |
705 | /**
706 | * Intercept wpml-config.xml parsing to display loaded configuration files
707 | * for debugging purposes.
708 | *
709 | * @param string $file
710 | *
711 | * @return string
712 | * @global object $sitepress
713 | */
714 | function display_configuration_for_debug( $file ) {
715 | // Get url and name.
716 | if ( is_object( $file ) ) {
717 | $url = ICL_REMOTE_WPML_CONFIG_FILES_INDEX . 'wpml-config/' . $file->admin_text_context . '/wpml-config.xml';
718 | $name = $file->admin_text_context;
719 | $class = 'dashicons-admin-site';
720 | } else {
721 | $url = str_replace( WP_CONTENT_DIR, WP_CONTENT_URL, $file );
722 | $name = basename( dirname( $url ) );
723 | $class = '';
724 | }
725 |
726 | // Display link to file.
727 | echo '' . $name . ' ';
728 | if ( ! empty( $class ) ) {
729 | echo ' ';
730 | }
731 | echo ' ';
732 |
733 | // Display validation errors if any found.
734 | if ( is_string( $file ) && file_exists( $file ) ) {
735 | $validate = new WPML_XML_Config_Validate( WPML_PLUGIN_PATH . '/res/xsd/wpml-config.xsd' );
736 | $validate->from_file( $file );
737 | $errors = wp_list_pluck( $validate->get_errors(), 'message' );
738 | if ( ! empty( $errors ) ) {
739 | $errors = array_unique( $errors );
740 | // TODO: add some style.
741 | echo '' . implode( ' ', $errors ) . '
';
742 | }
743 | }
744 |
745 | return $file;
746 | }
747 |
748 | }
749 |
--------------------------------------------------------------------------------
/inc/wpml-compatibility-test-tools.functions.php:
--------------------------------------------------------------------------------
1 | get_language_details( $lang );
18 |
19 | if ( isset( $language_details['english_name'] ) ) {
20 | $template = str_replace( '%language_name%', $language_details['english_name'], $template );
21 | }
22 |
23 | if ( isset( $language_details['code'] ) ) {
24 | $template = str_replace( '%language_code%', $language_details['code'], $template );
25 | }
26 |
27 | if ( isset( $language_details['display_name'] ) ) {
28 | $template = str_replace( '%language_native_name%', $language_details['display_name'], $template );
29 | }
30 |
31 | return $template;
32 | }
33 |
34 | /**
35 | *
36 | * Return list of contexts for string translation
37 | *
38 | * @return mixed
39 | */
40 | function wpml_ctt_st_contexts() {
41 | return icl_st_get_contexts( false );
42 | }
43 |
44 | /**
45 | *
46 | * Generate language checkboxes
47 | *
48 | * @param array $selected_languages - arrach of languages (code) that should be checked
49 | *
50 | * @return string
51 | */
52 | function wpml_ctt_active_languages_output( $selected_languages = array() ) {
53 | $active_langs = apply_filters( 'wpml_active_languages', NULL, 'orderby=id&order=asc' );
54 | $default_lang = apply_filters( 'wpml_default_language', NULL );
55 |
56 | // Remove default language from list.
57 | unset( $active_langs[$default_lang] );
58 |
59 | if ( empty( $active_langs ) ) {
60 | return sprintf( __( 'No active languages set. You can enable languages here .', 'wpml-compatibility-test-tools' ), admin_url( 'admin.php?page=sitepress-multilingual-cms/menu/languages.php' ) );
61 | }
62 |
63 | $theme_lang_inputs = '';
76 |
77 | return $theme_lang_inputs;
78 | }
79 |
80 | /**
81 | *
82 | * Return names of all custom fields
83 | *
84 | * @return mixed
85 | */
86 | function wpml_get_custom_fields() {
87 | global $wpdb;
88 |
89 | return $wpdb->get_results( "SELECT DISTINCT(meta_key) FROM $wpdb->postmeta" );
90 | }
91 |
92 | /**
93 | *
94 | * Returning through AJAX selected option array as JSON.
95 | *
96 | */
97 | add_action( 'wp_ajax_wpml_ctt_action', 'wpml_ctt_options_list_ajax' );
98 | function wpml_ctt_options_list_ajax() {
99 | check_ajax_referer( 'wctt-generate', '_wctt_mighty_nonce' );
100 |
101 | $data = array();
102 | $options = isset( $_POST['options'] ) ? (array) $_POST['options'] : array();
103 |
104 | $safe_options = wpml_ctt_options_list();
105 |
106 | foreach ( $options as $option ) {
107 | // Dealing with unwanted.
108 | if ( ! array_key_exists( $option, $safe_options ) ) {
109 | $data = ["{$option}" => 'No way Jose!'];
110 | break;
111 | }
112 |
113 | // Dealing with bad nested serialization.
114 | if ( ! is_serialized( get_option( $option ) ) ) {
115 | $data[ $option ] = get_option( $option );
116 | } else {
117 | $data[ $option ] = '*** WARNING: NESTED SERIALIZATION DETECTED, WILL NOT WORK WITH WPML! ***';
118 | }
119 | }
120 |
121 | echo json_encode( $data );
122 | wp_die();
123 | }
124 |
125 | /**
126 | *
127 | * Creating options list by filtering results from wp_options table.
128 | *
129 | * @return array
130 | *
131 | */
132 | function wpml_ctt_options_list() {
133 | $exclude_list = array(
134 |
135 | /* WP default ones */
136 |
137 | 'siteurl',
138 | 'home',
139 | 'blogname',
140 | 'blogdescription',
141 | 'users_can_register',
142 | 'admin_email',
143 | 'start_of_week',
144 | 'use_balanceTags',
145 | 'use_smilies',
146 | 'require_name_email',
147 | 'comments_notify',
148 | 'posts_per_rss',
149 | 'rss_use_excerpt',
150 | 'mailserver_url',
151 | 'mailserver_login',
152 | 'mailserver_pass',
153 | 'mailserver_port',
154 | 'default_category',
155 | 'default_comment_status',
156 | 'default_ping_status',
157 | 'default_pingback_flag',
158 | 'posts_per_page',
159 | 'date_format',
160 | 'time_format',
161 | 'links_updated_date_format',
162 | 'comment_moderation',
163 | 'moderation_notify',
164 | 'permalink_structure',
165 | 'gzipcompression',
166 | 'hack_file',
167 | 'blog_charset',
168 | 'active_plugins',
169 | 'category_base',
170 | 'ping_sites',
171 | 'advanced_edit',
172 | 'comment_max_links',
173 | 'gmt_offset',
174 | 'default_email_category',
175 | 'template',
176 | 'stylesheet',
177 | 'comment_whitelist',
178 | 'comment_registration',
179 | 'html_type',
180 | 'use_trackback',
181 | 'default_role',
182 | 'db_version',
183 | 'uploads_use_yearmonth_folders',
184 | 'upload_path',
185 | 'blog_public',
186 | 'default_link_category',
187 | 'show_on_front',
188 | 'tag_base',
189 | 'show_avatars',
190 | 'avatar_rating',
191 | 'upload_url_path',
192 | 'thumbnail_size_w',
193 | 'thumbnail_size_h',
194 | 'thumbnail_crop',
195 | 'medium_size_w',
196 | 'medium_size_h',
197 | 'avatar_default',
198 | 'large_size_w',
199 | 'large_size_h',
200 | 'image_default_link_type',
201 | 'image_default_size',
202 | 'image_default_align',
203 | 'close_comments_for_old_posts',
204 | 'close_comments_days_old',
205 | 'thread_comments',
206 | 'thread_comments_depth',
207 | 'page_comments',
208 | 'comments_per_page',
209 | 'default_comments_page',
210 | 'comment_order',
211 | 'sticky_posts',
212 | 'widget_categories',
213 | 'widget_text',
214 | 'widget_rss',
215 | 'timezone_string',
216 | 'page_for_posts',
217 | 'page_on_front',
218 | 'default_post_format',
219 | 'link_manager_enabled',
220 | 'initial_db_version',
221 | 'wp_user_roles',
222 | 'widget_search',
223 | 'widget_recent-posts',
224 | 'widget_recent-comments',
225 | 'widget_archives',
226 | 'widget_meta',
227 | 'sidebars_widgets',
228 | 'cron',
229 | 'rewrite_rules',
230 | 'can_compress_scripts',
231 | 'recently_activated',
232 | 'blacklist_keys',
233 | 'moderation_keys',
234 | 'links_recently_updated_prepend',
235 | 'links_recently_updated_append',
236 | 'links_recently_updated_time',
237 | 'embed_autourls',
238 | 'embed_size_w',
239 | 'embed_size_h',
240 | 'secret',
241 | 'use_linksupdate',
242 | 'rss_language',
243 | 'default_post_edit_rows',
244 | 'enable_app',
245 | 'enable_xmlrpc',
246 | 'recently_edited',
247 | 'auto_core_update_notified',
248 | 'db_upgraded',
249 |
250 | /* WPML added ones */
251 |
252 | '_icl_cache',
253 | '_wpml_media',
254 | 'icl_adl_settings',
255 | 'icl_admin_messages',
256 | '_icl_admin_option_names',
257 | 'icl_sitepress_settings',
258 | 'icl_sitepress_version',
259 | 'icl_translation_jobs_basket',
260 | 'widget_icl_lang_sel_widget',
261 | 'wp_icl_non_translators_cached',
262 | 'wp_icl_translators_cached',
263 | 'wpml_config_files_arr',
264 | 'wpml_config_index',
265 | 'wpml_config_index_updated',
266 | 'wpml-package-translation-db-updates-run',
267 | 'wpml-package-translation-refresh-required',
268 | 'wpml-package-translation-string-packages-table-updated',
269 | 'wpml-package-translation-string-table-updated',
270 | 'WPML_CMS_NAV_VERSION',
271 | 'wpml_tm_version',
272 | 'wp_installer_settings',
273 | 'wpml_cms_nav_settings',
274 | 'wpml_ctt_settings' );
275 |
276 | $options = wpml_ctt_load_alloptions();
277 |
278 | foreach ( $options as $name => $value ) {
279 | if ( in_array( $name, $exclude_list ) || ( ! stristr( $name, '_transient' ) === false ) ) {
280 | unset( $options[$name] );
281 | }
282 | }
283 |
284 | return $options;
285 | }
286 |
287 | /**
288 | *
289 | * Validate radio values.
290 | *
291 | * @param $value
292 | *
293 | * @return mixed
294 | */
295 | function wpml_ctt_validate_radio( $value ) {
296 | $allowed = array(
297 | 'translate',
298 | 'copy-once',
299 | 'ignore',
300 | 'copy',
301 | 'file',
302 | 'dir',
303 | '2',
304 | '1',
305 | '0'
306 | );
307 |
308 | if ( in_array( $value, $allowed, true ) ) {
309 | // When set to display as translated.
310 | if ( $value === '2' ) {
311 | return '1';
312 | }
313 |
314 | return $value;
315 | }
316 |
317 | return '';
318 | }
319 |
320 | /**
321 | * Loads and caches all options.
322 | *
323 | * @global wpdb $wpdb WordPress database abstraction object.
324 | *
325 | * @return array List of all options.
326 | */
327 | function wpml_ctt_load_alloptions() {
328 | global $wpdb;
329 |
330 | if ( ! wp_installing() || ! is_multisite() ) {
331 | $alloptions = wp_cache_get( 'wpml_ctt_all_options', 'options' );
332 | } else {
333 | $alloptions = false;
334 | }
335 |
336 | if ( ! $alloptions ) {
337 | $suppress = $wpdb->suppress_errors();
338 | $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options ORDER BY option_name" );
339 |
340 | $wpdb->suppress_errors( $suppress );
341 |
342 | $alloptions = array();
343 |
344 | foreach ( (array) $alloptions_db as $o ) {
345 | $alloptions[ $o->option_name ] = $o->option_value;
346 | }
347 | if ( ! wp_installing() || ! is_multisite() ) {
348 | wp_cache_add( 'wpml_ctt_all_options', $alloptions, 'options' );
349 | }
350 | }
351 |
352 | return $alloptions;
353 | }
354 |
355 | /**
356 | * Display an entry from a wpml-config.xml file.
357 | *
358 | * @param array $entry
359 | */
360 | function wpml_ctt_parse_entry( $entry ) {
361 | if ( isset( $entry['tag']['value'] ) ) {
362 | // This is for items from the shortcodes section.
363 | echo '' . $entry['tag']['value'] . ' ';
364 | if ( ! empty( $entry['attributes']['attribute'] ) ) {
365 | if ( isset( $entry['attributes']['attribute']['value'] ) ) {
366 | $entry['attributes']['attribute'] = array( $entry['attributes']['attribute'] );
367 | }
368 | $attributes = wp_list_pluck($entry['attributes']['attribute'], 'value' );
369 | echo ': ' . implode( ', ', $attributes );
370 | }
371 | echo ' ';
372 | } else if ( isset( $entry['attr']['name'] ) ) {
373 | // This part if for admin-texts and language-switcher-settings.
374 | echo '' . $entry['attr']['name'] . ' : ';
375 | echo $entry['value'] . ' ';
376 | if ( ! empty( $entry['key'] ) ) {
377 | echo '';
378 | foreach ( $entry['key'] as $key ) {
379 | wpml_ctt_parse_entry( $key );
380 | }
381 | echo ' ';
382 | }
383 | } else {
384 | // This is for any other type of entry.
385 | echo '' . $entry['value'] . ' : ';
386 | foreach ( $entry['attr'] as $key => $value ) {
387 | echo $key . ' => ' . $value . ' ';
388 | }
389 | }
390 | }
391 |
392 | function mltools_shortcode_helper_add_hooks() {
393 |
394 | $debug_enabled = WPML_Compatibility_Test_Tools::get_option( 'shortcode_enable_debug', false );
395 |
396 | if ( $debug_enabled && is_user_logged_in() ) {
397 |
398 | $debug_values_enabled = WPML_Compatibility_Test_Tools::get_option( 'shortcode_enable_debug_value', false );
399 | $default_ignored_tags = mltools_shortcode_helper_get_default_ignored_tags();
400 | $ignored_tags = array_merge( $default_ignored_tags, array_map( 'trim',
401 | explode( ',', WPML_Compatibility_Test_Tools::get_option( 'shortcode_ignored_tags', '' ) )
402 | ) );
403 |
404 | $shortcode_attribute_filter = new MLTools_Shortcode_Attribute_Filter( $ignored_tags );
405 | $shortcode_attribute_filter->add_hooks();
406 |
407 | MLTools_Shortcode_WPML_Config_Parser::add_hooks();
408 |
409 | if ( ! is_admin() ) {
410 | add_action( 'shutdown', 'mltools_shortcode_helper_unregistered_print_xml', 20 );
411 | if ( $debug_values_enabled ) {
412 | add_action( 'shutdown', 'mltools_shortcode_helper_unregistered_print_captured_values', 30 );
413 | }
414 | }
415 |
416 | }
417 | }
418 |
419 | function mltools_shortcode_helper_get_default_ignored_tags() {
420 | $default = array(
421 | 'vc_row',
422 | 'vc_column',
423 | 'vc_row_inner',
424 | 'vc_column_inner',
425 | 'vc_basic_grid',
426 | 'vc_empty_space',
427 | 'vc_icon',
428 | 'vc_separator',
429 | 'audio',
430 | 'caption',
431 | 'embed',
432 | 'gallery',
433 | 'playlist',
434 | 'video',
435 | 'wp_caption',
436 | 'wpml-string',
437 | 'wpml_language_form_field',
438 | 'wpml_language_selector_footer',
439 | 'wpml_language_selector_widget',
440 | 'wpml_language_switcher',
441 | );
442 | sort( $default );
443 |
444 | return $default;
445 | }
446 |
447 | function mltools_shortcode_helper_unregistered_print_xml() {
448 |
449 | $output = mltools_shortcode_helper_unregistered_get_xml_output();
450 |
451 | if ( $output === false ) {
452 |
453 | user_error( 'MLTools shortcode helper: WPML_Config not loaded' );
454 |
455 | } elseif ( is_string( $output ) && ! empty( $output ) ) {
456 |
457 | echo ''
458 | . htmlentities( $output ) . ' ';
459 | }
460 | }
461 |
462 | function mltools_shortcode_helper_unregistered_print_captured_values(){
463 |
464 | $unregistered_tags = mltools_shortcode_helper_get_unregistered_tags();
465 | $captured_values = get_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME_VALUES, array() );
466 |
467 | foreach ( $captured_values as $tag => $values ) {
468 |
469 | if ( array_key_exists( $tag, $unregistered_tags )) {
470 |
471 | echo '' . $tag . ' ';
472 |
473 | foreach ( $values['attributes'] as $attr_name => $attr_value ) {
474 | echo "{$attr_name}: {$attr_value} ";
475 | }
476 |
477 | echo ' ';
478 | }
479 | }
480 | }
481 |
482 | /**
483 | * @return bool|string
484 | */
485 | function mltools_shortcode_helper_unregistered_get_xml_output() {
486 |
487 | $xml_helper = new MLTools_XML_Helper();
488 | $captured_tags = mltools_shortcode_helper_get_unregistered_tags();
489 |
490 | if ( $captured_tags === false ) {
491 | return false;
492 | }
493 |
494 | if ( is_array( $captured_tags ) && ! empty( $captured_tags ) ) {
495 | return $xml_helper->get_dom_shortcodes( $captured_tags );
496 | }
497 |
498 | return '';
499 | }
500 |
501 | /**
502 | * @return bool|array
503 | */
504 | function mltools_shortcode_helper_get_unregistered_tags() {
505 |
506 | $wpml_config = MLTools_Shortcode_WPML_Config_Parser::get_config();
507 |
508 | if ( $wpml_config === false ) {
509 | return false;
510 | }
511 |
512 | $captured_tags = get_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME, array() );
513 | $default_ignored_tags = mltools_shortcode_helper_get_default_ignored_tags();
514 | $ignored_tags = array_merge( $default_ignored_tags, array_map( 'trim',
515 | explode( ',', WPML_Compatibility_Test_Tools::get_option( 'shortcode_ignored_tags', '' ) )
516 | ) );
517 |
518 | foreach ( $captured_tags as $tag => $config ) {
519 | if ( array_key_exists( $tag, $wpml_config ) || in_array( $tag, $ignored_tags ) ) {
520 | unset( $captured_tags[ $tag ] );
521 | }
522 | }
523 |
524 | return $captured_tags;
525 | }
--------------------------------------------------------------------------------
/inc/wpml-modify-duplicate-strings.class.php:
--------------------------------------------------------------------------------
1 | filter = $filter;
17 | $this->template = $template;
18 | add_filter( 'wpml_duplicate_generic_string', array( $this, 'duplicate_generic_string' ), 10, 3 );
19 | }
20 |
21 | /**
22 | *
23 | * Add information about language to string based on context
24 | *
25 | * @param $string - string to modify
26 | * @param $lang - language code
27 | * @param $context - array(
28 | * 'context' => 'post' or 'custom_field' or 'taxonomy',
29 | * 'attribute' => 'title' or 'content' or 'excerpt' (for a post), 'value' (for a custom field), '{taxonomy_name}' (for a taxonomy),
30 | * 'key' => '{post_id}' | '{meta_key}' | '{term_id}',
31 | * );
32 | *
33 | * @return string
34 | */
35 | public function duplicate_generic_string( $string, $lang, $context ) {
36 |
37 | // Check context
38 | $filter_context = isset( $context['context'] ) ? $context['context'] : '';
39 | $attribute = isset( $context['attribute'] ) ? $context['attribute'] : '';
40 |
41 | // Check if user required to filter given string type (based on selected settings in admin panel)
42 | if ( isset( $this->filter[ $filter_context ] ) ) {
43 | // Special case for taxonomy
44 | if ( in_array( $filter_context, array( 'taxonomy', 'taxonomy_slug' ) ) ) {
45 | if ( ! isset( $this->filter[ $filter_context ]['all'] ) ) {
46 | return $string;
47 | }
48 | } elseif ( ! isset( $this->filter[ $filter_context ][ $attribute ] ) ) {
49 | return $string;
50 | }
51 |
52 | } else {
53 | return $string;
54 | }
55 |
56 | // Based on context
57 | switch ( $filter_context ) {
58 | case 'post':
59 |
60 | // Exception for empty excerpt field
61 | if ( ( 0 === strcmp( $attribute, 'excerpt' ) ) && ( empty( $string ) ) ) {
62 | break;
63 | }
64 |
65 | $string = wpml_ctt_prepare_string( $this->template, $string, $lang );
66 | break;
67 | case 'taxonomy':
68 | $string = wpml_ctt_prepare_string( $this->template, $string, $lang );
69 | break;
70 | case 'taxonomy_slug' :
71 | $string = $this->add_language_name_to_slug( $string, $lang );
72 | break;
73 | case 'custom_field' :
74 | $string = $this->add_language_name_to_custom_field( $string, $lang, $context );
75 | break;
76 | }
77 |
78 | // By default return the same value
79 | return $string;
80 | }
81 |
82 | /**
83 | * Add language name to slug
84 | *
85 | * @param $string
86 | * @param $lang
87 | *
88 | * @return string
89 | */
90 | private function add_language_name_to_slug( $string, $lang ) {
91 | global $sitepress;
92 |
93 | $language_details = $sitepress->get_language_details( $lang );
94 |
95 | if ( isset( $language_details['english_name'] ) ) {
96 | return sanitize_title_with_dashes( $language_details['english_name'] . '-' . $string, 'save' );
97 | }
98 |
99 | return $string;
100 | }
101 |
102 | /**
103 | *
104 | * Add language name to custom field (only if is set to translate)
105 | *
106 | * @param $string
107 | * @param $lang
108 | * @param $context
109 | *
110 | * @return string
111 | */
112 | private function add_language_name_to_custom_field( $string, $lang, $context ) {
113 |
114 | // Get settings - $this->settings is not set when creating duplicate (not updating)
115 | global $sitepress_settings;
116 | $settings =& $sitepress_settings['translation-management'];
117 |
118 | // Check for custom fields to translate
119 | if ( isset( $settings['custom_fields_translation'] ) ) {
120 | // Get information about custom fields to translate
121 | $custom_fields_translation = $settings['custom_fields_translation'];
122 |
123 | if ( isset( $custom_fields_translation[$context['key']] ) ) {
124 |
125 | // If custom field is set to translate (id = 2)
126 | if ( $custom_fields_translation[$context['key']] == 2 ) {
127 | // Add language information
128 | return wpml_ctt_prepare_string( $this->template, $string, $lang );
129 | }
130 | }
131 | }
132 |
133 | return $string;
134 | }
135 | }
--------------------------------------------------------------------------------
/menus/settings/auto-translate-duplicate.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | " . __( 'Translation Dashboard', 'wpml-compatibility-test-tools' ) . "" ); ?>
11 |
12 |
13 |
14 |
15 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/menus/settings/auto-translate-strings.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | scanned for strings.', 'wpml-compatibility-test-tools' ), admin_url( 'admin.php?page=sitepress-multilingual-cms/menu/theme-localization.php' ) ); ?>
11 |
12 |
13 |
14 |
15 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/menus/settings/custom-fields-translation.php:
--------------------------------------------------------------------------------
1 | get_custom_fields();
5 | $translation_preferences = $mt_custom_fields_translation->determine_translation_preference();
6 | ?>
7 |
8 |
9 |
10 |
11 |
12 |
Custom XML Configuration tab in WPML.', 'wpml-compatibility-test-tools' ), esc_url( admin_url( 'admin.php?page=tm%2Fmenu%2Fsettings&sm=custom-xml-config' ) ) ); ?>
13 |
14 |
15 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/menus/settings/generator.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/menus/settings/overview.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | $config ) : ?>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/menus/settings/settings.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/menus/settings/shortcode-helper.php:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/multilingual-tools.php:
--------------------------------------------------------------------------------
1 | th,
151 | #wctt-generator thead tr:not(:first-child) > th {
152 | padding: 0 0 3px 10px;
153 | }
154 | .column-desc {
155 | width: 200px;
156 | font-size: 13px !important;
157 | }
158 | #at-toggle {
159 | display: none;
160 | }
161 | #wctt-generator input[name="save"] {
162 | margin: 2px 2px 0 20px;
163 | }
164 | #wctt-generator input[type="submit"] {
165 | float:right;
166 | margin-bottom:5px;
167 | }
168 | #wctt-generator .wctt input[type="text"] {
169 | display: block;
170 | width: 100%;
171 | float: left;
172 | padding: 5px;
173 | }
174 | #save {
175 | padding: 20px;
176 | float: right;
177 | }
178 | .remove {
179 | float: right;
180 | line-height:32px;
181 | width: 10px;
182 | }
183 | #mt-shortcodes .wctt td {
184 | width: 50%;
185 | }
186 | #mt-shortcodes .wctt tr:hover {
187 | background: none !important;
188 | }
189 | #mt-shortcodes tfoot th {
190 | text-align: center;
191 | }
192 | #add-shortcode-button {
193 | float: right;
194 | }
195 | #shortcode-attr-tfield {
196 | width: calc(100% - 22px) !important;
197 | }
198 | ::-webkit-input-placeholder {
199 | color: #BBBBBB;
200 | }
201 |
202 | :-moz-placeholder { /* Firefox 18- */
203 | color: #BBBBBB;
204 | }
205 |
206 | ::-moz-placeholder { /* Firefox 19+ */
207 | color: #BBBBBB;
208 | }
209 |
210 | :-ms-input-placeholder {
211 | color: #BBBBBB;
212 | }
213 | #mt-shortcodes .wctt .td-left {
214 | padding: 15px 7px 15px 15px;
215 | }
216 | #mt-shortcodes .wctt .td-right {
217 | padding: 15px 15px 15px 7px;
218 | }
219 | .status {
220 | position: absolute;
221 | padding: 5px 5px 5px 10px;
222 | }
223 | .spinner {
224 | position: relative;
225 | float: right;
226 | background: url('../img/spinner.gif') no-repeat left;
227 | height: 16px;
228 | width: 16px;
229 | margin-top: 2px;
230 | }
--------------------------------------------------------------------------------
/res/img/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/spinner.gif
--------------------------------------------------------------------------------
/res/img/wctt-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/wctt-icon.png
--------------------------------------------------------------------------------
/res/img/wctt-multiselect-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/wctt-multiselect-bg.png
--------------------------------------------------------------------------------
/res/js/mt-script.js:
--------------------------------------------------------------------------------
1 | jQuery(function () {
2 |
3 | var option = [],
4 | result = jQuery('#result'),
5 | submitButton = jQuery('#wctt-generator').find('[type="submit"]'),
6 | dropdownToggle = jQuery('a#strings_auto_translate_context.toggle'),
7 | dropdown = jQuery('#dropdown'),
8 | multiSelect = jQuery('#multiSelect'),
9 | progress = jQuery('span.progress'),
10 | spinner = jQuery('span.spinner'),
11 | submitAutoTranslate = jQuery('#strings_auto_translate_action_translate'),
12 | buttons = jQuery('.button');
13 |
14 | optionCount();
15 | buttonToggle();
16 |
17 | // Toggle drop-down on mouse gesture.
18 | dropdown.click(function (event) {
19 | event.stopPropagation();
20 |
21 | dropdown.find('ul').slideToggle({
22 | duration: 50,
23 | start: function () {
24 | dropdownToggle.toggle();
25 | }
26 | });
27 | });
28 |
29 | dropdown.find('ul').on('click', function (event) {
30 | event.stopPropagation();
31 | });
32 |
33 | // Update option count on checkbox selection.
34 | dropdown.find('input[type="checkbox"]').change(function () {
35 | optionCount();
36 | });
37 |
38 | // Hiding elements if clicked elsewhere
39 | jQuery(document).on('click', function () {
40 | dropdown.find('ul').hide();
41 | dropdownToggle.hide();
42 | });
43 |
44 | // Enable submit button if any checkbox is selected.
45 | jQuery(document).on('click', '[type="checkbox"]', function () {
46 | buttonToggle();
47 | });
48 |
49 | multiSelect.find('[type="checkbox"]').click(function () {
50 |
51 | // Collecting options from Multi-Select.
52 | option = multiSelect.find('[type="checkbox"]:checked').map(function (_, i) {
53 | return jQuery(i).val();
54 | }).get();
55 |
56 | if (option.length !== 0) {
57 |
58 | var data = {
59 | 'options': option,
60 | 'action': "wpml_ctt_action",
61 | '_wctt_mighty_nonce': jQuery('#_wctt_mighty_nonce').val()
62 | };
63 |
64 | jQuery.post(mt_data.ajax_url, data, function (response) {
65 | var output, data = JSON.parse(response);
66 |
67 | output = '';
85 |
86 | jQuery('#tree').remove();
87 |
88 | var content = jQuery(output).hide();
89 | result.append(content);
90 | content.fadeIn();
91 |
92 | jQuery('#at-notice').hide();
93 | jQuery('tr#at-toggle').show();
94 | });
95 | } else {
96 | jQuery('#tree').remove();
97 | jQuery('#at-notice').fadeIn();
98 | jQuery('tr#at-toggle').hide();
99 | }
100 | });
101 |
102 | // Multi-check options tree.
103 | result.on('click', '[type="checkbox"]', function () {
104 | var current = jQuery(this);
105 |
106 | if (this.checked) {
107 | current.parentsUntil('ul#tree').children('[type="checkbox"]').prop('checked', true);
108 | current.siblings('ul').find('[type="checkbox"]').prop('checked', true);
109 | } else {
110 | current.parent().find('[type="checkbox"]').prop('checked', false);
111 | }
112 | });
113 |
114 | jQuery("#string_auto_translate_predefined_templates").change(function () {
115 | jQuery("#strings_auto_translate_template").val(jQuery("#string_auto_translate_predefined_templates").find("option:selected").text());
116 | });
117 |
118 | jQuery("#duplicate_strings_predefined_templates").change(function () {
119 | jQuery("#duplicate_strings_template").val(jQuery("#duplicate_strings_predefined_templates").find("option:selected").text());
120 | });
121 |
122 | // Provides toggle all functionality.
123 | jQuery('.toggle').click(function (event) {
124 | event.preventDefault();
125 | event.stopPropagation();
126 |
127 | var group = jQuery('input[id=' + this.id + ']');
128 |
129 | if (group.attr('type') === 'radio') {
130 | group.prop('checked', true);
131 | jQuery('input[type="checkbox"][id="' + this.id.slice(0, -2) + '"]').prop('checked', true);
132 | buttonToggle();
133 | } else {
134 | group.prop('checked', !group.prop('checked'));
135 | buttonToggle();
136 | optionCount();
137 | }
138 | });
139 |
140 | // Automatically check checkbox if radio is changed.
141 | jQuery('input[type="radio"]').change(function () {
142 | jQuery(this).closest('tr').find('input[type="checkbox"]').prop('checked', true);
143 | buttonToggle();
144 | });
145 |
146 | // Button toggle disable.
147 | function buttonToggle() {
148 | var $nonemptyTextFields = jQuery('input[type=text]').not('[id="shortcode-attr-tfield"]').filter(function () {
149 | return this.value !== ''
150 | });
151 |
152 | submitButton.attr('disabled',
153 | !jQuery('[type="checkbox"]').not('[class="option"]').is(':checked') &&
154 | $nonemptyTextFields.length === 0);
155 | }
156 |
157 | // Count selected strings and options from dropdown.
158 | function optionCount() {
159 | var selectedContexts = dropdown.find('[type="checkbox"]:checked'),
160 | placeholder = jQuery('.placeholder'),
161 | stringsCount = 0,
162 | labels = {
163 | string: 'string',
164 | context: 'context'
165 | };
166 |
167 | selectedContexts.each(function (_, i) {
168 | stringsCount += parseInt(jQuery(i).parent().text().match(/\((.*)\)/).pop());
169 | });
170 |
171 | if (selectedContexts.length > 0) {
172 | placeholder.text('- ' + returnCount(stringsCount, labels.string) + ' selected in ' + returnCount(selectedContexts.length, labels.context) + ' -');
173 | } else {
174 | placeholder.text('- Select -');
175 | }
176 |
177 | function returnCount(total, string) {
178 | if (total > 1) {
179 | string += 's';
180 | }
181 |
182 | return total + ' ' + string;
183 | }
184 | }
185 |
186 |
187 |
188 | /*
189 | * SHORTCODES
190 | */
191 |
192 | var shortcodes = jQuery('#mt-shortcodes'),
193 | shortcodeNotice = jQuery('#shortcode-notice'),
194 | shortcodeButton = jQuery('#add-shortcode-button');
195 |
196 | // Add shortcode
197 |
198 | shortcodeButton.click(function (event) {
199 | event.preventDefault();
200 |
201 | var output = '';
202 | output += ' ';
203 | output += ' ';
204 | output += ' ';
205 | output += 'X ';
206 |
207 | var content = jQuery(output).hide();
208 |
209 | shortcodes.find('tbody').append(content);
210 | content.fadeIn();
211 | shortcodeNotice.hide();
212 | });
213 |
214 | // Remove shortcode
215 |
216 | shortcodes.find('.wctt').on('click', '.remove', function (event) {
217 | event.preventDefault();
218 |
219 | var shortcode = jQuery(this).closest('#mt-shortcode');
220 |
221 | shortcode.fadeOut(function () {
222 | this.remove();
223 | });
224 |
225 | showShortcodeNotice();
226 | });
227 |
228 | // Remove all shortcodes
229 |
230 | shortcodes.find('#remove-all').click(function (event) {
231 | event.preventDefault();
232 |
233 | shortcodes.find('tbody #mt-shortcode').fadeOut(function () {
234 | this.remove();
235 | });
236 |
237 | showShortcodeNotice();
238 | });
239 |
240 | function showShortcodeNotice() {
241 | shortcodes.find('tbody #mt-shortcode').promise().done(function () {
242 |
243 | if (shortcodes.find('tbody').find('#mt-shortcode').length === 0) {
244 | shortcodeNotice.fadeIn();
245 | }
246 |
247 | buttonToggle();
248 | });
249 | }
250 |
251 | shortcodes.on('keyup', 'input[type=text]', function () {
252 | buttonToggle();
253 | });
254 |
255 | /*
256 | * STRINGS AUTO TRANSLATE
257 | */
258 | submitAutoTranslate.on('click', function () {
259 | const formData = loadData();
260 |
261 | if (checkData(formData, mt_data.labels) && confirm(mt_data.labels.question)) {
262 | buttons.attr('disabled', true);
263 | generateStringTranslations(formData);
264 | }
265 | });
266 |
267 | function generateStringTranslations(formData, responseData) {
268 | let data = Object.assign({
269 | 'action': 'generate_strings_translations_action',
270 | 'contexts': formData.contexts,
271 | 'languages': formData.languages,
272 | 'template': formData.template,
273 | '_mt_mighty_nonce': jQuery('#_mt_mighty_nonce').val()
274 | }, responseData);
275 |
276 | jQuery.post(mt_data.ajax_url, data, function (response) {
277 | progress.text(response.progress + '%');
278 | spinner.css('visibility', 'visible');
279 |
280 | if (response === 0) {
281 | responseMsg('Response error.')
282 | return;
283 | } else if (response !== 1) {
284 | generateStringTranslations(formData, response);
285 | } else {
286 | responseMsg('Done.')
287 | }
288 |
289 | function responseMsg(message) {
290 | buttons.attr('disabled', false);
291 | spinner.css('visibility', 'hidden');
292 | progress.text(message);
293 | setTimeout(function () {
294 | progress.text('')
295 | }, 5000);
296 | }
297 | });
298 | }
299 |
300 | function loadData() {
301 | return {
302 | languages: jQuery('.active_languages:checkbox:checked').map(function (_, i) {
303 | return jQuery(i).val();
304 | }).get(),
305 | contexts: jQuery('.strings_auto_translate_context:checkbox:checked').map(function (_, i) {
306 | return jQuery(i).val();
307 | }).get(),
308 | template: jQuery('#strings_auto_translate_template').val()
309 | };
310 | }
311 |
312 | function checkData(data, labels) {
313 | let message = '';
314 |
315 | if (data.contexts.length === 0) {
316 | message += labels.no_context_notice + '\n'
317 | }
318 |
319 | if (data.languages.length === 0) {
320 | message += labels.no_selected_language_notice + '\n'
321 | }
322 |
323 | if (data.template === '') {
324 | message += labels.no_template_notice
325 | }
326 |
327 | if (message !== '') {
328 | alert(message);
329 | return false;
330 | } else
331 | return true;
332 | }
333 |
334 | jQuery('#wpml-cf-form').on('submit', function (e) {
335 | e.preventDefault();
336 |
337 | var data = jQuery(this).serialize();
338 |
339 | jQuery.post(ajaxurl, data, function (response) {
340 | // Decode the response before setting it as the textarea value
341 | var decodedResponse = jQuery('
').html(response).text();
342 | jQuery('#xml-output').text(decodedResponse);
343 |
344 | // Enable the copy button if the textarea is not empty
345 | if (decodedResponse.trim() !== '') {
346 | jQuery('#copy-xml').prop('disabled', false);
347 | }
348 | });
349 | });
350 |
351 | jQuery('#copy-xml').on('click', function (e) {
352 | e.preventDefault();
353 |
354 | var xmlOutput = document.getElementById('xml-output');
355 | xmlOutput.select();
356 |
357 | try {
358 | // Copy the selected text to the clipboard
359 | document.execCommand('copy');
360 | alert('XML copied to clipboard!');
361 | } catch (err) {
362 | console.log('Oops, unable to copy');
363 | }
364 | });
365 |
366 | });
367 |
--------------------------------------------------------------------------------