table_counters = $wpdb->prefix . 'rcp_paginas_vistas_recuento'; $this->table_pages = $wpdb->prefix . 'rcp_paginas_vistas'; $this->log_file = plugin_dir_path(__FILE__) . 'ip_view_limit_debug.txt'; register_activation_hook(__FILE__, [$this, 'activate']); register_uninstall_hook(__FILE__, ['IP_View_Limit_Plugin', 'uninstall']); add_action('admin_menu', [$this, 'admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('template_redirect', [$this, 'enforce_limit'], 1); add_action('template_redirect', [$this, 'prepare_bar'], 20); // calcula %count% y %limit% add_action('wp_body_open', [$this, 'output_bar_markup'], 0); // imprime la barra add_action('wp_footer', [$this, 'output_bar_markup'], 0); // fallback si el theme no llama wp_body_open add_action('wp_head', [$this, 'print_custom_css']); } /* ===================== DEFAULTS / SETTINGS ===================== */ public function default_options() { return [ 'enabled' => 1, 'apply_to_logged' => 0, 'limit' => 3, 'reset_days' => 1, 'redirect_url' => '/suscripcion-vip/', 'excluded_exact' => "/login/\n/register/\n/suscripcion-vip/\n/blog/", 'excluded_regex' => "#^/(?:buscar-|buscador-apus)#\n#^/wp-(?:admin|login|json)/#\n#^/$#\n#\\.(?:jpg|jpeg|png|gif|webp|svg|css|js)$#i", // NUEVO: regex o lista (se permite "blog|otra categoria" sin delimitadores) 'excluded_cats_regex' => "", 'show_bar' => 1, 'bar_html' => '
Te restan (%count%) consultas de %limit%. Hazte VIP ahora.
', 'bar_css' => "/* IP View Limit Bar */\n.ipvl-bar{position:sticky;top:0;z-index:9999;background:#fff3cd;border-bottom:1px solid #ffeeba;padding:8px;text-align:center;font:14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}\n.ipvl-bar a{text-decoration:none}\n", 'debug' => 1, // PROXIES / BOTS 'trust_proxy_headers' => 1, 'trusted_proxy_ips' => "", 'ignore_bots_regex' => "#(googlebot|googleother|bingbot|duckduckbot|slurp|baiduspider|yandex(bot)?|ahrefsbot|semrush(bot)?|mj12bot|facebookexternalhit|meta-externalagent|twitterbot|telegrambot|whatsapp|slackbot|discordbot|linkedinbot|pinterest|bitlybot|python-requests|curl|wget|awariobot|oai-searchbot|gptbot|chatgpt-user|barkrowler|mediapartners-google|adsbot-google|applebot|screaming\\s?frog|simplepie|feedfetcher|uptimerobot|statuscake|monitors?bot)#i", ]; } public function activate() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset_collate = $wpdb->get_charset_collate(); $sql1 = "CREATE TABLE {$this->table_counters} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, ip VARBINARY(16) NOT NULL, period_start DATETIME NOT NULL, page_count INT UNSIGNED NOT NULL DEFAULT 0, last_seen DATETIME NOT NULL, PRIMARY KEY (id), KEY ip_period (ip, period_start) ) $charset_collate;"; $sql2 = "CREATE TABLE {$this->table_pages} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, counter_id BIGINT UNSIGNED NOT NULL, post_id BIGINT UNSIGNED NOT NULL, first_seen DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY counter_post (counter_id, post_id), KEY post_id (post_id) ) $charset_collate;"; $this->log('activate.start', [ 'tables' => [$this->table_counters, $this->table_pages], 'charset' => $charset_collate ]); dbDelta($sql1); $this->db_error_snapshot('activate.create_counters'); dbDelta($sql2); $this->db_error_snapshot('activate.create_pages'); $opts = get_option($this->opts_key); if (!is_array($opts)) { add_option($this->opts_key, $this->default_options(), '', 'no'); $this->log('activate.options_default_set', ['key' => $this->opts_key]); } else { $this->log('activate.options_exist', ['key' => $this->opts_key]); } $this->log('activate.done', []); } public static function uninstall() { $self = self::instance(); delete_option($self->opts_key); $self->log('uninstall.options_deleted', ['key' => $self->opts_key]); } private function get_options() { $opts = get_option($this->opts_key, []); $defaults = $this->default_options(); return wp_parse_args($opts, $defaults); } public function admin_menu() { add_options_page('IP View Limit', 'IP View Limit', 'manage_options', 'ip-view-limit', [$this, 'settings_page']); } public function register_settings() { register_setting($this->opts_key, $this->opts_key, [$this, 'sanitize_options']); add_settings_section('ipvl_main', 'Configuración', '__return_false', $this->opts_key); $fields = [ ['enabled', 'Habilitar', 'checkbox'], ['apply_to_logged', 'Aplicar también a usuarios autenticados', 'checkbox'], ['limit', 'Páginas únicas permitidas por periodo', 'number'], ['reset_days', 'Días para reiniciar conteo', 'number'], ['redirect_url', 'URL de redirección al alcanzar el límite', 'text'], ['excluded_exact', 'Exclusiones por URL exacta (una por línea)', 'textarea'], ['excluded_regex', 'Exclusiones por Regex de URL (una por línea)', 'textarea'], ['excluded_cats_regex', 'Exclusiones por categorías (regex de NOMBRE o SLUG; una por línea)', 'textarea'], ['show_bar', 'Mostrar barra de conteo', 'checkbox'], ['bar_html', 'HTML de la barra (usa %count% y %limit%)', 'textarea'], ['bar_css', 'CSS personalizado de la barra', 'textarea'], // Debug / Proxies / Bots ['debug', 'Habilitar debug a archivo .txt', 'checkbox'], ['trust_proxy_headers', 'Confiar en cabeceras de proxy (X-Forwarded-For / CF-Connecting-IP)', 'checkbox'], ['trusted_proxy_ips', 'Lista de IPs de proxy de confianza (una por línea; vacío = confiar en todos)', 'textarea'], ['ignore_bots_regex', 'Regex de User-Agent para ignorar bots (no contabilizar vistas)', 'textarea'], ]; foreach ($fields as $f) { add_settings_field($f[0], $f[1], [$this, 'field_render'], $this->opts_key, 'ipvl_main', ['key' => $f[0], 'type' => $f[2]]); } } public function sanitize_options($input) { $opts = $this->get_options(); $opts['enabled'] = !empty($input['enabled']) ? 1 : 0; $opts['apply_to_logged'] = !empty($input['apply_to_logged']) ? 1 : 0; $opts['limit'] = max(0, intval($input['limit'] ?? $opts['limit'])); $opts['reset_days'] = max(1, intval($input['reset_days'] ?? $opts['reset_days'])); $opts['redirect_url'] = esc_url_raw($input['redirect_url'] ?? $opts['redirect_url']); $opts['excluded_exact'] = trim(wp_unslash($input['excluded_exact'] ?? $opts['excluded_exact'])); $opts['excluded_regex'] = trim(wp_unslash($input['excluded_regex'] ?? $opts['excluded_regex'])); $opts['excluded_cats_regex'] = trim(wp_unslash($input['excluded_cats_regex'] ?? ($opts['excluded_cats_regex'] ?? ''))); $opts['show_bar'] = !empty($input['show_bar']) ? 1 : 0; $opts['bar_html'] = wp_kses_post($input['bar_html'] ?? $opts['bar_html']); $opts['bar_css'] = wp_kses_post($input['bar_css'] ?? $opts['bar_css']); $opts['debug'] = !empty($input['debug']) ? 1 : 0; $opts['trust_proxy_headers'] = !empty($input['trust_proxy_headers']) ? 1 : 0; $opts['trusted_proxy_ips'] = trim(wp_unslash($input['trusted_proxy_ips'] ?? $opts['trusted_proxy_ips'])); $opts['ignore_bots_regex'] = trim(wp_unslash($input['ignore_bots_regex'] ?? $opts['ignore_bots_regex'])); $this->log('settings.saved', [ 'opts' => [ 'enabled' => $opts['enabled'], 'apply_to_logged' => $opts['apply_to_logged'], 'limit' => $opts['limit'], 'reset_days' => $opts['reset_days'], 'redirect_url' => $opts['redirect_url'], 'show_bar' => $opts['show_bar'], 'debug' => $opts['debug'], 'trust_proxy_headers' => $opts['trust_proxy_headers'] ] ]); return $opts; } public function field_render($args) { $key = $args['key']; $type = $args['type']; $opts = $this->get_options(); $val = $opts[$key] ?? ''; if ($type === 'checkbox') { echo ''; } elseif ($type === 'number') { echo ''; } elseif ($type === 'text') { echo ''; } elseif ($type === 'textarea') { echo ''; } } public function settings_page() { ?>

IP View Limit

opts_key); do_settings_sections($this->opts_key); submit_button(); ?>

Archivo de debug: log_file)); ?> (carpeta del plugin)

