PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
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 / src / modules / wpml / wpml-config.php

wpml-config.php in Polylang 3.8.6, at src/modules/wpml/wpml-config.php

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