post_type, array('post', 'page'), true)) { return true; } $id = $post->ID; if ($post->post_type === 'page') { $countries = get_option('wplocation_page_location_' . $id, array()); $action = get_option('wplocation_page_action_option_' . $id, 'include'); } else { $countries = get_post_meta($id, 'wplocation_post_location_' . $id, true); $action = get_post_meta($id, 'wplocation_post_action_option_' . $id, true); } if (!wplocation_rule_allows($countries, $action, $iso)) { return false; } if ($post->post_type === 'post') { foreach (get_the_category($id) as $category) { $term_id = $category->term_id; if (!wplocation_rule_allows(get_term_meta($term_id, 'wplocation_cat_location_' . $term_id, true), get_term_meta($term_id, 'wplocation_action_option_' . $term_id, true), $iso)) { return false; } } } return true; } /** Collect only objects with saved location rules, not the entire site. */ function wplocation_denied_objects($iso) { global $wpdb; static $cache = array(); $cache_key = get_current_blog_id() . '|' . ($iso ?: 'unknown'); if (isset($cache[$cache_key])) { return $cache[$cache_key]; } $posts = array(); $categories = array(); $prefix = 'wplocation_post_location_'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Discover only rule keys; the complete result is memoized per blog and country for this request. $keys = $wpdb->get_col($wpdb->prepare("SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like($prefix) . '%')); foreach ($keys as $key) { $suffix = substr($key, strlen($prefix)); if (!ctype_digit($suffix)) { continue; } $id = (int) $suffix; if ($id && get_post_type($id) === 'post' && !wplocation_rule_allows(get_post_meta($id, $key, true), get_post_meta($id, 'wplocation_post_action_option_' . $id, true), $iso)) { $posts[] = $id; } } $prefix = 'wplocation_page_location_'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Discover only rule keys; the complete result is memoized per blog and country for this request. $keys = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like($prefix) . '%')); foreach ($keys as $key) { $suffix = substr($key, strlen($prefix)); if (!ctype_digit($suffix)) { continue; } $id = (int) $suffix; if ($id && get_post_type($id) === 'page' && !wplocation_rule_allows(get_option($key, array()), get_option('wplocation_page_action_option_' . $id, 'include'), $iso)) { $posts[] = $id; } } $prefix = 'wplocation_cat_location_'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Discover only rule keys; the complete result is memoized per blog and country for this request. $keys = $wpdb->get_col($wpdb->prepare("SELECT DISTINCT meta_key FROM {$wpdb->termmeta} WHERE meta_key LIKE %s", $wpdb->esc_like($prefix) . '%')); foreach ($keys as $key) { $suffix = substr($key, strlen($prefix)); if (!ctype_digit($suffix)) { continue; } $id = (int) $suffix; if ($id && !wplocation_rule_allows(get_term_meta($id, $key, true), get_term_meta($id, 'wplocation_action_option_' . $id, true), $iso)) { $categories[] = $id; } } // A taxonomy filter applied to a mixed or custom-type query can hide // unrelated products and documentation. Exclude only actual posts instead. if ($categories) { $categories = array_values(array_unique($categories)); $placeholders = implode(',', array_fill(0, count($categories), '%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- %d placeholders are generated for validated term IDs; the complete result is memoized for this request. $category_posts = $wpdb->get_col($wpdb->prepare("SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE p.post_type = 'post' AND tt.taxonomy = 'category' AND tt.term_id IN ($placeholders)", $categories)); $posts = array_merge($posts, array_map('intval', $category_posts)); } $cache[$cache_key] = array(array_values(array_unique($posts)), array_values(array_unique($categories))); return $cache[$cache_key]; } /** Filter before pagination for frontend queries and WordPress post/page REST queries. */ function wplocation_filter_query($query) { static $collecting = false; if ($collecting) { return; } $is_rest = defined('REST_REQUEST') && REST_REQUEST; if ((!$is_rest && is_admin()) || (defined('WP_CLI') && WP_CLI)) { return; } if ($is_rest) { $types = (array) $query->get('post_type'); if (!array_intersect($types, array('post', 'page', 'any'))) { return; } } // Editors need the editing collection to contain material they can manage. // Public REST views continue to use visitor targeting, even when logged in. if ($is_rest && $query->get('wplocation_editor_query')) { $capability = $query->get('post_type') === 'page' ? 'edit_others_pages' : 'edit_others_posts'; if (current_user_can($capability)) { return; } } // Let the singular guard issue the 404; excluding the object here can // cause WordPress's canonical redirect to expose its permalink first. if (!$is_rest && $query->is_main_query() && $query->is_singular()) { return; } $iso = wplocation_get_country_iso(wplocation_get_ipaddress()); $collecting = true; try { list($denied, $categories) = wplocation_denied_objects($iso); } finally { $collecting = false; } if ($denied) { $include = $query->get('post__in'); if (is_array($include) && $include) { $remaining = array_values(array_diff(array_map('intval', $include), $denied)); $query->set('post__in', $remaining ? $remaining : array(0)); } else { $current = $query->get('post__not_in'); $query->set('post__not_in', array_values(array_unique(array_merge(is_array($current) ? $current : array(), $denied)))); } } } add_action('pre_get_posts', 'wplocation_filter_query', 20); function wplocation_mark_editor_rest_query($args, $request) { if ($request->get_param('context') === 'edit') { $args['wplocation_editor_query'] = true; } return $args; } add_filter('rest_post_query', 'wplocation_mark_editor_rest_query', 10, 2); add_filter('rest_page_query', 'wplocation_mark_editor_rest_query', 10, 2); /** Ask compatible caches not to store personalized frontend HTML. */ function wplocation_prevent_unsafe_cache() { if (is_admin() || (defined('REST_REQUEST') && REST_REQUEST)) { return; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- DONOTCACHEPAGE is the established page-cache interoperability flag. if (!defined('DONOTCACHEPAGE')) { define('DONOTCACHEPAGE', true); } if (!headers_sent()) { nocache_headers(); } } add_action('send_headers', 'wplocation_prevent_unsafe_cache', 0); /** Direct singular requests can bypass post__not_in in WordPress. */ function wplocation_check_singular() { if (!is_singular(array('post', 'page'))) { return; } $post = get_queried_object(); $iso = wplocation_get_country_iso(wplocation_get_ipaddress()); if (!wplocation_content_allowed($post, $iso)) { global $wp_query; $wp_query->set_404(); add_filter('redirect_canonical', '__return_false'); status_header(404); nocache_headers(); } } add_action('template_redirect', 'wplocation_check_singular', 0); /** Intercept core REST item reads before response preparation. */ function wplocation_rest_pre_dispatch($result, $server, $request) { if (null !== $result || !in_array($request->get_method(), array('GET', 'HEAD'), true)) { return $result; } if (!preg_match('#^/wp/v2/(posts|pages)/(\d+)$#', $request->get_route(), $match)) { return $result; } $post = get_post((int) $match[2]); if (!$post || $post->post_type !== ($match[1] === 'posts' ? 'post' : 'page') || current_user_can('edit_post', $post->ID)) { return $result; } $iso = wplocation_get_country_iso(wplocation_get_ipaddress()); if (!wplocation_content_allowed($post, $iso)) { return new WP_Error('rest_post_invalid_id', __('Invalid post ID.', 'wplocation'), array('status' => 404)); } return $result; } add_filter('rest_pre_dispatch', 'wplocation_rest_pre_dispatch', 10, 3); /** Prevent shared caching only for REST reads that can expose country-aware content. */ function wplocation_rest_cache_headers($served, $result, $request) { if (!in_array($request->get_method(), array('GET', 'HEAD'), true)) { return $served; } $route = rtrim($request->get_route(), '/'); if (!preg_match('#^/wp/v2/(posts|pages)(/[0-9]+)?$#', $route) && $route !== '/wp/v2/search') { return $served; } if (!headers_sent()) { nocache_headers(); } return $served; } add_filter('rest_pre_serve_request', 'wplocation_rest_cache_headers', 10, 3);