OwlCyberSecurity - MANAGER
Edit File: wpml-config.php
<?php /** * @package Polylang */ /** * Reads and interprets the file wpml-config.xml * See http://wpml.org/documentation/support/language-configuration-files/ * The language switcher configuration is not interpreted * * @since 1.0 * * @phpstan-type ParsedMetas array< * non-falsy-string, * array{ * action: string, * encoding: 'json'|'' * } * > */ class PLL_WPML_Config { /** * Singleton instance * * @var PLL_WPML_Config|null */ protected static $instance; /** * The content of all read xml files. * * @var SimpleXMLElement[] */ protected $xmls = array(); /** * The list of xml file paths. * * @var string[]|null * * @phpstan-var array<string, string>|null */ protected $files; /** * List of rules to extract strings to translate from blocks. * * @var array * * @phpstan-var array{ * xpath?: array<non-empty-string, list<non-empty-string>>, * key?: array<non-empty-string, array<array|true>> * }|null */ protected $parsing_rules = null; /** * Contains the list of path in `open_basedir`. * * @var string[]|null */ private $open_basedir_paths; /** * Cache for parsed metas. * * @var array * * @phpstan-var array<non-falsy-string, ParsedMetas> */ private $parsed_metas = array(); /** * Constructor * * @since 1.0 */ public function __construct() { if ( extension_loaded( 'simplexml' ) ) { $this->init(); } } /** * Access to the single instance of the class * * @since 1.7 * * @return PLL_WPML_Config */ public static function instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Finds the wpml-config.xml files to parse and setup filters * * @since 1.0 * * @return void */ public function init() { $this->xmls = array(); $files = $this->get_files(); if ( empty( $files ) ) { return; } if ( ! extension_loaded( 'simplexml' ) ) { return; } // Read all files. foreach ( $files as $context => $file ) { $xml = simplexml_load_file( $file ); if ( false !== $xml ) { $this->xmls[ $context ] = $xml; } } if ( empty( $this->xmls ) ) { return; } add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 20, 2 ); add_filter( 'pll_copy_term_metas', array( $this, 'copy_term_metas' ), 20, 2 ); add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 ); add_filter( 'pll_get_taxonomies', array( $this, 'translate_taxonomies' ), 10, 2 ); // Export. add_filter( 'pll_post_metas_to_export', array( $this, 'post_metas_to_export' ) ); add_filter( 'pll_term_metas_to_export', array( $this, 'term_metas_to_export' ) ); add_filter( 'pll_post_meta_encodings', array( $this, 'add_post_meta_encodings' ), 20 ); add_filter( 'pll_term_meta_encodings', array( $this, 'add_term_meta_encodings' ), 20 ); add_filter( 'pll_blocks_xpath_rules', array( $this, 'translate_blocks' ) ); add_filter( 'pll_blocks_rules_for_attributes', array( $this, 'translate_blocks_attributes' ) ); foreach ( $this->xmls as $context => $xml ) { $keys = $xml->xpath( 'admin-texts/key' ); if ( ! is_array( $keys ) ) { continue; } foreach ( $keys as $key ) { $name = $this->get_field_attribute( $key, 'name' ); if ( false === strpos( $name, '*' ) ) { $this->register_or_translate_option( $context, $name, $key ); continue; } $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#'; $names = preg_grep( $pattern, array_keys( wp_load_alloptions() ) ); if ( ! is_array( $names ) ) { continue; } foreach ( $names as $_name ) { $this->register_or_translate_option( $context, $_name, $key ); } } } } /** * Returns all wpml-config.xml files in MU plugins, plugins, theme, child theme, and Polylang custom directory. * * @since 3.1 * * @return string[] A context identifier as array key, a file path as array value. * * @phpstan-return array<string, string> */ public function get_files() { if ( is_array( $this->files ) ) { return $this->files; } $this->files = array_merge( // Plugins. $this->get_plugin_files(), // Theme and child theme. $this->get_theme_files(), // MU Plugins. $this->get_mu_plugin_files(), // Custom. $this->get_custom_files() ); return $this->files; } /** * Adds post metas to the list of metas to copy when creating a new translation. * * @since 1.0 * * @param string[] $metas The list of post metas to copy or synchronize. * @param bool $sync True for sync, false for copy. * @return string[] The list of post metas to copy or synchronize. * * @phpstan-param array<non-falsy-string> $metas */ public function copy_post_metas( $metas, $sync ) { return $this->filter_metas_to_copy( (array) $metas, 'custom-fields/custom-field', (bool) $sync ); } /** * Adds term metas to the list of metas to copy when creating a new translation. * * @since 2.6 * * @param string[] $metas The list of term metas to copy or synchronize. * @param bool $sync True for sync, false for copy. * @return string[] The list of term metas to copy or synchronize. * * @phpstan-param array<non-falsy-string> $metas */ public function copy_term_metas( $metas, $sync ) { return $this->filter_metas_to_copy( (array) $metas, 'custom-term-fields/custom-term-field', (bool) $sync ); } /** * Adds post meta keys to export. * * @since 3.3 * @see PLL_Export_Metas * * @param array $keys { * A recursive array containing nested meta sub-keys to translate. * Ex: array( * 'meta_to_translate_1' => 1, * 'meta_to_translate_2' => 1, * 'meta_to_translate_3' => array( * 'sub_key_to_translate_1' => 1, * 'sub_key_to_translate_2' => array( * 'sub_sub_key_to_translate_1' => 1, * ), * ), * ) * } * @return array * * @phpstan-param array<non-falsy-string, mixed> $keys * @phpstan-return array<non-falsy-string, mixed> */ public function post_metas_to_export( $keys ) { // Add keys that have the `action` attribute set to `translate`. $keys = $this->add_metas_to_export( (array) $keys, 'custom-fields/custom-field' ); // Deal with sub-field translations. foreach ( $this->xmls as $xml ) { $fields = $xml->xpath( 'custom-fields-texts/key' ); if ( ! is_array( $fields ) ) { // No 'custom-fields-texts' nodes. continue; } foreach ( $fields as $field ) { $name = $this->get_field_attribute( $field, 'name' ); if ( '' === $name ) { // Wrong configuration: empty `name` attribute (meta name). continue; } if ( ! array_key_exists( $name, $keys ) ) { // Wrong configuration: the field is not in `custom-fields/custom-field`. continue; } $keys = $this->xml_to_array( $field, $keys, 1 ); } } return $keys; } /** * Adds term meta keys to export. * Note: sub-key translations are not currently supported by WPML. * * @since 3.3 * @see PLL_Export_Metas * * @param array $keys { * An array containing meta keys to translate. * Ex: array( * 'meta_to_translate_1' => 1, * 'meta_to_translate_2' => 1, * 'meta_to_translate_3' => 1, * ) * } * @return array * * @phpstan-param array<non-falsy-string, mixed> $keys * @phpstan-return array<non-falsy-string, mixed> */ public function term_metas_to_export( $keys ) { // Add keys that have the `action` attribute set to `translate`. return $this->add_metas_to_export( (array) $keys, 'custom-term-fields/custom-term-field' ); } /** * Specifies the encoding for post metas. * * @since 3.6 * * @param string[] $metas An array containing meta names as array keys, and their encoding as array values. * @return string[] * * @phpstan-param array<non-falsy-string, non-falsy-string> $metas */ public function add_post_meta_encodings( $metas ) { return $this->add_metas_encodings( (array) $metas, 'custom-fields/custom-field' ); } /** * Specifies the encoding for term metas. * * @since 3.6 * * @param string[] $metas An array containing meta names as array keys, and their encoding as array values. * @return string[] * * @phpstan-param array<non-falsy-string, non-falsy-string> $metas */ public function add_term_meta_encodings( $metas ) { return $this->add_metas_encodings( (array) $metas, 'custom-term-fields/custom-term-field' ); } /** * Language and translation management for custom post types. * * @since 1.0 * * @param string[] $types The list of post type names for which Polylang manages language and translations. * @param bool $hide True when displaying the list in Polylang settings. * @return string[] The list of post type names for which Polylang manages language and translations. */ public function translate_types( $types, $hide ) { foreach ( $this->xmls as $xml ) { $pts = $xml->xpath( 'custom-types/custom-type' ); if ( ! is_array( $pts ) ) { continue; } foreach ( $pts as $pt ) { $translate = $this->get_field_attribute( $pt, 'translate' ); if ( '1' === $translate && ! $hide ) { $types[ (string) $pt ] = (string) $pt; } else { 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 } } } return $types; } /** * Language and translation management for custom taxonomies. * * @since 1.0 * * @param string[] $taxonomies The list of taxonomy names for which Polylang manages language and translations. * @param bool $hide True when displaying the list in Polylang settings. * @return string[] The list of taxonomy names for which Polylang manages language and translations. */ public function translate_taxonomies( $taxonomies, $hide ) { foreach ( $this->xmls as $xml ) { $taxos = $xml->xpath( 'taxonomies/taxonomy' ); if ( ! is_array( $taxos ) ) { continue; } foreach ( $taxos as $tax ) { $translate = $this->get_field_attribute( $tax, 'translate' ); if ( '1' === $translate && ! $hide ) { $taxonomies[ (string) $tax ] = (string) $tax; } else { unset( $taxonomies[ (string) $tax ] ); // the theme/plugin author decided what to do with the taxonomy so don't allow the user to change this } } } return $taxonomies; } /** * Translation management for strings in blocks content. * * @since 3.3 * * @param string[][] $parsing_rules Rules as Xpath expressions to evaluate in the blocks content. * @return string[][] Rules completed with ones from wpml-config file. * * @phpstan-param array<string,array<string>> $parsing_rules * @phpstan-return array<string,array<string>> */ public function translate_blocks( $parsing_rules ) { return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'xpath' ) ); } /** * Translation management for blocks attributes. * * @since 3.3 * @since 3.6 Format changed from `array<string>` to `array<non-empty-string, array|true>`. * * @param array $parsing_rules Rules for blocks attributes to translate. * @return array Rules completed with ones from wpml-config file. * * @phpstan-param array<non-empty-string, array|true> $parsing_rules * @phpstan-return array<non-empty-string, array|true> */ public function translate_blocks_attributes( $parsing_rules ) { return array_merge( $parsing_rules, $this->get_blocks_parsing_rules( 'key' ) ); } /** * Returns rules to extract translatable strings from blocks. * * @since 3.3 * * @param string $rule_tag Tag name to extract. * @return string[][] The rules. * * @phpstan-param 'xpath'|'key' $rule_tag * @phpstan-return ( * $rule_tag is 'xpath' ? array<non-empty-string, list<non-empty-string>> : array<non-empty-string, array<array|true>> * ) */ protected function get_blocks_parsing_rules( $rule_tag ) { if ( null === $this->parsing_rules ) { $this->parsing_rules = $this->extract_blocks_parsing_rules(); } return isset( $this->parsing_rules[ $rule_tag ] ) ? $this->parsing_rules[ $rule_tag ] : array(); } /** * Extract all rules from WPML config file to translate strings for blocks. * * @since 3.3 * * @return string[][][] Rules completed with ones from wpml-config file. * * @phpstan-return array{ * xpath?: array<non-empty-string, list<non-empty-string>>, * key?: array<non-empty-string, array<array|true>> * } */ protected function extract_blocks_parsing_rules() { $parsing_rules = array(); foreach ( $this->xmls as $xml ) { $blocks = $xml->xpath( 'gutenberg-blocks/gutenberg-block' ); if ( ! is_array( $blocks ) ) { continue; } foreach ( $blocks as $block ) { $translate = $this->get_field_attribute( $block, 'translate' ); if ( '1' !== $translate ) { continue; } $block_name = $this->get_field_attribute( $block, 'type' ); if ( '' === $block_name ) { continue; } foreach ( $block->children() as $child ) { $rule = ''; $child_tag = $child->getName(); switch ( $child_tag ) { case 'xpath': $rule = trim( (string) $child ); if ( '' !== $rule ) { $parsing_rules['xpath'][ $block_name ][] = $rule; } break; case 'key': $rule = $this->get_field_attributes( $child ); if ( empty( $rule ) ) { break; } if ( isset( $parsing_rules['key'][ $block_name ] ) ) { $parsing_rules['key'][ $block_name ] = $this->array_merge_recursive( $parsing_rules['key'][ $block_name ], $rule ); } else { $parsing_rules['key'][ $block_name ] = $rule; } break; } } } } return $parsing_rules; } /** * Merge two arrays recursively. * Unlike `array_merge_recursive()`, this method doesn't change the type of the values. * * @since 3.6 * * @param array $array1 Array to merge into. * @param array $array2 Array to merge. * @return array */ protected function array_merge_recursive( array $array1, array $array2 ): array { foreach ( $array2 as $key => $value ) { if ( is_array( $value ) && isset( $array1[ $key ] ) && is_array( $array1[ $key ] ) ) { $array1[ $key ] = $this->array_merge_recursive( $array1[ $key ], $value ); } else { $array1[ $key ] = $value; } } return $array1; } /** * Registers or translates the strings for an option * * @since 2.8 * * @param string $context The group in which the strings will be registered. * @param string $name Option name. * @param SimpleXMLElement $key XML node. * @return void */ protected function register_or_translate_option( $context, $name, $key ) { $option_keys = $this->xml_to_array( $key ); new PLL_Translate_Option( $name, reset( $option_keys ), array( 'context' => $context ) ); } /** * Recursively transforms xml nodes to an array, ready for PLL_Translate_Option. * * @since 2.9 * @since 3.3 Type-hinted the parameters `$key` and `$arr`. * @since 3.3 `$arr` is not passed by reference anymore. * @since 3.3 Added the parameter `$fill_value`. * * @param SimpleXMLElement $key XML node. * @param array $arr Array of option keys to translate. * @param mixed $fill_value Value to use when filling entries. Default is true. * @return array */ protected function xml_to_array( SimpleXMLElement $key, array $arr = array(), $fill_value = true ) { $name = $this->get_field_attribute( $key, 'name' ); if ( '' === $name ) { return $arr; } $children = $key->children(); if ( count( $children ) ) { foreach ( $children as $child ) { if ( ! isset( $arr[ $name ] ) || ! is_array( $arr[ $name ] ) ) { $arr[ $name ] = array(); } $arr[ $name ] = $this->xml_to_array( $child, $arr[ $name ], $fill_value ); } } else { $arr[ $name ] = $fill_value; // Multiline as in WPML. } return $arr; } /** * Get the value of an attribute. * * @since 3.3 * * @param SimpleXMLElement $field A XML node. * @param string $attribute_name Node of the attribute. * @return string */ private function get_field_attribute( SimpleXMLElement $field, $attribute_name ) { $attributes = $field->attributes(); if ( empty( $attributes ) || ! isset( $attributes[ $attribute_name ] ) ) { return ''; } return trim( (string) $attributes[ $attribute_name ] ); } /** * Gets attributes values recursively. * * @since 3.6 * * @param SimpleXMLElement $field A XML node. * @return array An array of attributes. * * @phpstan-return array<non-empty-string, array|true> */ private function get_field_attributes( SimpleXMLElement $field ): array { $name = $this->get_field_attribute( $field, 'name' ); if ( '' === $name ) { return array(); } $children = $field->children(); if ( 0 === $children->count() ) { return array( $name => true ); } $sub_attributes = array(); foreach ( $children as $child ) { $sub = $this->get_field_attributes( $child ); if ( empty( $sub ) ) { continue; } $sub_attributes[ $name ] = array_merge( $sub_attributes[ $name ] ?? array(), $sub ); } return $sub_attributes; } /** * Returns all wpml-config.xml files in MU plugins. * * @since 3.3 * * @return string[] A context identifier as array key, a file path as array value. * * @phpstan-return array<string, string> */ private function get_mu_plugin_files() { if ( ! is_readable( WPMU_PLUGIN_DIR ) || ! is_dir( WPMU_PLUGIN_DIR ) ) { return array(); } $files = array(); // Search for top level wpml-config.xml file. $file_path = WPMU_PLUGIN_DIR . '/wpml-config.xml'; if ( is_readable( $file_path ) ) { $files['mu-plugins'] = $file_path; } // Search in proxy loaded MU plugins. foreach ( new DirectoryIterator( WPMU_PLUGIN_DIR ) as $file_info ) { if ( ! $this->is_dir( $file_info ) ) { continue; } $file_path = $file_info->getPathname() . '/wpml-config.xml'; if ( is_readable( $file_path ) ) { $files[ 'mu-plugins/' . $file_info->getFilename() ] = $file_path; } } return $files; } /** * Returns all wpml-config.xml files in plugins. * * @since 3.3 * * @return string[] A context identifier as array key, a file path as array value. * * @phpstan-return array<string, string> */ private function get_plugin_files() { $files = array(); $plugins = array(); if ( is_multisite() ) { // 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. $sitewide_plugins = get_site_option( 'active_sitewide_plugins', array() ); if ( ! empty( $sitewide_plugins ) && is_array( $sitewide_plugins ) ) { $plugins = array_keys( $sitewide_plugins ); } } // By-site plugins. $active_plugins = get_option( 'active_plugins', array() ); if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) { $plugins = array_merge( $plugins, $active_plugins ); } $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . '%s/wpml-config.xml'; foreach ( $plugins as $plugin ) { if ( ! is_string( $plugin ) || '' === $plugin ) { continue; } $file_dir = dirname( $plugin ); $file_path = sprintf( $plugin_path, $file_dir ); if ( is_readable( $file_path ) ) { $files[ "plugins/{$file_dir}" ] = $file_path; } } return $files; } /** * Returns all wpml-config.xml files in theme and child theme. * * @since 3.3 * * @return string[] A context identifier as array key, a file path as array value. * * @phpstan-return array<string, string> */ private function get_theme_files() { $files = array(); // Theme. $template_path = get_template_directory(); $file_path = "{$template_path}/wpml-config.xml"; if ( is_readable( $file_path ) ) { $files[ 'themes/' . get_template() ] = $file_path; } // Child theme. $stylesheet_path = get_stylesheet_directory(); $file_path = "{$stylesheet_path}/wpml-config.xml"; if ( $stylesheet_path !== $template_path && is_readable( $file_path ) ) { $files[ 'themes/' . get_stylesheet() ] = $file_path; } return $files; } /** * Returns the wpml-config.xml file in Polylang custom directory. * * @since 3.3 * * @return string[] A context identifier as array key, a file path as array value. * * @phpstan-return array<string, string> */ private function get_custom_files() { $file_path = PLL_LOCAL_DIR . '/wpml-config.xml'; if ( ! is_readable( $file_path ) ) { return array(); } return array( 'Polylang' => $file_path, ); } /** * Tells if the given "file info" object represents a directory. * This takes care of not triggering a `open_basedir` restriction error when the file symlinks a file that is not in * `open_basedir`. * * @see https://wordpress.org/support/topic/fatal-error-open_basedir-restricton/ * * @since 3.5.1 * * @param DirectoryIterator $file_info A "file info" object that we know its path (but maybe not its real path) is * in `open_basedir`. * @return bool */ private function is_dir( DirectoryIterator $file_info ): bool { if ( $file_info->isDot() ) { return false; } if ( $file_info->getPathname() === $file_info->getRealPath() ) { // Not a symlink: not going to trigger a `open_basedir` restriction error. return $file_info->isDir(); } /* * Symlink: make sure the file's real path is in `open_basedir` before checking it is a dir. * Which means that the `open_basedir` check is done only for symlinked files. */ return $this->is_allowed_dir( $file_info->getRealPath() ) && $file_info->isDir(); } /** * Checks whether access to a given directory is allowed. * This takes into account the PHP `open_basedir` restrictions, so that Polylang does not try to access directories * it is not allowed to. * * Inspired by `WP_Automatic_Updater::is_allowed_dir()` and `wp-includes/ID3/getid3.php`. * * @since 3.5.1 * * @param string $dir The directory to check. * @return bool True if access to the directory is allowed, false otherwise. */ private function is_allowed_dir( string $dir ): bool { $dir = trim( $dir ); if ( '' === $dir ) { return false; } $open_basedir_paths = $this->get_open_basedir_paths(); if ( empty( $open_basedir_paths ) ) { return true; } $dir = $this->normalize_path( $dir ); foreach ( $open_basedir_paths as $path ) { if ( str_starts_with( $dir, $path ) ) { return true; } } return false; } /** * Returns the list of paths in `open_basedir`. The purpose is to compare a formatted path to this list. * Note: all paths are suffixed by `DIRECTORY_SEPARATOR`, even paths to files. * * @since 3.5.1 * * @return string[] An array of formatted paths. */ private function get_open_basedir_paths(): array { if ( is_array( $this->open_basedir_paths ) ) { return $this->open_basedir_paths; } $this->open_basedir_paths = array(); $open_basedir = ini_get( 'open_basedir' ); // Can be `false` or an empty string. if ( empty( $open_basedir ) ) { return $this->open_basedir_paths; } $open_basedir_list = explode( PATH_SEPARATOR, $open_basedir ); foreach ( $open_basedir_list as $basedir ) { $basedir = trim( $basedir ); if ( '' === $basedir ) { continue; } $this->open_basedir_paths[] = $this->normalize_path( $basedir ); } $this->open_basedir_paths = array_unique( $this->open_basedir_paths ); return $this->open_basedir_paths; } /** * Formats a path for string comparison. * 1. Slashes and back-slashes are replaced by `DIRECTORY_SEPARATOR`. * 2. The path is suffixed by `DIRECTORY_SEPARATOR` (even non-directory elements). * * @since 3.5.1 * * @param string $path A file path. * @return string * * @phpstan-param non-empty-string $path * @phpstan-return non-empty-string */ private function normalize_path( string $path ): string { $path = str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $path ); if ( substr( $path, -1, 1 ) !== DIRECTORY_SEPARATOR ) { $path .= DIRECTORY_SEPARATOR; } return $path; } /** * Adds (or removes) meta names to the list of metas to copy or synchronize. * * @since 3.6 * * @param string[] $metas The list of meta names to copy or synchronize. * @param string $xpath Xpath to the meta fields in the xml files. * @param bool $sync Either sync is enabled or not. * @return string[] * * @phpstan-param array<non-falsy-string> $metas * @phpstan-param non-falsy-string $xpath */ private function filter_metas_to_copy( array $metas, string $xpath, bool $sync ): array { $parsed_metas = $this->parse_xml_metas( $xpath ); $metas_to_remove = array(); foreach ( $parsed_metas as $name => $parsed_meta ) { if ( 'copy' === $parsed_meta['action'] || ( ! $sync && in_array( $parsed_meta['action'], array( 'translate', 'copy-once' ), true ) ) ) { $metas[] = $name; } else { $metas_to_remove[] = $name; } } return array_diff( $metas, $metas_to_remove ); } /** * Adds meta keys to export. * * @since 3.6 * * @param array $metas { * An array containing meta keys to translate. * Ex: array( * 'meta_to_translate_1' => 1, * 'meta_to_translate_2' => 1, * 'meta_to_translate_3' => array( ... ), * ) * } * @param string $xpath Xpath to the meta fields in the xml files. * @return array * * @phpstan-param array<non-falsy-string, mixed> $metas * @phpstan-param non-falsy-string $xpath * @phpstan-return array<non-falsy-string, mixed> */ private function add_metas_to_export( array $metas, string $xpath ) { $fields = $this->parse_xml_metas( $xpath ); foreach ( $fields as $name => $field ) { if ( 'translate' === $field['action'] ) { $metas[ $name ] = 1; } } return $metas; } /** * Adds encoding of metas. * * @since 3.6 * * @param string[] $metas The list of encodings for each metas. Meta names are array keys, encodings are array values. * @param string $xpath Xpath to the meta fields in the xml files. * @return string[] * * @phpstan-param array<non-falsy-string, non-falsy-string> $metas * @phpstan-param non-falsy-string $xpath */ private function add_metas_encodings( array $metas, string $xpath ): array { $parsed_metas = $this->parse_xml_metas( $xpath ); foreach ( $parsed_metas as $name => $parsed_meta ) { if ( ! empty( $parsed_meta['encoding'] ) ) { $metas[ $name ] = $parsed_meta['encoding']; } } return $metas; } /** * Parses all xml files for metas. * Results are cached for each `$xpath`. * * @since 3.6 * * @param string $xpath Xpath to the meta fields in the xml files. * @return array * * @phpstan-param non-falsy-string $xpath * @phpstan-return ParsedMetas */ private function parse_xml_metas( string $xpath ): array { if ( isset( $this->parsed_metas[ $xpath ] ) ) { return $this->parsed_metas[ $xpath ]; } $this->parsed_metas[ $xpath ] = array(); foreach ( $this->xmls as $xml ) { $custom_fields = $xml->xpath( $xpath ); if ( ! is_array( $custom_fields ) ) { continue; } foreach ( $custom_fields as $custom_field ) { $name = (string) $custom_field; if ( empty( $name ) ) { continue; } $data = array( 'action' => $this->get_field_attribute( $custom_field, 'action' ), 'encoding' => $this->get_field_attribute( $custom_field, 'encoding' ), ); $data['encoding'] = 'json' === $data['encoding'] ? 'json' : ''; // Only JSON is supported for now. $this->parsed_metas[ $xpath ][ $name ] = $data; } } return $this->parsed_metas[ $xpath ]; } }