register_ajax_handlers(); // Get log details add_action('wp_ajax_actlogpro_get_log_details', array($this, 'get_log_details')); // Schedule cleanup task add_action('actlogpro_cleanup_old_logs', array($this, 'cleanup_old_logs')); if (!wp_next_scheduled('actlogpro_cleanup_old_logs')) { wp_schedule_event(time(), 'daily', 'actlogpro_cleanup_old_logs'); } // Schedule email digest if enabled and not already scheduled // This ensures scheduling happens on plugin load if needed add_action('admin_init', array($this, 'ensure_email_digest_scheduled'), 20); // Add admin notice for premium settings reset add_action('admin_notices', array($this, 'display_premium_reset_notice')); // Add admin notice for caching plugin compatibility add_action('admin_notices', array($this, 'display_caching_plugin_notice')); // Add admin notice for plugin activation add_action('admin_notices', array($this, 'display_activation_notice')); // Initialize rating request functionality add_action('init', array($this, 'init_rating_request')); } /** * Initialize rating request functionality. * * @since 1.0.3 */ public function init_rating_request() { // Only run for administrators if (!current_user_can('manage_options')) { return; } // Load rating request class if not already loaded if (!class_exists('ACTLOGPRO_Rating_Request')) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-rating-request.php'; } // Initialize rating request functionality ACTLOGPRO_Rating_Request::init(); } /** * Register all AJAX handlers for the plugin * * @since 1.0.0 */ public function register_ajax_handlers() { add_action('wp_ajax_actlogpro_get_activity_logs', array($this, 'ajax_get_activity_logs')); add_action('wp_ajax_actlogpro_clear_logs', array($this, 'ajax_clear_logs')); add_action('wp_ajax_actlogpro_delete_selected_logs', array($this, 'ajax_delete_selected_logs')); add_action('wp_ajax_actlogpro_delete_logs', array($this, 'ajax_delete_selected_logs')); // Add this line to handle the correct action name add_action('wp_ajax_actlogpro_filter_logs', array($this, 'handle_ajax_filter_logs')); add_action('wp_ajax_actlogpro_save_settings', array($this, 'save_settings')); add_action('wp_ajax_actlogpro_generate_feed_token', array($this, 'ajax_generate_feed_token')); add_action('wp_ajax_actlogpro_reset_tracking_settings', array($this, 'ajax_reset_tracking_settings')); add_action('wp_ajax_actlogpro_reset_premium_settings', array($this, 'ajax_reset_premium_settings')); add_action('wp_ajax_actlogpro_dismiss_notice', array($this, 'ajax_dismiss_notice')); add_action('wp_ajax_actlogpro_get_ip_info', array($this, 'ajax_get_ip_info')); add_action('wp_ajax_actlogpro_get_user_card', array($this, 'ajax_get_user_card')); add_action('wp_ajax_actlogpro_toggle_stats_view', array($this, 'ajax_toggle_stats_view')); add_action('wp_ajax_actlogpro_refresh_stats_cache', array($this, 'ajax_refresh_stats_cache')); add_action('wp_ajax_actlogpro_log_quick_edit', array($this, 'ajax_log_quick_edit')); add_action('wp_ajax_actlogpro_get_settings', array($this, 'ajax_get_settings')); add_action('wp_ajax_actlogpro_add_object_to_exclusions', array($this, 'ajax_add_object_to_exclusions')); add_action('wp_ajax_actlogpro_dismiss_rating_request', array('ACTLOGPRO_Rating_Request', 'ajax_handle_dismissal')); // Live Sessions AJAX handlers add_action('wp_ajax_actlogpro_get_live_sessions', array($this, 'ajax_get_live_sessions')); add_action('wp_ajax_actlogpro_save_live_sessions_settings', array($this, 'ajax_save_live_sessions_settings')); add_action('wp_ajax_actlogpro_terminate_session', array($this, 'ajax_terminate_session')); add_action('wp_ajax_actlogpro_emergency_lockdown', array($this, 'ajax_emergency_lockdown')); // Recorded Notes AJAX handler add_action('wp_ajax_actlogpro_record_note', array($this, 'ajax_record_note')); // Newsletter subscription AJAX handler add_action('wp_ajax_actlogpro_newsletter_subscribe', array($this, 'ajax_newsletter_subscribe')); } /** * AJAX handler for dismissing admin notices permanently. * * Security: Requires 'manage_options' capability and valid nonce verification. * Allows administrators to dismiss plugin notices and alerts. * * @since 1.0.0 */ public function ajax_dismiss_notice() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check if notice ID is provided if (!isset($_POST['notice_id']) || empty(sanitize_key(wp_unslash($_POST['notice_id'])))) { wp_send_json_error(array('message' => __('Notice ID is required.', 'activity-log-pro'))); } // Sanitize notice ID $notice_id = sanitize_key(wp_unslash($_POST['notice_id'])); // Handle special case for activation notice if ($notice_id === 'alp-activation-notice') { // For activation notice, we need to handle it as user-specific // The global flag will be cleared when ALL users have dismissed it // For now, just continue with normal user-specific handling } // Handle regular user-specific notices // Get current user ID $user_id = get_current_user_id(); if (!$user_id) { wp_send_json_error(array('message' => __('User not logged in.', 'activity-log-pro'))); } // Get dismissed notices for this user $dismissed_notices = get_user_meta($user_id, 'actlogpro_dismissed_notices', true); if (!is_array($dismissed_notices)) { $dismissed_notices = array(); } // Check if notice is already dismissed if (isset($dismissed_notices[$notice_id])) { wp_send_json_success(array('message' => __('Notice was already dismissed.', 'activity-log-pro'))); return; } // Add this notice to the dismissed list $dismissed_notices[$notice_id] = time(); // Update user meta $updated = update_user_meta($user_id, 'actlogpro_dismissed_notices', $dismissed_notices); // update_user_meta returns false if the value hasn't changed, which is not an error // So we check if the value is properly stored by retrieving it $verify_dismissed = get_user_meta($user_id, 'actlogpro_dismissed_notices', true); if (is_array($verify_dismissed) && isset($verify_dismissed[$notice_id])) { // Special handling for activation notice - check if all users have dismissed it if ($notice_id === 'alp-activation-notice') { $this->check_activation_notice_global_dismissal(); } wp_send_json_success(array('message' => __('Notice dismissed successfully.', 'activity-log-pro'))); } else { wp_send_json_error(array('message' => __('Failed to dismiss notice.', 'activity-log-pro'))); } } /** * Check if all users have dismissed the activation notice and clear global flag if so * * @since 1.0.1 */ private function check_activation_notice_global_dismissal() { // Get all users who can manage options $users = get_users(array( 'capability' => 'manage_options', 'fields' => 'ID' )); if (empty($users)) { return; } // Check if all users have dismissed the activation notice $all_dismissed = true; foreach ($users as $user_id) { $dismissed_notices = get_user_meta($user_id, 'actlogpro_dismissed_notices', true); if (!is_array($dismissed_notices) || !isset($dismissed_notices['alp-activation-notice'])) { $all_dismissed = false; break; } } // If all users have dismissed it, clear the global flag if ($all_dismissed) { delete_option('actlogpro_show_activation_notice'); } } /** * Register the admin menu. * * @since 1.0.0 */ public function add_admin_menu() { // Get the admin menu location setting $menu_location = get_option('actlogpro_admin_menu_location', 'default'); // Define menu parameters $page_title = __('Activity Log Pro', 'activity-log-pro'); $menu_title = __('Activity Log Pro', 'activity-log-pro'); $capability = 'manage_options'; $menu_slug = 'activity-log-pro'; $callback = array($this, 'display_activity_log_page'); // Custom SVG icon - task completion/checklist optimized for WordPress standards $svg_icon = ''; $icon = 'data:image/svg+xml;base64,' . base64_encode($svg_icon); // Add main menu based on location setting switch ($menu_location) { case 'dashboard': // Add as submenu under Dashboard add_submenu_page( 'index.php', $page_title, $menu_title, $capability, $menu_slug, $callback ); break; case 'tools': // Add as submenu under Tools add_submenu_page( 'tools.php', $page_title, $menu_title, $capability, $menu_slug, $callback ); break; case 'top': // Add at top of main menu (position 3) add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback, $icon, 3 ); break; case 'bottom': // Add at bottom of main menu (position 99) add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback, $icon, 99 ); break; default: // Default WordPress menu order (position 30) add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback, $icon, 30 ); break; } // Only add Activity Log submenu if we're not under Dashboard or Tools if (!in_array($menu_location, array('dashboard', 'tools'))) { add_submenu_page( $menu_slug, __('Activity Logs', 'activity-log-pro'), __('Activity Logs', 'activity-log-pro'), $capability, $menu_slug ); } // For Dashboard/Tools, register submenus under the appropriate parent if (in_array($menu_location, array('dashboard', 'tools'))) { // Register submenus directly under their respective parent pages $parent_page = ($menu_location === 'dashboard') ? 'index.php' : 'tools.php'; // Add Settings submenu (properly registered to avoid PHP 8.1+ warnings) $settings_hook = add_submenu_page( $parent_page, __('Activity Log Pro Settings', 'activity-log-pro'), __('ALP Settings', 'activity-log-pro'), $capability, 'activity-log-pro-settings', array($this, 'display_settings_page') ); // Add Live Sessions submenu $live_sessions_hook = add_submenu_page( $parent_page, __('Activity Log Pro Live Sessions', 'activity-log-pro'), __('ALP Live Sessions', 'activity-log-pro'), $capability, 'activity-log-pro-live-sessions', array($this, 'display_live_sessions_page') ); // Add Export submenu (properly registered to avoid PHP 8.1+ warnings) $export_hook = add_submenu_page( $parent_page, __('Activity Log Pro Export', 'activity-log-pro'), __('ALP Export', 'activity-log-pro'), $capability, 'activity-log-pro-export', array($this, 'display_export_page') ); // Add Integrations submenu (properly registered to avoid PHP 8.1+ warnings) $integrations_hook = add_submenu_page( $parent_page, __('Activity Log Pro Integrations', 'activity-log-pro'), __('ALP Integrations', 'activity-log-pro'), $capability, 'activity-log-pro-integrations', array($this, 'display_integrations_page') ); // Add Stats & Insights submenu (properly registered to avoid PHP 8.1+ warnings) $stats_hook = add_submenu_page( $parent_page, __('Activity Log Pro Stats', 'activity-log-pro'), __('ALP Stats', 'activity-log-pro'), $capability, 'activity-log-pro-stats', array($this, 'display_stats_insights_page') ); // Add Upgrade to Premium submenu (properly registered to avoid PHP 8.1+ warnings) if (ACTLOGPRO_Premium_Helper::show_premium_ui()) { $upgrade_hook = add_submenu_page( $parent_page, __('Activity Log Pro Upgrade', 'activity-log-pro'), __('ALP Upgrade', 'activity-log-pro'), $capability, 'activity-log-pro-upgrade', array($this, 'display_upgrade_page') ); } // Hide the submenus from the visible menu while keeping them accessible via direct URL add_action('admin_head', function() use ($parent_page) { global $submenu; // Remove our plugin pages from the visible submenu while keeping them registered if (isset($submenu[$parent_page])) { foreach ($submenu[$parent_page] as $key => $menu_item) { if (isset($menu_item[2]) && strpos($menu_item[2], 'activity-log-pro-') === 0) { unset($submenu[$parent_page][$key]); } } } }); } else { // For regular menu locations, add submenus normally add_submenu_page( $menu_slug, __('Settings', 'activity-log-pro'), __('Settings', 'activity-log-pro'), $capability, 'activity-log-pro-settings', array($this, 'display_settings_page') ); add_submenu_page( $menu_slug, __('Live Sessions', 'activity-log-pro'), __('Live Sessions', 'activity-log-pro'), $capability, 'activity-log-pro-live-sessions', array($this, 'display_live_sessions_page') ); add_submenu_page( $menu_slug, __('Export', 'activity-log-pro'), __('Export', 'activity-log-pro'), $capability, 'activity-log-pro-export', array($this, 'display_export_page') ); add_submenu_page( $menu_slug, __('Integrations', 'activity-log-pro'), __('Integrations', 'activity-log-pro'), $capability, 'activity-log-pro-integrations', array($this, 'display_integrations_page') ); add_submenu_page( $menu_slug, __('Stats & Insights', 'activity-log-pro'), __('Stats & Insights', 'activity-log-pro'), $capability, 'activity-log-pro-stats', array($this, 'display_stats_insights_page') ); // Add Upgrade to Premium submenu (only show if premium is not enabled) if (ACTLOGPRO_Premium_Helper::show_premium_ui()) { add_submenu_page( $menu_slug, __('Upgrade to Premium', 'activity-log-pro'), __('Upgrade to Premium', 'activity-log-pro'), $capability, 'activity-log-pro-upgrade', array($this, 'display_upgrade_page') ); } } } /** * Register plugin settings. * * @since 1.0.0 */ public function register_settings() { register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_retention_period', array( 'sanitize_callback' => array($this, 'sanitize_integer') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_items_per_page', array( 'sanitize_callback' => array($this, 'sanitize_integer') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_user_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_post_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_media_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_comment_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_plugin_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_theme_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_widget_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_menu_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_option_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_wordpress_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_security_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // AI & Connectors logging (WP 7.0+ Connectors + official WordPress AI plugin). register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_ai_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_site_health', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // Email logging — Premium feature; options registered here so the shared settings form can save them. register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_email_events', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_track_email_exclude_own', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_flood_guard_enabled', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_flood_guard_limit', array( 'sanitize_callback' => array($this, 'sanitize_flood_guard_limit') )); // Event filtering and exclusions register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_excluded_roles', array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize_excluded_roles' ), 'default' => array(), )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_excluded_users', array( 'sanitize_callback' => array( $this, 'sanitize_excluded_users' ), 'default' => '', )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_excluded_option_names', array( 'sanitize_callback' => array($this, 'sanitize_excluded_options') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_excluded_post_types', array( 'type' => 'array', 'sanitize_callback' => array($this, 'sanitize_excluded_post_types'), 'default' => array() )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_exclude_attachments_for_excluded_posts', array( 'type' => 'boolean', 'sanitize_callback' => array($this, 'sanitize_checkbox'), 'default' => true )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_exclude_deleted_attachments_for_excluded_posts', array( 'type' => 'boolean', 'sanitize_callback' => array($this, 'sanitize_checkbox'), 'default' => false )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_show_private_post_types', array( 'type' => 'boolean', 'sanitize_callback' => array($this, 'sanitize_checkbox'), 'default' => false )); // Feed settings register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_enable_json_feed', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_enable_rss_feed', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // UI settings register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_allow_multiple_expanded_rows', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // IP Address settings - register for all users (premium validation happens in save_settings) register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_store_full_ip', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // Misc settings register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_login_notification_enabled', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_login_notification_text', array( 'sanitize_callback' => 'wp_kses_post' )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_admin_menu_location', array( 'sanitize_callback' => 'sanitize_text_field' )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_remove_data_on_uninstall', array( 'sanitize_callback' => array($this, 'sanitize_checkbox') )); // Register email digest settings here with other settings register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_enabled', array( 'sanitize_callback' => array('ACTLOGPRO_Email_Digest_Manager', 'sanitize_checkbox') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_frequency', array( 'sanitize_callback' => array('ACTLOGPRO_Email_Digest_Manager', 'sanitize_frequency') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_day', array( 'sanitize_callback' => array('ACTLOGPRO_Email_Digest_Manager', 'sanitize_day') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_time', array( 'sanitize_callback' => array('ACTLOGPRO_Email_Digest_Manager', 'sanitize_time') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_recipients', array( 'sanitize_callback' => array('ACTLOGPRO_Email_Digest_Manager', 'sanitize_email_recipients') )); register_setting('actlogpro_activity_log_pro_settings', 'actlogpro_email_digest_last_sent', array( 'sanitize_callback' => 'absint' )); } /** * Handle settings save for checkbox arrays * WordPress doesn't submit unchecked checkboxes, so we need to handle empty arrays manually * * @since 1.0.0 */ public function handle_settings_save() { // Check if we're saving settings // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WordPress Settings API handles nonce verification if (!isset($_POST['option_page']) || $_POST['option_page'] !== 'actlogpro_activity_log_pro_settings') { return; } // Verify nonce for security // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verification doesn't require sanitization if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'actlogpro_activity_log_pro_settings-options')) { return; } if (!current_user_can('manage_options')) { return; } // Check if our hidden field is present (means the excluded post types field was on the form) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Already verified nonce above if (!isset($_POST['actlogpro_excluded_post_types_submitted'])) { return; } // Get the submitted value (empty array if no checkboxes checked) // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified above, sanitized by sanitize_excluded_post_types() method below $submitted_value = isset($_POST['actlogpro_excluded_post_types']) ? wp_unslash($_POST['actlogpro_excluded_post_types']) : array(); // Sanitize using our existing method $sanitized_value = $this->sanitize_excluded_post_types($submitted_value); // Use the value from before this save: private types are only on the form if it was already enabled. $was_showing_private = get_option('actlogpro_show_private_post_types', '0') === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Already verified nonce above $show_private_post_types = isset($_POST['actlogpro_show_private_post_types']) ? '1' : '0'; update_option('actlogpro_show_private_post_types', $show_private_post_types); if (!$was_showing_private) { $sanitized_value = $this->preserve_hidden_private_excluded_post_types($sanitized_value); } // Directly update the option since WordPress Settings API isn't calling our callback update_option('actlogpro_excluded_post_types', $sanitized_value); // Handle the attachment exclusion checkboxes (WordPress doesn't submit unchecked checkboxes) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Already verified nonce above $exclude_attachments = isset($_POST['actlogpro_exclude_attachments_for_excluded_posts']) ? '1' : '0'; update_option('actlogpro_exclude_attachments_for_excluded_posts', $exclude_attachments); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Already verified nonce above $exclude_deleted_attachments = isset($_POST['actlogpro_exclude_deleted_attachments_for_excluded_posts']) ? '1' : '0'; update_option('actlogpro_exclude_deleted_attachments_for_excluded_posts', $exclude_deleted_attachments); $this->save_user_exclusion_settings(); } /** * Re-add excluded private post types that were not on the public-only form. * * @since 1.1.0 * @param array $sanitized_value Currently submitted public custom post types. * @return array */ private function preserve_hidden_private_excluded_post_types($sanitized_value) { if (!is_array($sanitized_value)) { $sanitized_value = array(); } $previous = get_option('actlogpro_excluded_post_types', array()); if (!is_array($previous) || empty($previous)) { return $sanitized_value; } foreach ($previous as $post_type) { $post_type = sanitize_key($post_type); if ('' === $post_type || in_array($post_type, $sanitized_value, true)) { continue; } if (!post_type_exists($post_type)) { continue; } $post_type_obj = get_post_type_object($post_type); if ($post_type_obj && empty($post_type_obj->public)) { $sanitized_value[] = $post_type; } } return $sanitized_value; } /** * Sanitize integer values * * @param mixed $value The value to sanitize * @return int Sanitized integer value */ public function sanitize_integer($value) { return absint($value); } /** * Sanitize checkbox values * * @param mixed $value The value to sanitize * @return string '1' if truthy, '0' otherwise */ public function sanitize_checkbox($value) { $result = !empty($value) ? '1' : '0'; return $result; } /** * Sanitize flood guard limit — only allow specific whitelist values. */ public function sanitize_flood_guard_limit($value) { $allowed = array(20, 50, 100, 200); $value = (int) $value; return in_array($value, $allowed, true) ? $value : 50; } /** * Sanitize excluded option names textarea * * @param string $value The value to sanitize * @return string Sanitized option names, one per line */ public function sanitize_excluded_options($value) { if (empty($value)) { return ''; } // Normalize line endings first $value = str_replace(array("\r\n", "\r"), "\n", $value); // Split by both newlines AND spaces to handle form submission issues $items = preg_split('/[\s\n]+/', trim($value), -1, PREG_SPLIT_NO_EMPTY); // Process each item $sanitized_lines = array(); foreach ($items as $item) { $item = trim($item); if (!empty($item)) { // Sanitize but preserve underscores, wildcards, etc. $item = wp_strip_all_tags($item); if (!empty($item)) { $sanitized_lines[] = $item; } } } // Join back with newlines return implode("\n", $sanitized_lines); } /** * Sanitize excluded post types setting * * @param mixed $value The value to sanitize * @return array Sanitized array of post type slugs */ public function sanitize_excluded_post_types($value) { // If no value provided, return empty array if (empty($value) || !is_array($value)) { return array(); } // Sanitize each post type slug $sanitized = array_map('sanitize_key', $value); // Validate that each post type actually exists $valid_post_types = array(); foreach ($sanitized as $post_type) { if (post_type_exists($post_type)) { $valid_post_types[] = $post_type; } } return $valid_post_types; } /** * Sanitize the excluded roles setting. * Strips the administrator role and returns a clean indexed array. * * @param mixed $value Raw submitted value. * @return array Sanitized array of role slugs. */ public function sanitize_excluded_roles( $value ) { if ( empty( $value ) || ! is_array( $value ) ) { return array(); } $sanitized = array_map( 'sanitize_key', $value ); // Hard-block: administrator can never be excluded. $sanitized = array_values( array_diff( $sanitized, array( 'administrator' ) ) ); // Keep only roles that actually exist on this site. $all_roles = wp_roles()->get_names(); return array_values( array_intersect( $sanitized, array_keys( $all_roles ) ) ); } /** * Sanitize the excluded users / emails setting. * One entry per line; blank lines stripped; entries lowercased. * * @param mixed $value Raw textarea string. * @return string Sanitized newline-delimited list. */ public function sanitize_excluded_users( $value ) { if ( empty( $value ) ) { return ''; } $value = str_replace( array( "\r\n", "\r" ), "\n", (string) $value ); $lines = explode( "\n", $value ); $clean = array(); foreach ( $lines as $line ) { $entry = strtolower( sanitize_text_field( trim( $line ) ) ); if ( '' !== $entry ) { $clean[] = $entry; } } return implode( "\n", array_unique( $clean ) ); } /** * Save excluded roles and users from the settings form. * * @return array Warning messages to display after save. */ private function save_user_exclusion_settings(): array { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Caller verifies nonce; sanitized by sanitize_excluded_roles() below. $submitted_roles = isset( $_POST['actlogpro_excluded_roles'] ) ? (array) wp_unslash( $_POST['actlogpro_excluded_roles'] ) : array(); update_option( 'actlogpro_excluded_roles', $this->sanitize_excluded_roles( $submitted_roles ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Caller verifies nonce. $submitted_users = isset( $_POST['actlogpro_excluded_users'] ) ? sanitize_textarea_field( wp_unslash( $_POST['actlogpro_excluded_users'] ) ) : ''; $sanitized_users = $this->sanitize_excluded_users( $submitted_users ); update_option( 'actlogpro_excluded_users', $sanitized_users ); $warnings = $this->get_excluded_users_warnings( $sanitized_users ); $this->maybe_add_excluded_users_warnings( $warnings ); return $warnings; } /** * Build warning messages for the excluded users list. * * @param string $saved_users Sanitized newline-delimited list. * @return array Warning message strings. */ private function get_excluded_users_warnings( string $saved_users ): array { if ( '' === $saved_users ) { return array(); } $warnings = array(); $entries = array_filter( explode( "\n", $saved_users ) ); $current_id = get_current_user_id(); $own_warned = false; foreach ( $entries as $entry ) { $user = get_user_by( 'login', $entry ); if ( ! $user ) { $user = get_user_by( 'email', $entry ); } if ( ! $user ) { $warnings[] = sprintf( /* translators: %s: username or email */ __( 'Unknown user or email address: "%s". This entry was saved but will not match any user.', 'activity-log-pro' ), $entry ); continue; } if ( in_array( 'administrator', (array) $user->roles, true ) ) { $warnings[] = sprintf( /* translators: %s: username */ __( 'Warning: "%s" has the administrator role. Their activity (except logins and logouts) will not be logged.', 'activity-log-pro' ), $user->user_login ); } if ( ! $own_warned && (int) $user->ID === (int) $current_id ) { $warnings[] = __( 'Warning: You have excluded your own account. Your activity will not appear in the log.', 'activity-log-pro' ); $own_warned = true; } } return $warnings; } /** * Register excluded-user warnings with the Settings API (non-AJAX saves). * * @param array $warnings Warning message strings. * @return void */ private function maybe_add_excluded_users_warnings( array $warnings ): void { foreach ( $warnings as $index => $message ) { add_settings_error( 'actlogpro_excluded_users', 'excluded_users_warning_' . $index, esc_html( $message ), 'warning' ); } } /** * Enqueue admin scripts and styles. * * @since 1.0.0 * @param string $hook Current admin page. */ public function enqueue_scripts($hook) { // Enqueue rating request styles on all admin pages (rating notices can appear anywhere) wp_enqueue_style('alp-rating-request-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-rating-request.css', array(), ACTLOGPRO_VERSION); wp_enqueue_script('alp-rating-request-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-rating-request.js', array('jquery'), ACTLOGPRO_VERSION, true); // Enqueue notices script on all admin pages (notices can appear anywhere) wp_enqueue_script('alp-notices-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-notices.js', array('jquery'), ACTLOGPRO_VERSION, true); // Localize notices script with AJAX data wp_localize_script('alp-notices-js', 'actlogproAdminData', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce') )); // Only load plugin-specific assets on plugin pages if (strpos($hook, 'activity-log-pro') === false) { return; } // Enqueue styles wp_enqueue_style('alp-admin-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-admin.css', array(), ACTLOGPRO_VERSION); wp_enqueue_style('alp-tooltip-fix', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-tooltip-fix.css', array('alp-admin-css'), ACTLOGPRO_VERSION); wp_enqueue_style('alp-export-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-export.css', array('alp-admin-css'), ACTLOGPRO_VERSION); // Note: License management scripts are now handled by the premium plugin // Enqueue jQuery UI styles and scripts wp_enqueue_style('jquery-ui-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/jquery-ui.min.css', array(), '1.12.1'); wp_enqueue_script('jquery-ui-datepicker'); wp_enqueue_script('jquery-ui-dialog'); // Add jQuery UI Dialog // Load Leaflet for premium users on pages that show the IP-info popup (main log + Live Sessions). $is_premium = !ACTLOGPRO_Premium_Helper::show_premium_ui(); $is_activity_log_page = (strpos($hook, 'activity-log-pro') !== false && strpos($hook, 'activity-log-pro-') === false); $is_live_sessions_page = (strpos($hook, 'activity-log-pro-live-sessions') !== false); if ($is_premium && ($is_activity_log_page || $is_live_sessions_page)) { // Enqueue Leaflet CSS wp_enqueue_style('leaflet-css', ACTLOGPRO_PLUGIN_URL . 'assets/css/leaflet.css', array(), '1.9.4'); // Enqueue Leaflet fixes CSS with proper dependency wp_enqueue_style('alp-leaflet-fixes', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-leaflet-fixes.css', array('leaflet-css'), ACTLOGPRO_VERSION); // Set CSS custom properties for dynamic URLs with proper escaping $leaflet_vars_css = sprintf( ':root { --alp-leaflet-layers-icon: url(%s); --alp-leaflet-layers-retina-icon: url(%s); --alp-leaflet-marker-icon: url(%s); }', esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/layers.png'), esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/layers-2x.png'), esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/marker-icon.png') ); wp_add_inline_style('alp-leaflet-fixes', $leaflet_vars_css); // Enqueue our custom map styles wp_enqueue_style('alp-map-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-map.css', array('alp-leaflet-fixes'), ACTLOGPRO_VERSION); // Enqueue Leaflet JavaScript wp_enqueue_script('leaflet-js', ACTLOGPRO_PLUGIN_URL . 'assets/js/leaflet.js', array(), '1.9.4', true); } // Enqueue admin script wp_enqueue_script('alp-admin-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-admin.js', array('jquery', 'jquery-ui-datepicker', 'jquery-ui-dialog', 'wp-i18n'), ACTLOGPRO_VERSION, true); // Enqueue Quick Tips module on main activity log page only $is_main_activity_page = (strpos($hook, 'activity-log-pro') !== false && strpos($hook, 'activity-log-pro-') === false); if ($is_main_activity_page) { wp_enqueue_script('alp-quick-tips-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/modules/alp-quick-tips.js', array('jquery'), ACTLOGPRO_VERSION, true); // Enqueue live monitor script for main activity log page wp_enqueue_script('alp-live-monitor-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/modules/alp-live-monitor.js', array('jquery'), ACTLOGPRO_VERSION, true); // Localize Quick Tips script with AJAX data wp_localize_script('alp-quick-tips-js', 'actlogpro_ajax', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce'), )); } // Enqueue live sessions assets on the Live Sessions page only $is_live_sessions_page = ( strpos( $hook, 'activity-log-pro-live-sessions' ) !== false ); if ( $is_live_sessions_page ) { wp_enqueue_style( 'alp-live-sessions-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-live-sessions.css', array( 'alp-admin-css' ), ACTLOGPRO_VERSION ); wp_enqueue_script( 'alp-live-sessions-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/modules/alp-live-sessions.js', array( 'jquery' ), ACTLOGPRO_VERSION, true ); wp_localize_script( 'alp-live-sessions-js', 'alpLiveSessions', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'actlogpro_ajax_nonce' ), 'isPremium' => ACTLOGPRO_Premium_Helper::is_premium_enabled(), 'upgradeUrl' => admin_url( 'admin.php?page=activity-log-pro-upgrade' ), 'i18n' => array( 'loading' => __( 'Loading sessions…', 'activity-log-pro' ), 'noSessions' => __( 'No active sessions found.', 'activity-log-pro' ), 'errorLoading' => __( 'Error loading sessions. Please try again.', 'activity-log-pro' ), // translators: %d: number of active sessions. 'sessionCount' => __( 'Active sessions: %d', 'activity-log-pro' ), 'you' => __( '(You)', 'activity-log-pro' ), 'terminate' => __( 'Terminate', 'activity-log-pro' ), 'traceActivity' => __( 'Trace Activity', 'activity-log-pro' ), 'traceActivityTitle' => __( 'View activity logs for this user session', 'activity-log-pro' ), 'confirmTerminate' => __( 'Are you sure you want to terminate this session? The user will be logged out immediately.', 'activity-log-pro' ), 'confirmSelfTerminate' => __( 'This is your own active session. Terminating it will log you out immediately. Are you sure you want to continue?', 'activity-log-pro' ), 'confirmLockdown' => __( 'EMERGENCY LOCKDOWN: This will immediately terminate all active sessions except your own. All logged-in users will be logged out. Are you sure?', 'activity-log-pro' ), 'terminateSuccess' => __( 'Session terminated successfully.', 'activity-log-pro' ), 'terminateError' => __( 'Failed to terminate session. Please try again.', 'activity-log-pro' ), // translators: %d: number of sessions terminated. 'lockdownSuccess' => __( 'Lockdown complete. %d session(s) terminated.', 'activity-log-pro' ), 'lockdownError' => __( 'Lockdown failed. Please try again.', 'activity-log-pro' ), 'premiumRequired' => __( 'This feature requires Activity Log Pro Premium.', 'activity-log-pro' ), 'upgradeToPremium' => __( 'Upgrade to Premium', 'activity-log-pro' ), // translators: %s: formatted date and time of the last refresh. 'updatedAt' => __( 'Last updated: %s', 'activity-log-pro' ), 'locationDisclaimer' => __( 'Approximate location only. VPNs may report incorrect locations.', 'activity-log-pro' ), 'pollError' => __( 'Live monitor paused — could not reach the server after 3 attempts.', 'activity-log-pro' ), 'sessionIdLabel' => __( 'Session ID', 'activity-log-pro' ), 'sessionIdTooltip' => __( 'A unique hash assigned by WordPress for this login session. Use it to cross-reference sessions in server access logs, or to distinguish two sessions with identical IP and browser.', 'activity-log-pro' ), 'sessionIdCopy' => __( 'Copy full Session ID', 'activity-log-pro' ), 'sessionIdCopied' => __( 'Copied!', 'activity-log-pro' ), 'sessionExpiryLabel' => __( 'Session Expiry', 'activity-log-pro' ), 'concurrentOnly' => __( 'Show users with concurrent sessions', 'activity-log-pro' ), 'settingsSaved' => __( 'Settings saved.', 'activity-log-pro' ), 'settingsError' => __( 'Failed to save settings.', 'activity-log-pro' ), 'perPageLabel' => __( 'Sessions per page', 'activity-log-pro' ), 'simultaneousLabel' => __( 'Simultaneous session limit', 'activity-log-pro' ), 'simultaneousDisabled' => __( 'Disabled', 'activity-log-pro' ), 'simultaneousHelp' => __( 'Maximum number of active sessions allowed per user at one time. When a user logs in and exceeds the limit, the oldest session is automatically terminated. Set to 0 to disable.', 'activity-log-pro' ), ), 'settings' => array( 'perPage' => (int) get_option( ACTLOGPRO_Live_Sessions::PER_PAGE_OPTION, ACTLOGPRO_Live_Sessions::DEFAULT_PER_PAGE ), 'activeMinutes' => class_exists( 'ACTLOGPRO_Live_Sessions_Activity' ) ? ACTLOGPRO_Live_Sessions_Activity::get_threshold_minutes() : 10, 'simultaneousLimit' => (int) get_option( 'actlogpro_live_sessions_simultaneous_limit', 0 ), ), ) ); } // Register ES modules handler that loads other modules // Don't use type="module" directly, load ES modules with a proper fallback wp_register_script('alp-modules-handler', ACTLOGPRO_PLUGIN_URL . 'admin/js/modules-handler.js', array('jquery'), ACTLOGPRO_VERSION, true); wp_enqueue_script('alp-modules-handler'); // Add a proper inline script to initialize modules once the page is loaded $modules_init = " document.addEventListener('DOMContentLoaded', function() { // Initialize any modules needed for the current page if (typeof window.ACTLOGPRO_ModulesHandler !== 'undefined') { window.ACTLOGPRO_ModulesHandler.init(); } }); "; wp_add_inline_script('alp-modules-handler', $modules_init); // Get the items per page setting $items_per_page = get_option('actlogpro_items_per_page', 10); // Get excluded option names for JavaScript $excluded_options_string = get_option('actlogpro_excluded_option_names', ''); $excluded_options_array = array_filter(array_map('trim', explode("\n", $excluded_options_string))); // Prepare admin data for localization $admin_data = array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce'), 'error_text' => __('An error occurred. Please try again.', 'activity-log-pro'), 'no_logs_text' => __('No logs found.', 'activity-log-pro'), 'confirm_clear_text' => __('Are you sure you want to clear all logs? This action cannot be undone.', 'activity-log-pro'), 'items_per_page' => $items_per_page, 'allow_multiple_expanded' => get_option('actlogpro_allow_multiple_expanded_rows', '0') === '1', 'admin_url' => admin_url(), 'upgrade_url' => ACTLOGPRO_Premium_Helper::get_upgrade_url(), 'plugin_version' => ACTLOGPRO_VERSION, 'i18n' => array( 'save_to_see_url' => __('Save settings and refresh the page to see the JSON feed URL.', 'activity-log-pro'), 'save_to_see_rss_url' => __('Save settings and refresh the page to see the RSS feed URL.', 'activity-log-pro'), 'excluded_users_notice_title' => __('Excluded Users', 'activity-log-pro'), 'excluded_users_jump_link' => __('Back to Excluded Users', 'activity-log-pro'), ), 'confirmResetSettings' => __('Are you sure you want to reset all tracking settings to default values?', 'activity-log-pro'), 'settingsSaved' => __('Settings saved successfully.', 'activity-log-pro'), 'tokenGenerated' => __('Feed token generated successfully.', 'activity-log-pro'), 'settingsReset' => __('Tracking settings have been reset to defaults.', 'activity-log-pro'), 'plugin_url' => esc_url(ACTLOGPRO_PLUGIN_URL), 'is_premium' => $is_premium, 'has_leaflet' => ($is_premium && ($is_activity_log_page || $is_live_sessions_page)), 'leaflet_icon_urls' => array( // Add icon URLs for JavaScript 'iconUrl' => esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/marker-icon.png'), 'iconRetinaUrl' => esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/marker-icon-2x.png'), 'shadowUrl' => esc_url(ACTLOGPRO_PLUGIN_URL . 'assets/images/leaflet/marker-shadow.png') ), 'excluded_options' => $excluded_options_array, 'day_divider_today' => __('Today', 'activity-log-pro'), 'day_divider_yesterday' => __('Yesterday', 'activity-log-pro'), 'day_divider_separator' => _x(' · ', 'Between relative day label and calendar date in log divider.', 'activity-log-pro'), // translators: %d: number of days (2–5) for the activity log day divider. 'day_divider_days_ago' => __( '%d days ago', 'activity-log-pro' ), // Recorded Notes i18n strings. 'record_note_success' => __( 'Note recorded successfully.', 'activity-log-pro' ), 'record_note_required' => __( 'Please enter a note message.', 'activity-log-pro' ), 'record_note_error' => __( 'Failed to record the note. Please try again.', 'activity-log-pro' ), 'record_note_future_date' => __( 'Event date cannot be in the future.', 'activity-log-pro' ), 'record_note_submit_label'=> __( 'Record Note', 'activity-log-pro' ), // Active filter chips i18n strings. 'filtered_by_prefix' => __( 'Filtered by:', 'activity-log-pro' ), 'chip_user' => __( 'User:', 'activity-log-pro' ), 'chip_severity' => __( 'Severity:', 'activity-log-pro' ), 'chip_event' => __( 'Event:', 'activity-log-pro' ), 'chip_action' => __( 'Action:', 'activity-log-pro' ), 'chip_ip' => __( 'IP:', 'activity-log-pro' ), 'chip_date_label' => __( 'Date:', 'activity-log-pro' ), 'chip_date_from' => _x( 'from', 'Date-from prefix in filter chip, e.g. "Date: from 1 Sep 2026"', 'activity-log-pro' ), 'chip_date_to' => _x( 'until', 'Date-to prefix in filter chip, e.g. "Date: until 16 Sep 2026"', 'activity-log-pro' ), // translators: Separator between start and end dates in a filter chip, e.g. "1 Sep 2026 – 16 Sep 2026". 'chip_date_range_sep' => _x( ' – ', 'Date range separator in filter chip', 'activity-log-pro' ), 'clear_all_filters' => __( 'Clear all', 'activity-log-pro' ), 'dismiss_filter' => __( 'Remove filter', 'activity-log-pro' ), 'today_date' => wp_date( 'Y-m-d' ), 'filter_presets' => array( 'today' => __( 'Today', 'activity-log-pro' ), 'failed_logins' => __( 'Failed logins', 'activity-log-pro' ), 'security' => __( 'Security', 'activity-log-pro' ), 'plugins' => __( 'Plugins', 'activity-log-pro' ), 'users' => __( 'Users', 'activity-log-pro' ), 'critical_high' => __( 'Critical & High', 'activity-log-pro' ), ), // Row quick-filter icon tooltip strings. 'filter_by_ip' => __( 'Filter by this IP', 'activity-log-pro' ), 'filter_by_user' => __( 'Filter by this user', 'activity-log-pro' ), 'filter_by_severity' => __( 'Filter by this severity', 'activity-log-pro' ), 'show_events_from_ip' => __( 'Show all events from this IP', 'activity-log-pro' ), // User card popover i18n strings. 'user_card_loading' => __( 'Loading user info…', 'activity-log-pro' ), 'user_card_error' => __( 'Failed to load user info.', 'activity-log-pro' ), 'user_card_online_now' => __( 'Online now', 'activity-log-pro' ), 'user_card_offline' => __( 'Offline', 'activity-log-pro' ), 'user_card_member_since' => __( 'Member since', 'activity-log-pro' ), 'user_card_total_events' => __( 'Total events', 'activity-log-pro' ), 'user_card_last_login' => __( 'Last login', 'activity-log-pro' ), /* translators: Tooltip for the last login metric on the user card. */ 'user_card_last_login_hint' => __( 'Last successful login recorded in the activity log.', 'activity-log-pro' ), 'user_card_last_activity'=> __( 'Last activity', 'activity-log-pro' ), 'user_card_view_profile' => __( 'View profile', 'activity-log-pro' ), 'user_card_show_events' => __( 'View this user\'s events', 'activity-log-pro' ), 'user_card_recent_logins'=> __( 'Recent logins', 'activity-log-pro' ), 'user_card_recent_activity' => __( 'Recent activity', 'activity-log-pro' ), 'user_card_failed_logins'=> __( 'Failed logins', 'activity-log-pro' ), 'user_card_distinct_ips' => __( 'Distinct IPs', 'activity-log-pro' ), 'user_card_security' => __( 'Security', 'activity-log-pro' ), 'user_card_premium_label'=> __( 'Premium Feature', 'activity-log-pro' ), 'user_card_upgrade_cta' => __( 'Upgrade to see login history and recent activity.', 'activity-log-pro' ), 'user_card_upgrade_btn' => __( 'Learn more about premium', 'activity-log-pro' ), // IP card popover i18n strings. 'ip_card_loading' => __( 'Loading IP information…', 'activity-log-pro' ), 'ip_card_cached' => __( 'Cached', 'activity-log-pro' ), 'ip_card_ip' => __( 'IP Address', 'activity-log-pro' ), 'ip_card_timezone' => __( 'Timezone', 'activity-log-pro' ), 'ip_card_postal' => __( 'Postal code', 'activity-log-pro' ), 'ip_card_coordinates' => __( 'Coordinates', 'activity-log-pro' ), 'ip_card_details' => __( 'Details', 'activity-log-pro' ), 'ip_card_connection' => __( 'Connection', 'activity-log-pro' ), 'ip_card_hostname' => __( 'Hostname', 'activity-log-pro' ), 'ip_card_location' => __( 'Location', 'activity-log-pro' ), 'ip_card_organization' => __( 'Organization', 'activity-log-pro' ), 'ip_card_copy' => __( 'Copy to clipboard', 'activity-log-pro' ), 'ip_card_map_notice' => __( 'IP location is approximate and shown for general reference.', 'activity-log-pro' ), // Log entry detail panel — field labels and help tooltips. 'log_detail_fields' => array( 'log_id' => array( 'label' => __( 'Log ID', 'activity-log-pro' ), 'tooltip' => __( 'Unique reference number for this log entry in the database.', 'activity-log-pro' ), ), 'created_at' => array( 'label' => __( 'Created At', 'activity-log-pro' ), 'tooltip' => __( 'Date and time when this event was recorded.', 'activity-log-pro' ), ), 'event_type' => array( 'label' => __( 'Event Type', 'activity-log-pro' ), 'tooltip' => __( 'The category of activity (for example: post, user, plugin, or email).', 'activity-log-pro' ), ), 'action' => array( 'label' => __( 'Action', 'activity-log-pro' ), 'tooltip' => __( 'What happened (for example: updated, deleted, activated, or sent).', 'activity-log-pro' ), ), 'severity' => array( 'label' => __( 'Severity', 'activity-log-pro' ), 'tooltip' => __( 'How important this event is: low, medium, high, or critical.', 'activity-log-pro' ), ), 'description' => array( 'label' => __( 'Description', 'activity-log-pro' ), 'tooltip' => __( 'A plain-language summary of what changed.', 'activity-log-pro' ), ), 'user_id' => array( 'label' => __( 'User ID', 'activity-log-pro' ), 'tooltip' => __( 'WordPress user ID of who performed the action. 0 means the system or an automated process.', 'activity-log-pro' ), ), 'username' => array( 'label' => __( 'Username', 'activity-log-pro' ), 'tooltip' => __( 'Login name of the user who performed the action.', 'activity-log-pro' ), ), 'display_name' => array( 'label' => __( 'Display Name', 'activity-log-pro' ), 'tooltip' => __( 'Name shown in WordPress for this user.', 'activity-log-pro' ), ), 'user_role' => array( 'label' => __( 'User Role', 'activity-log-pro' ), 'tooltip' => __( 'Permission level of the user at the time of the event.', 'activity-log-pro' ), ), 'profile_link' => array( 'label' => __( 'Profile Link', 'activity-log-pro' ), 'tooltip' => __( 'Link to this user\'s profile in the WordPress admin.', 'activity-log-pro' ), ), 'ip_address' => array( 'label' => __( 'IP Address', 'activity-log-pro' ), 'tooltip' => __( 'The IP address of the device or server that triggered this event.', 'activity-log-pro' ), ), 'user_agent' => array( 'label' => __( 'User Agent', 'activity-log-pro' ), 'tooltip' => __( 'Browser or app used when the event occurred.', 'activity-log-pro' ), ), 'referer' => array( 'label' => __( 'Referer', 'activity-log-pro' ), 'tooltip' => __( 'The page URL the request came from, if available.', 'activity-log-pro' ), ), 'request_method' => array( 'label' => __( 'Request Method', 'activity-log-pro' ), 'tooltip' => __( 'HTTP method used (for example: GET or POST).', 'activity-log-pro' ), ), 'request_uri' => array( 'label' => __( 'Request URI', 'activity-log-pro' ), 'tooltip' => __( 'The page or path that was accessed when the event occurred.', 'activity-log-pro' ), ), 'object_type' => array( 'label' => __( 'Object Type', 'activity-log-pro' ), 'tooltip' => __( 'The kind of item affected (for example: post, plugin, user, or email).', 'activity-log-pro' ), ), 'object_id' => array( 'label' => __( 'Object ID', 'activity-log-pro' ), 'tooltip' => __( 'WordPress database ID of the affected item. Shows N/A when there is no numeric ID (plugins, themes, emails, and similar).', 'activity-log-pro' ), ), 'object_name' => array( 'label' => __( 'Object Name', 'activity-log-pro' ), 'tooltip' => __( 'Name or title of the affected item.', 'activity-log-pro' ), ), 'object_status' => array( 'label' => __( 'Object Status', 'activity-log-pro' ), 'tooltip' => __( 'Current status after the event (for example: published, active, or private).', 'activity-log-pro' ), ), 'previous_status' => array( 'label' => __( 'Previous Status', 'activity-log-pro' ), 'tooltip' => __( 'Status before this event occurred.', 'activity-log-pro' ), ), ), 'log_detail_help_label' => __( 'Help', 'activity-log-pro' ), 'log_detail_sections' => array( 'title' => __( 'Complete Log Entry Details', 'activity-log-pro' ), 'note' => __( 'Recorded Note', 'activity-log-pro' ), 'basic' => __( 'Basic Information', 'activity-log-pro' ), 'user' => __( 'User Information', 'activity-log-pro' ), 'network' => __( 'Network Information', 'activity-log-pro' ), 'object' => __( 'Target Object', 'activity-log-pro' ), 'changes' => __( 'Value Changes', 'activity-log-pro' ), 'raw' => __( 'Raw Database Fields', 'activity-log-pro' ), 'metadata' => __( 'Complete Metadata', 'activity-log-pro' ), ), ); // Localize scripts wp_localize_script('alp-admin-js', 'actlogpro_ajax_object', $admin_data); wp_localize_script('alp-modules-handler', 'actlogproModulesData', $admin_data); // Set up translations if (function_exists('wp_set_script_translations')) { wp_set_script_translations('alp-admin-js', 'activity-log-pro'); } // Load integrations scripts on the integrations page if (strpos($hook, 'activity-log-pro-integrations') !== false) { wp_enqueue_style('alp-integrations-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-integrations.css', array('alp-admin-css'), ACTLOGPRO_VERSION); wp_enqueue_script('alp-integrations-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-integrations.js', array('jquery'), ACTLOGPRO_VERSION, true); // Localize integrations script wp_localize_script('alp-integrations-js', 'actlogpro_integrations_object', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce') )); } // Load Chart.js and stats-specific assets on the stats & insights page if (strpos($hook, 'activity-log-pro-stats') !== false) { // Enqueue stats page CSS wp_enqueue_style('alp-stats-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-stats.css', array('alp-admin-css'), ACTLOGPRO_VERSION); // Enqueue Chart.js wp_enqueue_script('chart-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/chart.min.js', array(), '4.5.1', true); // Enqueue stats page JavaScript wp_enqueue_script('alp-stats-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-stats.js', array('jquery', 'chart-js'), ACTLOGPRO_VERSION, true); // Localize script specifically for stats page wp_localize_script('alp-stats-js', 'actlogproAjax', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce') )); // Localize stats data for charts $this->localize_stats_data(); } // Load upgrade-specific assets on the upgrade page if (strpos($hook, 'activity-log-pro-upgrade') !== false) { // Enqueue upgrade page CSS wp_enqueue_style('alp-upgrade-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-upgrade.css', array('alp-admin-css'), ACTLOGPRO_VERSION); // Enqueue upgrade page JavaScript wp_enqueue_script('alp-upgrade-js', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-upgrade.js', array('jquery'), ACTLOGPRO_VERSION, true); // Enqueue LemonSqueezy checkout script wp_enqueue_script( 'lemonsqueezy-checkout', 'https://assets.lemonsqueezy.com/lemon.js', array(), '1.0.0', true ); // Add defer attribute to LemonSqueezy script add_filter('script_loader_tag', array($this, 'add_defer_to_lemonsqueezy'), 10, 2); } // Load newsletter script and additional styles on the settings page if (strpos($hook, 'activity-log-pro-settings') !== false) { // Enqueue additional settings styles to replace inline styles wp_enqueue_style( 'alp-settings-additional-css', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-settings-additional.css', array('alp-admin-css'), ACTLOGPRO_VERSION ); wp_enqueue_script( 'alp-newsletter', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-newsletter.js', array('jquery'), ACTLOGPRO_VERSION, true ); wp_localize_script('alp-newsletter', 'actlogproNewsletter', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('actlogpro_ajax_nonce'), )); } // Add datepicker initialization for export page if (strpos($hook, 'activity-log-pro-export') !== false) { $datepicker_script = " jQuery(document).ready(function($) { // Initialize datepickers $('.alp-datepicker').datepicker({ dateFormat: 'yy-mm-dd', changeMonth: true, changeYear: true }); }); "; wp_add_inline_script('jquery-ui-datepicker', $datepicker_script); } } /** * Display the plugin logo. * * @since 1.0.0 */ private function display_plugin_logo() { // Register the logo image $logo_url = ACTLOGPRO_PLUGIN_URL . 'assets/images/alp-logo-final.png'; $admin_url = admin_url('admin.php?page=activity-log-pro'); // Logo styling is now handled in alp-admin.css // Start the header section container echo '
'; // Generate the logo HTML using wp_get_attachment_image if possible echo ''; // Close the header section container echo '
'; } /** * Display plugin logo fallback when the image is not in the media library. * This uses WordPress's recommended image handling approach. * * @since 1.0.0 * @param string $logo_url URL of the logo image. */ private function display_plugin_logo_fallback($logo_url) { // Get image dimensions $width = 250; // Default width $height = 80; // Default height // Try to get actual dimensions using WordPress functions $image_path = str_replace(ACTLOGPRO_PLUGIN_URL, ACTLOGPRO_PLUGIN_DIR, $logo_url); if (file_exists($image_path)) { $image_size = @getimagesize($image_path); if ($image_size) { list($width, $height) = $image_size; } } // Even though we don't have a real attachment ID, we can simulate the attributes $srcset = "$logo_url {$width}w"; $sizes = "(max-width: {$width}px) 100vw, {$width}px"; // Create image HTML properly using WordPress standards $attrs = array( 'src' => $logo_url, 'alt' => 'Activity Log Pro', 'class' => 'alp-logo-image wp-image-0 attachment-medium size-medium', 'width' => $width, 'height' => $height, 'srcset' => $srcset, 'sizes' => $sizes, 'loading' => 'lazy' ); // Build attributes string for wp_kses $attr_html = ''; foreach ($attrs as $name => $value) { $attr_html .= ' ' . esc_attr($name) . '="' . esc_attr($value) . '"'; } // Define allowed HTML elements and attributes $allowed_html = array( 'img' => array( 'src' => true, 'srcset' => true, 'sizes' => true, 'alt' => true, 'class' => true, 'width' => true, 'height' => true, 'loading' => true, ), ); // Output the image using wp_kses to ensure security echo wp_kses('', $allowed_html); } /** * Display plugin image using WordPress standards. * * @since 1.0.0 * @param string $image_name Image filename in assets/images/ * @param string $alt_text Alt text for the image * @param string $css_class CSS class for the image * @param array $attributes Additional attributes * @return string HTML for the image */ private function display_plugin_image($image_name, $alt_text = '', $css_class = '', $attributes = array()) { $image_url = ACTLOGPRO_PLUGIN_URL . 'assets/images/' . $image_name; // Check if we have an attachment ID for the image $attachment_id = attachment_url_to_postid($image_url); if ($attachment_id) { // Use wp_get_attachment_image if we have an attachment ID $default_attributes = array( 'alt' => $alt_text, 'class' => $css_class ); $final_attributes = array_merge($default_attributes, $attributes); return wp_get_attachment_image($attachment_id, 'medium', false, $final_attributes); } else { // Use fallback method for plugin assets return $this->display_plugin_image_fallback($image_url, $alt_text, $css_class, $attributes); } } /** * Display plugin image fallback when not in media library. * * @since 1.0.0 * @param string $image_url URL of the image * @param string $alt_text Alt text for the image * @param string $css_class CSS class for the image * @param array $attributes Additional attributes * @return string HTML for the image */ private function display_plugin_image_fallback($image_url, $alt_text = '', $css_class = '', $attributes = array()) { // Get image dimensions $width = 250; // Default width $height = 80; // Default height // Try to get actual dimensions $image_path = str_replace(ACTLOGPRO_PLUGIN_URL, ACTLOGPRO_PLUGIN_DIR, $image_url); if (file_exists($image_path)) { $image_size = @getimagesize($image_path); if ($image_size) { list($width, $height) = $image_size; } } // Build default attributes $default_attrs = array( 'src' => $image_url, 'alt' => $alt_text, 'class' => $css_class, 'width' => $width, 'height' => $height, 'srcset' => "$image_url {$width}w", 'sizes' => "(max-width: {$width}px) 100vw, {$width}px", 'loading' => 'lazy' ); // Merge with custom attributes $attrs = array_merge($default_attrs, $attributes); // Build attributes string $attr_html = ''; foreach ($attrs as $name => $value) { if (!empty($value)) { $attr_html .= ' ' . esc_attr($name) . '="' . esc_attr($value) . '"'; } } // Define allowed HTML $allowed_html = array( 'img' => array( 'src' => true, 'srcset' => true, 'sizes' => true, 'alt' => true, 'class' => true, 'width' => true, 'height' => true, 'loading' => true, ), ); return wp_kses('', $allowed_html); } /** * Display the activity log page. * * @since 1.0.0 */ public function display_activity_log_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Check if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); $tabs_active = in_array($menu_location, array('dashboard', 'tools')); echo '
'; // Add page title if horizontal tabs are active if ($tabs_active) { echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Activity Logs', 'activity-log-pro')) . '

