PluginProbe
Polylang / 3.4.2
Polylang v3.4.2
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
polylang / modules / wpml / wpml-config.php

wpml-config.php in Polylang 3.4.2, at modules/wpml/wpml-config.php

720 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 /**
7 * Reads and interprets the file wpml-config.xml
8 * See http://wpml.org/documentation/support/language-configuration-files/
9 * The language switcher configuration is not interpreted
10 *
11 * @since 1.0
12 */
13 class PLL_WPML_Config {
14 /**
15 * Singleton instance
16 *
17 * @var PLL_WPML_Config|null
18 */
19 protected static $instance;
20
21 /**
22 * The content of all read xml files.
23 *
24 * @var SimpleXMLElement[]
25 */
26 protected $xmls = array();
27
28 /**
29 * The list of xml file paths.
30 *
31 * @var string[]|null
32 *
33 * @phpstan-var array<string, string>|null
34 */
35 protected $files;
36
37 /**
38 * List of rules to extract strings to translate from blocks.
39 *
40 * @var string[][][]|null
41 */
42 protected $parsing_rules = null;
43
44 /**
45 * Constructor
46 *
47 * @since 1.0
48 */
49 public function __construct() {
50 if ( extension_loaded( 'simplexml' ) ) {
51 $this->init();
52 }
53 }
54
55 /**
56 * Access to the single instance of the class
57 *
58 * @since 1.7
59 *
60 * @return PLL_WPML_Config
61 */
62 public static function instance() {
63 if ( empty( self::$instance ) ) {
64 self::$instance = new self();
65 }
66 return self::$instance;
67 }
68
69 /**
70 * Finds the wpml-config.xml files to parse and setup filters
71 *
72 * @since 1.0
73 *
74 * @return void
75 */
76 public function init() {
77 $this->xmls = array();
78 $files = $this->get_files();
79
80 if ( empty( $files ) ) {
81 return;
82 }
83
84 if ( ! extension_loaded( 'simplexml' ) ) {
85 return;
86 }
87
88 // Read all files.
89 foreach ( $files as $context => $file ) {
90 $xml = simplexml_load_file( $file );
91 if ( false !== $xml ) {
92 $this->xmls[ $context ] = $xml;
93 }
94 }
95
96 if ( empty( $this->xmls ) ) {
97 return;
98 }
99
100 add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 20, 2 );
101 add_filter( 'pll_copy_term_metas', array( $this, 'copy_term_metas' ), 20, 2 );
102 add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 );
103 add_filter( 'pll_get_taxonomies', array( $this, 'translate_taxonomies' ), 10, 2 );
104 add_filter( 'pll_blocks_xpath_rules', array( $this, 'translate_blocks' ) );
105 add_filter( 'pll_blocks_rules_for_attributes', array( $this, 'translate_blocks_attributes' ) );
106
107 // Export.
108 add_filter( 'pll_post_metas_to_export', array( $this, 'post_metas_to_export' ) );
109 add_filter( 'pll_term_metas_to_export', array( $this, 'term_metas_to_export' ) );
110
111 foreach ( $this->xmls as $context => $xml ) {
112 $keys = $xml->xpath( 'admin-texts/key' );
113
114 if ( ! is_array( $keys ) ) {
115 continue;
116 }
117
118 foreach ( $keys as $key ) {
119 $name = $this->get_field_attribute( $key, 'name' );
120
121 if ( false === strpos( $name, '*' ) ) {
122 $this->register_or_translate_option( $context, $name, $key );
123 continue;
124 }
125
126 $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
127 $names = preg_grep( $pattern, array_keys( wp_load_alloptions() ) );
128
129 if ( ! is_array( $names ) ) {
130 continue;
131 }
132
133 foreach ( $names as $_name ) {
134 $this->register_or_translate_option( $context, $_name, $key );
135 }
136 }
137 }
138 }
139
140 /**
141 * Returns all wpml-config.xml files in MU plugins, plugins, theme, child theme, and Polylang custom directory.
142 *
143 * @since 3.1
144 *
145 * @return string[] A context identifier as array key, a file path as array value.
146 *
147 * @phpstan-return array<string, string>
148 */
149 public function get_files() {
150 if ( is_array( $this->files ) ) {
151 return $this->files;
152 }
153
154 $this->files = array_merge(
155 // Plugins.
156 $this->get_plugin_files(),
157 // Theme and child theme.
158 $this->get_theme_files(),
159 // MU Plugins.
160 $this->get_mu_plugin_files(),
161 // Custom.
162 $this->get_custom_files()
163 );
164
165 return $this->files;
166 }
167
168 /**
169 * Adds custom fields to the list of metas to copy when creating a new translation.
170 *
171 * @since 1.0
172 *
173 * @param string[] $metas The list of custom fields to copy or synchronize.
174 * @param bool $sync True for sync, false for copy.
175 * @return string[] The list of custom fields to copy or synchronize.
176 */
177 public function copy_post_metas( $metas, $sync ) {
178 foreach ( $this->xmls as $xml ) {
179 $cfs = $xml->xpath( 'custom-fields/custom-field' );
180
181 if ( ! is_array( $cfs ) ) {
182 continue;
183 }
184
185 foreach ( $cfs as $cf ) {
186 $action = $this->get_field_attribute( $cf, 'action' );
187
188 if ( 'copy' === $action || ( ! $sync && in_array( $action, array( 'translate', 'copy-once' ), true ) ) ) {
189 $metas[] = (string) $cf;
190 } else {
191 $metas = array_diff( $metas, array( (string) $cf ) );
192 }
193 }
194 }
195
196 return $metas;
197 }
198
199 /**
200 * Adds term metas to the list of metas to copy when creating a new translation.
201 *
202 * @since 2.6
203 *
204 * @param string[] $metas The list of term metas to copy or synchronize.
205 * @param bool $sync True for sync, false for copy.
206 * @return string[] The list of term metas to copy or synchronize.
207 */
208 public function copy_term_metas( $metas, $sync ) {
209 foreach ( $this->xmls as $xml ) {
210 $cfs = $xml->xpath( 'custom-term-fields/custom-term-field' );
211
212 if ( ! is_array( $cfs ) ) {
213 continue;
214 }
215
216 foreach ( $cfs as $cf ) {
217 $action = $this->get_field_attribute( $cf, 'action' );
218
219 if ( 'copy' === $action || ( ! $sync && in_array( $action, array( 'translate', 'copy-once' ), true ) ) ) {
220 $metas[] = (string) $cf;
221 } else {
222 $metas = array_diff( $metas, array( (string) $cf ) );
223 }
224 }
225 }
226
227 return $metas;
228 }
229
230 /**
231 * Adds post meta keys to export.
232 *
233 * @since 3.3
234 * @see PLL_Export_Metas
235 *
236 * @param array $keys {
237 * A recursive array containing nested meta sub-keys to translate.
238 * Ex: array(
239 * 'meta_to_translate_1' => 1,
240 * 'meta_to_translate_2' => 1,
241 * 'meta_to_translate_3' => array(
242 * 'sub_key_to_translate_1' => 1,
243 * 'sub_key_to_translate_2' => array(
244 * 'sub_sub_key_to_translate_1' => 1,
245 * ),
246 * ),
247 * )
248 * }
249 * @return array
250 *
251 * @phpstan-param array<string, mixed> $keys
252 * @phpstan-return array<string, mixed>
253 */
254 public function post_metas_to_export( $keys ) {
255 // Add keys that have the `action` attribute set to `translate`.
256 foreach ( $this->xmls as $xml ) {
257 $fields = $xml->xpath( 'custom-fields/custom-field' );
258
259 if ( ! is_array( $fields ) ) {
260 // No custom fields.
261 continue;
262 }
263
264 foreach ( $fields as $field ) {
265 $action = $this->get_field_attribute( $field, 'action' );
266
267 if ( 'translate' !== $action ) {
268 continue;
269 }
270
271 $keys[ (string) $field ] = 1;
272 }
273 }
274
275 // Deal with sub-field translations.
276 foreach ( $this->xmls as $xml ) {
277 $fields = $xml->xpath( 'custom-fields-texts/key' );
278
279 if ( ! is_array( $fields ) ) {
280 // No 'custom-fields-texts' nodes.
281 continue;
282 }
283
284 foreach ( $fields as $field ) {
285 $name = $this->get_field_attribute( $field, 'name' );
286
287 if ( '' === $name ) {
288 // Wrong configuration: empty `name` attribute (meta name).
289 continue;
290 }
291
292 if ( ! array_key_exists( $name, $keys ) ) {
293 // Wrong configuration: the field is not in `custom-fields/custom-field`.
294 continue;
295 }
296
297 $keys = $this->xml_to_array( $field, $keys, 1 );
298 }
299 }
300
301 return $keys;
302 }
303
304 /**
305 * Adds term meta keys to export.
306 * Note: sub-key translations are not currently supported by WPML.
307 *
308 * @since 3.3
309 * @see PLL_Export_Metas
310 *
311 * @param array $keys {
312 * An array containing meta keys to translate.
313 * Ex: array(
314 * 'meta_to_translate_1' => 1,
315 * 'meta_to_translate_2' => 1,
316 * 'meta_to_translate_3' => 1,
317 * )
318 * }
319 * @return array
320 *
321 * @phpstan-param array<string, mixed> $keys
322 * @phpstan-return array<string, mixed>
323 */
324 public function term_metas_to_export( $keys ) {
325 // Add keys that have the `action` attribute set to `translate`.
326 foreach ( $this->xmls as $xml ) {
327 $fields = $xml->xpath( 'custom-term-fields/custom-term-field' );
328
329 if ( ! is_array( $fields ) ) {
330 // No custom fields.
331 continue;
332 }
333
334 foreach ( $fields as $field ) {
335 $action = $this->get_field_attribute( $field, 'action' );
336
337 if ( 'translate' !== $action ) {
338 continue;
339 }
340
341 $keys[ (string) $field ] = 1;
342 }
343 }
344
345 return $keys;
346 }
347
348 /**
349 * Language and translation management for custom post types.
350 *
351 * @since 1.0
352 *
353 * @param string[] $types The list of post type names for which Polylang manages language and translations.
354 * @param bool $hide True when displaying the list in Polylang settings.
355 * @return string[] The list of post type names for which Polylang manages language and translations.
356 */
357 public function translate_types( $types, $hide ) {
358 foreach ( $this->xmls as $xml ) {
359 $pts = $xml->xpath( 'custom-types/custom-type' );
360
361 if ( ! is_array( $pts ) ) {
362 continue;
363 }
364
365 foreach ( $pts as $pt ) {
366 $translate = $this->get_field_attribute( $pt, 'translate' );
367
368 if ( '1' === $translate && ! $hide ) {
369 $types[ (string) $pt ] = (string) $pt;
370 } else {
371 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
372 }
373 }
374 }
375
376 return $types;
377 }
378
379 /**
380 * Language and translation management for custom taxonomies.
381 *
382 * @since 1.0
383 *
384 * @param string[] $taxonomies The list of taxonomy names for which Polylang manages language and translations.
385 * @param bool $hide True when displaying the list in Polylang settings.
386 * @return string[] The list of taxonomy names for which Polylang manages language and translations.
387 */
388 public function translate_taxonomies( $taxonomies, $hide ) {
389 foreach ( $this->xmls as $xml ) {
390 $taxos = $xml->xpath( 'taxonomies/taxonomy' );
391
392 if ( ! is_array( $taxos ) ) {
393 continue;
394 }
395
396 foreach ( $taxos as $tax ) {
397 $translate = $this->get_field_attribute( $tax, 'translate' );
398
399 if ( '1' === $translate && ! $hide ) {
400 $taxonomies[ (string) $tax ] = (string) $tax;
401 } else {
402 unset( $taxonomies[ (string) $tax ] ); // the theme/plugin author decided what to do with the taxonomy so don't allow the user to change this
403 }
404 }
405 }
406
407 return $taxonomies;
408 }
409
410 /**
411 * Translation management for strings in blocks content.
412 *
413 * @since 3.3
414 *
415 * @param string[][] $parsing_rules Rules as Xpath expressions to evaluate in the blocks content.
416 * @return string[][] Rules completed with ones from wpml-config file.
417 *
418 * @phpstan-param array<string,array<string>> $parsing_rules
419 * @phpstan-return array<string,array<string>>
420 */
421 public function translate_blocks( $parsing_rules ) {
422 return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'xpath' ) );
423 }
424
425 /**
426 * Translation management for blocks attributes.
427 *
428 * @since 3.3
429 *
430 * @param string[][] $parsing_rules Rules for blocks attributes to translate.
431 * @return string[][] Rules completed with ones from wpml-config file.
432 *
433 * @phpstan-param array<string,array<string>> $parsing_rules
434 * @phpstan-return array<string,array<string>>
435 */
436 public function translate_blocks_attributes( $parsing_rules ) {
437 return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'key' ) );
438 }
439
440 /**
441 * Returns rules to extract translatable strings from blocks.
442 *
443 * @since 3.3
444 *
445 * @param string $rule_tag Tag name to extract.
446 * @return string[][] The rules.
447 */
448 protected function get_blocks_parsing_rules( $rule_tag ) {
449
450 if ( null === $this->parsing_rules ) {
451 $this->parsing_rules = $this->extract_blocks_parsing_rules();
452 }
453
454 return isset( $this->parsing_rules[ $rule_tag ] ) ? $this->parsing_rules[ $rule_tag ] : array();
455 }
456
457 /**
458 * Extract all rules from WPML config file to translate strings for blocks.
459 *
460 * @since 3.3
461 *
462 * @return string[][][] Rules completed with ones from wpml-config file.
463 */
464 protected function extract_blocks_parsing_rules() {
465 $parsing_rules = array();
466
467 foreach ( $this->xmls as $xml ) {
468 $blocks = $xml->xpath( 'gutenberg-blocks/gutenberg-block' );
469
470 if ( ! is_array( $blocks ) ) {
471 continue;
472 }
473
474 foreach ( $blocks as $block ) {
475 $translate = $this->get_field_attribute( $block, 'translate' );
476
477 if ( '1' !== $translate ) {
478 continue;
479 }
480
481 $block_name = $this->get_field_attribute( $block, 'type' );
482
483 if ( '' === $block_name ) {
484 continue;
485 }
486
487 foreach ( $block->children() as $child ) {
488 $rule = '';
489 $child_tag = $child->getName();
490
491 switch ( $child_tag ) {
492 case 'xpath':
493 $rule = trim( (string) $child );
494 break;
495
496 case 'key':
497 $rule = $this->get_field_attribute( $child, 'name' );
498 break;
499 }
500
501 if ( '' !== $rule ) {
502 $parsing_rules[ $child_tag ][ $block_name ][] = $rule;
503 }
504 }
505 }
506 }
507
508 return $parsing_rules;
509 }
510
511 /**
512 * Registers or translates the strings for an option
513 *
514 * @since 2.8
515 *
516 * @param string $context The group in which the strings will be registered.
517 * @param string $name Option name.
518 * @param SimpleXMLElement $key XML node.
519 * @return void
520 */
521 protected function register_or_translate_option( $context, $name, $key ) {
522 $option_keys = $this->xml_to_array( $key );
523 new PLL_Translate_Option( $name, reset( $option_keys ), array( 'context' => $context ) );
524 }
525
526 /**
527 * Recursively transforms xml nodes to an array, ready for PLL_Translate_Option.
528 *
529 * @since 2.9
530 * @since 3.3 Type-hinted the parameters `$key` and `$arr`.
531 * @since 3.3 `$arr` is not passed by reference anymore.
532 * @since 3.3 Added the parameter `$fill_value`.
533 *
534 * @param SimpleXMLElement $key XML node.
535 * @param array $arr Array of option keys to translate.
536 * @param mixed $fill_value Value to use when filling entries. Default is true.
537 * @return array
538 */
539 protected function xml_to_array( SimpleXMLElement $key, array $arr = array(), $fill_value = true ) {
540 $name = $this->get_field_attribute( $key, 'name' );
541
542 if ( '' === $name ) {
543 return $arr;
544 }
545
546 $children = $key->children();
547
548 if ( count( $children ) ) {
549 foreach ( $children as $child ) {
550 if ( ! isset( $arr[ $name ] ) || ! is_array( $arr[ $name ] ) ) {
551 $arr[ $name ] = array();
552 }
553
554 $arr[ $name ] = $this->xml_to_array( $child, $arr[ $name ], $fill_value );
555 }
556 } else {
557 $arr[ $name ] = $fill_value; // Multiline as in WPML.
558 }
559
560 return $arr;
561 }
562
563 /**
564 * Get the value of an attribute.
565 *
566 * @since 3.3
567 *
568 * @param SimpleXMLElement $field A XML node.
569 * @param string $attribute_name Node of the attribute.
570 * @return string
571 */
572 private function get_field_attribute( SimpleXMLElement $field, $attribute_name ) {
573 $attributes = $field->attributes();
574
575 if ( empty( $attributes ) || ! isset( $attributes[ $attribute_name ] ) ) {
576 return '';
577 }
578
579 return trim( (string) $attributes[ $attribute_name ] );
580 }
581
582 /**
583 * Returns all wpml-config.xml files in MU plugins.
584 *
585 * @since 3.3
586 *
587 * @return string[] A context identifier as array key, a file path as array value.
588 *
589 * @phpstan-return array<string, string>
590 */
591 private function get_mu_plugin_files() {
592 if ( ! is_dir( WPMU_PLUGIN_DIR ) || ! is_readable( WPMU_PLUGIN_DIR ) ) {
593 return array();
594 }
595
596 $files = array();
597
598 // Search for top level wpml-config.xml file.
599 $file_path = WPMU_PLUGIN_DIR . '/wpml-config.xml';
600
601 if ( is_readable( $file_path ) ) {
602 $files['mu-plugins'] = $file_path;
603 }
604
605 // Search in proxy loaded MU plugins.
606 foreach ( new DirectoryIterator( WPMU_PLUGIN_DIR ) as $file_info ) {
607 if ( $file_info->isDot() || ! $file_info->isDir() ) {
608 continue;
609 }
610
611 $file_path = $file_info->getPathname() . '/wpml-config.xml';
612
613 if ( is_readable( $file_path ) ) {
614 $files[ 'mu-plugins/' . $file_info->getFilename() ] = $file_path;
615 }
616 }
617
618 return $files;
619 }
620
621 /**
622 * Returns all wpml-config.xml files in plugins.
623 *
624 * @since 3.3
625 *
626 * @return string[] A context identifier as array key, a file path as array value.
627 *
628 * @phpstan-return array<string, string>
629 */
630 private function get_plugin_files() {
631 $files = array();
632 $plugins = array();
633
634 if ( is_multisite() ) {
635 // 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.
636 $sitewide_plugins = get_site_option( 'active_sitewide_plugins', array() );
637
638 if ( ! empty( $sitewide_plugins ) && is_array( $sitewide_plugins ) ) {
639 $plugins = array_keys( $sitewide_plugins );
640 }
641 }
642
643 // By-site plugins.
644 $active_plugins = get_option( 'active_plugins', array() );
645
646 if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
647 $plugins = array_merge( $plugins, $active_plugins );
648 }
649
650 $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . '%s/wpml-config.xml';
651
652 foreach ( $plugins as $plugin ) {
653 if ( ! is_string( $plugin ) || '' === $plugin ) {
654 continue;
655 }
656
657 $file_dir = dirname( $plugin );
658 $file_path = sprintf( $plugin_path, $file_dir );
659
660 if ( is_readable( $file_path ) ) {
661 $files[ "plugins/{$file_dir}" ] = $file_path;
662 }
663 }
664
665 return $files;
666 }
667
668 /**
669 * Returns all wpml-config.xml files in theme and child theme.
670 *
671 * @since 3.3
672 *
673 * @return string[] A context identifier as array key, a file path as array value.
674 *
675 * @phpstan-return array<string, string>
676 */
677 private function get_theme_files() {
678 $files = array();
679
680 // Theme.
681 $template_path = get_template_directory();
682 $file_path = "{$template_path}/wpml-config.xml";
683
684 if ( is_readable( $file_path ) ) {
685 $files[ 'themes/' . get_template() ] = $file_path;
686 }
687
688 // Child theme.
689 $stylesheet_path = get_stylesheet_directory();
690 $file_path = "{$stylesheet_path}/wpml-config.xml";
691
692 if ( $stylesheet_path !== $template_path && is_readable( $file_path ) ) {
693 $files[ 'themes/' . get_stylesheet() ] = $file_path;
694 }
695
696 return $files;
697 }
698
699 /**
700 * Returns the wpml-config.xml file in Polylang custom directory.
701 *
702 * @since 3.3
703 *
704 * @return string[] A context identifier as array key, a file path as array value.
705 *
706 * @phpstan-return array<string, string>
707 */
708 private function get_custom_files() {
709 $file_path = PLL_LOCAL_DIR . '/wpml-config.xml';
710
711 if ( ! is_readable( $file_path ) ) {
712 return array();
713 }
714
715 return array(
716 'Polylang' => $file_path,
717 );
718 }
719 }
720