--- a/proxy-vpn-blocker/includes/class-proxy-vpn-blocker-settings.php
+++ b/proxy-vpn-blocker/includes/class-proxy-vpn-blocker-settings.php
@@ -137,6 +137,7 @@
array_push( $links, $settings_link );
return $links;
}
+
/**
* Get custom post type paths
*
@@ -155,60 +156,96 @@
return $paths;
}
-
/**
- * Get all detected paths
+ * Get dynamic plugin paths from WordPress rewrite rules
*
- * @return array
+ * Scans rewrite rules to detect virtual paths created by plugins
+ * that don't correspond to actual WordPress posts/pages.
+ *
+ * @return array Array of detected virtual paths
*/
- private static function pvb_get_all_detected_paths() {
- $paths = array();
+ public static function pvb_get_dynamic_plugin_paths() {
+ // Hard safety gates
+ if (
+ ( ! did_action( 'init' ) && ! is_admin() ) ||
+ wp_doing_ajax() ||
+ wp_doing_cron() ||
+ defined( 'WP_INSTALLING' )
+ ) {
+ return array();
+ }
- // 1. Get paths from custom post types.
- if ( method_exists( 'Proxy_VPN_Blocker_Settings', 'get_custom_post_type_paths' ) ) {
- $paths = array_merge( $paths, self::get_custom_post_type_paths() );
+ $cache_key = 'pvb_dynamic_plugin_paths';
+ $cache_ttl = DAY_IN_SECONDS;
+
+ $cached = get_transient( $cache_key );
+ if ( is_array( $cached ) ) {
+ return $cached;
}
- // 2. Get paths from rewrite rules (if defined).
- if ( function_exists( 'pvb_get_dynamic_plugin_paths' ) ) {
- $paths = array_merge( $paths, pvb_get_dynamic_plugin_paths() );
+ global $wp_rewrite;
+
+ if ( empty( $wp_rewrite->rules ) || ! is_array( $wp_rewrite->rules ) ) {
+ return array();
}
- // 3. BuddyPress specific paths — only if active.
- if ( function_exists( 'bp_core_get_directory_page_ids' ) ) {
- $page_ids = bp_core_get_directory_page_ids();
+ $paths = array();
- foreach ( $page_ids as $component => $page_id ) {
- $slug = get_page_uri( $page_id );
- if ( ! empty( $slug ) ) {
- $paths[] = trailingslashit( $slug );
- }
+ foreach ( $wp_rewrite->rules as $pattern => $query ) {
+ if ( strpos( $query, 'index.php?' ) !== 0 ) {
+ continue;
}
- }
- // 4. Include already saved paths (to preserve custom user entries).
- $saved_paths = get_option( 'pvb_defined_protected_paths' );
- if ( is_array( $saved_paths ) ) {
- $paths = array_merge( $paths, $saved_paths );
- }
+ if (
+ strpos( $pattern, '^wp-' ) === 0 ||
+ strpos( $pattern, '^index.php' ) === 0 ||
+ preg_match( '#(attachment|category|tag|author|date|search|feed|page|comments|embed|wc-api)#', $pattern ) ||
+ $pattern === '^/?$'
+ ) {
+ continue;
+ }
- // 5. Normalize and remove duplicates.
- $paths = array_map( 'trim', $paths );
- $paths = array_map( 'untrailingslashit', $paths );
- $paths = array_filter(
- $paths,
- function ( $path ) {
- return ! is_numeric( $path );
+ $clean = trim( $pattern, '^$' );
+ $clean = rtrim( $clean, '/?' );
+
+ // Skip complex regex
+ if ( preg_match( '/[()[]*+.|\\|]/', $clean ) ) {
+ continue;
}
- );
- $paths = array_unique( $paths );
- $paths = array_filter( $paths ); // remove empty values.
- // 6. Return as trailing slashes again for consistency.
- foreach ( $paths as $i => $path ) {
- $paths[ $i ] = trailingslashit( $path );
+ $path = trim( $clean, '/' );
+
+ if (
+ empty( $path ) ||
+ is_numeric( $path ) ||
+ strlen( $path ) < 2 ||
+ strlen( $path ) > 50 ||
+ ! preg_match( '/^[a-zA-Z0-9_-/]+$/', $path )
+ ) {
+ continue;
+ }
+
+ $skip = array(
+ 'admin','login','register','dashboard','profile','settings',
+ 'media','upload','download','api','rest','oauth','auth',
+ 'cart','checkout','account','orders','downloads','subscriptions'
+ );
+
+ if ( in_array( $path, $skip, true ) ) {
+ continue;
+ }
+
+ $paths[] = trailingslashit( $path );
+
+ if ( count( $paths ) >= 100 ) {
+ break;
+ }
}
+ $paths = array_values( array_unique( $paths ) );
+
+ set_transient( $cache_key, $paths, $cache_ttl );
+
return $paths;
}
@@ -217,14 +254,30 @@
*
* @return array
*/
- private static function pvb_get_all_path_options() {
+ public static function pvb_get_all_path_options() {
+ error_log( 'PVB DEBUG: pvb_get_all_path_options() called' );
+
$options = array();
- $all_paths = self::pvb_get_all_detected_paths();
+ $all_paths = self::pvb_get_all_detected_paths_dynamic();
$saved_paths = get_option( 'pvb_defined_protected_paths' );
+
+ // Handle both old and new data structures for backward compatibility.
if ( ! is_array( $saved_paths ) ) {
$saved_paths = array();
}
+ // Extract configured paths from new structure.
+ $configured_paths = array();
+ foreach ( $saved_paths as $path_key => $path_config ) {
+ if ( is_array( $path_config ) && isset( $path_config['method'] ) ) {
+ // New structure - path is key.
+ $configured_paths[] = $path_key;
+ } elseif ( is_string( $path_config ) ) {
+ // Old structure - path is value.
+ $configured_paths[] = $path_config;
+ }
+ }
+
// Use detected paths as base, ensure key = value.
foreach ( $all_paths as $path ) {
$path = trailingslashit( untrailingslashit( $path ) );
@@ -232,7 +285,7 @@
}
// Add custom entries not in detected list.
- foreach ( $saved_paths as $path ) {
+ foreach ( $configured_paths as $path ) {
$path = trailingslashit( untrailingslashit( $path ) );
if ( ! isset( $options[ $path ] ) ) {
$options[ $path ] = 'Custom: ' . $path;
@@ -243,6 +296,69 @@
}
/**
+ * Get all detected paths with dynamic loading for admin
+ *
+ * @return array
+ */
+ private static function pvb_get_all_detected_paths_dynamic() {
+ $paths = array();
+
+ // 1. Get paths from custom post types.
+ $paths = array_merge( $paths, self::get_custom_post_type_paths() );
+
+ // 2. Get paths from rewrite rules (dynamically loaded).
+ $dynamic_paths = self::pvb_get_dynamic_plugin_paths();
+ $paths = array_merge( $paths, $dynamic_paths );
+
+ // 3. BuddyPress specific paths — only if active.
+ if ( function_exists( 'bp_core_get_directory_page_ids' ) ) {
+ $page_ids = bp_core_get_directory_page_ids();
+
+ foreach ( $page_ids as $component => $page_id ) {
+ $slug = get_page_uri( $page_id );
+ if ( ! empty( $slug ) ) {
+ $paths[] = trailingslashit( $slug );
+ }
+ }
+ }
+
+ // 4. Include already saved paths (to preserve custom user entries).
+ $saved_paths = get_option( 'pvb_defined_protected_paths' );
+ if ( is_array( $saved_paths ) ) {
+ $paths = array_merge( $paths, array_keys( $saved_paths ) );
+ }
+
+ // 5. Normalize and remove duplicates.
+ $paths = array_map(
+ function ( $path ) {
+ return is_string( $path ) ? trim( $path ) : $path;
+ },
+ $paths
+ );
+ $paths = array_map(
+ function ( $path ) {
+ return is_string( $path ) ? untrailingslashit( $path ) : $path;
+ },
+ $paths
+ );
+ $paths = array_filter(
+ $paths,
+ function ( $path ) {
+ return ! is_numeric( $path );
+ }
+ );
+ $paths = array_unique( $paths );
+ $paths = array_filter( $paths ); // remove empty values.
+
+ // 6. Return as trailing slashes again for consistency.
+ foreach ( $paths as $i => $path ) {
+ $paths[ $i ] = trailingslashit( $path );
+ }
+
+ return $paths;
+ }
+
+ /**
* Build settings fields
*
* @return array Fields to be displayed on settings page
--- a/proxy-vpn-blocker/includes/class-proxy-vpn-blocker.php
+++ b/proxy-vpn-blocker/includes/class-proxy-vpn-blocker.php
@@ -114,7 +114,7 @@
* @since 1.0
* @return void
*/
- public function __construct( $file = '', $version = '3.5.3' ) {
+ public function __construct( $file = '', $version = '3.5.4' ) {
$this->_version = $version;
$this->_token = 'proxy_vpn_blocker';
@@ -366,7 +366,7 @@
* @see proxy_vpn_blocker()
* @return Main proxy_vpn_blocker instance
*/
- public static function instance( $file = '', $version = '3.5.3' ) {
+ public static function instance( $file = '', $version = '3.5.4' ) {
if ( is_null( self::$_instance ) ) {
self::$_instance = new self( $file, $version );
--- a/proxy-vpn-blocker/includes/lib/class-proxy-vpn-blocker-admin-api.php
+++ b/proxy-vpn-blocker/includes/lib/class-proxy-vpn-blocker-admin-api.php
@@ -66,6 +66,11 @@
$data = '';
}
+ // Backward compatibility: Convert premium format data to free format for defined_protected_paths.
+ if ( $field['id'] === 'defined_protected_paths' && is_array( $data ) && ! empty( $data ) && is_array( reset( $data ) ) ) {
+ $data = array_keys( $data );
+ }
+
$html = '';
switch ( $field['type'] ) {
--- a/proxy-vpn-blocker/includes/post-additions.php
+++ b/proxy-vpn-blocker/includes/post-additions.php
@@ -116,6 +116,16 @@
* Handle the custom bulk action to set posts or pages as blocked.
*/
function handle_pvb_set_postspages_bulk_action() {
+ // Check user permissions.
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( __( 'You do not have sufficient permissions to access this page.', 'proxy-vpn-blocker' ) );
+ }
+
+ // Verify nonce for security.
+ if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-posts' ) ) {
+ wp_die( __( 'Security check failed.', 'proxy-vpn-blocker' ) );
+ }
+
// Check if the action is set.
$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
@@ -170,6 +180,16 @@
* Handle the custom bulk action to unset posts or pages as blocked.
*/
function handle_pvb_unset_postspages_bulk_action() {
+ // Check user permissions.
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( __( 'You do not have sufficient permissions to access this page.', 'proxy-vpn-blocker' ) );
+ }
+
+ // Verify nonce for security.
+ if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-posts' ) ) {
+ wp_die( __( 'Security check failed.', 'proxy-vpn-blocker' ) );
+ }
+
// Check if the action is set.
$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
--- a/proxy-vpn-blocker/includes/setup-wizard/pvb-setup-wizard.php
+++ b/proxy-vpn-blocker/includes/setup-wizard/pvb-setup-wizard.php
@@ -147,7 +147,6 @@
<?php
}
?>
- <input type="text" autocomplete="off" placeholder="XXXXXX-XXXXXX-XXXXXX-XXXXXX" spellcheck="false" id="pvb_proxycheckio_API_Key_field" name="pvb_proxycheckio_API_Key_field" value="<?php echo esc_attr( get_option( 'pvb_proxycheckio_API_Key_field' ) ); ?>" class="regular-text">
<p class="option-description">
<?php esc_html_e( 'Enter your proxycheck.io API key to enable advanced proxy and VPN detection features. If you don't have an API key, you can still use the service with limited queries.', 'proxy-vpn-blocker' ); ?>
</p>
--- a/proxy-vpn-blocker/proxy-vpn-blocker-function.php
+++ b/proxy-vpn-blocker/proxy-vpn-blocker-function.php
@@ -11,7 +11,7 @@
* Plugin Name: Proxy & VPN Blocker
* Plugin URI: https://proxyvpnblocker.com
* description: Proxy & VPN Blocker prevents Proxies, VPN's and other unwanted visitors from accessing pages, posts and more, using Proxycheck.io API data.
- * Version: 3.5.3
+ * Version: 3.5.4
* Author: Proxy & VPN Blocker
* Author URI: https://profiles.wordpress.org/rickstermuk
* License: GPLv2
@@ -26,8 +26,8 @@
}
-$version = '3.5.3';
-$update_date = 'December 5th 2025';
+$version = '3.5.4';
+$update_date = 'December 21st 2025';
// Set the proxycheck.io v3 API version we are using.
$proxycheck_api_version = '10-October-2025';
@@ -499,24 +499,46 @@
// Check if current path matches any protected path.
$is_protected = false;
- foreach ( $blocked_paths as $protected_path ) {
- $protected_path = trim( $protected_path, '/' );
-
- if ( empty( $protected_path ) ) {
- continue;
+ // Handle both data formats:
+ // 1. Premium format: array('path/' => array('method' => '...', 'redirect_url' => '...'))
+ // 2. Simple format: array('path/', 'path2/')
+ if ( ! empty( $blocked_paths ) && is_array( $blocked_paths ) ) {
+ // Check if it's the premium format (associative array with path keys)
+ $path_keys = array();
+ foreach ( $blocked_paths as $key => $value ) {
+ if ( is_array( $value ) && isset( $value['method'] ) ) {
+ // Premium format: key is the path
+ $path_keys[] = $key;
+ } elseif ( is_string( $value ) ) {
+ // Simple format: value is the path
+ $path_keys[] = $value;
+ }
}
- // Check for exact match.
- if ( $request_path === $protected_path ) {
- $is_protected = true;
- break;
+ // If no paths were extracted, try treating keys as paths (fallback)
+ if ( empty( $path_keys ) ) {
+ $path_keys = array_keys( $blocked_paths );
}
- // Check if request path starts with protected path (for sub-pages).
- // e.g., protecting "courses" also protects "courses/advanced-php".
- if ( strpos( $request_path . '/', $protected_path . '/' ) === 0 ) {
- $is_protected = true;
- break;
+ foreach ( $path_keys as $protected_path ) {
+ $protected_path = trim( $protected_path, '/' );
+
+ if ( empty( $protected_path ) ) {
+ continue;
+ }
+
+ // Check for exact match.
+ if ( $request_path === $protected_path ) {
+ $is_protected = true;
+ break;
+ }
+
+ // Check if request path starts with protected path (for sub-pages).
+ // e.g., protecting "courses" also protects "courses/advanced-php".
+ if ( strpos( $request_path . '/', $protected_path . '/' ) === 0 ) {
+ $is_protected = true;
+ break;
+ }
}
}
--- a/proxy-vpn-blocker/pvb-db-upgrade.php
+++ b/proxy-vpn-blocker/pvb-db-upgrade.php
@@ -81,7 +81,10 @@
captcha_passed varchar(100) NOT NULL,
blocked_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
api_type varchar(100) NOT NULL,
- PRIMARY KEY (id)
+ PRIMARY KEY (id),
+ INDEX idx_blocked_at (blocked_at),
+ INDEX idx_blocked_at_ip (blocked_at, ip_address),
+ INDEX idx_blocked_at_country (blocked_at, country, country_iso)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
@@ -208,7 +211,10 @@
block_method varchar(100) NOT NULL,
captcha_passed varchar(100) NOT NULL,
blocked_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
- PRIMARY KEY (id)
+ PRIMARY KEY (id),
+ INDEX idx_blocked_at (blocked_at),
+ INDEX idx_blocked_at_ip (blocked_at, ip_address),
+ INDEX idx_blocked_at_country (blocked_at, country, country_iso)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
@@ -306,7 +312,10 @@
captcha_passed varchar(100) NOT NULL,
blocked_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
api_type varchar(100) NOT NULL,
- PRIMARY KEY (id)
+ PRIMARY KEY (id),
+ INDEX idx_blocked_at (blocked_at),
+ INDEX idx_blocked_at_ip (blocked_at, ip_address),
+ INDEX idx_blocked_at_country (blocked_at, country, country_iso)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';