get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table_counters)); $exists_p = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table_pages)); if (!$exists_c || !$exists_p) { $this->log('ensure_tables.create_needed', ['counters_exists' => (bool)$exists_c, 'pages_exists' => (bool)$exists_p]); $this->activate(); } else { $this->log('ensure_tables.ok', ['counters' => $this->table_counters, 'pages' => $this->table_pages]); } } private function is_trusted_proxy($ip, $opts) { $list_raw = (string)$opts['trusted_proxy_ips']; $list = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $list_raw))); if (empty($list)) return true; return in_array($ip, $list, true); } private function ip_is_public($ip) { return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } private function get_client_ip_info() { $opts = $this->get_options(); $server_ip = $_SERVER['REMOTE_ADDR'] ?? ''; $candidate = ''; if (!empty($opts['trust_proxy_headers']) && $server_ip && $this->is_trusted_proxy($server_ip, $opts)) { if (!empty($_SERVER['HTTP_CF_CONNECTING_IP']) && $this->ip_is_public($_SERVER['HTTP_CF_CONNECTING_IP'])) { $candidate = $_SERVER['HTTP_CF_CONNECTING_IP']; } if (!$candidate && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $parts = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])); foreach ($parts as $p) { if ($this->ip_is_public($p)) { $candidate = $p; break; } } } if (!$candidate && !empty($_SERVER['HTTP_CLIENT_IP']) && $this->ip_is_public($_SERVER['HTTP_CLIENT_IP'])) { $candidate = $_SERVER['HTTP_CLIENT_IP']; } } if (!$candidate && $this->ip_is_public($server_ip)) $candidate = $server_ip; if (!$candidate) $candidate = '0.0.0.0'; // **NORMALIZACIÓN**: IPv6 mapeada a IPv4 → usar IPv4 pura if (stripos($candidate, '::ffff:') === 0) { $v4 = substr($candidate, 7); if (filter_var($v4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) $candidate = $v4; } $bin = @inet_pton($candidate); if ($bin === false) $bin = @inet_pton('0.0.0.0'); return ['readable' => $candidate, 'bin' => $bin, 'server' => $server_ip]; } private function ua_is_bot($opts, $ua) { $pattern = (string)$opts['ignore_bots_regex']; if ($pattern === '') return false; set_error_handler(function(){}); $match = @preg_match($pattern, (string)$ua) === 1; restore_error_handler(); return $match; } private function current_period_start($days) { $now = current_time('timestamp', true); // UTC $start_day = strtotime(gmdate('Y-m-d 00:00:00', $now)); $epoch_days = floor($start_day / DAY_IN_SECONDS); $bucket_start_days = $epoch_days - ($epoch_days % max(1, $days)); return gmdate('Y-m-d H:i:s', $bucket_start_days * DAY_IN_SECONDS); } private function request_path() { $req = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '/'; $parts = @parse_url($req); return isset($parts['path']) ? (string)$parts['path'] : '/'; } private function is_excluded_url($url, $opts, &$reason = '') { $path = $this->request_path(); // Exact $exact_lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string)$opts['excluded_exact']))); foreach ($exact_lines as $line) { if ($line === '') continue; $cmp = untrailingslashit($line); $path_norm = untrailingslashit($path); if ($cmp === $path_norm) { $reason = 'exact:' . $line; return true; } } // Regex $regex_lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string)$opts['excluded_regex']))); foreach ($regex_lines as $pattern) { if ($pattern === '') continue; set_error_handler(function(){}); $match = @preg_match($pattern, $path); restore_error_handler(); if ($match === 1) { $reason = 'regex:' . $pattern; return true; } } return false; } /* ======= helpers para regex de categorías ======= */ private function normalize_regex_or_list($line) { $line = trim($line); if ($line === '') return null; // Si parece un regex con delimitadores estándar, úsalo tal cual. if (preg_match('/^([#\\/~%!@`\\|;:]).*\\1[imsxuADSUXJ]*$/', $line)) { return $line; } // Si es "token1|token2|..." sin delimitadores, construimos un regex exacto, case-insensitive. $parts = array_map('trim', explode('|', $line)); $tokens = array(); foreach ($parts as $p) { if ($p === '') continue; $tokens[] = preg_quote($p, '#'); } if (empty($tokens)) return null; return '#^(?:' . implode('|', $tokens) . ')$#i'; } private function patterns_from_lines($raw) { $out = []; $lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string)$raw))); foreach ($lines as $ln) { $norm = $this->normalize_regex_or_list($ln); if ($norm) $out[] = $norm; } return $out; } private function value_matches_patterns($value, $patterns) { foreach ($patterns as $pat) { set_error_handler(function(){}); $ok = @preg_match($pat, $value) === 1; restore_error_handler(); if ($ok) return $pat; } return false; } // EXCLUSIÓN por CATEGORÍAS / TAXONOMÍAS (por NOMBRE o SLUG; incluye ancestros si aplica) private function is_excluded_by_category($post_id, $opts, &$reason = '') { $patterns = $this->patterns_from_lines($opts['excluded_cats_regex'] ?? ''); if (empty($patterns)) return false; $post_type = get_post_type($post_id); if (!$post_type) return false; // Recorremos TODAS las taxonomías registradas para este post type $tax_objects = get_object_taxonomies($post_type, 'objects'); if (empty($tax_objects)) return false; foreach ($tax_objects as $tax) { // Solo públicas o consultables if (!$tax->public && !$tax->publicly_queryable) continue; $terms = get_the_terms($post_id, $tax->name); if (is_wp_error($terms) || empty($terms)) continue; foreach ($terms as $t) { // candidatos: slug y nombre del término $candidates = [ (string)$t->slug, (string)$t->name ]; // si la taxonomía es jerárquica, añadir ancestros (slug y nombre) $is_hier = function_exists('is_taxonomy_hierarchical') ? is_taxonomy_hierarchical($tax->name) : !empty($tax->hierarchical); if ($is_hier) { $anc_ids = get_ancestors($t->term_id, $tax->name, 'taxonomy'); if (!empty($anc_ids)) { foreach ($anc_ids as $aid) { $anc = get_term($aid, $tax->name); if ($anc && !is_wp_error($anc)) { $candidates[] = (string)$anc->slug; $candidates[] = (string)$anc->name; } } } } // probamos cada candidato contra los patrones foreach ($candidates as $val) { $matched = $this->value_matches_patterns($val, $patterns); if ($matched !== false) { $reason = 'taxonomy:' . $tax->name . ' term:' . $val . ' matched:' . $matched; return true; } } } } return false; } private function should_apply($opts, &$why_not = '') { if (empty($opts['enabled'])) { $why_not = 'disabled'; return false; } if (!is_singular()) { $why_not = 'not_singular'; return false; } if (is_user_logged_in() && empty($opts['apply_to_logged'])) { $why_not = 'logged_ignored'; return false; } return true; } private function get_post_id() { $post = get_queried_object(); return isset($post->ID) ? intval($post->ID) : 0; } private function get_or_create_counter($ip_bin, $period_start) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table_counters} WHERE ip = %s AND period_start = %s LIMIT 1", $ip_bin, $period_start ), ARRAY_A ); $this->db_error_snapshot('query.select_counter'); if ($row) return $row; $ins = $wpdb->insert($this->table_counters, [ 'ip' => $ip_bin, 'period_start' => $period_start, 'page_count' => 0, 'last_seen' => gmdate('Y-m-d H:i:s') ], ['%s', '%s', '%d', '%s']); $this->db_error_snapshot('query.insert_counter', ['insert_result' => $ins]); if (!$ins) return null; $id = $wpdb->insert_id; $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_counters} WHERE id = %d", $id), ARRAY_A); $this->db_error_snapshot('query.select_counter_after_insert'); return $row; } private function has_seen_post($counter_id, $post_id) { global $wpdb; $exists = $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$this->table_pages} WHERE counter_id = %d AND post_id = %d LIMIT 1", $counter_id, $post_id ) ); $this->db_error_snapshot('query.has_seen_post', ['counter_id' => $counter_id, 'post_id' => $post_id]); return !empty($exists); } private function add_seen_post($counter_id, $post_id) { global $wpdb; $ins = $wpdb->insert($this->table_pages, [ 'counter_id' => $counter_id, 'post_id' => $post_id, 'first_seen' => gmdate('Y-m-d H:i:s') ], ['%d', '%d', '%s']); $this->db_error_snapshot('query.insert_seen_post', ['insert_result' => $ins, 'counter_id' => $counter_id, 'post_id' => $post_id]); return $ins; } private function increment_counter($id) { global $wpdb; $upd = $wpdb->query( $wpdb->prepare( "UPDATE {$this->table_counters} SET page_count = page_count + 1, last_seen = %s WHERE id = %d", gmdate('Y-m-d H:i:s'), $id ) ); $this->db_error_snapshot('query.increment_counter', ['update_result' => $upd, 'id' => $id]); return $upd; } private function redirect_limit_reached($opts, $from_url) { $redir = $opts['redirect_url']; if (empty($redir)) $redir = wp_login_url(); $target = add_query_arg(['ref' => rawurlencode($from_url)], $redir); $this->log('redirect.limit_reached', ['from' => $from_url, 'to' => $target]); wp_safe_redirect($target, 302); exit; } /* ===================== ENFORCER / BAR ===================== */ public function enforce_limit() { if (is_admin()) return; $opts = $this->get_options(); $this->ensure_tables_exist(); // 1) Ignorar bots $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; if ($this->ua_is_bot($opts, $ua)) { $this->log('enforce.skip_bot', ['ua' => $ua]); return; } // 2) Excluir por URL $current_url = $this->current_url(); $excluded_reason = ''; if ($this->is_excluded_url($current_url, $opts, $excluded_reason)) { $this->log('enforce.skip_excluded', ['path' => $this->request_path(), 'reason' => $excluded_reason]); return; } // 3) Reglas de aplicación $why_not = ''; if (!$this->should_apply($opts, $why_not)) { $this->log('enforce.skip', ['reason' => $why_not]); return; } // 4) Post y exclusión por CATEGORÍAS (nombre o slug; incluye ancestros) $post_id = $this->get_post_id(); if ($post_id <= 0) { $this->log('enforce.skip_no_post', []); return; } $cat_reason = ''; if ($this->is_excluded_by_category($post_id, $opts, $cat_reason)) { $this->log('enforce.skip_excluded_category', ['post_id' => $post_id, 'reason' => $cat_reason]); return; } // 5) IP y periodo $ip = $this->get_client_ip_info(); $period_start = $this->current_period_start((int)$opts['reset_days']); $this->log('enforce.start', [ 'url' => $current_url, 'path' => $this->request_path(), 'opts_core' => [ 'enabled' => $opts['enabled'], 'apply_to_logged' => $opts['apply_to_logged'], 'limit' => $opts['limit'], 'reset_days' => $opts['reset_days'], 'redirect_url' => $opts['redirect_url'], 'show_bar' => $opts['show_bar'], 'trust_proxy_headers' => $opts['trust_proxy_headers'] ], 'ip_info' => ['client' => $ip['readable'], 'server' => $ip['server']] ]); $counter = $this->get_or_create_counter($ip['bin'], $period_start); if (!$counter || !isset($counter['id'])) { $this->log('enforce.fail_open_no_counter', [ 'ip' => $ip['readable'], 'period_start' => $period_start, 'wpdb_error' => $this->wpdb_last_error() ]); return; // fail-open } $limit = max(0, (int)$opts['limit']); if ($limit === 0) { $this->log('enforce.fail_open_zero_limit', []); return; // fail-open } $count = (int)$counter['page_count']; $already_seen = $this->has_seen_post((int)$counter['id'], $post_id); $this->log('enforce.counter_status', [ 'ip' => $ip['readable'], 'post_id' => $post_id, 'period_start' => $period_start, 'count' => $count, 'limit' => $limit, 'already_seen' => $already_seen ]); if (!$already_seen) { if ($count < $limit) { $ok1 = $this->add_seen_post((int)$counter['id'], $post_id); $ok2 = $this->increment_counter((int)$counter['id']); $this->log('enforce.increment', ['add_seen_ok' => (bool)$ok1, 'incr_ok' => (bool)$ok2]); return; } else { $this->log('enforce.limit_reached', ['count' => $count, 'limit' => $limit]); $this->redirect_limit_reached($opts, $current_url); } } else { $this->log('enforce.already_seen', ['post_id' => $post_id, 'no_increment' => true]); return; } } public function prepare_bar() { if (is_admin()) return; $opts = $this->get_options(); if (empty($opts['show_bar'])) return; // No a bots $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; if ($this->ua_is_bot($opts, $ua)) return; // No en URLs excluidas $current_url = $this->current_url(); $excluded_reason = ''; if ($this->is_excluded_url($current_url, $opts, $excluded_reason)) return; // Reglas de aplicación $why_not = ''; if (!$this->should_apply($opts, $why_not)) return; $post_id = $this->get_post_id(); if ($post_id <= 0) return; // No en categorías excluidas $cat_reason = ''; if ($this->is_excluded_by_category($post_id, $opts, $cat_reason)) return; // Datos de conteo $ip = $this->get_client_ip_info(); $period_start = $this->current_period_start((int)$opts['reset_days']); $counter = $this->get_or_create_counter($ip['bin'], $period_start); if (!$counter || !isset($counter['id'])) return; $limit = max(0, (int)$opts['limit']); if ($limit === 0) return; $count = (int)$counter['page_count']; $already_seen = $this->has_seen_post((int)$counter['id'], $post_id); $remaining = $limit - $count; if (!$already_seen && $count < $limit) $remaining = max(0, $remaining - 1); if ($remaining < 0) $remaining = 0; $bar_html = (string)$opts['bar_html']; if ($bar_html === '') return; $this->bar_markup = str_replace( ['%count%', '%limit%'], [esc_html((string)$remaining), esc_html((string)$limit)], $bar_html ); } public function output_bar_markup() { if ($this->bar_printed) return; if ($this->bar_markup === '') return; echo $this->bar_markup; $this->bar_printed = true; } public function print_custom_css() { $opts = $this->get_options(); if (!empty($opts['show_bar']) && !empty($opts['bar_css'])) { echo ""; } } /* ===================== LOGGING ===================== */ private function current_url() { $scheme = is_ssl() ? 'https://' : 'http://'; $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : parse_url(home_url(), PHP_URL_HOST); $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; return $scheme . $host . $uri; } private function wpdb_last_error() { global $wpdb; return isset($wpdb->last_error) ? $wpdb->last_error : ''; } private function db_error_snapshot($event, $extra = []) { global $wpdb; $payload = array_merge([ 'last_error' => $this->wpdb_last_error(), 'last_query' => isset($wpdb->last_query) ? (string)$wpdb->last_query : null, 'last_result_count' => (isset($wpdb->last_result) && is_array($wpdb->last_result)) ? count($wpdb->last_result) : null ], $extra); $this->log($event, $payload); return $payload['last_error']; } private function log($event, array $data = []) { $opts = get_option($this->opts_key, []); $debug_enabled = isset($opts['debug']) ? (int)$opts['debug'] === 1 : 1; if (!$debug_enabled) return; $entry = [ 'ts' => gmdate('c'), 'event' => $event, 'req' => [ 'uri' => $_SERVER['REQUEST_URI'] ?? null, 'method' => $_SERVER['REQUEST_METHOD'] ?? null, 'referer' => $_SERVER['HTTP_REFERER'] ?? null, 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null, 'remote' => $_SERVER['REMOTE_ADDR'] ?? null, ], 'data' => $data, ]; $json = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) $json = '{"ts":"' . gmdate('c') . '","event":"' . esc_attr($event) . '","data":""}'; $this->log_write_line($json . PHP_EOL); } private function log_write_line($line) { if (file_exists($this->log_file)) { $size = @filesize($this->log_file); if ($size !== false && $size > ($this->log_max_mb * 1024 * 1024)) { @rename($this->log_file, $this->log_file . '.' . time() . '.bak'); } } $ok = @file_put_contents($this->log_file, $line, FILE_APPEND | LOCK_EX); if ($ok === false) { error_log('[IPVL] ' . $line); } } } IP_View_Limit_Plugin::instance();