'; echo '
'; } // Include the main activity log display template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/activity-log-display.php'; echo '
'; } /** * Display the Live Sessions page. * * @since 1.1.0 */ public function display_live_sessions_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $this->display_plugin_logo(); $this->display_horizontal_tabs(); $menu_location = get_option( 'actlogpro_admin_menu_location', 'default' ); if ( in_array( $menu_location, array( 'dashboard', 'tools' ), true ) ) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo esc_html( __( 'Live Sessions', 'activity-log-pro' ) ) . '

'; echo '
'; } include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/live-sessions-display.php'; if ( in_array( $menu_location, array( 'dashboard', 'tools' ), true ) ) { echo '
'; } } /** * Display the settings page. * * @since 1.0.0 */ public function display_settings_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Add page title if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Settings', 'activity-log-pro')) . '

'; echo '
'; } // Include the settings template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/settings-display.php'; // Close wrap if opened if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; } } /** * AJAX handler for getting activity logs. * * @since 1.0.0 */ public function ajax_get_activity_logs() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Get saved items per page setting $saved_per_page = get_option('actlogpro_items_per_page', 20); // Get parameters $page = isset($_POST['page']) ? absint(wp_unslash($_POST['page'])) : 1; $per_page = isset($_POST['per_page']) ? absint(wp_unslash($_POST['per_page'])) : $saved_per_page; $date_from = isset($_POST['date_from']) ? sanitize_text_field(wp_unslash($_POST['date_from'])) : ''; $date_to = isset($_POST['date_to']) ? sanitize_text_field(wp_unslash($_POST['date_to'])) : ''; $user = isset($_POST['user']) ? sanitize_text_field(wp_unslash($_POST['user'])) : ''; $event_type = isset($_POST['event_type']) ? sanitize_text_field(wp_unslash($_POST['event_type'])) : ''; $action_filter = isset($_POST['action_filter']) ? sanitize_text_field(wp_unslash($_POST['action_filter'])) : ''; $ip_address = isset($_POST['ip_address']) ? sanitize_text_field(wp_unslash($_POST['ip_address'])) : ''; $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : ''; $severity = isset($_POST['severity']) ? sanitize_text_field(wp_unslash($_POST['severity'])) : ''; $filter_preset = isset($_POST['filter_preset']) ? sanitize_key(wp_unslash($_POST['filter_preset'])) : ''; $allowed_presets = array( 'failed_logins', 'critical_high' ); if ( ! in_array( $filter_preset, $allowed_presets, true ) ) { $filter_preset = ''; } // Validate severity against allowed values to prevent DB injection if ( ! empty($severity) && ! ACTLOGPRO_Severity::is_valid($severity) ) { $severity = ''; } // Get sort parameters $sort_column = isset($_POST['sort_column']) ? sanitize_sql_orderby(wp_unslash($_POST['sort_column'])) : 'created_at'; $sort_direction = isset($_POST['sort_direction']) && sanitize_text_field(wp_unslash($_POST['sort_direction'])) === 'asc' ? 'ASC' : 'DESC'; // Validate sort column $allowed_columns = array('id', 'severity', 'user_login', 'user_role', 'ip_address', 'event_type', 'object_type', 'object_name', 'action', 'created_at'); if (!in_array($sort_column, $allowed_columns)) { $sort_column = 'created_at'; } // Prepare query args for database abstraction layer $query_args = array( 'page' => $page, 'per_page' => $per_page, 'orderby' => $sort_column, 'order' => $sort_direction, 'user' => $user, 'event_type' => $event_type, 'action' => $action_filter, 'ip_address' => $ip_address, 'date_from' => $date_from, 'date_to' => $date_to, 'search' => $search, 'severity' => $severity, 'filter_preset' => $filter_preset, ); // Load database class if not already loaded if (!class_exists('ACTLOGPRO_Database')) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-database.php'; } // Get total count using the database abstraction layer $total = ACTLOGPRO_Database::get_filtered_logs_count($query_args); // Get logs using the database abstraction layer $logs = ACTLOGPRO_Database::get_filtered_logs($query_args); // Format logs for display $formatted_logs = array(); foreach ($logs as $log) { $description = $log->description; // Modify description for plugin events to use the new format if ($log->event_type === 'plugin') { $metadata = ACTLOGPRO_Database::decode_log_metadata( $log->metadata ); // Check if it's an activation event if ($log->action === 'activate') { // Always reformat the description for plugin activation events if (strpos($description, 'Plugin Activated:') === false) { // Extract plugin name from either format if (preg_match('/Plugin "(.+?)" activated/', $description, $matches)) { $plugin_name = $matches[1]; $description = 'Plugin Activated: ' . $plugin_name; } else { $plugin_name = $log->object_name; $description = 'Plugin Activated: ' . $plugin_name; } } } // Check if it's a deactivation event if ($log->action === 'deactivate') { // Extract plugin name from the description if (preg_match('/Plugin "(.+?)" deactivated/', $description, $matches)) { $plugin_name = $matches[1]; $description = 'Plugin Deactivated: ' . $plugin_name; } } // Check if it's an update event if ($log->action === 'update') { // Extract plugin name and version from the description if (preg_match('/Plugin "(.+?)" was updated to version (.+?)/', $description, $matches)) { $plugin_name = $matches[1]; $version = $matches[2]; $description = 'Plugin Updated: ' . $plugin_name . ' to version ' . $version; } } // Clear any existing version/location info from the description $description = preg_replace('/
Plugin Version:<\/strong>.*?(
|$)/i', '', $description); $description = preg_replace('/
Install location:<\/strong>.*?(
|$)/i', '', $description); // Add plugin version if available if (!empty($metadata['plugin_version'])) { $description .= '
Plugin Version: ' . esc_html($metadata['plugin_version']); } // Add plugin path if available - check both possible keys and format to show only directory if (!empty($metadata['plugin_path'])) { $plugin_dir = dirname($metadata['plugin_path']) . '/'; $description .= '
Install location: ' . esc_html($plugin_dir); } elseif (!empty($metadata['plugin_file'])) { // Use the plugin_file directly without prepending WP_PLUGIN_DIR $plugin_dir = dirname($metadata['plugin_file']) . '/'; $description .= '
Install location: ' . esc_html($plugin_dir); } else { $description .= '
Install location: Not available'; } } $log_severity = ( isset( $log->severity ) && ACTLOGPRO_Severity::is_valid( $log->severity ) ) ? $log->severity : ACTLOGPRO_Severity::LEVEL_DEFAULT; $formatted_logs[] = array( 'id' => $log->id, 'severity' => $log_severity, 'severity_label' => ACTLOGPRO_Severity::get_label( $log_severity ), 'severity_class' => ACTLOGPRO_Severity::get_css_class( $log_severity ), 'user' => array( 'id' => $log->user_id, 'login' => $log->user_login, 'display_name' => $log->user_display_name, 'role' => $log->user_role ), 'ip_address' => $log->ip_address, 'event_type' => $log->event_type, 'object' => array( 'type' => $log->object_type, 'id' => $log->object_id, 'name' => $log->object_name ), 'action' => $log->action, 'description' => $description, 'metadata' => ACTLOGPRO_Database::decode_log_metadata( $log->metadata ), 'created_at' => $log->created_at ); } // Calculate total pages $total_pages = ceil($total / $per_page); // Send response wp_send_json_success(array( 'logs' => $formatted_logs, 'total' => $total, 'total_pages' => $total_pages, 'current_page' => $page )); } /** * AJAX handler for clearing all logs. * * Security: Requires 'manage_options' capability and valid nonce verification. * This function permanently deletes all activity log entries from the database. * * @since 1.0.0 */ public function ajax_clear_logs() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Load database class require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-database.php'; // Clear logs using the database abstraction class $result = ACTLOGPRO_Database::clear_all_logs(); if ($result !== false) { // Clear all caches related to activity logs if (function_exists('wp_cache_flush_group')) { wp_cache_flush_group('activity-log-pro'); } else { // For older WordPress versions without group flushing // Set a flag to indicate that the cache needs refreshing update_option('actlogpro_cache_invalidated', time()); } wp_send_json_success(array('message' => __('All logs have been cleared successfully.', 'activity-log-pro'))); } else { wp_send_json_error(array('message' => __('An error occurred while clearing logs.', 'activity-log-pro'))); } } /** * AJAX handler for deleting selected logs. * * Security: Requires 'manage_options' capability and valid nonce verification. * This function permanently deletes specific activity log entries from the database. * * @since 1.0.0 */ public function ajax_delete_selected_logs() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Check if log_ids exists in $_POST and sanitize if (!isset($_POST['log_ids'])) { $log_ids = array(); } else { $raw_log_ids = map_deep(wp_unslash($_POST['log_ids']), 'sanitize_text_field'); $log_ids = array(); if (is_array($raw_log_ids)) { foreach ($raw_log_ids as $id) { $log_ids[] = absint($id); } } else { $log_ids[] = absint($raw_log_ids); } } if (empty($log_ids) || !is_array($log_ids)) { wp_send_json_error(array('message' => __('No logs selected for deletion.', 'activity-log-pro'))); } // Load database class if not already loaded if (!class_exists('ACTLOGPRO_Database')) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-database.php'; } // Delete logs using the database abstraction layer $result = ACTLOGPRO_Database::delete_logs_by_ids($log_ids); // If logs were deleted successfully if ($result !== false) { wp_send_json_success(array( /* translators: %d: Number of logs deleted */ 'message' => sprintf(__('%d logs have been deleted successfully.', 'activity-log-pro'), $result), 'deleted_count' => $result )); } else { wp_send_json_error(array('message' => __('An error occurred while deleting logs.', 'activity-log-pro'))); } } /** * AJAX handler for recording a manual note into the activity log. * * Security: Requires 'manage_options' capability and valid nonce verification. * Premium fields (backdate, attribution) are silently ignored when premium is not active. * * @since 1.2.0 */ public function ajax_record_note() { // Verify nonce. if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'actlogpro_ajax_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'activity-log-pro' ) ) ); } // Check capability — only administrators may record notes. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to record notes.', 'activity-log-pro' ) ) ); } // Validate and sanitize required message field. $message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : ''; if ( empty( $message ) ) { wp_send_json_error( array( 'message' => __( 'A note message is required.', 'activity-log-pro' ) ) ); } // Sanitize optional free fields. $details = isset( $_POST['details'] ) ? sanitize_textarea_field( wp_unslash( $_POST['details'] ) ) : ''; $severity = isset( $_POST['severity'] ) ? sanitize_text_field( wp_unslash( $_POST['severity'] ) ) : 'info'; // Premium-only fields — ignored entirely when premium is not active. $event_datetime = null; $attributed_user_id = null; if ( ACTLOGPRO_Premium_Helper::is_premium_enabled() ) { // Backdate: accept YYYY-MM-DD HH:MM, disallow future timestamps. if ( ! empty( $_POST['event_datetime'] ) ) { $raw_dt = sanitize_text_field( wp_unslash( $_POST['event_datetime'] ) ); if ( preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $raw_dt ) ) { $submitted_ts = strtotime( $raw_dt ); if ( $submitted_ts && $submitted_ts <= current_time( 'timestamp' ) ) { $event_datetime = $raw_dt . ':00'; // Normalise to full MySQL datetime. } } } // Attribution: validate user exists and is not a subscriber. if ( ! empty( $_POST['attributed_user_id'] ) ) { $uid = absint( wp_unslash( $_POST['attributed_user_id'] ) ); $attr_user = $uid > 0 ? get_userdata( $uid ) : false; if ( $attr_user && $attr_user->ID && ! in_array( 'subscriber', (array) $attr_user->roles, true ) ) { $attributed_user_id = $uid; } } } // Load and call the recorded notes class. if ( ! class_exists( 'ACTLOGPRO_Recorded_Notes' ) ) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-recorded-notes.php'; } $log_id = ACTLOGPRO_Recorded_Notes::record_note( $message, $details, $severity, $event_datetime, $attributed_user_id ); if ( $log_id ) { wp_send_json_success( array( 'message' => __( 'Note recorded successfully.', 'activity-log-pro' ), 'log_id' => $log_id, ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to record the note. Please try again.', 'activity-log-pro' ) ) ); } } /** * Clean up old logs based on retention settings. * * @since 1.0.0 */ public function cleanup_old_logs() { $retention_days = get_option('actlogpro_retention_period', 30); // If retention is set to 0 (forever), don't delete anything if ($retention_days <= 0) { return; } $deleted = ACTLOGPRO_Database::delete_old_logs($retention_days); if ($deleted) { // Log this action ACTLOGPRO_Logger::log_activity( 'system', 'logs', 0, 'actlogpro_activity_log_pro_all_logs', 'cleanup', /* translators: %1$d: Number of logs deleted, %2$d: Number of days in retention period */ sprintf(__('Deleted %1$d logs older than %2$d days', 'activity-log-pro'), $deleted, $retention_days) ); } } /** * Get log details for a specific log entry */ public function get_log_details() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(['message' => __('Security check failed.', 'activity-log-pro')]); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'activity-log-pro')]); } // Get log ID $log_id = isset($_POST['log_id']) ? absint(wp_unslash($_POST['log_id'])) : 0; if (!$log_id) { wp_send_json_error(['message' => __('Invalid log ID.', 'activity-log-pro')]); } // Ensure the database class is loaded require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-database.php'; // Check if log is in cache $log_cache_key = 'actlogpro_log_' . $log_id; $log = wp_cache_get($log_cache_key, 'activity-log-pro'); if (false === $log) { // Not in cache, get from database using our abstraction method $log = ACTLOGPRO_Database::get_log_by_id($log_id); // Cache the result for 1 hour if ($log) { wp_cache_set($log_cache_key, $log, 'activity-log-pro', HOUR_IN_SECONDS); } } if (!$log) { wp_send_json_error(['message' => __('Log entry not found.', 'activity-log-pro')]); } // Format the log data $formatted_log = $this->format_log_for_display($log); // Add context data if available if (!empty($log['metadata'])) { $formatted_log['context'] = ACTLOGPRO_Database::decode_log_metadata( $log['metadata'] ); } else { $formatted_log['context'] = []; } wp_send_json_success($formatted_log); } /** * Format log data for display */ private function format_log_for_display($log) { // Format user data $user_data = []; if (!empty($log['user_id']) && $log['user_id'] != 0) { // Regular user - get data from WordPress user system $user = get_userdata($log['user_id']); if ($user) { $user_data = [ 'id' => $user->ID, 'login' => $user->user_login, 'display_name' => $user->display_name, 'role' => !empty($user->roles) ? reset($user->roles) : '' ]; } } elseif ($log['user_id'] == 0 || !empty($log['user_login'])) { // System user or user with stored login data - use stored data from log $user_data = [ 'id' => $log['user_id'], 'login' => $log['user_login'] ?? 'system', 'display_name' => $log['user_display_name'] ?? 'System', 'role' => 'Automated Process' ]; } // Format object data $object_data = []; if (!empty($log['object_type'])) { $object_data = [ 'type' => $log['object_type'], 'name' => $log['object_name'] ]; } // Get metadata $metadata = ACTLOGPRO_Database::decode_log_metadata( $log['metadata'] ); $severity = $log['severity'] ?? ACTLOGPRO_Severity::LEVEL_DEFAULT; return [ 'id' => $log['id'], 'severity' => ACTLOGPRO_Severity::is_valid($severity) ? $severity : ACTLOGPRO_Severity::LEVEL_DEFAULT, 'severity_label' => ACTLOGPRO_Severity::get_label($severity), 'severity_class' => ACTLOGPRO_Severity::get_css_class($severity), 'created_at' => $log['created_at'], 'user_id' => $log['user_id'], 'user' => $user_data, 'ip_address' => $log['ip_address'], 'event_type' => $log['event_type'], 'object' => $object_data, 'action' => $log['action'], 'description' => $log['description'], 'metadata' => $metadata ]; } /** * AJAX handler for saving settings * * Security: Requires 'manage_options' capability and valid nonce verification. * This function processes and updates all plugin settings via AJAX. * * @since 1.0.0 */ public function save_settings() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(__('Security check failed.', 'activity-log-pro')); return; } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(__('You do not have permission to save settings.', 'activity-log-pro')); return; } // Save each setting $settings = array( 'actlogpro_retention_period', 'actlogpro_items_per_page', 'actlogpro_track_user_events', 'actlogpro_track_post_events', 'actlogpro_track_media_events', 'actlogpro_track_comment_events', 'actlogpro_track_plugin_events', 'actlogpro_track_theme_events', 'actlogpro_track_widget_events', 'actlogpro_track_menu_events', 'actlogpro_track_option_events', 'actlogpro_track_wordpress_events', 'actlogpro_track_security_events', 'actlogpro_track_site_health', 'actlogpro_track_email_events', 'actlogpro_track_email_exclude_own', 'actlogpro_flood_guard_enabled', 'actlogpro_flood_guard_limit', 'actlogpro_enable_json_feed', 'actlogpro_enable_rss_feed', 'actlogpro_allow_multiple_expanded_rows', 'actlogpro_login_notification_enabled', 'actlogpro_login_notification_text', 'actlogpro_admin_menu_location', 'actlogpro_store_full_ip', 'actlogpro_remove_data_on_uninstall', 'actlogpro_excluded_option_names', // Email digest settings 'actlogpro_email_digest_enabled', 'actlogpro_email_digest_frequency', 'actlogpro_email_digest_day', 'actlogpro_email_digest_time', 'actlogpro_email_digest_recipients' ); // Check if admin menu location was changed $old_menu_location = get_option('actlogpro_admin_menu_location', 'default'); $new_menu_location = isset($_POST['actlogpro_admin_menu_location']) ? sanitize_text_field(wp_unslash($_POST['actlogpro_admin_menu_location'])) : 'default'; $menu_location_changed = ($old_menu_location !== $new_menu_location); $updated_settings = array(); foreach ($settings as $setting) { // Login Flood Guard fields are disabled (omitted from POST) when Security events is off — keep stored values. if ('actlogpro_flood_guard_enabled' === $setting || 'actlogpro_flood_guard_limit' === $setting) { if (!isset($_POST['actlogpro_track_security_events'])) { continue; } } // Only process if the setting exists in POST data if (isset($_POST[$setting])) { // Apply both unslash and sanitization at access point // The specific sanitization depends on the type of setting $clean_value = ''; // Initialize clean value // Get raw value from POST // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized below based on setting type $raw_value = $_POST[$setting]; // Use wp_kses_post for login notification text to preserve HTML if ($setting === 'actlogpro_login_notification_text') { $clean_value = wp_kses_post(wp_unslash($raw_value)); } elseif ($setting === 'actlogpro_flood_guard_limit') { $clean_value = (string) $this->sanitize_flood_guard_limit(wp_unslash($raw_value)); // Use specialized sanitization for email digest settings } elseif ($setting === 'actlogpro_email_digest_enabled') { $clean_value = ACTLOGPRO_Email_Digest_Manager::sanitize_checkbox(wp_unslash($raw_value)); } elseif ($setting === 'actlogpro_email_digest_frequency') { $clean_value = ACTLOGPRO_Email_Digest_Manager::sanitize_frequency(wp_unslash($raw_value)); } elseif ($setting === 'actlogpro_email_digest_day') { $clean_value = ACTLOGPRO_Email_Digest_Manager::sanitize_day(wp_unslash($raw_value)); } elseif ($setting === 'actlogpro_email_digest_time') { $clean_value = ACTLOGPRO_Email_Digest_Manager::sanitize_time(wp_unslash($raw_value)); } elseif ($setting === 'actlogpro_email_digest_recipients') { $clean_value = ACTLOGPRO_Email_Digest_Manager::sanitize_email_recipients(wp_unslash($raw_value)); } else { $clean_value = sanitize_text_field(wp_unslash($raw_value)); } // Premium validation for IP address setting if ($setting === 'actlogpro_store_full_ip') { if (!ACTLOGPRO_Premium_Helper::is_premium_enabled() && $clean_value === '1') { // Force to hide IP if not premium and trying to store full IP $clean_value = '0'; $updated_settings['premium_blocked'] = 'actlogpro_store_full_ip'; } } // Update option with sanitized value update_option($setting, $clean_value); $updated_settings[$setting] = $clean_value; } else { // For checkboxes and radio buttons, if not set, save as 0 // Radio buttons like actlogpro_admin_menu_location should not be reset to 0 if (strpos($setting, 'track_') !== false || strpos($setting, 'enable_') !== false || $setting === 'actlogpro_store_full_ip' || $setting === 'actlogpro_allow_multiple_expanded_rows' || $setting === 'actlogpro_email_digest_enabled' || $setting === 'actlogpro_flood_guard_enabled') { update_option($setting, '0'); $updated_settings[$setting] = '0'; } } } // Refresh tracking settings cache require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-events.php'; $events = new ACTLOGPRO_Events(); $events->refresh_tracking_settings(); // Check if email digest settings changed and reschedule if needed $email_digest_settings_changed = false; $email_digest_settings = array( 'actlogpro_email_digest_enabled', 'actlogpro_email_digest_frequency', 'actlogpro_email_digest_day', 'actlogpro_email_digest_time' ); foreach ($email_digest_settings as $digest_setting) { if (isset($updated_settings[$digest_setting])) { $email_digest_settings_changed = true; break; } } // Reschedule email digest if any email digest settings changed if ($email_digest_settings_changed) { ACTLOGPRO_Email_Digest_Manager::reschedule_email_digest(); } // Save excluded roles and users (checkbox array + textarea, not in $settings loop). $exclusion_warnings = $this->save_user_exclusion_settings(); // Prepare response message $message = __('Settings saved successfully.', 'activity-log-pro'); if ($menu_location_changed) { $message = __('Settings saved successfully. Please refresh the page to see the admin menu in its new location.', 'activity-log-pro'); } // Return success with the updated settings wp_send_json_success(array( 'message' => $message, 'menu_location_changed' => $menu_location_changed, 'settings' => $updated_settings, 'exclusion_warnings' => $exclusion_warnings, )); } /** * Display the export page. * * @since 1.0.0 */ public function display_export_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Add page title if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Export', 'activity-log-pro')) . '

