Restrict WordPress Pages or Products by Country

WordPress Geolocation Access Control

Conditionally restrict access to WordPress posts, pages and products by country, based on visitor IP geo-location lookups. Based on the country code, we’ll either allow access, redirect the visitor to another location, or redirect to the login page. We’ll do all this without using a plug-in.

To make this work, we’re going to split the project into two smaller problems:

One: Get the client/browser IP address and turn it into useful geographical data.

Two: If we can determine a geo-located two-character ISO country code (e,g. “GB”, “DE, “IN”, “US”, etc…), apply our conditional-access logic.

loading
Your IP
City
Country
Your IP geo-location

IP Geo-location

There are lots of ways we can turn an IP address into geo-location data, and they all have various dependencies, such as PHP libraries and proprietary databases. But we’re going to avoid these dependencies and interrogate an external API from ipgeolocation.io. You can get a free developer account, which will be fine for most cases.

Restrict Access

Once we’ve got the two-character ISO country code for the visitor, we can use the same technique as in our Private WordPress Site tutorial to redirect the visitor to a different URL.

Scaffold the Code

Start by creating a new file in your custom child theme’s folder called wpt-access-control.php – paste the following into it. Make sure to set WPTAC_GEOIP_API_KEY to your API Key from ipgeolocation.io.

<?php

/**
 * IP Geo-location content restrictions.
 *
 * https://wp-tutorials.tech/add-functionality/block-pages-by-country/
 */

defined('WPINC') || die();


// IMPORTANT: Paste your API Key from https://ipgeolocation.io
const WPTAC_GEOIP_API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';


// Set to zero if you want to disable caching the geo-location data.
const WPTAC_IP_INFO_TTL = DAY_IN_SECONDS; // 0;


/**
 * Fetch geo-lcoation data for an IP address. If no address is specified,
 * use the client browser's IP  address.
 */
function wptac_get_ip_info(string $ip_address = '') {
	$wptac_ip_info = null;

	// ...
	// ...
	// ...

	return $wptac_ip_info;
}


/**
 * Run some conditional checks on the visitor's IP address and maybe redirect
 * them to an alternative URL.
 */
function wptac_template_redirect() {
	// ...
	// ...
	// ...
}

add_action('template_redirect', 'wptac_template_redirect');


/**
 * Diagnostic shortcode:
 * Useful for testing the link to the IP Geolocation API.
 */
function wptac_do_shortcode_geo_info($atts) {
	$html = '';

	if (is_admin()) {
		// Don't do anything.
	} elseif (wp_doing_ajax()) {
		// Don't do anything.
	} else {
		$ip_info = wptac_get_ip_info();

		$html .= '<div class="ipgeo-diagnostics">';
		if (!empty($ip_info)) {
			$html .= sprintf('<pre>%s</pre>', print_r($ip_info, true));
		} else {
			$html .= 'Failed to get IP info';
		}
		$html .= '</div>';
	}

	return $html;
}
add_shortcode('wpt_ipgeo', 'wptac_do_shortcode_geo_info');

Notice how we’ve got a diagnostic shortcode in there, called “wpt_ipgeo”. We can use this to test the connection to the IP geo-location API.

Open your child theme’s function.php and add the following couple of lines:

// WP Access Control by IP Address / Country
require_once dirname(__FILE__) . '/wpt-access-control.php';

Save all that, reload your site and make sure nothing’s broken. Now we can start filling in the gaps.

IP Geo Location

The IP geo-location code fits into a single function, so all we need to do is edit wpt-access-control.php and replace the wptac_get_ip_info() function with the following:

function wptac_get_ip_info(string $ip_address = '') {
	$wptac_ip_info = null;
	$is_laoded_from_cache = false;

	// If no IP address is specified, get the browser IP address - the address
	// of the original HTTP request.
	if (!empty($ip_address)) {
		// ...
	} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
		$ip_address = filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP);
	} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
		$ip_address = filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP);
	} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
		$ip_address = filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP);
	} else {
		$ip_address = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP);
	}

	if (empty($ip_address)) {
		// If we can't determine the IP address, we can't do a lookup.
	} elseif (empty($ip_info_key = 'geoip_' . $ip_address)) {
		// We should never end up in here.
	} elseif ((WPTAC_IP_INFO_TTL > 0) && (($wptac_ip_info = get_transient($ip_info_key)) !== false)) {
		// We've already cached the geo-location data, so we don't need to
		// do another lookup.
		$is_laoded_from_cache = true;
	} else {
		$url = sprintf('https://api.ipgeolocation.io/ipgeo?apiKey=%s&ip=%s', urlencode(WPTAC_GEOIP_API_KEY), urlencode($ip_address));
		$args = array('timeout' => 5);

		// Call the ipgeolcation.io API.
		if (!is_array($response = wp_remote_get($url, $args))) {
			// Bad response from the server.
		} elseif (!is_array($wptac_ip_info = json_decode(wp_remote_retrieve_body($response), true))) {
			// Bad response from the server.
			$wptac_ip_info = null;
		} elseif (!array_key_exists('country_code2', $wptac_ip_info)) {
			// No country code, so there's no need to proceed.
			$wptac_ip_info = null;
		} else {
			// All good.
			// We now have an array with our geo-location data:
			//
			// $wptac_ip_info
			//

			// Store the geolocation data as a transient.
			if (WPTAC_IP_INFO_TTL > 0) {
				set_transient($ip_info_key, $wptac_ip_info, WPTAC_IP_INFO_TTL);
			}
		}
	}

	// Make sure we always return an array, even if we didn't find anything.
	if (!is_array($wptac_ip_info)) {
		$wptac_ip_info = array();
	}

	if ($wptac_ip_info) {
		$wptac_ip_info['is_cached'] = ($is_laoded_from_cache ? 'yes' : 'no');
	}

	return $wptac_ip_info;
}

