PluginProbe
Polylang / 3.7.7
Polylang v3.7.7
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
← All changes | modules/wpml/wpml-config.php +880 -97 3.0 → 3.7.7 View file →
@@ -8,14 +8,22 @@
8 8 * See http://wpml.org/documentation/support/language-configuration-files/
9 9 * The language switcher configuration is not interpreted
10 10 *
11 11 * @since 1.0
12 + *
13 + * @phpstan-type ParsedMetas array<
14 + * non-falsy-string,
15 + * array{
16 + * action: string,
17 + * encoding: 'json'|''
18 + * }
19 + * >
12 20 */
13 21 class PLL_WPML_Config {
14 22 /**
15 23 * Singleton instance
16 24 *
17 - * @var PLL_WPML_Config
25 + * @var PLL_WPML_Config|null
18 26 */
19 27 protected static $instance;
20 28
21 29 /**
@@ -22,11 +30,48 @@
22 30 * The content of all read xml files.
23 31 *
24 32 * @var SimpleXMLElement[]
25 33 */
26 - protected $xmls;
34 + protected $xmls = array();
27 35
28 36 /**
37 + * The list of xml file paths.
38 + *
39 + * @var string[]|null
40 + *
41 + * @phpstan-var array<string, string>|null
42 + */
43 + protected $files;
44 +
45 + /**
46 + * List of rules to extract strings to translate from blocks.
47 + *
48 + * @var array
49 + *
50 + * @phpstan-var array{
51 + * xpath?: array<non-empty-string, list<non-empty-string>>,
52 + * key?: array<non-empty-string, array<array|true>>
53 + * }|null
54 + */
55 + protected $parsing_rules = null;
56 +
57 + /**
58 + * Contains the list of path in `open_basedir`.
59 + *
60 + * @var string[]|null
61 + */
62 + private $open_basedir_paths;
63 +
64 + /**
65 + * Cache for parsed metas.
66 + *
67 + * @var array
68 + *
69 + * @phpstan-var array<non-falsy-string, ParsedMetas>
70 + */
71 + private $parsed_metas = array();
72 +
73 + /**
29 74 * Constructor
30 75 *
31 76 * @since 1.0
32 77 */
@@ -40,9 +85,9 @@
40 85 * Access to the single instance of the class
41 86 *
42 87 * @since 1.7
43 88 *
44 - * @return object
89 + * @return PLL_WPML_Config
45 90 */
46 91 public static function instance() {
47 92 if ( empty( self::$instance ) ) {
48 93 self::$instance = new self();
@@ -58,90 +103,110 @@
58 103 * @return void
59 104 */
60 105 public function init() {
61 106 $this->xmls = array();
107 + $files = $this->get_files();
62 108
63 - // Plugins
64 - // Don't forget sitewide active plugins thanks to Reactorshop http://wordpress.org/support/topic/polylang-and-yoast-seo-plugin/page/2?replies=38#post-4801829
65 - $plugins = ( is_multisite() && $sitewide_plugins = get_site_option( 'active_sitewide_plugins' ) ) && is_array( $sitewide_plugins ) ? array_keys( $sitewide_plugins ) : array();
66 - $plugins = array_merge( $plugins, get_option( 'active_plugins', array() ) );
109 + if ( empty( $files ) ) {
110 + return;
111 + }
67 112
68 - foreach ( $plugins as $plugin ) {
69 - if ( file_exists( $file = WP_PLUGIN_DIR . '/' . dirname( $plugin ) . '/wpml-config.xml' ) && false !== $xml = simplexml_load_file( $file ) ) {
70 - $this->xmls[ dirname( $plugin ) ] = $xml;
113 + if ( ! extension_loaded( 'simplexml' ) ) {
114 + return;
115 + }
116 +
117 + // Read all files.
118 + foreach ( $files as $context => $file ) {
119 + $xml = simplexml_load_file( $file );
120 + if ( false !== $xml ) {
121 + $this->xmls[ $context ] = $xml;
71 122 }
72 123 }
73 124
74 - // Theme
75 - if ( file_exists( $file = ( $template = get_template_directory() ) . '/wpml-config.xml' ) && false !== $xml = simplexml_load_file( $file ) ) {
76 - $this->xmls[ get_template() ] = $xml;
125 + if ( empty( $this->xmls ) ) {
126 + return;
77 127 }
78 128
79 - // Child theme
80 - if ( ( $stylesheet = get_stylesheet_directory() ) !== $template && file_exists( $file = $stylesheet . '/wpml-config.xml' ) && false !== $xml = simplexml_load_file( $file ) ) {
81 - $this->xmls[ get_stylesheet() ] = $xml;
82 - }
129 + add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 20, 2 );
130 + add_filter( 'pll_copy_term_metas', array( $this, 'copy_term_metas' ), 20, 2 );
131 + add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 );
132 + add_filter( 'pll_get_taxonomies', array( $this, 'translate_taxonomies' ), 10, 2 );
83 133
84 - // Custom
85 - if ( file_exists( $file = PLL_LOCAL_DIR . '/wpml-config.xml' ) && false !== $xml = simplexml_load_file( $file ) ) {
86 - $this->xmls['Polylang'] = $xml;
87 - }
134 + // Export.
135 + add_filter( 'pll_post_metas_to_export', array( $this, 'post_metas_to_export' ) );
136 + add_filter( 'pll_term_metas_to_export', array( $this, 'term_metas_to_export' ) );
137 + add_filter( 'pll_post_meta_encodings', array( $this, 'add_post_meta_encodings' ), 20 );
138 + add_filter( 'pll_term_meta_encodings', array( $this, 'add_term_meta_encodings' ), 20 );
139 + add_filter( 'pll_blocks_xpath_rules', array( $this, 'translate_blocks' ) );
140 + add_filter( 'pll_blocks_rules_for_attributes', array( $this, 'translate_blocks_attributes' ) );
88 141
89 - if ( ! empty( $this->xmls ) ) {
90 - add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 20, 2 );
91 - add_filter( 'pll_copy_term_metas', array( $this, 'copy_term_metas' ), 20, 2 );
92 - add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 );
93 - add_filter( 'pll_get_taxonomies', array( $this, 'translate_taxonomies' ), 10, 2 );
142 + $matcher = new PLL_Format_Util();
94 143
95 - foreach ( $this->xmls as $context => $xml ) {
96 - $keys = $xml->xpath( 'admin-texts/key' );
97 - if ( is_array( $keys ) ) {
98 - foreach ( $keys as $key ) {
99 - $attributes = $key->attributes();
100 - $name = (string) $attributes['name'];
144 + foreach ( $this->xmls as $context => $xml ) {
145 + $keys = $xml->xpath( 'admin-texts/key' );
101 146
102 - if ( false !== strpos( $name, '*' ) ) {
103 - $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
104 - $names = preg_grep( $pattern, array_keys( wp_load_alloptions() ) );
147 + if ( ! is_array( $keys ) ) {
148 + continue;
149 + }
105 150
106 - if ( is_array( $names ) ) {
107 - foreach ( $names as $_name ) {
108 - $this->register_or_translate_option( $context, $_name, $key );
109 - }
110 - }
111 - } else {
112 - $this->register_or_translate_option( $context, $name, $key );
113 - }
114 - }
151 + foreach ( $keys as $key ) {
152 + $name = $this->get_field_attribute( $key, 'name' );
153 +
154 + if ( ! $matcher->is_format( $name ) ) {
155 + $this->register_or_translate_option( $context, $name, $key );
156 + continue;
115 157 }
158 +
159 + $names = $matcher->filter_list( (array) wp_load_alloptions(), $name );
160 +
161 + foreach ( $names as $_name => $_val ) {
162 + $this->register_or_translate_option( $context, $_name, $key );
163 + }
116 164 }
117 165 }
118 166 }
119 167
120 168 /**
121 - * Adds custom fields to the list of metas to copy when creating a new translation.
169 + * Returns all wpml-config.xml files in MU plugins, plugins, theme, child theme, and Polylang custom directory.
122 170 *
171 + * @since 3.1
172 + *
173 + * @return string[] A context identifier as array key, a file path as array value.
174 + *
175 + * @phpstan-return array<string, string>
176 + */
177 + public function get_files() {
178 + if ( is_array( $this->files ) ) {
179 + return $this->files;
180 + }
181 +
182 + $this->files = array_merge(
183 + // Plugins.
184 + $this->get_plugin_files(),
185 + // Theme and child theme.
186 + $this->get_theme_files(),
187 + // MU Plugins.
188 + $this->get_mu_plugin_files(),
189 + // Custom.
190 + $this->get_custom_files()
191 + );
192 +
193 + return $this->files;
194 + }
195 +
196 + /**
197 + * Adds post metas to the list of metas to copy when creating a new translation.
198 + *
123 199 * @since 1.0
124 200 *
125 - * @param string[] $metas The list of custom fields to copy or synchronize.
201 + * @param string[] $metas The list of post metas to copy or synchronize.
126 202 * @param bool $sync True for sync, false for copy.
127 - * @return string[] The list of custom fields to copy or synchronize.
203 + * @return string[] The list of post metas to copy or synchronize.
204 + *
205 + * @phpstan-param array<non-falsy-string> $metas
128 206 */
129 207 public function copy_post_metas( $metas, $sync ) {
130 - foreach ( $this->xmls as $xml ) {
131 - $cfs = $xml->xpath( 'custom-fields/custom-field' );
132 - if ( is_array( $cfs ) ) {
133 - foreach ( $cfs as $cf ) {
134 - $attributes = $cf->attributes();
135 - if ( 'copy' == $attributes['action'] || ( ! $sync && in_array( $attributes['action'], array( 'translate', 'copy-once' ) ) ) ) {
136 - $metas[] = (string) $cf;
137 - } else {
138 - $metas = array_diff( $metas, array( (string) $cf ) );
139 - }
140 - }
141 - }
142 - }
143 - return $metas;
208 + return $this->filter_metas_to_copy( (array) $metas, 'custom-fields/custom-field', (bool) $sync );
144 209 }
145 210
146 211 /**
147 212 * Adds term metas to the list of metas to copy when creating a new translation.
@@ -150,27 +215,126 @@
150 215 *
151 216 * @param string[] $metas The list of term metas to copy or synchronize.
152 217 * @param bool $sync True for sync, false for copy.
153 218 * @return string[] The list of term metas to copy or synchronize.
219 + *
220 + * @phpstan-param array<non-falsy-string> $metas
154 221 */
155 222 public function copy_term_metas( $metas, $sync ) {
223 + return $this->filter_metas_to_copy( (array) $metas, 'custom-term-fields/custom-term-field', (bool) $sync );
224 + }
225 +
226 + /**
227 + * Adds post meta keys to export.
228 + *
229 + * @since 3.3
230 + * @see PLL_Export_Metas
231 + *
232 + * @param array $keys {
233 + * A recursive array containing nested meta sub-keys to translate.
234 + * Ex: array(
235 + * 'meta_to_translate_1' => 1,
236 + * 'meta_to_translate_2' => 1,
237 + * 'meta_to_translate_3' => array(
238 + * 'sub_key_to_translate_1' => 1,
239 + * 'sub_key_to_translate_2' => array(
240 + * 'sub_sub_key_to_translate_1' => 1,
241 + * ),
242 + * ),
243 + * )
244 + * }
245 + * @return array
246 + *
247 + * @phpstan-param array<non-falsy-string, mixed> $keys
248 + * @phpstan-return array<non-falsy-string, mixed>
249 + */
250 + public function post_metas_to_export( $keys ) {
251 + // Add keys that have the `action` attribute set to `translate`.
252 + $keys = $this->add_metas_to_export( (array) $keys, 'custom-fields/custom-field' );
253 +
254 + // Deal with sub-field translations.
156 255 foreach ( $this->xmls as $xml ) {
157 - $cfs = $xml->xpath( 'custom-term-fields/custom-term-field' );
158 - if ( is_array( $cfs ) ) {
159 - foreach ( $cfs as $cf ) {
160 - $attributes = $cf->attributes();
161 - if ( 'copy' == $attributes['action'] || ( ! $sync && in_array( $attributes['action'], array( 'translate', 'copy-once' ) ) ) ) {
162 - $metas[] = (string) $cf;
163 - } else {
164 - $metas = array_diff( $metas, array( (string) $cf ) );
165 - }
256 + $fields = $xml->xpath( 'custom-fields-texts/key' );
257 +
258 + if ( ! is_array( $fields ) ) {
259 + // No 'custom-fields-texts' nodes.
260 + continue;
261 + }
262 +
263 + foreach ( $fields as $field ) {
264 + $name = $this->get_field_attribute( $field, 'name' );
265 +
266 + if ( '' === $name ) {
267 + // Wrong configuration: empty `name` attribute (meta name).
268 + continue;
166 269 }
270 +
271 + if ( ! array_key_exists( $name, $keys ) ) {
272 + // Wrong configuration: the field is not in `custom-fields/custom-field`.
273 + continue;
274 + }
275 +
276 + $keys = $this->xml_to_array( $field, $keys, 1 );
167 277 }
168 278 }
169 - return $metas;
279 +
280 + return $keys;
170 281 }
171 282
172 283 /**
284 + * Adds term meta keys to export.
285 + * Note: sub-key translations are not currently supported by WPML.
286 + *
287 + * @since 3.3
288 + * @see PLL_Export_Metas
289 + *
290 + * @param array $keys {
291 + * An array containing meta keys to translate.
292 + * Ex: array(
293 + * 'meta_to_translate_1' => 1,
294 + * 'meta_to_translate_2' => 1,
295 + * 'meta_to_translate_3' => 1,
296 + * )
297 + * }
298 + * @return array
299 + *
300 + * @phpstan-param array<non-falsy-string, mixed> $keys
301 + * @phpstan-return array<non-falsy-string, mixed>
302 + */
303 + public function term_metas_to_export( $keys ) {
304 + // Add keys that have the `action` attribute set to `translate`.
305 + return $this->add_metas_to_export( (array) $keys, 'custom-term-fields/custom-term-field' );
306 + }
307 +
308 + /**
309 + * Specifies the encoding for post metas.
310 + *
311 + * @since 3.6
312 + *
313 + * @param string[] $metas An array containing meta names as array keys, and their encoding as array values.
314 + * @return string[]
315 + *
316 + * @phpstan-param array<non-falsy-string, non-falsy-string> $metas
317 + */
318 + public function add_post_meta_encodings( $metas ) {
319 + return $this->add_metas_encodings( (array) $metas, 'custom-fields/custom-field' );
320 + }
321 +
322 + /**
323 + * Specifies the encoding for term metas.
324 + *
325 + * @since 3.6
326 + *
327 + * @param string[] $metas An array containing meta names as array keys, and their encoding as array values.
328 + * @return string[]
329 + *
330 + * @phpstan-param array<non-falsy-string, non-falsy-string> $metas
331 + */
332 + public function add_term_meta_encodings( $metas ) {
333 + return $this->add_metas_encodings( (array) $metas, 'custom-term-fields/custom-term-field' );
334 + }
335 +
336 + /**
173 337 * Language and translation management for custom post types.
174 338 *
175 339 * @since 1.0
176 340 *
@@ -180,19 +344,24 @@
180 344 */
181 345 public function translate_types( $types, $hide ) {
182 346 foreach ( $this->xmls as $xml ) {
183 347 $pts = $xml->xpath( 'custom-types/custom-type' );
184 - if ( is_array( $pts ) ) {
185 - foreach ( $pts as $pt ) {
186 - $attributes = $pt->attributes();
187 - if ( 1 == $attributes['translate'] && ! $hide ) {
188 - $types[ (string) $pt ] = (string) $pt;
189 - } else {
190 - unset( $types[ (string) $pt ] ); // The theme/plugin author decided what to do with the post type so don't allow the user to change this
191 - }
348 +
349 + if ( ! is_array( $pts ) ) {
350 + continue;
351 + }
352 +
353 + foreach ( $pts as $pt ) {
354 + $translate = $this->get_field_attribute( $pt, 'translate' );
355 +
356 + if ( '1' === $translate && ! $hide ) {
357 + $types[ (string) $pt ] = (string) $pt;
358 + } else {
359 + unset( $types[ (string) $pt ] ); // The theme/plugin author decided what to do with the post type so don't allow the user to change this
192 360 }
193 361 }
194 362 }
363 +
195 364 return $types;
196 365 }
197 366
198 367 /**
@@ -206,30 +375,179 @@
206 375 */
207 376 public function translate_taxonomies( $taxonomies, $hide ) {
208 377 foreach ( $this->xmls as $xml ) {
209 378 $taxos = $xml->xpath( 'taxonomies/taxonomy' );
210 - if ( is_array( $taxos ) ) {
211 - foreach ( $taxos as $tax ) {
212 - $attributes = $tax->attributes();
213 - if ( 1 == $attributes['translate'] && ! $hide ) {
214 - $taxonomies[ (string) $tax ] = (string) $tax;
215 - } else {
216 - unset( $taxonomies[ (string) $tax ] ); // the theme/plugin author decided what to do with the taxonomy so don't allow the user to change this
379 +
380 + if ( ! is_array( $taxos ) ) {
381 + continue;
382 + }
383 +
384 + foreach ( $taxos as $tax ) {
385 + $translate = $this->get_field_attribute( $tax, 'translate' );
386 +
387 + if ( '1' === $translate && ! $hide ) {
388 + $taxonomies[ (string) $tax ] = (string) $tax;
389 + } else {
390 + unset( $taxonomies[ (string) $tax ] ); // the theme/plugin author decided what to do with the taxonomy so don't allow the user to change this
391 + }
392 + }
393 + }
394 +
395 + return $taxonomies;
396 + }
397 +
398 + /**
399 + * Translation management for strings in blocks content.
400 + *
401 + * @since 3.3
402 + *
403 + * @param string[][] $parsing_rules Rules as Xpath expressions to evaluate in the blocks content.
404 + * @return string[][] Rules completed with ones from wpml-config file.
405 + *
406 + * @phpstan-param array<string,array<string>> $parsing_rules
407 + * @phpstan-return array<string,array<string>>
408 + */
409 + public function translate_blocks( $parsing_rules ) {
410 + return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'xpath' ) );
411 + }
412 +
413 + /**
414 + * Translation management for blocks attributes.
415 + *
416 + * @since 3.3
417 + * @since 3.6 Format changed from `array<string>` to `array<non-empty-string, array|true>`.
418 + *
419 + * @param array $parsing_rules Rules for blocks attributes to translate.
420 + * @return array Rules completed with ones from wpml-config file.
421 + *
422 + * @phpstan-param array<non-empty-string, array|true> $parsing_rules
423 + * @phpstan-return array<non-empty-string, array|true>
424 + */
425 + public function translate_blocks_attributes( $parsing_rules ) {
426 + return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'key' ) );
427 + }
428 +
429 + /**
430 + * Returns rules to extract translatable strings from blocks.
431 + *
432 + * @since 3.3
433 + *
434 + * @param string $rule_tag Tag name to extract.
435 + * @return string[][] The rules.
436 + *
437 + * @phpstan-param 'xpath'|'key' $rule_tag
438 + * @phpstan-return (
439 + * $rule_tag is 'xpath' ? array<non-empty-string, list<non-empty-string>> : array<non-empty-string, array<array|true>>
440 + * )
441 + */
442 + protected function get_blocks_parsing_rules( $rule_tag ) {
443 +
444 + if ( null === $this->parsing_rules ) {
445 + $this->parsing_rules = $this->extract_blocks_parsing_rules();
446 + }
447 +
448 + return $this->parsing_rules[ $rule_tag ] ?? array();
449 + }
450 +
451 + /**
452 + * Extract all rules from WPML config file to translate strings for blocks.
453 + *
454 + * @since 3.3
455 + *
456 + * @return string[][][] Rules completed with ones from wpml-config file.
457 + *
458 + * @phpstan-return array{
459 + * xpath?: array<non-empty-string, list<non-empty-string>>,
460 + * key?: array<non-empty-string, array<array|true>>
461 + * }
462 + */
463 + protected function extract_blocks_parsing_rules() {
464 + $parsing_rules = array();
465 +
466 + foreach ( $this->xmls as $xml ) {
467 + $blocks = $xml->xpath( 'gutenberg-blocks/gutenberg-block' );
468 +
469 + if ( ! is_array( $blocks ) ) {
470 + continue;
471 + }
472 +
473 + foreach ( $blocks as $block ) {
474 + $translate = $this->get_field_attribute( $block, 'translate' );
475 +
476 + if ( '1' !== $translate ) {
477 + continue;
478 + }
479 +
480 + $block_name = $this->get_field_attribute( $block, 'type' );
481 +
482 + if ( '' === $block_name ) {
483 + continue;
484 + }
485 +
486 + foreach ( $block->children() as $child ) {
487 + $rule = '';
488 + $child_tag = $child->getName();
489 +
490 + switch ( $child_tag ) {
491 + case 'xpath':
492 + $rule = trim( (string) $child );
493 +
494 + if ( '' !== $rule ) {
495 + $parsing_rules['xpath'][ $block_name ][] = $rule;
496 + }
497 + break;
498 +
499 + case 'key':
500 + $rule = $this->get_field_attributes( $child );
501 +
502 + if ( empty( $rule ) ) {
503 + break;
504 + }
505 +
506 + if ( isset( $parsing_rules['key'][ $block_name ] ) ) {
507 + $parsing_rules['key'][ $block_name ] = $this->array_merge_recursive( $parsing_rules['key'][ $block_name ], $rule );
508 + } else {
509 + $parsing_rules['key'][ $block_name ] = $rule;
510 + }
511 + break;
217 512 }
218 513 }
219 514 }
220 515 }
221 - return $taxonomies;
516 +
517 + return $parsing_rules;
222 518 }
223 519
224 520 /**
521 + * Merges two arrays recursively.
522 + * Unlike `array_merge_recursive()`, this method doesn't change the type of the values.
523 + *
524 + * @since 3.6
525 + *
526 + * @param array $array1 Array to merge into.
527 + * @param array $array2 Array to merge.
528 + * @return array
529 + */
530 + protected function array_merge_recursive( array $array1, array $array2 ): array {
531 + foreach ( $array2 as $key => $value ) {
532 + if ( is_array( $value ) && isset( $array1[ $key ] ) && is_array( $array1[ $key ] ) ) {
533 + $array1[ $key ] = $this->array_merge_recursive( $array1[ $key ], $value );
534 + } else {
535 + $array1[ $key ] = $value;
536 + }
537 + }
538 +
539 + return $array1;
540 + }
541 +
542 + /**
225 543 * Registers or translates the strings for an option
226 544 *
227 545 * @since 2.8
228 546 *
229 - * @param string $context The group in which the strings will be registered.
230 - * @param string $name Option name.
231 - * @param object $key XML node.
547 + * @param string $context The group in which the strings will be registered.
548 + * @param string $name Option name.
549 + * @param SimpleXMLElement $key XML node.
232 550 * @return void
233 551 */
234 552 protected function register_or_translate_option( $context, $name, $key ) {
235 553 $option_keys = $this->xml_to_array( $key );
@@ -239,24 +557,489 @@
239 557 /**
240 558 * Recursively transforms xml nodes to an array, ready for PLL_Translate_Option.
241 559 *
242 560 * @since 2.9
561 + * @since 3.3 Type-hinted the parameters `$key` and `$arr`.
562 + * @since 3.3 `$arr` is not passed by reference anymore.
563 + * @since 3.3 Added the parameter `$fill_value`.
243 564 *
244 - * @param object $key XML node.
245 - * @param array $arr Array of option keys to translate.
565 + * @param SimpleXMLElement $key XML node.
566 + * @param array $arr Array of option keys to translate.
567 + * @param mixed $fill_value Value to use when filling entries. Default is true.
246 568 * @return array
247 569 */
248 - protected function xml_to_array( $key, &$arr = array() ) {
249 - $attributes = $key->attributes();
250 - $name = (string) $attributes['name'];
570 + protected function xml_to_array( SimpleXMLElement $key, array $arr = array(), $fill_value = true ) {
571 + $name = $this->get_field_attribute( $key, 'name' );
572 +
573 + if ( '' === $name ) {
574 + return $arr;
575 + }
576 +
251 577 $children = $key->children();
252 578
253 579 if ( count( $children ) ) {
254 580 foreach ( $children as $child ) {
255 - $arr[ $name ] = $this->xml_to_array( $child, $arr[ $name ] );
581 + if ( ! isset( $arr[ $name ] ) || ! is_array( $arr[ $name ] ) ) {
582 + $arr[ $name ] = array();
583 + }
584 +
585 + $arr[ $name ] = $this->xml_to_array( $child, $arr[ $name ], $fill_value );
256 586 }
257 587 } else {
258 - $arr[ $name ] = true; // Multiline as in WPML.
588 + $arr[ $name ] = $fill_value; // Multiline as in WPML.
259 589 }
590 +
260 591 return $arr;
592 + }
593 +
594 + /**
595 + * Get the value of an attribute.
596 + *
597 + * @since 3.3
598 + *
599 + * @param SimpleXMLElement $field A XML node.
600 + * @param string $attribute_name Node of the attribute.
601 + * @return string
602 + */
603 + private function get_field_attribute( SimpleXMLElement $field, $attribute_name ) {
604 + $attributes = $field->attributes();
605 +
606 + if ( empty( $attributes ) || ! isset( $attributes[ $attribute_name ] ) ) {
607 + return '';
608 + }
609 +
610 + return trim( (string) $attributes[ $attribute_name ] );
611 + }
612 +
613 + /**
614 + * Gets attributes values recursively.
615 + *
616 + * @since 3.6
617 + *
618 + * @param SimpleXMLElement $field A XML node.
619 + * @return array An array of attributes.
620 + *
621 + * @phpstan-return array<non-empty-string, array|true>
622 + */
623 + private function get_field_attributes( SimpleXMLElement $field ): array {
624 + $name = $this->get_field_attribute( $field, 'name' );
625 +
626 + if ( '' === $name ) {
627 + return array();
628 + }
629 +
630 + $children = $field->children();
631 +
632 + if ( 0 === $children->count() ) {
633 + return array( $name => true );
634 + }
635 +
636 + $sub_attributes = array();
637 +
638 + foreach ( $children as $child ) {
639 + $sub = $this->get_field_attributes( $child );
640 +
641 + if ( empty( $sub ) ) {
642 + continue;
643 + }
644 +
645 + $sub_attributes[ $name ] = array_merge( $sub_attributes[ $name ] ?? array(), $sub );
646 + }
647 +
648 + return $sub_attributes;
649 + }
650 +
651 + /**
652 + * Returns all wpml-config.xml files in MU plugins.
653 + *
654 + * @since 3.3
655 + *
656 + * @return string[] A context identifier as array key, a file path as array value.
657 + *
658 + * @phpstan-return array<string, string>
659 + */
660 + private function get_mu_plugin_files() {
661 + if ( ! is_readable( WPMU_PLUGIN_DIR ) || ! is_dir( WPMU_PLUGIN_DIR ) ) {
662 + return array();
663 + }
664 +
665 + $files = array();
666 +
667 + // Search for top level wpml-config.xml file.
668 + $file_path = WPMU_PLUGIN_DIR . '/wpml-config.xml';
669 +
670 + if ( is_readable( $file_path ) ) {
671 + $files['mu-plugins'] = $file_path;
672 + }
673 +
674 + // Search in proxy loaded MU plugins.
675 + foreach ( new DirectoryIterator( WPMU_PLUGIN_DIR ) as $file_info ) {
676 + if ( ! $this->is_dir( $file_info ) ) {
677 + continue;
678 + }
679 +
680 + $file_path = $file_info->getPathname() . '/wpml-config.xml';
681 +
682 + if ( is_readable( $file_path ) ) {
683 + $files[ 'mu-plugins/' . $file_info->getFilename() ] = $file_path;
684 + }
685 + }
686 +
687 + return $files;
688 + }
689 +
690 + /**
691 + * Returns all wpml-config.xml files in plugins.
692 + *
693 + * @since 3.3
694 + *
695 + * @return string[] A context identifier as array key, a file path as array value.
696 + *
697 + * @phpstan-return array<string, string>
698 + */
699 + private function get_plugin_files() {
700 + $files = array();
701 + $plugins = array();
702 +
703 + if ( is_multisite() ) {
704 + // Don't forget sitewide active plugins thanks to Reactorshop http://wordpress.org/support/topic/polylang-and-yoast-seo-plugin/page/2?replies=38#post-4801829.
705 + $sitewide_plugins = get_site_option( 'active_sitewide_plugins', array() );
706 +
707 + if ( ! empty( $sitewide_plugins ) && is_array( $sitewide_plugins ) ) {
708 + $plugins = array_keys( $sitewide_plugins );
709 + }
710 + }
711 +
712 + // By-site plugins.
713 + $active_plugins = get_option( 'active_plugins', array() );
714 +
715 + if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
716 + $plugins = array_merge( $plugins, $active_plugins );
717 + }
718 +
719 + $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . '%s/wpml-config.xml';
720 +
721 + foreach ( $plugins as $plugin ) {
722 + if ( ! is_string( $plugin ) || '' === $plugin ) {
723 + continue;
724 + }
725 +
726 + $file_dir = dirname( $plugin );
727 + $file_path = sprintf( $plugin_path, $file_dir );
728 +
729 + if ( is_readable( $file_path ) ) {
730 + $files[ "plugins/{$file_dir}" ] = $file_path;
731 + }
732 + }
733 +
734 + return $files;
735 + }
736 +
737 + /**
738 + * Returns all wpml-config.xml files in theme and child theme.
739 + *
740 + * @since 3.3
741 + *
742 + * @return string[] A context identifier as array key, a file path as array value.
743 + *
744 + * @phpstan-return array<string, string>
745 + */
746 + private function get_theme_files() {
747 + $files = array();
748 +
749 + // Theme.
750 + $template_path = get_template_directory();
751 + $file_path = "{$template_path}/wpml-config.xml";
752 +
753 + if ( is_readable( $file_path ) ) {
754 + $files[ 'themes/' . get_template() ] = $file_path;
755 + }
756 +
757 + // Child theme.
758 + $stylesheet_path = get_stylesheet_directory();
759 + $file_path = "{$stylesheet_path}/wpml-config.xml";
760 +
761 + if ( $stylesheet_path !== $template_path && is_readable( $file_path ) ) {
762 + $files[ 'themes/' . get_stylesheet() ] = $file_path;
763 + }
764 +
765 + return $files;
766 + }
767 +
768 + /**
769 + * Returns the wpml-config.xml file in Polylang custom directory.
770 + *
771 + * @since 3.3
772 + *
773 + * @return string[] A context identifier as array key, a file path as array value.
774 + *
775 + * @phpstan-return array<string, string>
776 + */
777 + private function get_custom_files() {
778 + $file_path = PLL_LOCAL_DIR . '/wpml-config.xml';
779 +
780 + if ( ! is_readable( $file_path ) ) {
781 + return array();
782 + }
783 +
784 + return array(
785 + 'Polylang' => $file_path,
786 + );
787 + }
788 +
789 + /**
790 + * Tells if the given "file info" object represents a directory.
791 + * This takes care of not triggering a `open_basedir` restriction error when the file symlinks a file that is not in
792 + * `open_basedir`.
793 + *
794 + * @see https://wordpress.org/support/topic/fatal-error-open_basedir-restricton/
795 + *
796 + * @since 3.5.1
797 + *
798 + * @param DirectoryIterator $file_info A "file info" object that we know its path (but maybe not its real path) is
799 + * in `open_basedir`.
800 + * @return bool
801 + */
802 + private function is_dir( DirectoryIterator $file_info ): bool {
803 + if ( $file_info->isDot() ) {
804 + return false;
805 + }
806 +
807 + if ( $file_info->getPathname() === $file_info->getRealPath() ) {
808 + // Not a symlink: not going to trigger a `open_basedir` restriction error.
809 + return $file_info->isDir();
810 + }
811 +
812 + /*
813 + * Symlink: make sure the file's real path is in `open_basedir` before checking it is a dir.
814 + * Which means that the `open_basedir` check is done only for symlinked files.
815 + */
816 + return $this->is_allowed_dir( $file_info->getRealPath() ) && $file_info->isDir();
817 + }
818 +
819 + /**
820 + * Checks whether access to a given directory is allowed.
821 + * This takes into account the PHP `open_basedir` restrictions, so that Polylang does not try to access directories
822 + * it is not allowed to.
823 + *
824 + * Inspired by `WP_Automatic_Updater::is_allowed_dir()` and `wp-includes/ID3/getid3.php`.
825 + *
826 + * @since 3.5.1
827 + *
828 + * @param string $dir The directory to check.
829 + * @return bool True if access to the directory is allowed, false otherwise.
830 + */
831 + private function is_allowed_dir( string $dir ): bool {
832 + $dir = trim( $dir );
833 +
834 + if ( '' === $dir ) {
835 + return false;
836 + }
837 +
838 + $open_basedir_paths = $this->get_open_basedir_paths();
839 +
840 + if ( empty( $open_basedir_paths ) ) {
841 + return true;
842 + }
843 +
844 + $dir = $this->normalize_path( $dir );
845 +
846 + foreach ( $open_basedir_paths as $path ) {
847 + if ( str_starts_with( $dir, $path ) ) {
848 + return true;
849 + }
850 + }
851 +
852 + return false;
853 + }
854 +
855 + /**
856 + * Returns the list of paths in `open_basedir`. The purpose is to compare a formatted path to this list.
857 + * Note: all paths are suffixed by `DIRECTORY_SEPARATOR`, even paths to files.
858 + *
859 + * @since 3.5.1
860 + *
861 + * @return string[] An array of formatted paths.
862 + */
863 + private function get_open_basedir_paths(): array {
864 + if ( is_array( $this->open_basedir_paths ) ) {
865 + return $this->open_basedir_paths;
866 + }
867 +
868 + $this->open_basedir_paths = array();
869 + $open_basedir = ini_get( 'open_basedir' ); // Can be `false` or an empty string.
870 +
871 + if ( empty( $open_basedir ) ) {
872 + return $this->open_basedir_paths;
873 + }
874 +
875 + $open_basedir_list = explode( PATH_SEPARATOR, $open_basedir );
876 +
877 + foreach ( $open_basedir_list as $basedir ) {
878 + $basedir = trim( $basedir );
879 +
880 + if ( '' === $basedir ) {
881 + continue;
882 + }
883 +
884 + $this->open_basedir_paths[] = $this->normalize_path( $basedir );
885 + }
886 +
887 + $this->open_basedir_paths = array_unique( $this->open_basedir_paths );
888 +
889 + return $this->open_basedir_paths;
890 + }
891 +
892 + /**
893 + * Formats a path for string comparison.
894 + * 1. Slashes and back-slashes are replaced by `DIRECTORY_SEPARATOR`.
895 + * 2. The path is suffixed by `DIRECTORY_SEPARATOR` (even non-directory elements).
896 + *
897 + * @since 3.5.1
898 + *
899 + * @param string $path A file path.
900 + * @return string
901 + *
902 + * @phpstan-param non-empty-string $path
903 + * @phpstan-return non-empty-string
904 + */
905 + private function normalize_path( string $path ): string {
906 + $path = str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $path );
907 +
908 + if ( substr( $path, -1, 1 ) !== DIRECTORY_SEPARATOR ) {
909 + $path .= DIRECTORY_SEPARATOR;
910 + }
911 +
912 + return $path;
913 + }
914 +
915 + /**
916 + * Adds (or removes) meta names to the list of metas to copy or synchronize.
917 + *
918 + * @since 3.6
919 + *
920 + * @param string[] $metas The list of meta names to copy or synchronize.
921 + * @param string $xpath Xpath to the meta fields in the xml files.
922 + * @param bool $sync Either sync is enabled or not.
923 + * @return string[]
924 + *
925 + * @phpstan-param array<non-falsy-string> $metas
926 + * @phpstan-param non-falsy-string $xpath
927 + */
928 + private function filter_metas_to_copy( array $metas, string $xpath, bool $sync ): array {
929 + $parsed_metas = $this->parse_xml_metas( $xpath );
930 + $metas_to_remove = array();
931 +
932 + foreach ( $parsed_metas as $name => $parsed_meta ) {
933 + if ( 'copy' === $parsed_meta['action'] || ( ! $sync && in_array( $parsed_meta['action'], array( 'translate', 'copy-once' ), true ) ) ) {
934 + $metas[] = $name;
935 + } else {
936 + $metas_to_remove[] = $name;
937 + }
938 + }
939 +
940 + return array_diff( $metas, $metas_to_remove );
941 + }
942 +
943 + /**
944 + * Adds meta keys to export.
945 + *
946 + * @since 3.6
947 + *
948 + * @param array $metas {
949 + * An array containing meta keys to translate.
950 + * Ex: array(
951 + * 'meta_to_translate_1' => 1,
952 + * 'meta_to_translate_2' => 1,
953 + * 'meta_to_translate_3' => array( ... ),
954 + * )
955 + * }
956 + * @param string $xpath Xpath to the meta fields in the xml files.
957 + * @return array
958 + *
959 + * @phpstan-param array<non-falsy-string, mixed> $metas
960 + * @phpstan-param non-falsy-string $xpath
961 + * @phpstan-return array<non-falsy-string, mixed>
962 + */
963 + private function add_metas_to_export( array $metas, string $xpath ) {
964 + $fields = $this->parse_xml_metas( $xpath );
965 +
966 + foreach ( $fields as $name => $field ) {
967 + if ( 'translate' === $field['action'] ) {
968 + $metas[ $name ] = 1;
969 + }
970 + }
971 +
972 + return $metas;
973 + }
974 +
975 + /**
976 + * Adds encoding of metas.
977 + *
978 + * @since 3.6
979 + *
980 + * @param string[] $metas The list of encodings for each metas. Meta names are array keys, encodings are array values.
981 + * @param string $xpath Xpath to the meta fields in the xml files.
982 + * @return string[]
983 + *
984 + * @phpstan-param array<non-falsy-string, non-falsy-string> $metas
985 + * @phpstan-param non-falsy-string $xpath
986 + */
987 + private function add_metas_encodings( array $metas, string $xpath ): array {
988 + $parsed_metas = $this->parse_xml_metas( $xpath );
989 +
990 + foreach ( $parsed_metas as $name => $parsed_meta ) {
991 + if ( ! empty( $parsed_meta['encoding'] ) ) {
992 + $metas[ $name ] = $parsed_meta['encoding'];
993 + }
994 + }
995 +
996 + return $metas;
997 + }
998 +
999 + /**
1000 + * Parses all xml files for metas.
1001 + * Results are cached for each `$xpath`.
1002 + *
1003 + * @since 3.6
1004 + *
1005 + * @param string $xpath Xpath to the meta fields in the xml files.
1006 + * @return array
1007 + *
1008 + * @phpstan-param non-falsy-string $xpath
1009 + * @phpstan-return ParsedMetas
1010 + */
1011 + private function parse_xml_metas( string $xpath ): array {
1012 + if ( isset( $this->parsed_metas[ $xpath ] ) ) {
1013 + return $this->parsed_metas[ $xpath ];
1014 + }
1015 +
1016 + $this->parsed_metas[ $xpath ] = array();
1017 +
1018 + foreach ( $this->xmls as $xml ) {
1019 + $custom_fields = $xml->xpath( $xpath );
1020 +
1021 + if ( ! is_array( $custom_fields ) ) {
1022 + continue;
1023 + }
1024 +
1025 + foreach ( $custom_fields as $custom_field ) {
1026 + $name = (string) $custom_field;
1027 +
1028 + if ( empty( $name ) ) {
1029 + continue;
1030 + }
1031 +
1032 + $data = array(
1033 + 'action' => $this->get_field_attribute( $custom_field, 'action' ),
1034 + 'encoding' => $this->get_field_attribute( $custom_field, 'encoding' ),
1035 + );
1036 +
1037 + $data['encoding'] = 'json' === $data['encoding'] ? 'json' : ''; // Only JSON is supported for now.
1038 +
1039 + $this->parsed_metas[ $xpath ][ $name ] = $data;
1040 + }
1041 + }
1042 +
1043 + return $this->parsed_metas[ $xpath ];
261 1044 }
262 1045 }