'; echo '
'; } // Include the export template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/export-display.php'; // Close wrap if opened if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; } } /** * Display the integrations page. * * @since 1.0.0 */ public function display_integrations_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Make sure the integration manager class is loaded if (!class_exists('ACTLOGPRO_Integration_Manager')) { require_once ACTLOGPRO_PLUGIN_DIR . 'integrations/class-alp-integration-manager.php'; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Add page title if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Integrations', 'activity-log-pro')) . '

'; echo '
'; } // Include the integrations template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/integrations-display.php'; // Close wrap if opened if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; } } /** * Display the stats and insights page. * * @since 1.0.0 */ public function display_stats_insights_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Add page title if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Stats & Insights', 'activity-log-pro')) . '

'; echo '
'; } // Include the stats and insights template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/stats-insights-display.php'; // Close wrap if opened if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; } } /** * Localize stats data for charts. * * @since 1.0.0 */ private function localize_stats_data() { // Get activity data for the past 30 days $activity_data = ACTLOGPRO_Database::get_activity_data_by_days(30); $labels_30 = $activity_data['labels']; $counts_30 = $activity_data['counts']; $users_30 = $activity_data['users']; // Get data for 14 days $labels_14 = array_slice($labels_30, -14); $counts_14 = array_slice($counts_30, -14); $users_14 = array_slice($users_30, -14); // Get data for 7 days $labels_7 = array_slice($labels_30, -7); $counts_7 = array_slice($counts_30, -7); $users_7 = array_slice($users_30, -7); // Get event type data $event_types = ACTLOGPRO_Database::get_event_types_with_counts(8); $event_labels = array(); $event_counts = array(); $event_colors = array( '#1e88e5', '#26a69a', '#7cb342', '#ffa000', '#ef6c00', '#d81b60', '#8e24aa', '#5e35b1', '#039be5', '#00897b', '#7cb342', '#ffb300' ); foreach ($event_types as $event) { $event_labels[] = ucfirst($event['event_type']); $event_counts[] = (int) $event['count']; } // Get action type data $action_types = ACTLOGPRO_Database::get_action_types_with_counts(8); $action_labels = array(); $action_counts = array(); $action_colors = array( '#42a5f5', '#66bb6a', '#ffca28', '#ef5350', '#ab47bc', '#ec407a', '#7e57c2', '#5c6bc0' ); foreach ($action_types as $action) { $action_labels[] = ucfirst($action['action']); $action_counts[] = (int) $action['count']; } // Get activity data by day and hour $activity_by_time = array(); // Define days of week $days = array( __('Sunday', 'activity-log-pro'), __('Monday', 'activity-log-pro'), __('Tuesday', 'activity-log-pro'), __('Wednesday', 'activity-log-pro'), __('Thursday', 'activity-log-pro'), __('Friday', 'activity-log-pro'), __('Saturday', 'activity-log-pro') ); // Initialize activity data array foreach ($days as $day_index => $day) { $activity_by_time[$day_index] = array_fill(0, 24, 0); } // Query to get activity by day and hour $time_results = ACTLOGPRO_Database::get_activity_by_day_and_hour(); // Fill the activity data array foreach ($time_results as $result) { $day_index = $result['day_of_week'] - 1; $hour = $result['hour_of_day']; $activity_by_time[$day_index][$hour] = (int)$result['count']; } // Prepare localized data $localized_data = array( 'labels_7' => $labels_7, 'counts_7' => $counts_7, 'users_7' => $users_7, 'labels_14' => $labels_14, 'counts_14' => $counts_14, 'users_14' => $users_14, 'labels_30' => $labels_30, 'counts_30' => $counts_30, 'users_30' => $users_30, 'event_labels' => $event_labels, 'event_counts' => $event_counts, 'event_colors' => $event_colors, 'action_labels' => $action_labels, 'action_counts' => $action_counts, 'action_colors' => $action_colors, 'activity_by_time' => $activity_by_time, 'days' => $days, 'labels' => array( 'activities' => __('Activities', 'activity-log-pro'), 'active_users' => __('Active Users', 'activity-log-pro'), 'todays_insights' => __('Today\'s Insights', 'activity-log-pro'), 'view_logs' => __('View logs', 'activity-log-pro'), ) ); // Localize the script wp_localize_script('alp-stats-js', 'actlogproStatsData', $localized_data); } /** * Display the upgrade to premium page. * * @since 1.0.0 */ public function display_upgrade_page() { // Check user capabilities if (!current_user_can('manage_options')) { return; } // Display the logo before including the template $this->display_plugin_logo(); // Display horizontal tabs if menu is under Dashboard or Tools $this->display_horizontal_tabs(); // Add page title if horizontal tabs are active $menu_location = get_option('actlogpro_admin_menu_location', 'default'); if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo esc_html(__('Upgrade to Premium', 'activity-log-pro')) . '

'; echo '
'; } // Include the upgrade page template include_once ACTLOGPRO_PLUGIN_DIR . 'admin/partials/upgrade-display.php'; // Close wrap if opened if (in_array($menu_location, array('dashboard', 'tools'))) { echo '
'; } } /** * AJAX handler for generating feed tokens. * * @since 1.0.0 */ public function ajax_generate_feed_token() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Invalid security token.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Check if feed type is provided if (!isset($_POST['feed_type']) || empty($_POST['feed_type'])) { wp_send_json_error(array('message' => __('Feed type is required.', 'activity-log-pro'))); } // Get feed type $feed_type = isset($_POST['feed_type']) ? sanitize_text_field(wp_unslash($_POST['feed_type'])) : ''; // Validate feed type if ($feed_type !== 'json' && $feed_type !== 'rss') { wp_send_json_error(array('message' => __('Invalid feed type.', 'activity-log-pro'))); } // Include the feed class if it's not already included if (!class_exists('ACTLOGPRO_Feed')) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-feed.php'; } // Use a transient to prevent concurrent token generation requests $transient_key = 'actlogpro_generating_' . $feed_type . '_token'; if (get_transient($transient_key)) { wp_send_json_error(array( 'message' => __('A token is already being generated. Please wait a moment and try again.', 'activity-log-pro') )); return; } // Set transient to prevent concurrent requests (5 seconds should be enough) set_transient($transient_key, true, 5); try { // Generate new token $feed = new ACTLOGPRO_Feed(); $token = $feed->generate_token($feed_type); // Delete the transient delete_transient($transient_key); // Return success wp_send_json_success(array( 'message' => __('Token generated successfully.', 'activity-log-pro'), 'token' => $token )); } catch (Exception $e) { // Delete the transient in case of error delete_transient($transient_key); // Return error wp_send_json_error(array( 'message' => __('An error occurred while generating the token: ', 'activity-log-pro') . $e->getMessage() )); } } /** * AJAX handler for resetting tracking settings. * * @since 1.1.0 */ public function ajax_reset_tracking_settings() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Invalid security token.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Reset tracking settings require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-events.php'; $events = new ACTLOGPRO_Events(); $reset_settings = $events->reset_tracking_settings(); // Return success wp_send_json_success(array( 'message' => __('Tracking settings have been reset to default values.', 'activity-log-pro'), 'settings' => $reset_settings )); } /** * AJAX handler for resetting premium settings to defaults. * * @since 1.0.0 */ public function ajax_reset_premium_settings() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Reset premium settings to defaults ACTLOGPRO_Premium_Helper::reset_premium_settings_to_defaults(); // Return success wp_send_json_success(array( 'message' => __('Premium settings have been reset to default values.', 'activity-log-pro') )); } /** * Handle AJAX request to filter logs * * Security: Requires 'manage_options' capability and valid nonce verification. */ public function handle_ajax_filter_logs() { // Check nonce for security if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(__('Security check failed.', 'activity-log-pro')); return; } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(__('You do not have permission to perform this action.', 'activity-log-pro')); return; } // Rest of the method... } /** * Display horizontal tab navigation for Dashboard/Tools menu locations. * * @since 1.0.0 */ private function display_horizontal_tabs() { $menu_location = get_option('actlogpro_admin_menu_location', 'default'); // Only show tabs when menu is under Dashboard or Tools if (!in_array($menu_location, array('dashboard', 'tools'))) { return; } // Get current page slug // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- UI-only tab display, no data processing $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : 'activity-log-pro'; // Define tab items with icons and URLs $tabs = array( 'activity-log-pro' => array( 'title' => __('Activity Logs', 'activity-log-pro'), 'icon' => 'dashicons-list-view', 'url' => admin_url('admin.php?page=activity-log-pro') ), 'activity-log-pro-settings' => array( 'title' => __('Settings', 'activity-log-pro'), 'icon' => 'dashicons-admin-settings', 'url' => admin_url('admin.php?page=activity-log-pro-settings') ), 'activity-log-pro-export' => array( 'title' => __('Export', 'activity-log-pro'), 'icon' => 'dashicons-download', 'url' => admin_url('admin.php?page=activity-log-pro-export') ), 'activity-log-pro-integrations' => array( 'title' => __('Integrations', 'activity-log-pro'), 'icon' => 'dashicons-admin-plugins', 'url' => admin_url('admin.php?page=activity-log-pro-integrations') ), 'activity-log-pro-stats' => array( 'title' => __('Stats & Insights', 'activity-log-pro'), 'icon' => 'dashicons-chart-bar', 'url' => admin_url('admin.php?page=activity-log-pro-stats') ) ); // Add Upgrade to Premium tab if premium UI should be shown if (ACTLOGPRO_Premium_Helper::show_premium_ui()) { $tabs['activity-log-pro-upgrade'] = array( 'title' => __('Upgrade to Premium', 'activity-log-pro'), 'icon' => 'dashicons-star-filled', 'url' => admin_url('admin.php?page=activity-log-pro-upgrade') ); } echo '
'; foreach ($tabs as $slug => $tab) { $active_class = ($current_page === $slug) ? ' alp-tab-active' : ''; echo ''; echo ''; echo '' . esc_html($tab['title']) . ''; echo ''; } echo '
'; } /** * AJAX handler for newsletter subscription via Lemon Squeezy. * * @since 1.0.0 */ public function ajax_newsletter_subscribe() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); return; } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); return; } $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : ''; $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; if (empty($name) || empty($email) || !is_email($email)) { wp_send_json_error(array('message' => __('Please provide a valid name and email address.', 'activity-log-pro'))); return; } $response = wp_remote_post( 'https://activitylogpro.lemonsqueezy.com/email-subscribe/external', array( 'timeout' => 15, 'redirection' => 5, 'body' => array( 'name' => $name, 'email' => $email, ), ) ); if (is_wp_error($response)) { wp_send_json_error(array('message' => __('There was an error subscribing to the newsletter. Please try again later.', 'activity-log-pro'))); return; } $response_code = wp_remote_retrieve_response_code($response); // Lemon Squeezy returns 302 on success, or 200 after redirect is followed. if (in_array($response_code, array(200, 302), true)) { wp_send_json_success(array('message' => __('Thank you for subscribing!', 'activity-log-pro'))); return; } wp_send_json_error(array('message' => __('There was an error subscribing to the newsletter. Please try again later.', 'activity-log-pro'))); } /** * AJAX handler for IP geolocation lookup using ipinfo.io * * @since 1.0.0 */ public function ajax_get_ip_info() { // Check nonce for security if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); return; } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); return; } // Get and validate IP address $ip_address = isset($_POST['ip_address']) ? sanitize_text_field(wp_unslash($_POST['ip_address'])) : ''; if (empty($ip_address)) { wp_send_json_error(array('message' => __('IP address is required.', 'activity-log-pro'))); return; } // Validate IP address format if (!filter_var($ip_address, FILTER_VALIDATE_IP)) { wp_send_json_error(array('message' => __('Invalid IP address format.', 'activity-log-pro'))); return; } // Check cache first (cache for 24 hours) $cache_key = 'actlogpro_ip_info_' . md5($ip_address); $cached_data = get_transient($cache_key); if ($cached_data !== false) { wp_send_json_success(array( 'ip_data' => $cached_data, 'cached' => true )); return; } // Make request to ipinfo.io API $api_url = 'https://ipinfo.io/' . $ip_address . '/json'; $response = wp_remote_get($api_url, array( 'timeout' => 10, 'user-agent' => 'Activity Log Pro WordPress Plugin', 'headers' => array( 'Accept' => 'application/json' ) )); // Check for errors if (is_wp_error($response)) { wp_send_json_error(array( 'message' => __('Failed to retrieve IP information: ', 'activity-log-pro') . $response->get_error_message() )); return; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { wp_send_json_error(array( 'message' => __('API request failed with status code: ', 'activity-log-pro') . $response_code )); return; } $body = wp_remote_retrieve_body($response); $ip_data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_send_json_error(array( 'message' => __('Failed to parse API response.', 'activity-log-pro') )); return; } // Sanitize the response data $sanitized_data = array( 'ip' => isset($ip_data['ip']) ? sanitize_text_field($ip_data['ip']) : '', 'hostname' => isset($ip_data['hostname']) ? sanitize_text_field($ip_data['hostname']) : '', 'city' => isset($ip_data['city']) ? sanitize_text_field($ip_data['city']) : '', 'region' => isset($ip_data['region']) ? sanitize_text_field($ip_data['region']) : '', 'country' => isset($ip_data['country']) ? sanitize_text_field($ip_data['country']) : '', 'loc' => isset($ip_data['loc']) ? sanitize_text_field($ip_data['loc']) : '', 'org' => isset($ip_data['org']) ? sanitize_text_field($ip_data['org']) : '', 'postal' => isset($ip_data['postal']) ? sanitize_text_field($ip_data['postal']) : '', 'timezone' => isset($ip_data['timezone']) ? sanitize_text_field($ip_data['timezone']) : '' ); // Cache the result for 24 hours set_transient($cache_key, $sanitized_data, DAY_IN_SECONDS); // Return success response wp_send_json_success(array( 'ip_data' => $sanitized_data, 'cached' => false )); } /** * AJAX handler for the user card popover. * * Returns basic user info for the free tier. Premium data is appended * via the `actlogpro_user_card_data` filter so the premium plugin stays * decoupled from this class. * * @since 1.2.0 */ public function ajax_get_user_card() { // Verify nonce. if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'actlogpro_ajax_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'activity-log-pro' ) ) ); return; } // Require manage_options capability. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'activity-log-pro' ) ) ); return; } $user_id = isset( $_POST['user_id'] ) ? absint( wp_unslash( $_POST['user_id'] ) ) : 0; if ( $user_id <= 0 ) { wp_send_json_error( array( 'message' => __( 'Invalid user ID.', 'activity-log-pro' ) ) ); return; } $user = get_userdata( $user_id ); if ( ! $user ) { wp_send_json_error( array( 'message' => __( 'User not found.', 'activity-log-pro' ) ) ); return; } // Fetch total events + last activity via the database abstraction layer. $stats = ACTLOGPRO_Database::get_user_card_stats( $user_id ); // Determine whether the user has at least one non-expired session. $is_online = false; $session_tokens = get_user_meta( $user_id, 'session_tokens', true ); if ( is_array( $session_tokens ) && ! empty( $session_tokens ) ) { $now = time(); foreach ( $session_tokens as $session ) { if ( ! empty( $session['expiration'] ) && (int) $session['expiration'] > $now ) { $is_online = true; break; } } } $card_data = array( 'user_id' => $user_id, 'display_name' => sanitize_text_field( $user->display_name ), 'user_login' => sanitize_user( $user->user_login ), 'email' => sanitize_email( $user->user_email ), 'roles' => array_map( 'sanitize_text_field', (array) $user->roles ), 'registered' => sanitize_text_field( $user->user_registered ), 'avatar_url' => esc_url_raw( get_avatar_url( $user_id, array( 'size' => 48 ) ) ), 'profile_url' => esc_url_raw( admin_url( 'user-edit.php?user_id=' . $user_id ) ), 'is_online' => $is_online, 'total_events' => $stats['total_events'], 'last_login' => sanitize_text_field( $stats['last_login'] ), 'last_activity' => sanitize_text_field( $stats['last_activity'] ), 'is_premium' => ACTLOGPRO_Premium_Helper::is_premium_enabled(), ); /** * Allows extensions (e.g. premium) to append extra data to the user card. * * @since 1.2.0 * @param array $card_data Populated card data. * @param int $user_id Target user ID. * @param WP_User $user Target WP_User object. */ $card_data = apply_filters( 'actlogpro_user_card_data', $card_data, $user_id, $user ); wp_send_json_success( array( 'user_data' => $card_data ) ); } /** * Display admin notice when plugin is activated * * @since 1.0.0 */ public function display_activation_notice() { // Check if we should show the activation notice if (!get_option('actlogpro_show_activation_notice', false)) { return; } // Check if user has already dismissed this notice $user_id = get_current_user_id(); $dismissed_notices = get_user_meta($user_id, 'actlogpro_dismissed_notices', true); if (is_array($dismissed_notices) && isset($dismissed_notices['alp-activation-notice'])) { // User has dismissed it, but we still need to clear the global flag delete_option('actlogpro_show_activation_notice'); return; } // Only show to users who can manage options if (!current_user_can('manage_options')) { return; } // Get plugin version $version = ACTLOGPRO_VERSION; // Build settings and help URLs $settings_url = admin_url('admin.php?page=activity-log-pro-settings'); $help_url = admin_url('admin.php?page=activity-log-pro-settings&tab=help'); echo '
'; echo '