Save that, then add the “wpt_ipgeo” diagnostic shortcode to some content and check the response from the API. If it’s working properly, you’ll see an array full of interesting location-data. Check out the final “is_cached” element – it’ll let us know if the transient-caching is working correctly. The first response should be uncached, and subsequent calls should show is_cached => yes.

The code should be easy enough to read through, and the logic breaks down like this:

  • If we’ve not been passed an IP address, then…
    • Get the client IP address of the original HTTP Request. We look for this in a few places in case we’re behind a proxy, such as Cloudflare
  • Create a key (string) that’s unique to this IP address
  • If the key does exist as a transient, then…
    • Fetch the geo-location data from the transient, using the “key”
  • else…
    • Create the URL for the geo-location API request
    • Create a small $args array for the request
    • Call the ipgeolocation.io API
    • If the response from the API is good, then…
      • Store the geo-location data in a transient.
  • Return the geo-location data, or an empty array if the API call failed
IP Geolocation Shortcode
IP Geolocation shortcode
Caching data in a transient
Caching in a “transient”

That’s the tricky bit done – we’ve called the external API and got some geo-location data for our visitors.

tip You can replace the call to ipgeolocation.io API with an alternative lookup, if you want to. You could pull it from the WooCommerce MaxMind database, or maybe just use the native PHP GeoIP functions (if they’re available on your server). As long as the function returns an array with the ‘country_code2’ element set to a two-character ISO country code, you’re all set.

Access Control Based on Country

Go back into wpt-access-control.php and replace the contents of wptac_template_redirect() with the following:

function wptac_template_redirect() {
	// Which URL should unquthorised visitors be sent to.
	$redirect_url = site_url('/'); // << The site's front page.

	// Set this to false to be more restrictive. Be careful though
	// - you don't want to block access to the front page.
	$is_authorised = true;

	if (is_front_page()) {
		$is_authorised = true;
	} elseif (empty($ip_info = wptac_get_ip_info())) {
		//
	} elseif (is_single()) {
		// Only visitors from GB, DE, US and IN are allowed to read Posts.
		$is_authorised = in_array($ip_info['country_code2'], array('DE', 'GB', 'IN', 'US'));
	} elseif (function_exists('is_product') && is_product()) {
		// Visitors from Austria are not allowed to view WooCommerce single product pages.
		$is_authorised = !in_array($ip_info['country_code2'], array('AT'));
	} else {
		// ...
	}

	if (!$is_authorised) {
		error_log('Redirect unauthorised visitor');

		if (!empty($redirect_url)) {
			// Redirect to a specified URL.
			wp_safe_redirect($redirect_url);
			exit;
		} else {
			// Redirect to the login page.
			// auth_redirect();
		}
	}
}

This is just an example of the sorts of things you can do. In here, we’ve got examples of:

  • An always-allow list of countries that can view single post pages.
  • A block-list of countries that are not authorised to view product pages.

Testing the Restrictions

Testing can be tricky with this sort of thing. You can do things like “fake” the IP geo-location data by hacking the country code on-the-fly, but it’s not a fair test of the code or the process.

While developing the code for this tutorial, here’s what I did:

  1. Started a virtual machine running Debian Linux.
  2. Installed Nord VPN on the virtual machine.
  3. Connected to a VPN endpoint in Germany to check that I could read posts and view products.
  4. Connected to a VPN endpoint in Austria. When I tried to view a product, I got redirected to the site’s front page.

Have a play with it – there’s quite a lot you can do with it but there are some important caveats:

  • If you use a page caching plugin… cached pages will be served before the template_redirect action is triggered. You might still be able to use code from this tutorial, but it depends on which caching plugin you’re using.
  • If you’re going to store visitor IP geo-location data in a transient, even for 24 hours, you should probably mention this in your privacy policy. There could be GDPR implications.

Have fun with your country-specific content 🇬🇧 🇺🇸 🇩🇪 🇮🇳 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment