'sanitize_text_field', 'default' => '', )); register_setting('ad_unblock_options', 'ad_unblock_site_domain', array( 'sanitize_callback' => array($this, 'sanitize_site_domain'), 'default' => '', )); register_setting('ad_unblock_options', 'ad_unblock_custom_api_endpoint', array( 'sanitize_callback' => 'esc_url_raw', 'default' => '', )); register_setting('ad_unblock_options', 'ad_unblock_page_rules', array( 'sanitize_callback' => array($this, 'sanitize_page_rules'), 'default' => array( 'all_pages' => 'yes', 'url_patterns' => '', 'categories' => array(), 'tags' => array(), ), )); add_settings_section( 'ad_unblock_section_main', esc_html__('Main Settings', 'ad-unblock'), array($this, 'render_section_main'), 'ad_unblock' ); add_settings_field( 'ad_unblock_verification_code', esc_html__('Account ID', 'ad-unblock'), array($this, 'render_verification_code_field'), 'ad_unblock', 'ad_unblock_section_main' ); add_settings_field( 'ad_unblock_page_rules', esc_html__('Enable On Pages', 'ad-unblock'), array($this, 'render_page_rules_field'), 'ad_unblock', 'ad_unblock_section_main' ); add_settings_field( 'ad_unblock_site_domain', esc_html__('Site Domain', 'ad-unblock'), array($this, 'render_site_domain_field'), 'ad_unblock', 'ad_unblock_section_main' ); add_settings_field( 'ad_unblock_custom_api_endpoint', esc_html__('API Endpoint', 'ad-unblock'), array($this, 'render_custom_api_endpoint_field'), 'ad_unblock', 'ad_unblock_section_main' ); } /** * Sanitize the site domain setting * * Stored in the same normalized form the API keys its per-domain script * lists on, so a pasted URL ("https://www.example.com/") and a bare host * ("www.example.com") resolve to the same list. An unusable value is * stored as empty, which falls back to the site's own domain. */ public function sanitize_site_domain($input) { return $this->normalize_domain(is_string($input) ? $input : ''); } /** * Sanitize page rules */ public function sanitize_page_rules($input) { $sanitized = array( 'all_pages' => isset($input['all_pages']) && $input['all_pages'] === 'yes' ? 'yes' : 'no', 'url_patterns' => isset($input['url_patterns']) ? sanitize_textarea_field($input['url_patterns']) : '', 'categories' => array(), 'tags' => array(), ); if (isset($input['categories']) && is_array($input['categories'])) { $sanitized['categories'] = array_map('intval', $input['categories']); } if (isset($input['tags']) && is_array($input['tags'])) { $sanitized['tags'] = array_map('intval', $input['tags']); } return $sanitized; } /** * Render the admin page */ public function render_admin_page() { if (!current_user_can('manage_options')) { return; } ?>

Ad Unblock Logo AdUnblock

  1. ad-unblock.com', 'ad-unblock'), array('a' => array('href' => array(), 'target' => array()))); ?>

render_cache_status(); ?>

'; echo '

' . esc_html__('Configure your Ad Unblock integration settings below.', 'ad-unblock') . '

'; echo ''; } /** * Render verification code field */ public function render_verification_code_field() { $verification_code = get_option('ad_unblock_verification_code', ''); ?>

get_default_site_domain(); $configured = $this->normalize_domain(get_option('ad_unblock_site_domain', '')); $effective = '' !== $configured ? $configured : $detected; ?>

' . esc_html($effective) . '' ); } else { printf( /* translators: 1: the configured domain, 2: the domain detected from the site address */ esc_html__('Currently sending %1$s. Clear this field to follow your site address (%2$s) instead.', 'ad-unblock'), '' . esc_html($configured) . '', '' . esc_html('' !== $detected ? $detected : '—') . '' ); } ?>


'yes', 'url_patterns' => '', 'categories' => array(), 'tags' => array(), )); ?>