' . esc_html__('Thanks for installing Activity Log Pro v', 'activity-log-pro') . esc_html($version) . esc_html__(' - Enjoy the plugin - Activity Log Pro Team', 'activity-log-pro') . '

'; echo '

' . esc_html__('Check: ', 'activity-log-pro'); echo '' . esc_html__('Plugin Settings', 'activity-log-pro') . ' | '; echo '' . esc_html__('Documentation', 'activity-log-pro') . ''; echo '

'; echo '
'; // Note: We don't delete the option here - it will be deleted when user dismisses the notice } /** * Display admin notice when premium settings have been reset * * @since 1.0.0 */ public function display_premium_reset_notice() { // Show once and immediately clear — same behaviour as WP core plugin notices if ( get_option( 'actlogpro_premium_settings_reset_notice', false ) ) { delete_option( 'actlogpro_premium_settings_reset_notice' ); $message = __( 'Activity Log Pro - Premium features have been disabled.', 'activity-log-pro' ); echo '
'; echo '

' . esc_html( $message ) . '

'; echo '
'; } } /** * AJAX handler for toggling stats view preference. * * @since 1.0.0 */ public function ajax_toggle_stats_view() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check if view type is provided if (!isset($_POST['view_type']) || empty(sanitize_key(wp_unslash($_POST['view_type'])))) { wp_send_json_error(array('message' => __('View type is required.', 'activity-log-pro'))); } // Sanitize view type $view_type = sanitize_key(wp_unslash($_POST['view_type'])); // Set user preference using helper class $success = ACTLOGPRO_Stats_Helper::set_user_preferred_view($view_type); if ($success) { // Get updated stats data - use WordPress local time to match event logging $today = current_time('Y-m-d'); if ($view_type === 'security') { $stats_config = ACTLOGPRO_Stats_Helper::get_security_stats_config($today); $insights = ACTLOGPRO_Stats_Helper::get_security_insights($today); } else { $stats_config = ACTLOGPRO_Stats_Helper::get_business_stats_config($today); $insights = ACTLOGPRO_Stats_Helper::get_business_insights($today); } wp_send_json_success(array( 'message' => __('View preference updated successfully.', 'activity-log-pro'), 'view_type' => $view_type, 'stats' => $stats_config, 'insights' => $insights )); } else { wp_send_json_error(array('message' => __('Failed to update view preference.', 'activity-log-pro'))); } } /** * AJAX handler for refreshing stats cache */ public function ajax_refresh_stats_cache() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); } // Refresh stats cache ACTLOGPRO_Stats_Helper::refresh_stats_cache(); wp_send_json_success(array('message' => __('Stats cache refreshed successfully.', 'activity-log-pro'))); } /** * Add defer attribute to LemonSqueezy script * * @param string $tag The script tag * @param string $handle The script handle * @return string Modified script tag */ public function add_defer_to_lemonsqueezy($tag, $handle) { if ('lemonsqueezy-checkout' === $handle) { return str_replace(' src', ' defer src', $tag); } return $tag; } /** * Display admin notice when popular caching plugins are detected * * @since 1.0.0 */ public function display_caching_plugin_notice() { // Check if user has already dismissed this notice $user_id = get_current_user_id(); $dismissed_notices = get_user_meta($user_id, 'actlogpro_dismissed_notices', true); if (is_array($dismissed_notices) && isset($dismissed_notices['alp-caching-plugin-notice'])) { return; } // Only show on Activity Log Pro pages $screen = get_current_screen(); if (!$screen || strpos($screen->id, 'activity-log-pro') === false) { return; } // Detect popular caching plugins $detected_plugins = $this->detect_caching_plugins(); if (empty($detected_plugins)) { return; } $plugin_names = implode(', ', $detected_plugins); echo '
'; echo '

' . esc_html__('Activity Log Pro - Caching Plugin Detected', 'activity-log-pro') . '

'; // translators: %s is the name(s) of detected caching plugin(s) echo '

' . sprintf(esc_html__('We detected that you have %s active. If you experience issues with RSS/JSON feed tokens not updating, please:', 'activity-log-pro'), '' . esc_html($plugin_names) . '' ) . '

'; echo '
    '; echo '
  • ' . esc_html__('Clear your cache after generating new feed tokens', 'activity-log-pro') . '
  • '; echo '
  • ' . esc_html__('Configure your caching plugin to exclude feed URLs from caching', 'activity-log-pro') . '
  • '; echo '
  • ' . esc_html__('Add feed URLs to your caching plugin\'s exclusion list', 'activity-log-pro') . '
  • '; echo '
