table_name = $wpdb->prefix . 'ai_monitor_logs';
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
add_action('plugins_loaded', array($this, 'init'));
add_action('admin_init', array($this, 'run_upgrade_routine'));
add_action('admin_notices', array($this, 'display_upgrade_errors'));
add_action('admin_notices', array($this, 'display_review_notice'));
add_action('admin_post_almaweb_review_notice', array($this, 'handle_review_notice'));
add_action('admin_notices', array($this, 'display_cache_notice'));
add_action('admin_post_almaweb_cache_notice', array($this, 'handle_cache_notice'));
add_action('admin_init', array($this, 'ensure_weekly_report_scheduled'));
// Pour les utilisateurs existants qui n'ont pas de date d'installation
if (!get_option('almaweb_install_date')) {
update_option('almaweb_install_date', time());
}
}
public function init() {
$this->load_dependencies();
add_action('init', array($this, 'track_visitor'), 1);
add_action('almaweb_daily_cleanup', array($this, 'run_cleanup'));
add_action('almaweb_weekly_report', array($this, 'run_weekly_report'));
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
}
private function load_dependencies() {
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-ip-checker.php';
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-bot-detector.php';
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-referrer-detector.php';
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-database.php';
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-business-kpis.php';
require_once ALMAWEB_PLUGIN_DIR . 'includes/class-email-report.php';
require_once ALMAWEB_PLUGIN_DIR . 'admin/class-admin-dashboard.php';
require_once ALMAWEB_PLUGIN_DIR . 'admin/class-admin-settings.php';
}
public function activate() {
// Pour les nouvelles installations, on crée la table avec la version la plus récente du schéma
$this->create_complete_database_table();
$this->set_default_options();
$this->schedule_cleanup();
$this->ensure_weekly_report_scheduled();
// Enregistrer la date d'installation pour la notice de review
if (!get_option('almaweb_install_date')) {
update_option('almaweb_install_date', time());
}
flush_rewrite_rules();
}
/**
* Affiche la notice d'invitation à laisser un avis
*/
public function display_review_notice() {
// Seulement sur les pages du plugin
$screen = get_current_screen();
if (!$screen || strpos($screen->id, 'almaweb') === false) {
return;
}
$status = get_option('almaweb_review_notice_status');
if ($status === 'dismissed') {
return;
}
$install_date = get_option('almaweb_install_date', time());
$remind_at = get_option('almaweb_review_remind_at', 0);
// Pas encore 14 jours
if (time() < $install_date + (14 * DAY_IN_SECONDS)) {
return;
}
// En attente d'un rappel qui n'est pas encore échu
if ($remind_at && time() < $remind_at) {
return;
}
$review_url = 'https://wordpress.org/support/plugin/almaweb-ai-visitor-analytics/reviews/#new-post';
$dismiss_url = wp_nonce_url(admin_url('admin-post.php?action=almaweb_review_notice&response=dismissed'), 'almaweb_review_notice');
$later_url = wp_nonce_url(admin_url('admin-post.php?action=almaweb_review_notice&response=later'), 'almaweb_review_notice');
?>
id, 'almaweb') === false) {
return;
}
if (get_option('almaweb_cache_notice_dismissed')) {
return;
}
$dismiss_url = wp_nonce_url(admin_url('admin-post.php?action=almaweb_cache_notice'), 'almaweb_cache_notice');
?>
send_weekly_report();
}
/**
* Garantit que le cron du rapport hebdomadaire est planifié quand l'option est active.
* Couvre la réactivation du plugin et les mises à jour automatiques, qui ne repassent
* pas par la sauvegarde des réglages.
*/
public function ensure_weekly_report_scheduled() {
$settings = get_option('almaweb_settings', []);
if (empty($settings['report_enabled'])) {
return;
}
if (wp_next_scheduled('almaweb_weekly_report')) {
return;
}
$day = isset($settings['report_day']) ? max(0, min(6, intval($settings['report_day']))) : 1;
$now = current_time('timestamp');
$diff = ($day - (int) gmdate('w', $now) + 7) % 7;
$diff = $diff === 0 ? 7 : $diff;
wp_schedule_event(strtotime("+{$diff} days midnight", $now), 'weekly', 'almaweb_weekly_report');
}
/**
* Crée la table de base de données avec le schéma complet pour les nouvelles installations.
*/
public function create_complete_database_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_agent varchar(300) NOT NULL,
ip_address varchar(45) NOT NULL,
bot_name varchar(100) DEFAULT NULL,
bot_type varchar(50) DEFAULT NULL,
bot_family varchar(50) DEFAULT NULL,
bot_intention varchar(50) DEFAULT NULL,
is_ai_bot tinyint(1) DEFAULT 0,
is_stealth tinyint(1) DEFAULT 0,
detection_method varchar(20) DEFAULT 'user_agent',
request_url varchar(1200) NOT NULL,
referer varchar(500) DEFAULT NULL,
action_type varchar(50) DEFAULT 'visit',
page_visited varchar(500) DEFAULT NULL,
http_accept varchar(200) DEFAULT NULL,
http_accept_language varchar(100) DEFAULT NULL,
visited_at datetime DEFAULT '0000-00-00 00:00:00',
ai_referrer_source varchar(200) DEFAULT NULL,
ai_referrer_type varchar(50) DEFAULT NULL,
ai_referrer_family varchar(100) DEFAULT NULL,
is_from_ai tinyint(1) DEFAULT 0,
session_duration int(11) DEFAULT 0,
pages_in_session int(11) DEFAULT 1,
is_bounce tinyint(1) DEFAULT 1,
is_return_visitor tinyint(1) DEFAULT 0,
has_conversion tinyint(1) DEFAULT 0,
conversion_type varchar(50) DEFAULT NULL,
conversion_value decimal(10,2) DEFAULT NULL,
is_blocked tinyint(1) DEFAULT 0,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY bot_name (bot_name),
KEY is_ai_bot (is_ai_bot),
KEY visited_at (visited_at),
KEY bot_type (bot_type),
KEY bot_family (bot_family),
KEY bot_intention (bot_intention),
KEY ai_referrer_source (ai_referrer_source),
KEY ai_referrer_family (ai_referrer_family),
KEY is_from_ai (is_from_ai),
KEY is_blocked (is_blocked)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
update_option('almaweb_db_version', ALMAWEB_VERSION);
}
private function set_default_options() {
if (!get_option('almaweb_settings')) {
$default_options = ['track_enabled' => true, 'track_registered_users' => false, 'retention_days' => 90];
add_option('almaweb_settings', $default_options);
}
}
public function run_upgrade_routine() {
$db_version = get_option('almaweb_db_version', '1.0.0');
if (version_compare($db_version, ALMAWEB_VERSION, '<')) {
if (version_compare($db_version, '1.1.0', '<')) {
$this->run_manual_schema_update_to_1_1_0();
}
if (version_compare($db_version, '1.2.0', '<')) {
$this->run_manual_schema_update_to_1_2_0();
}
if (version_compare($db_version, '1.2.2', '<')) {
$this->run_manual_schema_update_to_1_2_2();
}
update_option('almaweb_db_version', ALMAWEB_VERSION);
}
}
public function display_upgrade_errors() {
$error = get_transient('almaweb_upgrade_error');
if ($error) {
echo 'AlmaWeb AI Visitor Analytics Upgrade Error:
' . esc_html($error) . '
';
delete_transient('almaweb_upgrade_error');
}
}
private function run_manual_schema_update_to_1_1_0() {
global $wpdb;
$table_name = $this->table_name;
$wpdb->suppress_errors(false);
$columns_to_add = [
'ai_referrer_source' => "VARCHAR(200) NULL DEFAULT NULL", 'ai_referrer_type' => "VARCHAR(50) NULL DEFAULT NULL",
'ai_referrer_family' => "VARCHAR(100) NULL DEFAULT NULL", 'is_from_ai' => "TINYINT(1) NULL DEFAULT 0",
'session_id' => "VARCHAR(100) NULL DEFAULT NULL", 'session_duration' => "INT(11) NULL DEFAULT 0",
'pages_in_session' => "INT(11) NULL DEFAULT 1", 'is_bounce' => "TINYINT(1) NULL DEFAULT 1",
'is_return_visitor' => "TINYINT(1) NULL DEFAULT 0", 'has_conversion' => "TINYINT(1) NULL DEFAULT 0",
'conversion_type' => "VARCHAR(50) NULL DEFAULT NULL", 'conversion_value' => "DECIMAL(10,2) NULL DEFAULT NULL"
];
$raw_columns_query = $wpdb->get_results("DESCRIBE {$table_name}");
if (!$raw_columns_query) { return; }
$existing_columns = wp_list_pluck($raw_columns_query, 'Field');
foreach ($columns_to_add as $column_name => $column_definition) {
if (!in_array($column_name, $existing_columns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $column_name and $column_definition are hardcoded in this method, not user input
$wpdb->query("ALTER TABLE {$table_name} ADD COLUMN `" . esc_sql($column_name) . "` {$column_definition}");
if (!empty($wpdb->last_error)) {
set_transient('almaweb_upgrade_error', "Failed to add column '{$column_name}'. Error: " . $wpdb->last_error, 120);
return;
}
}
}
$keys_to_add = ['ai_referrer_source', 'ai_referrer_family', 'is_from_ai', 'session_id', 'has_conversion'];
foreach ($keys_to_add as $key_name) {
$key_exists = $wpdb->get_var($wpdb->prepare("SHOW INDEX FROM {$table_name} WHERE Key_name = %s", $key_name));
if (!$key_exists) {
$wpdb->query("ALTER TABLE {$table_name} ADD KEY `{$key_name}` (`{$key_name}`)");
if (!empty($wpdb->last_error)) {
set_transient('almaweb_upgrade_error', "Failed to add index '{$key_name}'. Error: " . $wpdb->last_error, 120);
return;
}
}
}
}
private function run_manual_schema_update_to_1_2_0() {
global $wpdb;
$table_name = $this->table_name;
$raw_columns_query = $wpdb->get_results("DESCRIBE {$table_name}");
if (!$raw_columns_query) { return; }
$existing_columns = wp_list_pluck($raw_columns_query, 'Field');
if (!in_array('is_blocked', $existing_columns)) {
$wpdb->query("ALTER TABLE {$table_name} ADD COLUMN `is_blocked` TINYINT(1) NULL DEFAULT 0");
$key_exists = $wpdb->get_var($wpdb->prepare("SHOW INDEX FROM {$table_name} WHERE Key_name = %s", 'is_blocked'));
if (!$key_exists) { $wpdb->query("ALTER TABLE {$table_name} ADD KEY `is_blocked` (`is_blocked`)"); }
}
}
private function run_manual_schema_update_to_1_2_2() {
global $wpdb;
$table_name = $this->table_name;
// Supprimer les colonnes inutiles
$columns_to_drop = ['country', 'city', 'time_spent', 'session_id', 'http_accept_encoding', 'request_method'];
$raw_columns_query = $wpdb->get_results("DESCRIBE {$table_name}");
if (!$raw_columns_query) { return; }
$existing_columns = wp_list_pluck($raw_columns_query, 'Field');
foreach ($columns_to_drop as $col) {
if (in_array($col, $existing_columns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column name is hardcoded
$wpdb->query("ALTER TABLE {$table_name} DROP COLUMN `{$col}`");
if (!empty($wpdb->last_error)) {
set_transient('almaweb_upgrade_error', "Failed to drop column '{$col}': " . $wpdb->last_error, 120);
return;
}
}
}
// Réduire les tailles et convertir TEXT -> VARCHAR
$alterations = [
'user_agent' => "varchar(300) NOT NULL DEFAULT ''",
'ip_address' => "varchar(45) NOT NULL DEFAULT ''",
'bot_name' => "varchar(100) DEFAULT NULL",
'bot_type' => "varchar(50) DEFAULT NULL",
'bot_family' => "varchar(50) DEFAULT NULL",
'detection_method' => "varchar(20) DEFAULT 'user_agent'",
'request_url' => "varchar(1200) NOT NULL DEFAULT ''",
'referer' => "varchar(500) DEFAULT NULL",
'page_visited' => "varchar(500) DEFAULT NULL",
'http_accept' => "varchar(200) DEFAULT NULL",
];
foreach ($alterations as $col => $definition) {
if (in_array($col, $existing_columns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $col and $definition are hardcoded in this method, not user input
$wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN `" . esc_sql($col) . "` {$definition}");
if (!empty($wpdb->last_error)) {
set_transient('almaweb_upgrade_error', "Failed to alter column '{$col}': " . $wpdb->last_error, 120);
return;
}
}
}
// Supprimer les index devenus inutiles
$indexes_to_drop = ['is_stealth', 'detection_method', 'session_id', 'has_conversion'];
foreach ($indexes_to_drop as $idx) {
$key_exists = $wpdb->get_var($wpdb->prepare("SHOW INDEX FROM {$table_name} WHERE Key_name = %s", $idx));
if ($key_exists) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- index name is hardcoded
$wpdb->query("ALTER TABLE {$table_name} DROP INDEX `{$idx}`");
}
}
}
public function track_visitor() {
$settings = get_option('almaweb_settings');
if (empty($settings['track_enabled']) || ((defined('DOING_AJAX') && DOING_AJAX) || is_admin()) || (empty($settings['track_registered_users']) && is_user_logged_in())) {
return;
}
// Ne pas logger les appels wp-cron (aucune valeur analytique)
$request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
if (strpos($request_uri, 'wp-cron.php') !== false || (defined('DOING_CRON') && DOING_CRON)) {
return;
}
$bot_detector = new Almaweb_Bot_Detector();
$visitor_data = $bot_detector->analyze_request();
$ai_referrer = false;
$referrer_detector = new Almaweb_Referrer_Detector();
// 1. Détection via utm_source (prioritaire)
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tracking parameter, read-only
$utm_source = isset($_GET['utm_source']) ? sanitize_text_field(wp_unslash($_GET['utm_source'])) : '';
if (!empty($utm_source)) {
$ai_referrer = $referrer_detector->detect_ai_from_utm_source($utm_source);
}
// 2. Si non détecté via utm_source, détection via HTTP_REFERER
if (!$ai_referrer) {
$referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
if (!empty($referer)) {
$ai_referrer = $referrer_detector->detect_ai_referrer($referer);
}
}
// Si un référent AI est détecté, enrichir les données du visiteur
if ($ai_referrer) {
$visitor_data = array_merge($visitor_data, [
'ai_referrer_source' => $ai_referrer['name'],
'ai_referrer_type' => $ai_referrer['type'],
'ai_referrer_family' => $ai_referrer['family'],
'is_from_ai' => true,
]);
}
// Firewall: Check if bot should be blocked
$is_blocked = $this->should_block_bot($visitor_data, $settings);
$visitor_data['is_blocked'] = $is_blocked ? 1 : 0;
$database = new Almaweb_Database();
$database->log_visit($visitor_data);
// Block after logging (so we keep the data)
if ($is_blocked) {
status_header(403);
exit;
}
}
public function add_admin_menu() {
add_menu_page(__('AlmaWeb AI Visitor Analytics', 'almaweb-ai-visitor-analytics'), __('AI Analytics', 'almaweb-ai-visitor-analytics'), 'manage_options', 'almaweb-ai-visitor-analytics', array($this, 'render_dashboard'), 'dashicons-visibility', 30);
add_submenu_page('almaweb-ai-visitor-analytics', __('Dashboard', 'almaweb-ai-visitor-analytics'), __('Dashboard', 'almaweb-ai-visitor-analytics'), 'manage_options', 'almaweb-ai-visitor-analytics', array($this, 'render_dashboard'));
// Sous-menu Sources IA
add_submenu_page(
'almaweb-ai-visitor-analytics',
__('AI Sources', 'almaweb-ai-visitor-analytics'),
__('AI Sources', 'almaweb-ai-visitor-analytics'),
'manage_options',
'almaweb-ai-visitor-analytics-referrers',
array($this, 'render_referrers')
);
add_submenu_page('almaweb-ai-visitor-analytics', __('Logs', 'almaweb-ai-visitor-analytics'), __('Logs', 'almaweb-ai-visitor-analytics'), 'manage_options', 'almaweb-ai-visitor-analytics-logs', array($this, 'render_logs'));
add_submenu_page('almaweb-ai-visitor-analytics', __('Settings', 'almaweb-ai-visitor-analytics'), __('Settings', 'almaweb-ai-visitor-analytics'), 'manage_options', 'almaweb-ai-visitor-analytics-settings', array($this, 'render_settings'));
}
public function enqueue_admin_assets($hook) {
if (strpos($hook, 'almaweb-ai-visitor-analytics') === false) return;
wp_enqueue_style('almaweb-ai-visitor-analytics-admin', ALMAWEB_PLUGIN_URL . 'assets/css/admin.css', array(), ALMAWEB_VERSION);
wp_enqueue_script('chartjs', ALMAWEB_PLUGIN_URL . 'assets/js/vendor/chart.min.js', array(), '4.5.1', true);
wp_enqueue_script('almaweb-ai-visitor-analytics-admin', ALMAWEB_PLUGIN_URL . 'assets/js/admin.js', array('chartjs'), ALMAWEB_VERSION, true);
}
public function render_dashboard() { $dashboard = new Almaweb_Admin_Dashboard(); $dashboard->render(); $this->render_footer(); }
public function render_logs() { require_once ALMAWEB_PLUGIN_DIR . 'admin/views/logs.php'; $this->render_footer(); }
public function render_settings() { $settings = new Almaweb_Admin_Settings(); $settings->render(); $this->render_footer(); }
public function render_referrers() { require_once ALMAWEB_PLUGIN_DIR . 'admin/views/referrers.php'; $this->render_footer(); }
public function render_footer() {
/* translators: %s: Link to AlmaWeb website. */
echo '';
}
private function schedule_cleanup() { if (!wp_next_scheduled('almaweb_daily_cleanup')) wp_schedule_event(time(), 'daily', 'almaweb_daily_cleanup'); }
private function should_block_bot($visitor_data, $settings) {
$blocked_intentions = $settings['blocked_intentions'] ?? [];
$blocked_families = $settings['blocked_families'] ?? [];
if (empty($blocked_intentions) && empty($blocked_families)) { return false; }
if (!empty($visitor_data['bot_intention']) && in_array($visitor_data['bot_intention'], $blocked_intentions, true)) { return true; }
if (!empty($visitor_data['bot_family']) && in_array($visitor_data['bot_family'], $blocked_families, true)) { return true; }
return false;
}
public function run_cleanup() {
$settings = get_option('almaweb_settings');
$retention_days = isset($settings['retention_days']) ? intval($settings['retention_days']) : 90;
$database = new Almaweb_Database();
$database->cleanup_old_logs($retention_days);
}
}
function almaweb_ai_visitor_analytics() { return almaweb_ai_visitor_analytics::get_instance(); }
almaweb_ai_visitor_analytics();