get_cache_status(); ?>
✓ ⚠
⚠ ✓
(server time)
(s remaining)
'yes', 'url_patterns' => '', 'categories' => array(), 'tags' => array(), )); // All pages option is enabled if (isset($page_rules['all_pages']) && $page_rules['all_pages'] === 'yes') { return true; } // Check URL patterns if (!empty($page_rules['url_patterns'])) { $current_url = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''; if (empty($current_url)) { return false; } $patterns = explode("\n", $page_rules['url_patterns']); foreach ($patterns as $pattern) { $pattern = trim($pattern); if (empty($pattern)) { continue; } // Convert wildcard pattern to regex $regex = str_replace( array('\*', '/'), array('.*', '\/'), preg_quote($pattern, '/') ); if (preg_match('/^' . $regex . '/i', $current_url)) { return true; } } } // Check categories and tags if (is_singular()) { global $post; // Check categories if (!empty($page_rules['categories'])) { $post_categories = wp_get_post_categories($post->ID); foreach ($post_categories as $category) { if (in_array($category, $page_rules['categories'])) { return true; } } } // Check tags if (!empty($page_rules['tags'])) { $post_tags = wp_get_post_tags($post->ID, array('fields' => 'ids')); foreach ($post_tags as $tag) { if (in_array($tag, $page_rules['tags'])) { return true; } } } } // Check category archive pages if (is_category() && !empty($page_rules['categories'])) { $category = get_queried_object_id(); if (in_array($category, $page_rules['categories'])) { return true; } } // Check tag archive pages if (is_tag() && !empty($page_rules['tags'])) { $tag = get_queried_object_id(); if (in_array($tag, $page_rules['tags'])) { return true; } } return false; } /** * Get the script sources from API with caching */ private function get_script_sources() { $cache_key = 'ad_unblock_script_sources_' . get_current_blog_id(); $error_cache_key = $cache_key . '_error'; // Get cached data and check if it's valid $cached_data = get_transient($cache_key); $is_cache_valid = $this->is_cache_data_valid($cached_data); $is_in_error_state = $this->is_in_error_state($error_cache_key); // Return valid cache if not in error state if ($is_cache_valid && !$is_in_error_state) { return $cached_data['data']; } // Try to fetch fresh data from API $fresh_data = $this->fetch_script_sources_from_api(); // If API call succeeded, update cache and return fresh data if (false !== $fresh_data) { $this->refresh_script_sources_cache($cache_key, $error_cache_key, $fresh_data); return $fresh_data; } // API failed - set error state $this->set_error_state($error_cache_key); // Return stale cache if available, otherwise empty array if ($cached_data && isset($cached_data['data']) && is_array($cached_data['data'])) { return $cached_data['data']; } return array(); } /** * Check if cached data is valid and not expired */ private function is_cache_data_valid($cached_data) { return false !== $cached_data && is_array($cached_data) && isset($cached_data['timestamp'], $cached_data['data']) && is_array($cached_data['data']) && !empty($cached_data['data']) && $this->cache_matches_request($cached_data) && (time() - $cached_data['timestamp']) < self::CACHE_DURATION; } /** * Check that cached data was fetched for the request we would make now * * A changed domain, endpoint or site address has to re-fetch rather than * keep serving another request's script list. Entries written before the * domain parameter existed carry no request URL and so never match, which * refreshes them once on upgrade. */ private function cache_matches_request($cached_data) { $cached_url = isset($cached_data['request_url']) ? $cached_data['request_url'] : ''; return $cached_url === $this->get_api_request_url(); } /** * Check if we're currently in an error state */ private function is_in_error_state($error_cache_key) { $error_data = get_transient($error_cache_key); if (false === $error_data || !is_array($error_data) || !isset($error_data['timestamp'])) { return false; } // Consider error state active for 60 seconds return (time() - $error_data['timestamp']) < 60; } /** * Reduce any host-bearing string to a bare hostname * * Mirrors the normalization the API applies to the domain it receives: * lowercase, no scheme, userinfo, port, path or trailing dot. Anything * that is not left looking like a hostname returns an empty string. */ private function normalize_domain($value) { if (!is_string($value)) { return ''; } $host = strtolower(trim($value)); if ('' === $host) { return ''; } $scheme_end = strpos($host, '://'); if (false !== $scheme_end) { $host = substr($host, $scheme_end + 3); } // Path, query and fragment. $host = preg_split('#[/?\#]#', $host)[0]; // Userinfo, then port. $at = strrpos($host, '@'); if (false !== $at) { $host = substr($host, $at + 1); } $colon = strpos($host, ':'); if (false !== $colon) { $host = substr($host, 0, $colon); } $host = rtrim($host, '.'); // An internationalized domain has to travel as punycode, because that // is the form the API's per-domain lists are keyed on. if (preg_match('/[^\x20-\x7e]/', $host) && function_exists('idn_to_ascii')) { $ascii = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); if (is_string($ascii) && '' !== $ascii) { $host = strtolower($ascii); } } return preg_match('/^[a-z0-9][a-z0-9.-]*$/', $host) ? $host : ''; } /** * The domain of this WordPress site, from its configured site address */ private function get_default_site_domain() { $domain = $this->normalize_domain(wp_parse_url(home_url(), PHP_URL_HOST)); if ('' === $domain && isset($_SERVER['HTTP_HOST'])) { $domain = $this->normalize_domain(sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']))); } return $domain; } /** * The domain sent to the API, configured or auto-detected */ private function get_site_domain() { $configured = $this->normalize_domain(get_option('ad_unblock_site_domain', '')); return '' !== $configured ? $configured : $this->get_default_site_domain(); } /** * Get the API endpoint to use (custom or default) */ private function get_api_endpoint() { $custom_endpoint = get_option('ad_unblock_custom_api_endpoint', ''); return !empty($custom_endpoint) ? $custom_endpoint : self::API_ENDPOINT; } /** * Build the API request URL for this site * * The `domain` parameter tells the service which site is asking, so it can * answer with a script host reserved for this domain when a filter list has * blocked the shared one for it. A request without the parameter still gets * the default list, so an empty domain is not an error. */ private function get_api_request_url() { $api_endpoint = $this->get_api_endpoint(); $domain = $this->get_site_domain(); if ('' === $domain) { return $api_endpoint; } // Passed unencoded: normalize_domain() has already restricted the value // to characters that are literal in a query string, and add_query_arg() // does not encode what it is given. return add_query_arg('domain', $domain, $api_endpoint); } /** * Fetch script sources from API */ private function fetch_script_sources_from_api() { $response = wp_remote_get($this->get_api_request_url(), array('timeout' => 10)); if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) { return false; } $body = wp_remote_retrieve_body($response); if (empty($body)) { return false; } $script_sources = json_decode($body, true); // Validate JSON structure and content if (!is_array($script_sources) || empty($script_sources)) { return false; } // Additional validation: ensure all items are strings (URLs) foreach ($script_sources as $source) { if (!is_string($source) || empty($source)) { return false; } } return $script_sources; } /** * Refresh script sources cache with fresh data and clear error state */ private function refresh_script_sources_cache($cache_key, $error_cache_key, $script_sources) { // Clear error state since we have fresh data delete_transient($error_cache_key); // Store fresh data with timestamp $cache_data = array( 'data' => $script_sources, 'timestamp' => time(), 'request_url' => $this->get_api_request_url() ); set_transient($cache_key, $cache_data, WEEK_IN_SECONDS); } /** * Set error state with timestamp */ private function set_error_state($error_cache_key) { $error_data = array( 'timestamp' => time() ); set_transient($error_cache_key, $error_data, HOUR_IN_SECONDS); } /** * Insert the script into the page head */ public function insert_script() { $verification_code = get_option('ad_unblock_verification_code', ''); if (empty($verification_code) || !$this->should_enable_on_current_page()) { return; } $script_sources = $this->get_script_sources(); if (empty($script_sources) || !is_array($script_sources)) { return; } // Get the first script source only $script_url = reset($script_sources); // Get cache status and full cached data for meta tag $cache_status = $this->get_cache_status(); $cache_key = 'ad_unblock_script_sources_' . get_current_blog_id(); $cached_data = get_transient($cache_key); // Prepare cache details for meta tag $cache_details = array( 'verification' => $verification_code, 'domain' => $this->get_site_domain(), 'cache_active' => $cache_status['has_cache'] ? 'true' : 'false', 'error_state' => $cache_status['has_error'] ? 'true' : 'false', 'sources_count' => $cache_status['cache_data_count'], 'expires_in' => $cache_status['time_until_expiry'], 'timestamp' => time(), 'cached_sources' => $script_sources, 'cache_timestamp' => (false !== $cached_data && is_array($cached_data) && isset($cached_data['timestamp'])) ? $cached_data['timestamp'] : null ); // Add verification code and cache details as a meta tag in DOM echo '' . "\n"; echo '' . "\n"; // Printed directly rather than enqueued, for three reasons: // // 1. wp_enqueue_script() cannot emit a valueless attribute, and it // always builds the tag with `src` first — Cloudflare only honours // `data-cfasync` when it appears BEFORE `src`. // 2. Enqueued head scripts print at wp_head priority 9, after styles. // This runs at priority 0, so recovery loads ahead of the ad code. // 3. The `strategy` argument is WP 6.3+; on older WP that array is a // truthy $in_footer and the tag would land in the footer. // // The two attributes opt the tag out of Cloudflare Rocket Loader and // WP Rocket's Delay JavaScript Execution, both of which would defer or // rewrite it. Neither covers WP Rocket's separate "Load JavaScript // deferred" option — that one has no attribute, and needs the script // URL in its Excluded JavaScript Files list. // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript echo '' . "\n"; } /** * Enqueue admin styles and scripts */ public function enqueue_admin_assets($hook) { if ('settings_page_ad-unblock' !== $hook) { return; } wp_enqueue_style( 'ad-unblock-admin', plugin_dir_url(__FILE__) . 'assets/css/admin.css', array(), AD_UNBLOCK_VERSION ); // Enqueue Select2 wp_enqueue_style( 'ad-unblock-select2-css', plugin_dir_url(__FILE__) . 'assets/css/admin-select2.css' ); wp_enqueue_script( 'ad-unblock-select2-js', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', array('jquery'), null, true ); wp_enqueue_style( 'select2-css', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css' ); wp_enqueue_script( 'ad-unblock-admin-js', plugin_dir_url(__FILE__) . 'assets/js/admin.js', array('jquery', 'ad-unblock-select2-js'), AD_UNBLOCK_VERSION, true ); wp_localize_script('ad-unblock-admin-js', 'adUnblock', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ad-unblock-admin-nonce'), 'cache_status' => $this->get_cache_status() )); } /** * AJAX handler to search for terms (categories, tags) */ public function ajax_search_terms() { check_ajax_referer('ad-unblock-admin-nonce', 'nonce'); $search = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : ''; $taxonomy = isset($_GET['taxonomy']) ? sanitize_text_field(wp_unslash($_GET['taxonomy'])) : 'category'; if (!taxonomy_exists($taxonomy)) { wp_send_json_error(array('message' => 'Invalid taxonomy.')); } $terms = get_terms(array( 'taxonomy' => $taxonomy, 'name__like' => $search, 'hide_empty' => false, 'number' => 20, )); $results = array(); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $term) { $results[] = array( 'id' => $term->term_id, 'text' => $term->name, ); } } wp_send_json_success($results); } /** * AJAX handler to clear script cache */ public function ajax_clear_cache() { check_ajax_referer('ad-unblock-admin-nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions.')); } $this->clear_script_cache(); wp_send_json_success(array( 'message' => 'Cache cleared successfully.', 'cache_status' => $this->get_cache_status() )); } /** * AJAX handler to get current cache status */ public function ajax_get_cache_status() { check_ajax_referer('ad-unblock-admin-nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions.')); } wp_send_json_success(array( 'cache_status' => $this->get_cache_status() )); } /** * Clear the script cache */ public function clear_script_cache() { $cache_key = 'ad_unblock_script_sources_' . get_current_blog_id(); $error_cache_key = $cache_key . '_error'; delete_transient($cache_key); delete_transient($error_cache_key); } /** * Get cache status information */ private function get_cache_status() { $cache_key = 'ad_unblock_script_sources_' . get_current_blog_id(); $error_cache_key = $cache_key . '_error'; $cached_data = get_transient($cache_key); $error_data = get_transient($error_cache_key); $has_cache = false; $cache_timestamp = null; $cache_data_count = 0; $time_until_expiry = 0; $cache_expires = null; // Check if we have valid cached data with timestamp if (false !== $cached_data && is_array($cached_data) && isset($cached_data['timestamp'], $cached_data['data'])) { $has_cache = true; $cache_timestamp = $cached_data['timestamp']; $cache_data_count = is_array($cached_data['data']) ? count($cached_data['data']) : 0; $cache_expiry_time = $cache_timestamp + self::CACHE_DURATION; $time_until_expiry = max(0, $cache_expiry_time - time()); $cache_expires = $cache_expiry_time; } // Check error state - only consider recent errors (within 60 seconds) $has_error = false; if (false !== $error_data && is_array($error_data) && isset($error_data['timestamp'])) { $has_error = (time() - $error_data['timestamp']) < 60; } return array( 'has_cache' => $has_cache, 'has_error' => $has_error, 'cache_expires' => $cache_expires, 'cache_data_count' => $cache_data_count, 'time_until_expiry' => $time_until_expiry ); } /** * Add settings link to plugin actions */ public function add_plugin_action_links($links) { $settings_link = '' . esc_html__('Settings', 'ad-unblock') . ''; array_unshift($links, $settings_link); return $links; } /** * Add additional links to plugin row meta */ public function add_plugin_row_meta($links, $file) { if (plugin_basename(__FILE__) === $file) { $row_meta = array( 'docs' => '' . esc_html__('Documentation', 'ad-unblock') . '', 'support' => '' . esc_html__('Support', 'ad-unblock') . '', ); return array_merge($links, $row_meta); } return $links; } } // Initialize the plugin Ad_Unblock::get_instance();