'; echo '

' . esc_html__('Feed URLs typically end with /feed/activity-log/ or /wp-json/activity-log-pro/v1/feed/', 'activity-log-pro') . '

'; echo '
'; } /** * Detect popular caching plugins * * @since 1.0.0 * @return array List of detected caching plugin names */ private function detect_caching_plugins() { $caching_plugins = array( 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'cache-enabler/cache-enabler.php' => 'Cache Enabler', 'comet-cache/comet-cache.php' => 'Comet Cache', 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird', 'sg-cachepress/sg-cachepress.php' => 'SG Optimizer', 'wp-optimize/wp-optimize.php' => 'WP-Optimize', 'autoptimize/autoptimize.php' => 'Autoptimize', 'breeze/breeze.php' => 'Breeze', 'swift-performance-lite/performance.php' => 'Swift Performance', 'hyper-cache/plugin.php' => 'Hyper Cache' ); $detected = array(); foreach ($caching_plugins as $plugin_file => $plugin_name) { if (is_plugin_active($plugin_file)) { $detected[] = $plugin_name; } } return $detected; } /** * AJAX handler for adding object to exclusions * Adds an option name to the Excluded Option Names list * * @since 1.0.0 */ public function ajax_add_object_to_exclusions() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); return; } // Check user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); return; } // Check for required data if (!isset($_POST['object_name']) || empty($_POST['object_name'])) { wp_send_json_error(array('message' => __('Object name is required.', 'activity-log-pro'))); return; } // Sanitize the object name $object_name = sanitize_text_field(wp_unslash($_POST['object_name'])); // Get current excluded option names $excluded_options = get_option('actlogpro_excluded_option_names', ''); // Convert to array $excluded_array = array_filter(array_map('trim', explode("\n", $excluded_options))); // Check if already excluded if (in_array($object_name, $excluded_array, true)) { wp_send_json_error(array('message' => __('This option is already in the exclusion list.', 'activity-log-pro'))); return; } // Add to exclusions $excluded_array[] = $object_name; // Convert back to string $excluded_string = implode("\n", $excluded_array); // Update option $updated = update_option('actlogpro_excluded_option_names', $excluded_string); if ($updated || in_array($object_name, $excluded_array, true)) { wp_send_json_success(array( 'message' => sprintf( /* translators: %s is the option name added to exclusions */ __('Added "%s" to exclusions. Refresh to see changes.', 'activity-log-pro'), $object_name ) )); } else { wp_send_json_error(array('message' => __('Failed to add exclusion. Please try again.', 'activity-log-pro'))); } } /** * AJAX handler for logging quick edit changes * * @since 1.0.0 */ public function ajax_log_quick_edit() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', 'activity-log-pro'))); return; } // Check user capabilities - must have manage_options (admin access required) if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'activity-log-pro'))); return; } // Check for required data if (!isset($_POST['post_id']) || !isset($_POST['old_title']) || !isset($_POST['new_title'])) { wp_send_json_error(array('message' => __('Missing required data.', 'activity-log-pro'))); return; } $post_id = absint(wp_unslash($_POST['post_id'])); $old_title = sanitize_text_field(wp_unslash($_POST['old_title'])); $new_title = sanitize_text_field(wp_unslash($_POST['new_title'])); // Get post $post = get_post($post_id); if (!$post) { wp_send_json_error(array('message' => __('Post not found.', 'activity-log-pro'))); return; } // Check if user can edit this specific post if (!current_user_can('edit_post', $post_id)) { wp_send_json_error(array('message' => __('Insufficient permissions to edit this post.', 'activity-log-pro'))); return; } // Only log if tracking is enabled if (get_option('actlogpro_track_post_events', '1') === '1') { // Log the activity ACTLOGPRO_Logger::log_activity( 'post', $post->post_type, $post_id, $new_title, 'update', sprintf( /* translators: %1$s: old title, %2$s: new title */ __('Title changed from "%1$s" to "%2$s" using Quick Edit', 'activity-log-pro'), $old_title, $new_title ), array( 'old_title' => $old_title, 'new_title' => $new_title ) ); } wp_send_json_success(); } /** * AJAX handler for getting current plugin settings * * @since 1.0.0 */ public function ajax_get_settings() { // Verify nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'actlogpro_ajax_nonce')) { wp_send_json_error(__('Invalid security token.', 'activity-log-pro')); return; } // Get current settings $settings = array( 'items_per_page' => get_option('actlogpro_items_per_page', 25) ); wp_send_json_success($settings); } /** * Ensure email digest is properly scheduled on admin init. * * Only runs once per session and only schedules if needed. * Prevents performance issues while ensuring scheduling happens. * * @since 1.1.0 */ public function ensure_email_digest_scheduled() { // Only run once per session to avoid repeated checks static $already_checked = false; if ($already_checked) { return; } $already_checked = true; // Only schedule if email digest class is available if (class_exists('ACTLOGPRO_Email_Digest_Manager')) { ACTLOGPRO_Email_Digest_Manager::schedule_email_digest(); } } /** * Render the admin footer for all plugin pages * * @since 1.0.4 */ public function actlogpro_render_admin_footer() { ?> __( 'Security check failed.', 'activity-log-pro' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'activity-log-pro' ) ) ); } // Prevent any server or browser caching of live session data. nocache_headers(); $args = array( 'page' => isset( $_POST['page'] ) ? absint( wp_unslash( $_POST['page'] ) ) : 1, 'per_page' => isset( $_POST['per_page'] ) ? absint( wp_unslash( $_POST['per_page'] ) ) : 0, 'search' => isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '', 'role' => isset( $_POST['role'] ) ? sanitize_text_field( wp_unslash( $_POST['role'] ) ) : '', 'multi_only' => ! empty( $_POST['multi_only'] ), ); if ( ! class_exists( 'ACTLOGPRO_Live_Sessions' ) ) { require_once ACTLOGPRO_PLUGIN_DIR . 'includes/class-alp-live-sessions.php'; } $result = ACTLOGPRO_Live_Sessions::get_active_sessions( $args ); // Allow premium to enrich sessions (e.g. geolocation) before sending to client. foreach ( $result['sessions'] as &$session ) { $session = apply_filters( 'actlogpro_live_session_data', $session ); // Strip raw IP after enrichment — only display-safe data goes to the client. unset( $session['ip_raw'] ); } unset( $session ); wp_send_json_success( $result ); } /** * AJAX handler: terminate a specific user session. * Premium-gated. Includes self-termination guard with confirmation step. * * @since 1.1.0 */ public function ajax_terminate_session() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'actlogpro_ajax_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'activity-log-pro' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'activity-log-pro' ) ) ); } if ( ! ACTLOGPRO_Premium_Helper::is_premium_enabled() ) { wp_send_json_error( array( 'message' => __( 'This feature requires Activity Log Pro Premium.', 'activity-log-pro' ), 'premium_required' => true ) ); } $target_user_id = isset( $_POST['user_id'] ) ? absint( wp_unslash( $_POST['user_id'] ) ) : 0; $token_hash = isset( $_POST['token_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['token_hash'] ) ) : ''; $confirmed = isset( $_POST['confirmed'] ) && sanitize_text_field( wp_unslash( $_POST['confirmed'] ) ) === '1'; if ( ! $target_user_id || empty( $token_hash ) ) { wp_send_json_error( array( 'message' => __( 'Invalid session data.', 'activity-log-pro' ) ) ); } // Validate token hash is hex-string only (sha256 output) to prevent injection. if ( ! preg_match( '/^[a-f0-9]{40,64}$/i', $token_hash ) ) { wp_send_json_error( array( 'message' => __( 'Invalid session identifier.', 'activity-log-pro' ) ) ); } // Self-termination guard: require explicit confirmation. $current_hash = ACTLOGPRO_Live_Sessions::get_current_session_hash(); if ( $token_hash === $current_hash && ! $confirmed ) { wp_send_json_error( array( 'message' => __( 'Confirmation required.', 'activity-log-pro' ), 'requires_confirmation' => true, ) ); } // Delegate actual termination to the premium class. if ( ! class_exists( 'ACTLOGPRO_Live_Sessions_Premium' ) ) { wp_send_json_error( array( 'message' => __( 'Premium module not loaded.', 'activity-log-pro' ) ) ); } $result = ACTLOGPRO_Live_Sessions_Premium::terminate_session( $target_user_id, $token_hash ); if ( $result ) { wp_send_json_success( array( 'message' => __( 'Session terminated successfully.', 'activity-log-pro' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to terminate session. The session may have already expired.', 'activity-log-pro' ) ) ); } } /** * AJAX handler: emergency lockdown — terminate all sessions except the current admin's own. * Premium-gated. Requires explicit double-confirmation token. * * @since 1.1.0 */ public function ajax_emergency_lockdown() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'actlogpro_ajax_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'activity-log-pro' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'activity-log-pro' ) ) ); } if ( ! ACTLOGPRO_Premium_Helper::is_premium_enabled() ) { wp_send_json_error( array( 'message' => __( 'This feature requires Activity Log Pro Premium.', 'activity-log-pro' ), 'premium_required' => true ) ); } $confirmed = isset( $_POST['confirmed'] ) && sanitize_text_field( wp_unslash( $_POST['confirmed'] ) ) === '1'; if ( ! $confirmed ) { wp_send_json_error( array( 'message' => __( 'Confirmation required.', 'activity-log-pro' ), 'requires_confirmation' => true ) ); } if ( ! class_exists( 'ACTLOGPRO_Live_Sessions_Premium' ) ) { wp_send_json_error( array( 'message' => __( 'Premium module not loaded.', 'activity-log-pro' ) ) ); } $current_hash = ACTLOGPRO_Live_Sessions::get_current_session_hash(); $count = ACTLOGPRO_Live_Sessions_Premium::terminate_all_except( get_current_user_id(), $current_hash ); wp_send_json_success( array( 'message' => sprintf( /* translators: %d number of sessions terminated */ _n( 'Lockdown complete. %d session terminated.', 'Lockdown complete. %d sessions terminated.', $count, 'activity-log-pro' ), $count ), 'count' => $count, ) ); } /** * AJAX handler: save Live Sessions settings (per-page, simultaneous limit). * * @since 1.1.0 */ public function ajax_save_live_sessions_settings() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'actlogpro_ajax_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'activity-log-pro' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'activity-log-pro' ) ) ); } // Sessions per page (free + premium). if ( isset( $_POST['per_page'] ) ) { $per_page = max( 1, min( 200, absint( wp_unslash( $_POST['per_page'] ) ) ) ); update_option( ACTLOGPRO_Live_Sessions::PER_PAGE_OPTION, $per_page ); } // Active threshold minutes (free + premium). if ( isset( $_POST['active_minutes'] ) && class_exists( 'ACTLOGPRO_Live_Sessions_Activity' ) ) { $active_minutes = ACTLOGPRO_Live_Sessions_Activity::sanitize_threshold_minutes( absint( wp_unslash( $_POST['active_minutes'] ) ) ); update_option( ACTLOGPRO_Live_Sessions_Activity::THRESHOLD_OPTION, $active_minutes ); } // Simultaneous session limit — premium only. if ( isset( $_POST['simultaneous_limit'] ) && ACTLOGPRO_Premium_Helper::is_premium_enabled() ) { $limit = max( 0, absint( wp_unslash( $_POST['simultaneous_limit'] ) ) ); update_option( 'actlogpro_live_sessions_simultaneous_limit', $limit ); } wp_send_json_success( array( 'message' => __( 'Settings saved.', 'activity-log-pro' ) ) ); } }