ID) : '') ?: current_language(); } /** Catalog REST reads may choose only a language registered by the active provider. */ function request_language($request) { $language = $request instanceof \WP_REST_Request ? $request->get_param('lang') : null; if (!is_string($language) || $language === '') return ''; if ($language === current_language()) return $language; $active = apply_filters('wpml_active_languages', null, ['skip_missing' => 0]); if (is_array($active) && isset($active[$language])) return $language; if (function_exists('pll_languages_list') && in_array($language, pll_languages_list(['fields' => 'slug']), true)) return $language; return ''; } /** Map authored taxonomy selections; visitor values still use the current-language allowlist. */ function term_values($values, $taxonomy) { $language = current_language(); if (in_array($language, ['', 'all'], true)) return $values; return array_map(static function ($slug) use ($taxonomy, $language) { if (!is_string($slug)) return $slug; $term = with_language('all', static fn() => get_term_by('slug', $slug, $taxonomy)); if (!$term) return $slug; $id = apply_filters('wpml_object_id', $term->term_id, $taxonomy, false, $language); if (function_exists('pll_is_translated_taxonomy') && pll_is_translated_taxonomy($taxonomy) && function_exists('pll_get_term')) $id = pll_get_term($term->term_id, $language); $translated = $id ? get_term((int) $id, $taxonomy) : null; return $translated instanceof \WP_Term ? $translated->slug : $slug; }, (array) $values); } /** WPML 5 scopes nest; older versions restore by explicitly selecting the prior language. */ function restore_language($previous, $version = null) { $version = $version ?? (defined('ICL_SITEPRESS_VERSION') ? ICL_SITEPRESS_VERSION : ''); do_action('wpml_switch_language', $version !== '' && version_compare($version, '5.0', '>=') ? null : $previous); } /** Scope only this operation; never accept a visitor-selected language for signed feeds. */ function with_language($language, $callback) { if (!is_string($language) || $language === '') return $callback(); $overrides = &overrides(); $site = get_current_blog_id(); $had_override = array_key_exists($site, $overrides); $before = $overrides[$site] ?? ''; $previous = apply_filters('wpml_current_language', null); $switch = is_string($previous) && $previous !== $language; $overrides[$site] = $language; // Polylang's public custom-query API uses lang rather than a global switch. $posts = static function ($query) use ($language) { if (function_exists('pll_current_language')) $query->set('lang', $language === 'all' ? '' : $language); }; $terms = static function ($args) use ($language) { if (function_exists('pll_current_language')) $args['lang'] = $language === 'all' ? '' : $language; return $args; }; add_action('pre_get_posts', $posts, 1); add_filter('get_terms_args', $terms); try { if ($switch) do_action('wpml_switch_language', $language); return $callback(); } finally { if ($switch) restore_language($previous); if ($had_override) $overrides[$site] = $before; else unset($overrides[$site]); remove_action('pre_get_posts', $posts, 1); remove_filter('get_terms_args', $terms); } } /** Request-local cache identity covers blog switches, language changes and writes. */ function cache_key($key) { return md5(wp_json_encode([get_current_blog_id(), determine_locale(), current_language(), get_current_user_id(), wp_cache_get_last_changed('posts'), wp_cache_get_last_changed('terms'), wp_cache_get_last_changed('fqbp_catalog_context'), $key])); } function cached($key, $callback) { wp_cache_add_non_persistent_groups(['fqbp_catalog_context']); $key = cache_key($key); $value = wp_cache_get($key, 'fqbp_catalog_context'); if ($value !== false) return $value; $value = $callback(); wp_cache_set($key, $value, 'fqbp_catalog_context'); return $value; } /** Let the translation plugin build its own SQL without loading every post ID. */ function published_posts_sql($post_type) { return cached('posts-sql|' . $post_type, static function () use ($post_type) { $args = ['post_type' => $post_type, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'orderby' => 'none', 'suppress_filters' => false]; $language = current_language(); if (function_exists('pll_current_language')) $args['lang'] = $language === 'all' ? '' : $language; $query = new \WP_Query(); $skip = static fn($posts, $candidate) => $candidate === $query ? [] : $posts; add_filter('posts_pre_query', $skip, PHP_INT_MAX, 2); try { $query->query($args); return $query->request; } finally { remove_filter('posts_pre_query', $skip, PHP_INT_MAX); } }); } function published_count($post_type) { if (in_array(current_language(), ['', 'all'], true)) return (int) (wp_count_posts($post_type)->publish ?? 0); return cached('count|' . $post_type, static function () use ($post_type) { global $wpdb; $sql = published_posts_sql($post_type); // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Core WP_Query supplies the provider-filtered subquery; values are prepared and results use the language/site-scoped request cache. return (int) $wpdb->get_var("SELECT COUNT(DISTINCT ID) FROM ({$sql}) AS fqbp_posts"); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter }); } /** Batch catalog counts without changing each post type's provider/filter SQL. * Single count reads share these request-local entries; language/site/user and * content generations remain part of every key. Bound SQL size for large sites. */ function published_counts($post_types) { global $wpdb; $post_types = array_values(array_unique(array_filter(array_map('sanitize_key', (array) $post_types)))); if (in_array(current_language(), ['', 'all'], true)) { return array_combine($post_types, array_map(__NAMESPACE__ . '\\published_count', $post_types)); } wp_cache_add_non_persistent_groups(['fqbp_catalog_context']); $result = []; $missing = []; foreach ($post_types as $type) { $key = cache_key('count|' . $type); $count = wp_cache_get($key, 'fqbp_catalog_context'); if ($count !== false) $result[$type] = $count; else $missing[$type] = $key; } foreach (array_chunk($missing, 32, true) as $chunk) { $parts = []; foreach ($chunk as $type => $key) { $sql = $wpdb->add_placeholder_escape(published_posts_sql($type)); // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Core WP_Query supplies the provider-filtered subquery; values are prepared and results use the language/site-scoped request cache. $parts[] = $wpdb->prepare("SELECT %s AS post_type, COUNT(DISTINCT ID) AS total FROM ({$sql}) AS fqbp_posts", $type); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter } // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Core WP_Query supplies the provider-filtered subquery; values are prepared and results use the language/site-scoped request cache. $rows = $wpdb->get_results(implode(' UNION ALL ', $parts), ARRAY_A); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter if (!is_array($rows) || count($rows) !== count($chunk)) { // Retain the established individual-count path if a provider/database // cannot execute the combined request; do not cache missing rows as zero. foreach ($chunk as $type => $key) $result[$type] = published_count($type); continue; } foreach ($rows as $row) { $type = $row['post_type']; $result[$type] = (int) $row['total']; wp_cache_set($chunk[$type], $result[$type], 'fqbp_catalog_context'); } } return array_replace(array_fill_keys($post_types, 0), $result); } /** Counts include only published posts of this type in the current language. */ function taxonomy_catalog($post_type) { return cached('taxonomies|' . $post_type, static function () use ($post_type) { global $wpdb; $sql = $wpdb->add_placeholder_escape(published_posts_sql($post_type)); $result = []; foreach (get_object_taxonomies($post_type, 'objects') as $taxonomy) { if (!Taxonomies\is_catalogable($taxonomy)) continue; // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Core WP_Query supplies the provider-filtered subquery; values are prepared and results use the language/site-scoped request cache. $rows = $wpdb->get_results($wpdb->prepare( "SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total FROM %i AS tt INNER JOIN %i AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id INNER JOIN ({$sql}) AS p ON p.ID = tr.object_id WHERE tt.taxonomy = %s GROUP BY tt.term_id", $wpdb->term_taxonomy, $wpdb->term_relationships, $taxonomy->name ), ARRAY_A); // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, PluginCheck.Security.DirectDB.UnescapedDBParameter $counts = array_column($rows, 'total', 'term_id'); $args = ['taxonomy' => $taxonomy->name, 'include' => array_keys($counts), 'hide_empty' => false]; if (function_exists('pll_current_language')) $args['lang'] = current_language() === 'all' ? '' : current_language(); $found = $counts ? get_terms($args) : []; $terms = []; if (!is_wp_error($found)) foreach ($found as $term) $terms[] = ['id' => (int) $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => (int) $term->parent, 'count' => (int) $counts[$term->term_id]]; $result[] = ['slug' => $taxonomy->name, 'label' => $taxonomy->labels->name, 'public' => (bool) $taxonomy->public, 'singular_label' => $taxonomy->labels->singular_name, 'hierarchical' => (bool) $taxonomy->hierarchical, 'terms' => $terms]; } return $result; }); }