'; add_menu_page('WPLocation', 'WPLocation', 'manage_options', 'wplocation', 'wplocation_settings_page', 'data:image/svg+xml;base64,' . base64_encode($icon), 59); foreach (wplocation_admin_sections() as $key => $label) { if ($key === 'overview') { continue; } add_submenu_page('wplocation', $label . ' — WPLocation', $label, 'manage_options', 'wplocation-' . $key, 'wplocation_settings_page'); } // An already-open legacy form can still submit safely during the upgrade. add_submenu_page(null, 'WPLocation', 'WPLocation', 'manage_options', 'wplocation/lib/settings.php', 'wplocation_settings_page'); } function wplocation_admin_sections() { return array('overview' => __('Overview', 'wplocation'), 'categories' => __('Categories', 'wplocation'), 'pages' => __('Pages', 'wplocation'), 'posts' => __('Posts', 'wplocation'), 'shortcode' => __('Shortcodes', 'wplocation'), 'settings' => __('Settings', 'wplocation'), 'diagnostics' => __('Diagnostics', 'wplocation'), 'logs' => __('Activity log', 'wplocation'), 'addons' => __('Add-ons & Upgrade', 'wplocation')); } /** Preserve bookmarked legacy URLs without leaving a second menu entry. */ function wplocation_redirect_legacy_admin_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This GET only redirects a bookmarked admin URL; it changes no settings. if (!is_admin() || strtoupper(isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET') !== 'GET' || !current_user_can('manage_options') || !isset($_GET['page']) || sanitize_text_field(wp_unslash($_GET['page'])) !== 'wplocation/lib/settings.php') { return; } $args = array(); foreach (array('tab', 'rule', 's', 'paged', 'test_country') as $key) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- These parameters only select a read-only admin view. if (isset($_GET[$key]) && is_scalar($_GET[$key])) { $args[$key] = sanitize_text_field(wp_unslash($_GET[$key])); } } wp_safe_redirect(wplocation_admin_url($args)); exit; } add_action('init', 'wplocation_redirect_legacy_admin_page', 0); function wplocation_admin_assets($hook) { if (strpos($hook, 'wplocation') === false) { return; } wp_enqueue_style('wplocation-admin', plugins_url('assets/css/dashboard.css', dirname(__FILE__)), array(), WPLOCATION_VERSION); wp_enqueue_script('wplocation-admin', plugins_url('assets/js/admin.js', dirname(__FILE__)), array(), WPLOCATION_VERSION, true); } add_action('admin_enqueue_scripts', 'wplocation_admin_assets'); function wplocation_admin_url($args = array()) { $tab = isset($args['tab']) ? sanitize_key($args['tab']) : 'overview'; unset($args['tab']); if (!array_key_exists($tab, wplocation_admin_sections())) { $tab = 'overview'; } return add_query_arg(array_merge(array('page' => $tab === 'overview' ? 'wplocation' : 'wplocation-' . $tab), $args), admin_url('admin.php')); } function wplocation_render_next_database_check($hook) { $next = wp_next_scheduled($hook); if ($next) { echo '

' . esc_html__('Next scheduled check:', 'wplocation') . ' ' . esc_html(wp_date(get_option('date_format') . ' ' . get_option('time_format'), $next)) . '

'; } else { echo '

' . esc_html__('No database check is currently scheduled. Check that updates are enabled and WordPress cron is working.', 'wplocation') . '

'; } } function wplocation_settings_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You cannot manage WPLocation settings.', 'wplocation')); } $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : (strpos($page, 'wplocation-') === 0 ? substr($page, strlen('wplocation-')) : 'overview'); if (!array_key_exists($tab, wplocation_admin_sections())) { $tab = 'overview'; } $id = isset($_GET['rule']) ? absint($_GET['rule']) : 0; $ip_test_result = null; if ($tab === 'diagnostics' && isset($_POST['wplocation_test_ip'])) { check_admin_referer('wplocation_test_ip'); $address = isset($_POST['test_ip']) && is_string($_POST['test_ip']) ? trim(sanitize_text_field(wp_unslash($_POST['test_ip']))) : ''; if (!filter_var($address, FILTER_VALIDATE_IP)) { $ip_test_result = __('Enter a valid IPv4 or IPv6 address.', 'wplocation'); } else { $test_iso = wplocation_get_country_iso($address); $name = $test_iso ? wplocation_iso_to_country($test_iso) : false; $ip_test_result = $test_iso ? $test_iso . ($name ? ' — ' . $name : '') : __('No country was found. Check the database and its consent settings.', 'wplocation'); } } if (in_array($tab, array('settings', 'diagnostics'), true) && isset($_POST['wplocation_clear_database_lock'])) { check_admin_referer('wplocation_clear_database_lock'); $lock = wplocation_database_lock_value(); if (!isset($_POST['confirm_no_update']) || !wplocation_database_lock_is_old($lock) || !wplocation_release_database_lock($lock)) { wp_die(esc_html__('The lock could not be cleared. Confirm that no update is running and that the lock is at least one hour old.', 'wplocation')); } wplocation_record_event('lock_reset', 'success'); echo '

WPLocation

' . esc_html__('Database lock cleared.', 'wplocation') . '

' . esc_html__('Return to diagnostics', 'wplocation') . '

'; return; } if (in_array($tab, array('settings', 'diagnostics'), true) && isset($_POST['wplocation_use_configured_db'])) { check_admin_referer('wplocation_use_configured_db'); $source = wplocation_configured_database_path(); if (!is_string($source) || !is_file($source) || !is_readable($source)) { wp_die(esc_html__('The configured database is not readable.', 'wplocation')); } try { $reader = new \MaxMind\Db\Reader($source); $reader->metadata(); $reader->close(); } catch (Throwable $error) { wp_die(esc_html__('The configured database could not be read.', 'wplocation')); } $lock = wplocation_acquire_database_lock(); if (!$lock) { wp_die(esc_html__('A database operation is already running. Try again later.', 'wplocation')); } try { $source_changed = !wplocation_source_matches($source); if (!$source_changed) { wplocation_retire_selected_managed_database(); delete_option('wplocation_managed_database'); wplocation_purge_supported_cache(); } } finally { wplocation_release_database_lock($lock); } if ($source_changed) { wp_die(esc_html__('The database source changed. Reload settings and try again.', 'wplocation')); } wplocation_record_event('database_selection', 'success'); echo '

WPLocation

' . esc_html__('Configured database selected. Disable automatic updates if you maintain it externally, and purge other page and CDN caches.', 'wplocation') . '

' . esc_html__('Return to diagnostics', 'wplocation') . '

'; return; } if ($tab === 'settings' && isset($_POST['wplocation_save_maxmind_credentials'])) { check_admin_referer('wplocation_save_maxmind_credentials'); if (defined('WPLOCATION_MAXMIND_ACCOUNT_ID') || defined('WPLOCATION_MAXMIND_LICENSE_KEY')) { wp_die(esc_html__('MaxMind credentials are controlled by wp-config.php on this site.', 'wplocation')); } $account = isset($_POST['maxmind_account_id']) && is_string($_POST['maxmind_account_id']) ? trim(sanitize_text_field(wp_unslash($_POST['maxmind_account_id']))) : ''; $key = isset($_POST['maxmind_license_key']) && is_string($_POST['maxmind_license_key']) ? trim(sanitize_text_field(wp_unslash($_POST['maxmind_license_key']))) : ''; $clear = isset($_POST['clear_maxmind_credentials']); if (!$clear && (!ctype_digit($account) || strlen($account) > 20 || ($key !== '' && (strlen($key) > 256 || !preg_match('/^[A-Za-z0-9_-]+$/', $key))))) { wp_die(esc_html__('Enter a numeric MaxMind account ID and a valid license key.', 'wplocation')); } if (!$clear && $key === '' && !get_option('wplocation_maxmind_license_key')) { wp_die(esc_html__('Enter a MaxMind license key.', 'wplocation')); } $lock = wplocation_acquire_database_lock(); if (!$lock) { wp_die(esc_html__('A database operation is already running. Try again later.', 'wplocation')); } try { if ($clear) { delete_option('wplocation_maxmind_account_id'); delete_option('wplocation_maxmind_license_key'); update_option('wplocation_updates_enabled', 0, false); wplocation_update_schedule(); } else { update_option('wplocation_maxmind_account_id', $account, false); if ($key !== '') { update_option('wplocation_maxmind_license_key', $key, false); } } } finally { wplocation_release_database_lock($lock); } wplocation_record_event('maxmind_settings', 'success'); echo '

' . esc_html($clear ? __('Saved MaxMind credentials removed and updates disabled.', 'wplocation') : __('MaxMind account settings saved. The license key is never shown again.', 'wplocation')) . '

'; } if ($tab === 'settings' && isset($_POST['wplocation_maxmind_initial'])) { check_admin_referer('wplocation_maxmind_initial'); $directory = isset($_POST['maxmind_directory']) && is_string($_POST['maxmind_directory']) ? sanitize_text_field(wp_unslash($_POST['maxmind_directory'])) : ''; $result = isset($_POST['maxmind_download_consent']) ? wplocation_maxmind_install_initial($directory) : 'not_configured'; wplocation_record_event('maxmind_setup', $result); $message = $result === 'success' ? __('GeoLite2 Country is now active. The previous file was retained and automatic updates remain off; enable them below after testing a lookup.', 'wplocation') : __('MaxMind setup did not complete. The previous source was retained. Check the account, entitlement, private directory and provider response.', 'wplocation'); echo '

WPLocation

' . esc_html($message) . ' ' . esc_html($result) . '

' . esc_html__('Return to settings', 'wplocation') . '

'; return; } if (in_array($tab, array('settings', 'diagnostics'), true) && isset($_POST['wplocation_update_settings'])) { check_admin_referer('wplocation_update_settings'); $enabled = isset($_POST['enable_updates']) ? 1 : 0; list($account, $key) = wplocation_maxmind_credentials(); if ($enabled && (!ctype_digit($account) || $key === '' || !is_file(wplocation_database_path()))) { wp_die(esc_html__('Configure MaxMind credentials and a valid database path first.', 'wplocation')); } if ($enabled) { try { $reader = new \MaxMind\Db\Reader(wplocation_database_path()); $type = $reader->metadata()->databaseType; $reader->close(); } catch (Throwable $error) { wp_die(esc_html__('The selected database could not be read.', 'wplocation')); } if ($type !== 'GeoLite2-Country') { wp_die(esc_html__('Select a GeoLite2 Country database before enabling MaxMind updates.', 'wplocation')); } } $lock = wplocation_acquire_database_lock(); if (!$lock) { wp_die(esc_html__('A database operation is already running. Try again later.', 'wplocation')); } try { update_option('wplocation_updates_enabled', $enabled, false); wplocation_update_schedule(); } finally { wplocation_release_database_lock($lock); } wplocation_record_event('maxmind_settings', 'success'); echo '

WPLocation

' . esc_html__('Update settings saved.', 'wplocation') . '

' . esc_html__('Return to diagnostics', 'wplocation') . '

'; return; } if (in_array($tab, array('settings', 'diagnostics'), true) && isset($_POST['wplocation_run_update'])) { check_admin_referer('wplocation_run_update'); wplocation_scheduled_database_update(); echo '

WPLocation

' . esc_html__('Update attempted. Check the status in diagnostics and purge caches if it succeeded.', 'wplocation') . '

' . esc_html__('Return to diagnostics', 'wplocation') . '

'; return; } if (in_array($tab, array('settings', 'diagnostics'), true) && isset($_POST['wplocation_save_db'])) { check_admin_referer('wplocation_save_database'); if (defined('WPLOCATION_GEOIP_DB_PATH')) { wp_die(esc_html__('The database path is set in wp-config.php.', 'wplocation')); } $path = isset($_POST['database_path']) ? sanitize_text_field(wp_unslash($_POST['database_path'])) : ''; if ($path !== '') { $resolved = realpath($path); if (!$resolved || !is_file($resolved) || !is_readable($resolved) || !wplocation_private_database_directory(dirname($resolved)) || strtolower(pathinfo($resolved, PATHINFO_EXTENSION)) !== 'mmdb') { wp_die(esc_html__('Choose a readable MMDB file outside the public web directory.', 'wplocation')); } try { $reader = new \MaxMind\Db\Reader($resolved); $type = $reader->metadata()->databaseType; $reader->close(); } catch (Throwable $error) { wp_die(esc_html__('This file is not a readable MaxMind-format database.', 'wplocation')); } if ($type !== 'GeoLite2-Country') { wp_die(esc_html__('Choose a GeoLite2 Country database.', 'wplocation')); } $path = $resolved; } $lock = wplocation_acquire_database_lock(); if (!$lock) { wp_die(esc_html__('A database operation is already running. Try again later.', 'wplocation')); } $saved = false; try { $changed = update_option('wplocation_db_path', $path, false); $saved = get_option('wplocation_db_path', '') === $path; if ($changed && $saved) { wplocation_retire_selected_managed_database(); delete_option('wplocation_managed_database'); update_option('wplocation_updates_enabled', 0, false); wplocation_update_schedule(); wplocation_purge_supported_cache(); } } finally { wplocation_release_database_lock($lock); } if (!$saved) { wp_die(esc_html__('The database selection could not be saved. The previous source remains active.', 'wplocation')); } wplocation_record_event('database_source', 'success'); echo '

' . esc_html__('WPLocation', 'wplocation') . '

' . esc_html__('Database path saved.', 'wplocation') . '

' . esc_html__('Return to diagnostics', 'wplocation') . '

'; return; } $rule_form_error = ''; $rule_form_values = null; if (isset($_POST['wplocation_save_rule']) && in_array($tab, array('categories', 'pages', 'posts'), true)) { check_admin_referer('wplocation_save_' . $tab . '_' . $id); $valid = $tab === 'categories' ? get_term($id, 'category') : get_post($id); if (!$id || !$valid || is_wp_error($valid) || ($tab !== 'categories' && $valid->post_type !== ($tab === 'pages' ? 'page' : 'post'))) { wp_die(esc_html__('The selected content was not found.', 'wplocation')); } if ($tab !== 'categories' && !current_user_can('edit_post', $id)) { wp_die(esc_html__('You cannot edit this content.', 'wplocation')); } if ($tab === 'categories' && !current_user_can('manage_categories')) { wp_die(esc_html__('You cannot edit categories.', 'wplocation')); } $action = isset($_POST['rule_action']) ? sanitize_key(wp_unslash($_POST['rule_action'])) : ''; if (!in_array($action, array('all', 'include', 'exclude'), true)) { wp_die(esc_html__('Choose a valid audience.', 'wplocation')); } $countries = isset($_POST['countries']) && is_array($_POST['countries']) ? array_values(array_unique(array_intersect(array_map('sanitize_text_field', wp_unslash($_POST['countries'])), wplocation_countries_array()))) : array(); if ($action === 'all') { $countries = array(); $action = 'include'; } if (!$countries && isset($_POST['rule_action']) && sanitize_key(wp_unslash($_POST['rule_action'])) !== 'all') { $rule_form_error = __('Choose at least one country, or select Everyone to remove the restriction.', 'wplocation'); $rule_form_values = array($countries, sanitize_key(wp_unslash($_POST['rule_action']))); } elseif ($tab === 'categories') { update_term_meta($id, 'wplocation_cat_location_' . $id, $countries); update_term_meta($id, 'wplocation_action_option_' . $id, $action); } elseif ($tab === 'pages') { update_option('wplocation_page_location_' . $id, $countries); update_option('wplocation_page_action_option_' . $id, $action); } else { update_post_meta($id, 'wplocation_post_location_' . $id, $countries); update_post_meta($id, 'wplocation_post_action_option_' . $id, $action); } if (!$rule_form_error) { wplocation_purge_supported_cache(); wplocation_record_event('rule_saved', 'success'); echo '

' . esc_html__('Audience saved. Purge page and CDN caches before testing changes.', 'wplocation') . '

'; } } echo '

WPLocation

' . esc_html__('COUNTRY PERSONALIZATION', 'wplocation') . '

' . esc_html__('Create country rules, manage location data and see what needs attention.', 'wplocation') . '

' . esc_html__('Manage settings', 'wplocation') . '
'; if (!is_file(wplocation_database_path())) { echo '

' . esc_html__('No local country database is configured. Targeted content is hidden until location can be detected; see Diagnostics and check your cache.', 'wplocation') . '

'; } echo ''; if ($tab === 'overview') { wplocation_render_overview(); } elseif ($tab === 'shortcode') { echo '

' . esc_html__('CONTENT TOOLS', 'wplocation') . '

' . esc_html__('Shortcodes', 'wplocation') . '

' . esc_html__('Copy these examples into post or page content. A visitor whose country is unknown does not see targeted content.', 'wplocation') . '

'; echo '

' . esc_html__('Show content by country', 'wplocation') . '

' . esc_html__('Use country names or two-letter codes.', 'wplocation') . '

[wplocation_target country="US, PK"]Content for visitors in the US or Pakistan[/wplocation_target]
[wplocation_target exclude_country="US"]Content outside the US[/wplocation_target]
'; echo '

' . esc_html__('Display the detected country', 'wplocation') . '

' . esc_html__('Show the country name, code or flag emoji. Provide a fallback when no country can be detected. Flag appearance depends on the visitor’s device.', 'wplocation') . '

[wplocation_country fallback="your region"]
[wplocation_country format="code" fallback="Unknown"]
[wplocation_country format="flag" fallback="Unknown"]
'; } elseif ($tab === 'settings') { echo '

' . esc_html__('DATA SOURCES', 'wplocation') . '

' . esc_html__('Settings', 'wplocation') . '

' . esc_html__('Choose one local country database and its update method. Your rules stay in place if you change providers.', 'wplocation') . '

'; $current_type = ''; $current_path = wplocation_database_path(); if (is_file($current_path)) { try { $source_reader = new \MaxMind\Db\Reader($current_path); $current_type = (string) $source_reader->metadata()->databaseType; $source_reader->close(); } catch (Throwable $error) { $current_type = ''; } } $suggested_private_dir = ''; if (is_file($current_path)) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $suggested_private_dir = wplocation_private_database_directory(dirname($current_path), new WP_Filesystem_Direct(null)) ?: ''; } $current_label = $current_type === 'GeoLite2-Country' ? 'MaxMind GeoLite2 Country' : __('No valid country database selected', 'wplocation'); echo '

' . esc_html__('ACTIVE COUNTRY SOURCE', 'wplocation') . '

' . esc_html($current_label) . '

' . esc_html__('To switch sources, choose a setup path below. A successful switch disables the old automatic updater; enable updates for the new source after verifying a test lookup.', 'wplocation') . '

'; echo '

' . esc_html__('EXISTING FILE', 'wplocation') . '

' . esc_html__('Local database', 'wplocation') . '

' . esc_html__('Choose a licensed country MMDB on this server. Keep it outside the public web directory. Visitor IP addresses stay on your server during lookups.', 'wplocation') . '

'; if (defined('WPLOCATION_GEOIP_DB_PATH')) { echo '

' . esc_html__('The database path is managed in wp-config.php. Remove or change WPLOCATION_GEOIP_DB_PATH there before switching providers here.', 'wplocation') . '

'; } else { echo '
'; wp_nonce_field('wplocation_save_database'); echo '

' . esc_html__('Selecting a different file turns off automatic updates; your old file remains on disk.', 'wplocation') . '

'; } if (wplocation_active_database_path() !== wplocation_configured_database_path()) { echo '

' . esc_html__('The plugin is using a managed database copy. Older recorded copies are eligible for scheduled cleanup after two days; review provider deletion requirements and any other copies. Active file:', 'wplocation') . ' ' . esc_html(wplocation_active_database_path()) . '

'; wp_nonce_field('wplocation_use_configured_db'); echo '
'; } echo '

' . esc_html__('YOUR MAXMIND ACCOUNT', 'wplocation') . '

' . esc_html__('MaxMind GeoLite2 Country', 'wplocation') . '

' . esc_html__('Connect your own MaxMind account to download and update its licensed country database. Your account credentials go to MaxMind, then the signed file is downloaded from Cloudflare R2. Visitor IP addresses are not sent. WordPress cron requires site traffic or a server cron.', 'wplocation') . '

' . esc_html__('GeoLite terms', 'wplocation') . ' · ' . esc_html__('MaxMind privacy', 'wplocation') . '

'; if (defined('WPLOCATION_MAXMIND_ACCOUNT_ID') || defined('WPLOCATION_MAXMIND_LICENSE_KEY')) { echo '

' . esc_html__('This site supplies MaxMind credentials through wp-config.php. Those values take priority and cannot be changed here.', 'wplocation') . '

'; } else { echo '
'; wp_nonce_field('wplocation_save_maxmind_credentials'); echo ''; echo ''; echo '

' . esc_html__('Saved keys are stored as a non-autoloaded WordPress option, are never displayed here, and can appear in database backups. For stronger secret handling, define the account ID and key in wp-config.php instead.', 'wplocation') . '

'; echo '

'; } if (!defined('WPLOCATION_GEOIP_DB_PATH')) { echo '

' . esc_html__('Download and select GeoLite2 Country', 'wplocation') . '

' . esc_html__('Save your MaxMind account above first. Choose an existing writable private directory outside the web root. This action downloads a current database using your account and keeps the previous source if it fails.', 'wplocation') . '

'; wp_nonce_field('wplocation_maxmind_initial'); echo '


'; } echo '
'; wp_nonce_field('wplocation_update_settings'); echo '
'; if (get_option('wplocation_updates_enabled')) { $status = get_option('wplocation_update_status', array()); if (is_array($status) && isset($status['result'])) { echo '

' . esc_html__('Last update result:', 'wplocation') . ' ' . esc_html($status['result']) . '

'; } wplocation_render_next_database_check('wplocation_update_database_event'); echo '
'; wp_nonce_field('wplocation_run_update'); echo '
'; } if (is_file(wplocation_database_path())) { try { $reader = new \MaxMind\Db\Reader(wplocation_database_path()); $metadata = $reader->metadata(); $reader->close(); $epoch = (int) $metadata->buildEpoch; $built = $epoch > 0 ? gmdate('Y-m-d', $epoch) : ''; echo '

' . esc_html__('Database build date:', 'wplocation') . ' ' . esc_html($built ?: __('Unknown', 'wplocation')) . '

'; if ($epoch > 0 && $epoch < time() - 30 * DAY_IN_SECONDS) { echo '

' . esc_html__('The database is over 30 days old. Check the provider’s update and licensing requirements.', 'wplocation') . '

'; } } catch (Throwable $error) { echo '

' . esc_html__('The configured database could not be read. Check the file and permissions.', 'wplocation') . '

'; } } echo '
'; } elseif ($tab === 'diagnostics') { wplocation_render_diagnostics($ip_test_result); } elseif ($tab === 'addons') { wplocation_render_addons(); } elseif ($tab === 'logs') { wplocation_render_activity_log(); } elseif ($id) { wplocation_render_rule_editor($tab, $id, $rule_form_values, $rule_form_error); } else { wplocation_render_rule_list($tab); } echo '
'; } /** Informational catalog only: premium code lives in a separate add-on. */ function wplocation_render_addons() { echo '

' . esc_html__('EXTEND WPLocation', 'wplocation') . '

' . esc_html__('Add-ons & Upgrade', 'wplocation') . '

' . esc_html__('Free, Pro and add-ons are separate plugins. Check the WPLocation site for current availability, compatibility and pricing.', 'wplocation') . '

'; $offers = array( array(__('Included in Free', 'wplocation'), __('Post, page and category country rules, shortcodes, a local country database and diagnostics.', 'wplocation'), wplocation_admin_url(array('tab' => 'settings'))), array(__('WPLocation Pro', 'wplocation'), __('A separate licensed plugin for WooCommerce Markets, Geo Redirects, Forms & Campaigns and Geo Analytics. Free stays active alongside Pro.', 'wplocation'), 'https://wplocation.com/downloads/pro/'), array(__('Optional add-ons', 'wplocation'), __('CloudFront Country Source, Affiliate Link Localization and Agency Toolkit are separate plugins with their own setup and eligibility.', 'wplocation'), 'https://wplocation.com/add-ons/'), array(__('Future integrations', 'wplocation'), __('Additional integrations are described as planned on the website until released.', 'wplocation'), 'https://wplocation.com/add-ons/'), ); echo '
'; foreach ($offers as $offer) { echo ''; } echo '
'; } /** A short health view; configuration forms live on Settings. */ function wplocation_render_diagnostics($ip_test_result) { $path = wplocation_database_path(); $readable = is_string($path) && $path !== '' && is_file($path) && is_readable($path); $type = ''; $built = 0; $error = ''; if ($readable) { try { $reader = new \MaxMind\Db\Reader($path); $metadata = $reader->metadata(); $reader->close(); $type = (string) $metadata->databaseType; $built = (int) $metadata->buildEpoch; } catch (Throwable $exception) { $error = __('The database file cannot be read.', 'wplocation'); } } else { $error = __('No readable country database is selected.', 'wplocation'); } $stale = $built <= 0 || $built < time() - 30 * DAY_IN_SECONDS || $built > time() + DAY_IN_SECONDS; $ready = !$error && $type === 'GeoLite2-Country' && !$stale; echo '

' . esc_html__('SITE HEALTH', 'wplocation') . '

' . esc_html__('Diagnostics', 'wplocation') . '

' . esc_html__('Check whether country lookups can work, then test an address. Change providers and update settings on the Settings page.', 'wplocation') . '

'; echo '

' . esc_html__('DATABASE', 'wplocation') . '

' . esc_html($ready ? __('Ready', 'wplocation') : __('Needs attention', 'wplocation')) . '

' . esc_html($error ?: ($type !== 'GeoLite2-Country' ? __('Choose a GeoLite2 Country database.', 'wplocation') : ($stale ? __('The database date needs review.', 'wplocation') : __('The selected database can be read.', 'wplocation')))) . '

' . esc_html__('Source', 'wplocation') . '
' . esc_html($type ?: __('Unknown', 'wplocation')) . '
' . esc_html__('Build date', 'wplocation') . '
' . esc_html($built ? wp_date(get_option('date_format'), $built) : __('Unknown', 'wplocation')) . '
' . esc_html__('Manage database →', 'wplocation') . '
'; $provider = get_option('wplocation_updates_enabled') ? __('MaxMind', 'wplocation') : __('Off', 'wplocation'); $hook = 'wplocation_update_database_event'; $status = get_option('wplocation_update_status', array()); echo '

' . esc_html__('AUTOMATIC UPDATES', 'wplocation') . '

' . esc_html($provider) . '

' . esc_html__('Checks run through WordPress cron when traffic or server cron triggers it.', 'wplocation') . '

' . esc_html__('Last result', 'wplocation') . '
' . esc_html(is_array($status) && isset($status['result']) ? str_replace('_', ' ', sanitize_key($status['result'])) : __('No check recorded', 'wplocation')) . '
' . esc_html__('Next check', 'wplocation') . '
' . esc_html(wp_next_scheduled($hook) ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), wp_next_scheduled($hook)) : __('Not scheduled', 'wplocation')) . '
' . esc_html__('View activity log →', 'wplocation') . '
'; echo '

' . esc_html__('PRIVATE TEST', 'wplocation') . '

' . esc_html__('Test an IP address', 'wplocation') . '

' . esc_html__('Look up an IPv4 or IPv6 address using the local database. The test address is not saved or sent to a lookup provider.', 'wplocation') . '

'; wp_nonce_field('wplocation_test_ip'); echo '
'; if ($ip_test_result !== null) { echo '

' . esc_html__('Test result:', 'wplocation') . ' ' . esc_html($ip_test_result) . '

'; } echo '
'; $lock = wplocation_database_lock_value(); if (is_string($lock) && $lock !== '') { echo '

' . esc_html__('Database operation lock', 'wplocation') . '

' . esc_html__('A database operation is running or was interrupted. Settings changes must wait.', 'wplocation') . '

'; if (wplocation_database_lock_is_old($lock)) { echo '

' . esc_html__('This lock is over one hour old. Clear it only after checking that no update is still running.', 'wplocation') . '

'; wp_nonce_field('wplocation_clear_database_lock'); echo '

'; } echo '
'; } echo '

' . esc_html__('DELIVERY CHECKS', 'wplocation') . '

' . esc_html__('Cache safety', 'wplocation') . '

  • ' . esc_html__('Purge page and CDN caches after database, rule or plugin changes. A cache can serve a response before WordPress runs.', 'wplocation') . '
  • ' . esc_html__('Test a targeted URL from different visitor locations. IP location is approximate and cannot protect private content.', 'wplocation') . '
'; } /** Show setup state without sending an IP address to an external service. */ function wplocation_render_overview() { $state = __('Not configured', 'wplocation'); $detail = __('Choose a local country database to get started.', 'wplocation'); $healthy = false; if (is_string(wplocation_database_path()) && is_file(wplocation_database_path()) && is_readable(wplocation_database_path())) { try { $reader = new \MaxMind\Db\Reader(wplocation_database_path()); $meta = $reader->metadata(); $reader->close(); $epoch = (int) $meta->buildEpoch; if ($meta->databaseType !== 'GeoLite2-Country') { $state = __('Action needed', 'wplocation'); $detail = __('Choose a GeoLite2 Country database.', 'wplocation'); } elseif ($epoch <= 0 || $epoch < time() - 30 * DAY_IN_SECONDS || $epoch > time() + DAY_IN_SECONDS) { $state = __('Check data freshness', 'wplocation'); $detail = __('Check the database date and the provider’s update requirements.', 'wplocation'); } else { $state = __('Database readable', 'wplocation'); $detail = __('Your local database is configured. Check a visitor country in Diagnostics.', 'wplocation'); $healthy = true; } } catch (Throwable $error) { $state = __('Cannot read database', 'wplocation'); $detail = __('Check the database file and permissions.', 'wplocation'); } } $updating = (bool) get_option('wplocation_updates_enabled'); echo '
'; echo '

' . esc_html__('COUNTRY DATA', 'wplocation') . '

' . esc_html($state) . '

' . esc_html($healthy ? __('Ready to test', 'wplocation') : __('Review setup', 'wplocation')) . '

' . esc_html($detail) . '

' . esc_html__('Manage country data →', 'wplocation') . '
'; echo '

' . esc_html__('DATABASE UPDATES', 'wplocation') . '

' . esc_html($updating ? __('Scheduled checks enabled', 'wplocation') : __('Automatic checks off', 'wplocation')) . '

' . esc_html__('A current database improves location results. Scheduled checks rely on WordPress cron.', 'wplocation') . '

' . esc_html__('Manage updates →', 'wplocation') . '
'; echo '

' . esc_html__('CACHE SAFETY', 'wplocation') . '

' . esc_html__('Test your cache', 'wplocation') . '

' . esc_html__('Existing page and CDN caches can serve the wrong location. Purge and test them after changes.', 'wplocation') . '

' . esc_html__('See cache guidance →', 'wplocation') . '
'; echo '

' . esc_html__('GET STARTED', 'wplocation') . '

' . esc_html__('Make your content relevant by country', 'wplocation') . '

' . esc_html__('Choose a content type, select countries and test your saved rule before publishing.', 'wplocation') . '

'; foreach (array('posts' => __('Post rules', 'wplocation'), 'pages' => __('Page rules', 'wplocation'), 'categories' => __('Category rules', 'wplocation'), 'shortcode' => __('Shortcodes', 'wplocation')) as $key => $label) { echo '' . esc_html($label) . ''; } echo '

' . esc_html__('IP geolocation is an estimate. Use these controls for personalization, not for protecting confidential content.', 'wplocation') . '

'; } function wplocation_render_rule_list($tab) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Search only filters a read-only admin list. $search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Pagination only reads an admin list. $paged = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1; $limit = 20; if ($tab === 'categories') { $query = array('taxonomy' => 'category', 'hide_empty' => false, 'search' => $search); $count = (int) get_terms(array_merge($query, array('fields' => 'count'))); $items = get_terms(array_merge($query, array('number' => $limit, 'offset' => ($paged - 1) * $limit))); } else { $query = new WP_Query(array('post_type' => $tab === 'pages' ? 'page' : 'post', 'post_status' => array('publish', 'draft', 'private', 'future'), 'posts_per_page' => $limit, 'paged' => $paged, 's' => $search, 'orderby' => 'title', 'order' => 'ASC')); $items = $query->posts; $count = (int) $query->found_posts; } $list_help = $tab === 'posts' ? __('Choose a post to set its audience. Assigned category rules may restrict it further.', 'wplocation') : __('Choose an item to decide which visitors can see it. Everyone means no country restriction on this item.', 'wplocation'); echo '

' . esc_html__('VISIBILITY RULES', 'wplocation') . '

' . esc_html(ucfirst($tab)) . '

' . esc_html($list_help) . '

'; echo '
'; if (is_wp_error($items) || !$items) { echo ''; } else foreach ($items as $item) { $id = $tab === 'categories' ? $item->term_id : $item->ID; $name = $tab === 'categories' ? $item->name : $item->post_title; list($countries, $action) = wplocation_read_rule($tab, $id); echo ''; } echo '
' . esc_html__('Content', 'wplocation') . '' . esc_html__('Rule', 'wplocation') . '' . esc_html__('Countries', 'wplocation') . '
' . esc_html__('No matching content found.', 'wplocation') . '
' . esc_html($name ?: __('(Untitled)', 'wplocation')) . '' . esc_html($countries ? ($action === 'exclude' ? __('Everywhere except', 'wplocation') : __('Only selected', 'wplocation')) : __('Everyone', 'wplocation')) . '' . esc_html($countries ? implode(', ', array_slice($countries, 0, 4)) . (count($countries) > 4 ? ' +' . (count($countries) - 4) : '') : '—') . '
'; $pages = (int) ceil($count / $limit); if ($pages > 1) { echo '
' . wp_kses_post(paginate_links(array('base' => add_query_arg(array('paged' => '%#%'), wplocation_admin_url(array('tab' => $tab, 's' => $search))), 'format' => '', 'current' => $paged, 'total' => $pages))) . '
'; } echo '
'; } function wplocation_read_rule($tab, $id) { if ($tab === 'categories') { $countries = get_term_meta($id, 'wplocation_cat_location_' . $id, true); $action = get_term_meta($id, 'wplocation_action_option_' . $id, true); } elseif ($tab === 'pages') { $countries = get_option('wplocation_page_location_' . $id, array()); $action = get_option('wplocation_page_action_option_' . $id, ''); } else { $countries = get_post_meta($id, 'wplocation_post_location_' . $id, true); $action = get_post_meta($id, 'wplocation_post_action_option_' . $id, true); } return array(is_array($countries) ? $countries : array(), $action ?: 'include'); } function wplocation_render_rule_editor($tab, $id, $draft = null, $form_error = '') { $item = $tab === 'categories' ? get_term($id, 'category') : get_post($id); if (!$item || is_wp_error($item) || ($tab !== 'categories' && $item->post_type !== ($tab === 'pages' ? 'page' : 'post'))) { echo '

' . esc_html__('Content not found.', 'wplocation') . '

'; return; } list($saved_countries, $saved_action) = wplocation_read_rule($tab, $id); list($countries, $action) = $draft ?: array($saved_countries, $saved_countries ? $saved_action : 'all'); $title = $tab === 'categories' ? $item->name : ($item->post_title ?: __('(Untitled)', 'wplocation')); $type = $tab === 'categories' ? __('Category', 'wplocation') : ($tab === 'pages' ? __('Page', 'wplocation') : __('Post', 'wplocation')); $back_label = array('categories' => __('All categories', 'wplocation'), 'pages' => __('All pages', 'wplocation'), 'posts' => __('All posts', 'wplocation')); echo '
← ' . esc_html($back_label[$tab]) . '

' . esc_html($type) . ' · ' . esc_html__('VISIBILITY', 'wplocation') . '

' . esc_html($title) . '

' . esc_html__('Choose who can see this content based on their detected country.', 'wplocation') . '

'; $edit_link = $tab === 'categories' ? get_edit_term_link($id, 'category') : get_edit_post_link($id); if ($edit_link) { echo '' . esc_html__('Edit content', 'wplocation') . ''; } echo '
'; if ($form_error) { echo ''; } /* translators: %s: list of selected countries. */ $include_format = __('Only visitors in %s can see this content.', 'wplocation'); /* translators: %s: list of selected countries. */ $exclude_format = __('Visitors in %s cannot see this content.', 'wplocation'); /* translators: %s: country name. */ $remove_format = __('Remove %s', 'wplocation'); /* translators: %s: number of countries currently displayed. */ $match_format = __('%s country shown', 'wplocation'); /* translators: %s: number of countries currently displayed. */ $matches_format = __('%s countries shown', 'wplocation'); echo '
'; wp_nonce_field('wplocation_save_' . $tab . '_' . $id); echo '
1

' . esc_html__('Who should see it?', 'wplocation') . '

' . esc_html__('Choose one audience. You can change it anytime.', 'wplocation') . '

'; echo '
' . esc_html__('Choose an audience', 'wplocation') . '
'; $choices = array( 'all' => array(__('Everyone', 'wplocation'), __('No country restriction. This content stays visible even if a visitor’s location is unknown.', 'wplocation')), 'include' => array(__('Only selected countries', 'wplocation'), __('Show this content in the countries you choose. Hide it everywhere else.', 'wplocation')), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- This is a rule-mode label, not a query exclusion parameter. 'exclude' => array(__('Everywhere except selected countries', 'wplocation'), __('Hide this content in the countries you choose. Show it elsewhere.', 'wplocation')), ); foreach ($choices as $value => $copy) { echo ''; } echo '
'; echo '
2

' . esc_html__('Which countries?', 'wplocation') . '

' . esc_html__('Search and select one or more countries. You can remove a selection below.', 'wplocation') . '

' . esc_html__('Selected countries', 'wplocation') . '

'; foreach (wplocation_countries_array() as $country) { echo ''; } echo '
' . esc_html__('Changes appear after saving. Purge page and CDN caches if used.', 'wplocation') . '
'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This selects a read-only rule preview. $selected_test_country = isset($_GET['test_country']) && is_string($_GET['test_country']) ? strtoupper(sanitize_text_field(wp_unslash($_GET['test_country']))) : ''; $country_options = wplocation_country_code_map(); asort($country_options); echo '

' . esc_html__('Check the saved result', 'wplocation') . '

' . esc_html__('Choose a visitor country to see whether the saved rules show or hide this content. Save edits above first.', 'wplocation') . '

'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This renders a read-only rule preview. if (isset($_GET['test_country'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The country value is sanitized and only used to calculate preview output. $test = is_string($_GET['test_country']) ? strtoupper(sanitize_text_field(wp_unslash($_GET['test_country']))) : ''; if (preg_match('/^[A-Z]{2}$/', $test) && wplocation_iso_to_country($test)) { $allowed = $tab === 'posts' ? wplocation_content_allowed($item, $test) : wplocation_rule_allows($saved_countries, $saved_action, $test); $result = $allowed ? __('Visible', 'wplocation') : __('Hidden', 'wplocation'); /* translators: 1: country name; 2: two-letter country code. */ $test_description = sprintf(__('for a visitor in %1$s (%2$s).', 'wplocation'), wplocation_iso_to_country($test), $test); echo '

' . esc_html($result) . ' ' . esc_html($test_description) . '

'; } else { echo '

' . esc_html__('Choose a recognized country.', 'wplocation') . '

'; } } $preview_names = implode(', ', array_slice($countries, 0, 3)); if (count($countries) > 3) { $preview_names .= ' +' . (count($countries) - 3); } $initial_summary = $action === 'all' ? __('Everyone can see this content, including visitors whose country is unknown.', 'wplocation') : (!$countries ? __('Choose at least one country to finish this audience.', 'wplocation') : sprintf($action === 'exclude' ? $exclude_format : $include_format, $preview_names)); echo '
'; } /** Alert administrators when a previously working source disappears. */ function wplocation_missing_database_notice() { if (!current_user_can('manage_options') || (is_string(wplocation_database_path()) && is_file(wplocation_database_path()) && is_readable(wplocation_database_path()))) { return; } echo '

' . esc_html__('WPLocation cannot read a country database. Content with country rules may be hidden until the database is configured. Purge page and CDN caches after setup.', 'wplocation') . ' ' . esc_html__('Open diagnostics', 'wplocation') . '

'; } add_action('admin_notices', 'wplocation_missing_database_notice');