PluginProbe
Polylang / 3.7.1
Polylang v3.7.1
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.7.1, at modules/wpml/wpml-config.php

1,046 lines 27.3 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: 'json'|''
18 * }
19 * >
20 */
21 class PLL_WPML_Config {
22 /**
23 * Singleton instance
24 *
25 * @var PLL_WPML_Config|null
26 */
27 protected static $instance;
28
29 /**
30 * The content of all read xml files.
31 *
32 * @var SimpleXMLElement[]
33 */
34 protected $xmls = array();
35
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 /**
74 * Constructor
75 *
76 * @since 1.0
77 */
78 public function __construct() {
79 if ( extension_loaded( 'simplexml' ) ) {
80 $this->init();
81 }
82 }
83
84 /**
85 * Access to the single instance of the class
86 *
87 * @since 1.7
88 *
89 * @return PLL_WPML_Config
90 */
91 public static function instance() {
92 if ( empty( self::$instance ) ) {
93 self::$instance = new self();
94 }
95 return self::$instance;
96 }
97
98 /**
99 * Finds the wpml-config.xml files to parse and setup filters
100 *
101 * @since 1.0
102 *
103 * @return void
104 */
105 public function init() {
106 $this->xmls = array();
107 $files = $this->get_files();
108
109 if ( empty( $files ) ) {
110 return;
111 }
112
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;
122 }
123 }
124
125 if ( empty( $this->xmls ) ) {
126 return;
127 }
128
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 );
133
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' ) );
141
142 $matcher = new PLL_Format_Util();
143
144 foreach ( $this->xmls as $context => $xml ) {
145 $keys = $xml->xpath( 'admin-texts/key' );
146
147 if ( ! is_array( $keys ) ) {
148 continue;
149 }
150
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;
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 }
164 }
165 }
166 }
167
168 /**
169 * Returns all wpml-config.xml files in MU plugins, plugins, theme, child theme, and Polylang custom directory.
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 *
199 * @since 1.0
200 *
201 * @param string[] $metas The list of post metas to copy or synchronize.
202 * @param bool $sync True for sync, false for copy.
203 * @return string[] The list of post metas to copy or synchronize.
204 *
205 * @phpstan-param array<non-falsy-string> $metas
206 */
207 public function copy_post_metas( $metas, $sync ) {
208 return $this->filter_metas_to_copy( (array) $metas, 'custom-fields/custom-field', (bool) $sync );
209 }
210
211 /**
212 * Adds term metas to the list of metas to copy when creating a new translation.
213 *
214 * @since 2.6
215 *
216 * @param string[] $metas The list of term metas to copy or synchronize.
217 * @param bool $sync True for sync, false for copy.
218 * @return string[] The list of term metas to copy or synchronize.
219 *
220 * @phpstan-param array<non-falsy-string> $metas
221 */
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.
255 foreach ( $this->xmls as $xml ) {
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;
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 );
277 }
278 }
279
280 return $keys;
281 }
282
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 /**
337 * Language and translation management for custom post types.
338 *
339 * @since 1.0
340 *
341 * @param string[] $types The list of post type names for which Polylang manages language and translations.
342 * @param bool $hide True when displaying the list in Polylang settings.
343 * @return string[] The list of post type names for which Polylang manages language and translations.
344 */
345 public function translate_types( $types, $hide ) {
346 foreach ( $this->xmls as $xml ) {
347 $pts = $xml->xpath( 'custom-types/custom-type' );
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
360 }
361 }
362 }
363
364 return $types;
365 }
366
367 /**
368 * Language and translation management for custom taxonomies.
369 *
370 * @since 1.0
371 *
372 * @param string[] $taxonomies The list of taxonomy names for which Polylang manages language and translations.
373 * @param bool $hide True when displaying the list in Polylang settings.
374 * @return string[] The list of taxonomy names for which Polylang manages language and translations.
375 */
376 public function translate_taxonomies( $taxonomies, $hide ) {
377 foreach ( $this->xmls as $xml ) {
378 $taxos = $xml->xpath( 'taxonomies/taxonomy' );
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;
512 }
513 }
514 }
515 }
516
517 return $parsing_rules;
518 }
519
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 /**
543 * Registers or translates the strings for an option
544 *
545 * @since 2.8
546 *
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.
550 * @return void
551 */
552 protected function register_or_translate_option( $context, $name, $key ) {
553 $option_keys = $this->xml_to_array( $key );
554 new PLL_Translate_Option( $name, reset( $option_keys ), array( 'context' => $context ) );
555 }
556
557 /**
558 * Recursively transforms xml nodes to an array, ready for PLL_Translate_Option.
559 *
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`.
564 *
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.
568 * @return array
569 */
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
577 $children = $key->children();
578
579 if ( count( $children ) ) {
580 foreach ( $children as $child ) {
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 );
586 }
587 } else {
588 $arr[ $name ] = $fill_value; // Multiline as in WPML.
589 }
590
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 ];
1044 }
1045 }
1046