parent post type, populated before any deletion occurs. * Handles cases where automated feed plugins delete the parent post before its attachments. * * @var array */ private $attachment_parent_type_cache = array(); /** * Initialize the class and set up event listeners. * * @since 1.0.0 */ public function __construct() { // Verify table structure $this->verify_table_structure(); // User events add_action('wp_login', array($this, 'user_login'), 10, 2); add_action('wp_logout', array($this, 'store_user_info_before_logout'), 1); add_action('wp_logout', array($this, 'log_user_logout'), 999); add_action('user_register', array($this, 'user_registered')); add_action('profile_update', array($this, 'user_updated'), 10, 2); add_action('delete_user', array($this, 'user_deleted')); add_action('after_password_reset', array($this, 'password_reset'), 10, 2); // Post events add_action('transition_post_status', array($this, 'post_status_changed'), 10, 3); add_action('before_delete_post', array($this, 'cache_attachment_parent_type_before_delete'), 1); add_action('before_delete_post', array($this, 'post_deleted')); // Media events add_action('add_attachment', array($this, 'media_added')); add_action('edit_attachment', array($this, 'media_updated')); add_action('delete_attachment', array($this, 'media_deleted')); // Plugin events add_action('activated_plugin', array($this, 'plugin_activated')); add_action('deactivated_plugin', array($this, 'plugin_deactivated')); add_action('upgrader_process_complete', array($this, 'plugin_updated'), 10, 2); add_action('deleted_plugin', array($this, 'plugin_deleted'), 10, 2); // Theme events - LOGGING ONLY: These hooks listen to WordPress core theme events // and log them for audit trail purposes. They do NOT interfere with or change theme activation. add_action('switch_theme', array($this, 'theme_switched'), 10, 3); // Log when theme is switched (fires AFTER switch) add_action('after_switch_theme', array($this, 'theme_activated'), 10, 2); // Log theme activation (fires AFTER activation) add_action('upgrader_process_complete', array($this, 'theme_installed_or_updated'), 10, 2); // Log theme install/update add_action('delete_theme', array($this, 'theme_before_delete')); // Capture theme data before deletion for logging add_action('deleted_theme', array($this, 'theme_deleted'), 10, 2); // Log when theme is deleted add_action('customize_save_after', array($this, 'theme_customizer_updated')); // Log customizer changes // Note: Theme file editing via Theme Editor is not currently tracked due to lack of WordPress hooks // WordPress Core events add_action('upgrader_process_complete', array($this, 'wordpress_core_updated'), 10, 2); add_action('wp_upgrade', array($this, 'wordpress_core_upgraded'), 10, 2); // Capture WordPress version safely before updates add_action('admin_init', array($this, 'maybe_capture_wp_version')); add_action('wp_ajax_update-core', array($this, 'actlogpro_capture_wp_version_before_ajax_update'), 1); // Option events - register immediately since we're already in the init hook $this->register_option_hooks(); // Initialize Widget Logger new ACTLOGPRO_Widget_Logger(); // Menu events add_action('wp_update_nav_menu', array($this, 'menu_updated'), 10, 2); add_action('wp_create_nav_menu', array($this, 'menu_created'), 10, 2); add_action('delete_nav_menu', array($this, 'menu_before_delete')); add_action('wp_delete_nav_menu', array($this, 'menu_deleted')); // Term events add_action('delete_term', array($this, 'term_before_delete'), 10, 2); add_action('deleted_term', array($this, 'term_deleted'), 10, 4); add_action('pre_delete_term', array($this, 'capture_deleting_term'), 10, 5); // Category events add_action('created_category', array($this, 'category_created'), 10, 2); add_action('edited_category', array($this, 'category_updated'), 10, 2); add_action('delete_category', array($this, 'category_deleted'), 10, 4); // Tag events add_action('created_post_tag', array($this, 'tag_created'), 10, 2); add_action('edited_post_tag', array($this, 'tag_updated'), 10, 2); add_action('delete_post_tag', array($this, 'tag_deleted'), 10, 4); // Login failed event add_action('wp_login_failed', array($this, 'user_login_failed')); // Refresh tracking settings add_action('admin_init', array($this, 'refresh_tracking_settings')); } /** * Register option tracking hooks after WordPress is fully loaded. * * @since 1.0.0 */ public function register_option_hooks() { // Only register option hooks if WordPress user functions are available if (function_exists('wp_get_current_user')) { add_action('updated_option', array($this, 'option_updated'), 10, 3); } } /** * Verify the structure of the logs table. * * @since 1.0.0 */ private function verify_table_structure() { global $wpdb; $table_name = $wpdb->prefix . 'actlogpro_activity_log_pro_all_logs'; // Check if table exists $table_exists = ACTLOGPRO_Database::check_table_exists($table_name); if (!$table_exists) { return; } // Get table columns $columns = ACTLOGPRO_Database::get_table_columns($table_name); $column_names = ACTLOGPRO_Database::get_column_names($columns); // Check for required fields $required_fields = array( 'user_id', 'user_login', 'user_display_name', 'user_role', 'ip_address', 'event_type', 'object_type', 'object_id', 'object_name', 'action', 'description', 'metadata', 'created_at' ); $missing_fields = array(); foreach ($required_fields as $field) { if (!in_array($field, $column_names)) { $missing_fields[] = $field; } } if (!empty($missing_fields)) { } else { } } /** * Test function to check if wp_logout action is being fired. * This should be called from the constructor. */ private function test_wp_logout_action() { // Add a test action to wp_logout add_action('wp_logout', function() { }); } /** * Log user login event. * * @since 1.0.0 * @param string $user_login The user's login name. * @param WP_User $user The user object. */ public function user_login($user_login, $user) { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } // Make sure we have a valid user object if (!$user || !isset($user->ID) || $user->ID === 0) { return; } // Get user display name or fallback to login name $display_name = !empty($user->display_name) ? $user->display_name : $user_login; // Get user role $user_role = 'guest'; if (!empty($user->roles) && is_array($user->roles)) { $user_role = reset($user->roles); } // Get IP address using the public method $ip_address = ACTLOGPRO_Logger::get_ip_address(); // Reset Login Flood Guard counters on successful login delete_transient('actlogpro_flood_guard_count'); delete_transient('actlogpro_flood_guard_suppressed'); // Log the user login activity ACTLOGPRO_Database::log_user_login($user->ID, $user_login, $display_name, $user_role, $ip_address); } /** * Store user information before logout to ensure we can log it after the session is destroyed. * * @since 1.0.0 */ public function store_user_info_before_logout() { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } // Get current user $current_user = wp_get_current_user(); if ($current_user && $current_user->ID) { // Get user roles $user_roles = $current_user->roles; $user_role = !empty($user_roles) ? implode(', ', $user_roles) : ''; // Get IP address $ip_address = ACTLOGPRO_Logger::get_ip_address(); // Store user info in a transient $user_info = array( 'user_id' => $current_user->ID, 'user_login' => $current_user->user_login, 'user_display_name' => $current_user->display_name, 'user_role' => $user_role, 'ip_address' => $ip_address, 'timestamp' => current_time('mysql') ); // Set transient with 5-minute expiration set_transient('actlogpro_user_logout_' . $current_user->ID, $user_info, 5 * MINUTE_IN_SECONDS); } else { } } /** * Log user logout event using the stored user information. * * @since 1.0.0 */ public function log_user_logout() { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } // Get current user ID - we can't use get_current_user_id() here since the user session is already being destroyed // Instead, look for the user ID in the transients based on a cookie or a user ID in the hook // Check if user ID was passed directly to the hook (some plugins do this) $user_id = null; // Get the hook parameters directly (WP passes user ID in some cases) $hook_args = func_get_args(); if (!empty($hook_args) && is_numeric($hook_args[0])) { $user_id = intval($hook_args[0]); } // If no user ID from hook, try to get it from the auth cookie if (!$user_id) { // Try to extract user ID from auth cookie before it gets cleared // Use WordPress's COOKIEHASH to construct the exact cookie name $cookie_name = 'wordpress_logged_in_' . COOKIEHASH; // Check if the specific WordPress auth cookie exists if (isset($_COOKIE[$cookie_name])) { $auth_cookie = sanitize_text_field(wp_unslash($_COOKIE[$cookie_name])); if ($auth_cookie) { $cookie_parts = explode('|', $auth_cookie); if (!empty($cookie_parts[0])) { $cookie_user = sanitize_user($cookie_parts[0]); $user = get_user_by('login', $cookie_user); if ($user) { $user_id = $user->ID; } } } } } // If still no user ID, check all transients for recent logout info if (!$user_id) { $transient_like = '_transient_actlogpro_user_logout_%'; $transients = ACTLOGPRO_Database::get_transients_by_pattern($transient_like); if ($transients) { // Get the most recent transient (based on timestamp in the stored info) $most_recent = null; $most_recent_time = 0; foreach ($transients as $transient) { $transient_value = maybe_unserialize($transient->option_value); if (is_array($transient_value) && isset($transient_value['timestamp'])) { $timestamp = strtotime($transient_value['timestamp']); if ($timestamp > $most_recent_time) { $most_recent_time = $timestamp; $most_recent = $transient; $user_id = $transient_value['user_id']; } } } if ($most_recent) { // Use the option_name to extract the user ID as a fallback if (!$user_id) { $transient_name = str_replace('_transient_actlogpro_user_logout_', '', $most_recent->option_name); if (is_numeric($transient_name)) { $user_id = intval($transient_name); } } } } } if (!$user_id) { return; } // Get stored user info from transient $user_info = get_transient('actlogpro_user_logout_' . $user_id); if (!$user_info) { // If we have a user ID but no transient data, try to reconstruct the minimal required info $user = get_userdata($user_id); if ($user) { $user_role = !empty($user->roles) ? implode(', ', $user->roles) : ''; $user_info = array( 'user_id' => $user->ID, 'user_login' => $user->user_login, 'user_display_name' => $user->display_name, 'user_role' => $user_role, 'ip_address' => ACTLOGPRO_Logger::get_ip_address(), 'timestamp' => current_time('mysql') ); } else { return; } } // Prepare data for insertion $data = array( 'user_id' => $user_info['user_id'], 'user_login' => $user_info['user_login'], 'user_display_name' => $user_info['user_display_name'], 'user_role' => $user_info['user_role'], 'ip_address' => $user_info['ip_address'], 'event_type' => 'user', 'object_type' => 'user', 'object_id' => $user_info['user_id'], 'object_name' => $user_info['user_display_name'], 'action' => 'logout', 'severity' => 'low', 'description' => sprintf( /* translators: %s: User display name */ __('User Logged Out: %s', 'activity-log-pro'), $user_info['user_display_name'] ), 'created_at' => $user_info['timestamp'] ); // Insert into database using the database class $result = ACTLOGPRO_Database::log_activity($data); if ($result) { } else { } // Clean up the transient delete_transient('actlogpro_user_logout_' . $user_id); } /** * Log user registration event. * * @param int $user_id User ID. */ public function user_registered($user_id) { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } $user = get_userdata($user_id); ACTLOGPRO_Logger::log_activity( 'user', 'user', $user_id, $user->display_name, 'registered', sprintf( /* translators: %s: User display name */ __('New user registered: "%s"', 'activity-log-pro'), $user->display_name ), array('user_email' => $user->user_email, 'user_role' => implode(', ', $user->roles)), 'high' ); } /** * Log user profile update event. * * @param int $user_id User ID. * @param WP_User $old_user_data Object containing user's data prior to update. */ public function user_updated($user_id, $old_user_data) { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } $user = get_userdata($user_id); // Detect password changes $password_changed = ($old_user_data->user_pass !== $user->user_pass); // Detect role changes $old_roles = $old_user_data->roles; $new_roles = $user->roles; $role_changed = count(array_diff($old_roles, $new_roles)) > 0 || count(array_diff($new_roles, $old_roles)) > 0; $metadata = array( 'old_data' => array( 'display_name' => $old_user_data->display_name, 'user_email' => $old_user_data->user_email, 'roles' => implode(', ', $old_roles) ), 'new_data' => array( 'display_name' => $user->display_name, 'user_email' => $user->user_email, 'roles' => implode(', ', $new_roles) ) ); // Priority order: Password change > Role change > General profile update if ($password_changed) { $description = sprintf( /* translators: %s: User display name */ __('Password changed for user "%s"', 'activity-log-pro'), $user->display_name ); $action = 'password_changed'; $severity = 'high'; } elseif ($role_changed) { $description = sprintf( /* translators: 1: User display name, 2: Old roles, 3: New roles */ __('User "%1$s" roles changed from "%2$s" to "%3$s"', 'activity-log-pro'), $user->display_name, implode(', ', $old_roles), implode(', ', $new_roles) ); $action = 'role_changed'; $severity = 'critical'; } else { $description = sprintf( /* translators: %s: User display name */ __('User "%s" profile updated', 'activity-log-pro'), $user->display_name ); $action = 'updated'; $severity = 'low'; } ACTLOGPRO_Logger::log_activity( 'user', 'user', $user_id, $user->display_name, $action, $description, $metadata, $severity ); } /** * Log user deletion event. * * @param int $user_id User ID. */ public function user_deleted($user_id) { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } $user = get_userdata($user_id); ACTLOGPRO_Logger::log_activity( 'user', 'user', $user_id, $user->display_name, 'deleted', sprintf( /* translators: %s: User display name */ __('User "%s" deleted', 'activity-log-pro'), $user->display_name ), array('user_email' => $user->user_email, 'user_role' => implode(', ', $user->roles)), 'high' ); } /** * Log password reset event. * * @param WP_User $user User object. * @param string $new_pass New password. */ public function password_reset($user, $new_pass) { // Check if user event tracking is enabled if (!$this->is_tracking_enabled('user')) { return; } ACTLOGPRO_Logger::log_activity( 'user', 'user', $user->ID, $user->display_name, 'password_reset', sprintf( /* translators: %s: User display name */ __('Password reset for user "%s"', 'activity-log-pro'), $user->display_name ), array(), 'high' ); } /** * Log post status change event. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ public function post_status_changed($new_status, $old_status, $post) { // Skip auto-drafts and revisions if ($post->post_type === 'revision' || $post->post_status === 'auto-draft') { return; } // Skip navigation menu items - these should be handled by menu events setting if ($post->post_type === 'nav_menu_item') { // Check if menu event tracking is enabled for nav_menu_item posts if (!$this->is_tracking_enabled('menu')) { return; } } // Skip WordPress font management post types (wp_font_face, wp_font_family) // These are typically managed automatically and don't provide valuable audit information $font_post_types = array('wp_font_face', 'wp_font_family'); if (in_array($post->post_type, $font_post_types)) { return; } // Skip WooCommerce post types regardless of whether the integration is active // This ensures WooCommerce events are only logged by the WooCommerce integration $woocommerce_post_types = array('product', 'product_variation', 'shop_order', 'shop_coupon', 'shop_order_refund', 'shop_order_placehold'); if (in_array($post->post_type, $woocommerce_post_types)) { return; // Let the WooCommerce integration handle this if it's active } // Skip excluded custom post types (Premium feature) $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (!empty($excluded_post_types) && is_array($excluded_post_types) && in_array($post->post_type, $excluded_post_types, true)) { return; } // Skip if old and new status are the same if ($new_status === $old_status) { return; } $post_type_obj = get_post_type_object($post->post_type); $post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : $post->post_type; if ($old_status === 'new' && $new_status === 'auto-draft') { // Do nothing for auto-drafts return; } elseif ($old_status === 'auto-draft' && $new_status === 'draft') { $action = 'created'; $description = sprintf( /* translators: 1: Post type label, 2: Post title */ __('%1$s "%2$s" created', 'activity-log-pro'), $post_type_label, $post->post_title ); } elseif ($new_status === 'trash') { $action = 'trashed'; $description = sprintf( /* translators: 1: Post type label, 2: Post title */ __('%1$s "%2$s" moved to trash', 'activity-log-pro'), $post_type_label, $post->post_title ); } elseif ($old_status === 'trash') { $action = 'restored'; $description = sprintf( /* translators: 1: Post type label, 2: Post title */ __('%1$s "%2$s" restored from trash', 'activity-log-pro'), $post_type_label, $post->post_title ); } elseif ($new_status === 'publish' && $old_status !== 'publish') { $action = 'published'; $description = sprintf( /* translators: 1: Post type label, 2: Post title */ __('%1$s "%2$s" published', 'activity-log-pro'), $post_type_label, $post->post_title ); } else { $action = 'updated'; $description = sprintf( /* translators: 1: Post type label, 2: Post title, 3: Old status, 4: New status */ __('%1$s "%2$s" status changed from "%3$s" to "%4$s"', 'activity-log-pro'), $post_type_label, $post->post_title, $old_status, $new_status ); } ACTLOGPRO_Logger::log_activity( 'post', $post->post_type, $post->ID, $post->post_title, $action, $description, array( 'old_status' => $old_status, 'new_status' => $new_status, 'post_type' => $post->post_type, 'permalink' => get_permalink($post->ID) ), 'medium' ); } /** * Log post deletion event. * * @param int $post_id Post ID. */ public function post_deleted($post_id) { $post = get_post($post_id); // Skip if post doesn't exist or is a revision // Note: We don't skip 'trash' status here because permanent deletions occur on posts with 'trash' status // The status transition hooks handle the "move to trash" event separately if (!$post || $post->post_type === 'revision') { return; } // Skip navigation menu items - these should be handled by menu events setting if ($post->post_type === 'nav_menu_item') { // Check if menu event tracking is enabled for nav_menu_item posts if (!$this->is_tracking_enabled('menu')) { return; } } // Skip WordPress font management post types (wp_font_face, wp_font_family) // These are typically managed automatically and don't provide valuable audit information $font_post_types = array('wp_font_face', 'wp_font_family'); if (in_array($post->post_type, $font_post_types)) { return; } // Skip WooCommerce post types regardless of whether the integration is active // This ensures WooCommerce events are only logged by the WooCommerce integration $woocommerce_post_types = array('product', 'product_variation', 'shop_order', 'shop_coupon', 'shop_order_refund', 'shop_order_placehold'); if (in_array($post->post_type, $woocommerce_post_types)) { return; // Let the WooCommerce integration handle this if it's active } // Skip excluded custom post types (Premium feature) $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (!empty($excluded_post_types) && is_array($excluded_post_types) && in_array($post->post_type, $excluded_post_types, true)) { return; } // Skip attachment post type to prevent duplicate logging with media_deleted method if ($post->post_type === 'attachment') { return; // Let the media_deleted method handle attachment deletions exclusively } $post_type_obj = get_post_type_object($post->post_type); $post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : $post->post_type; // Check if this is a custom post type $is_custom_post_type = $post_type_obj && !$post_type_obj->_builtin; // Use transient to prevent duplicate logging for custom post types if ($is_custom_post_type) { $transient_key = 'actlogpro_cpt_deleted_' . $post_id; if (get_transient($transient_key)) { return; } // Set a transient for 5 seconds to prevent duplicate logging set_transient($transient_key, true, 5); // Custom description for custom post types $description = sprintf( /* translators: %1$s: post type label, %2$s: post title */ __('Custom Post Type: %1$s "%2$s" permanently deleted', 'activity-log-pro'), $post_type_label, $post->post_title ); } else { // Standard description for built-in post types $description = sprintf( /* translators: %1$s: post type label, %2$s: post title */ __('%1$s "%2$s" permanently deleted', 'activity-log-pro'), $post_type_label, $post->post_title ); } ACTLOGPRO_Logger::log_activity( 'post', $post->post_type, $post->ID, $post->post_title, 'deleted', $description, array('post_type' => $post->post_type), 'medium' ); } /** * Log custom post type creation event. * * @param int $post_id Post ID * @param WP_Post $post Post object * @param bool $update Whether this is an existing post being updated */ public function custom_post_type_created($post_id, $post, $update) { // Only log new posts, not updates if ($update) { return; } // Don't log auto-drafts, revisions, or standard post types if ($post->post_status === 'auto-draft' || wp_is_post_revision($post_id) || in_array($post->post_type, array('post', 'page', 'attachment'))) { return; } // Skip WooCommerce post types $woocommerce_post_types = array('product', 'product_variation', 'shop_order', 'shop_coupon', 'shop_order_refund', 'shop_order_placehold'); if (in_array($post->post_type, $woocommerce_post_types)) { return; } // Skip excluded custom post types (Premium feature) $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (!empty($excluded_post_types) && is_array($excluded_post_types) && in_array($post->post_type, $excluded_post_types, true)) { return; } // Check if this is a custom post type $post_type_obj = get_post_type_object($post->post_type); if (!$post_type_obj || !$post_type_obj->_builtin) { // Use transient to prevent duplicate logging $transient_key = 'actlogpro_cpt_created_' . $post_id; if (get_transient($transient_key)) { return; } // Set a transient for 5 seconds to prevent duplicate logging set_transient($transient_key, true, 5); $post_type_label = $post_type_obj->labels->singular_name; ACTLOGPRO_Logger::log_activity( 'post', $post->post_type, $post_id, $post->post_title, 'created', sprintf( /* translators: %1$s: post type label, %2$s: post title */ __('Custom Post Type: %1$s "%2$s" created', 'activity-log-pro'), $post_type_label, $post->post_title ), array( 'post_type' => $post->post_type, 'post_status' => $post->post_status, 'permalink' => get_permalink($post_id) ), 'low' ); } } /** * Log custom post type update event. * * @param int $post_id Post ID * @param WP_Post $post_after Post object following the update * @param WP_Post $post_before Post object before the update */ public function custom_post_type_updated($post_id, $post_after, $post_before) { // Don't log revisions, auto-drafts, or standard post types if (wp_is_post_revision($post_id) || $post_after->post_status === 'auto-draft' || in_array($post_after->post_type, array('post', 'page', 'attachment'))) { return; } // Don't log new posts (they're handled by custom_post_type_created) if ($post_before->post_status === 'auto-draft' || $post_before->post_status === 'new') { return; } // Skip WooCommerce post types $woocommerce_post_types = array('product', 'product_variation', 'shop_order', 'shop_coupon', 'shop_order_refund', 'shop_order_placehold'); if (in_array($post_after->post_type, $woocommerce_post_types)) { return; } // Skip excluded custom post types (Premium feature) $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (!empty($excluded_post_types) && is_array($excluded_post_types) && in_array($post_after->post_type, $excluded_post_types, true)) { return; } // Check if this is a custom post type $post_type_obj = get_post_type_object($post_after->post_type); if (!$post_type_obj || !$post_type_obj->_builtin) { // Use transient to prevent duplicate logging $transient_key = 'actlogpro_cpt_updated_' . $post_id . '_' . md5(serialize($post_after)); if (get_transient($transient_key)) { return; } // Set a transient for 5 seconds to prevent duplicate logging set_transient($transient_key, true, 5); $post_type_label = $post_type_obj->labels->singular_name; // Determine what changed $changes = array(); if ($post_before->post_title !== $post_after->post_title) { $changes['title'] = array( 'old' => $post_before->post_title, 'new' => $post_after->post_title ); } if ($post_before->post_content !== $post_after->post_content) { $changes['content'] = true; } if ($post_before->post_status !== $post_after->post_status) { $changes['status'] = array( 'old' => $post_before->post_status, 'new' => $post_after->post_status ); } // Create appropriate description based on what changed if (!empty($changes['status']) && $post_after->post_status === 'publish' && $post_before->post_status !== 'publish') { $description = sprintf( /* translators: %1$s: post type label, %2$s: post title */ __('Custom Post Type: %1$s "%2$s" published', 'activity-log-pro'), $post_type_label, $post_after->post_title ); $action = 'published'; } else if (!empty($changes['title']) && count($changes) === 1) { $description = sprintf( /* translators: %1$s: post type label, %2$s: old title, %3$s: new title */ __('Custom Post Type: %1$s title changed from "%2$s" to "%3$s"', 'activity-log-pro'), $post_type_label, $changes['title']['old'], $changes['title']['new'] ); $action = 'updated'; } else { $description = sprintf( /* translators: %1$s: post type label, %2$s: post title */ __('Custom Post Type: %1$s "%2$s" updated', 'activity-log-pro'), $post_type_label, $post_after->post_title ); $action = 'updated'; } ACTLOGPRO_Logger::log_activity( 'post', $post_after->post_type, $post_id, $post_after->post_title, $action, $description, array( 'post_type' => $post_after->post_type, 'changes' => $changes, 'permalink' => get_permalink($post_id) ), 'low' ); } } /** * Pre-cache attachment parent post types before a post is deleted. * Runs at priority 1 so it fires before anything else on before_delete_post. * This handles automated plugins (e.g. feed importers) that delete the parent * post before its attachments, leaving post_parent unresolvable at delete time. * * @param int $post_id Post ID about to be deleted. */ public function cache_attachment_parent_type_before_delete($post_id) { $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (empty($excluded_post_types) || !is_array($excluded_post_types)) { return; } $post = get_post($post_id); if (!$post) { return; } // If the post being deleted is itself an attachment, cache its parent type now. if ($post->post_type === 'attachment' && $post->post_parent) { $parent_post_type = get_post_type($post->post_parent); if ($parent_post_type && in_array($parent_post_type, $excluded_post_types, true)) { $this->attachment_parent_type_cache[$post_id] = $parent_post_type; } return; } // If the post being deleted is an excluded parent type, cache all its child attachments. if (in_array($post->post_type, $excluded_post_types, true)) { $attachment_ids = ACTLOGPRO_Database::get_attachment_ids_by_parent($post_id); foreach ($attachment_ids as $attachment_id) { $this->attachment_parent_type_cache[$attachment_id] = $post->post_type; } } } /** * Check if attachment should be excluded based on parent post type. * * @param int $attachment_id Attachment ID. * @return bool True if should be excluded, false otherwise. */ private function should_exclude_attachment($attachment_id) { // Check if feature is enabled $exclude_attachments = get_option('actlogpro_exclude_attachments_for_excluded_posts', '1') === '1'; if (!$exclude_attachments) { return false; } // Get the attachment post $attachment = get_post($attachment_id); if (!$attachment || !$attachment->post_parent) { return false; // No parent, log normally } // Get parent post type $parent_post_type = get_post_type($attachment->post_parent); if (!$parent_post_type) { return false; } // Get excluded post types $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); // Check if parent post type is in excluded list return in_array($parent_post_type, $excluded_post_types, true); } /** * Check if a deleted attachment should be excluded based on parent post type. * * @param int $attachment_id Attachment ID. * @return bool True if the deletion should be excluded, false otherwise. */ private function should_exclude_deleted_attachment($attachment_id) { // Check if the deleted-attachment exclusion feature is enabled $exclude_deleted = get_option('actlogpro_exclude_deleted_attachments_for_excluded_posts', '0') === '1'; if (!$exclude_deleted) { return false; } $excluded_post_types = get_option('actlogpro_excluded_post_types', array()); if (empty($excluded_post_types) || !is_array($excluded_post_types)) { return false; } // Use pre-cached parent type if available (set before parent post was deleted). if (isset($this->attachment_parent_type_cache[$attachment_id])) { return in_array($this->attachment_parent_type_cache[$attachment_id], $excluded_post_types, true); } // Fallback: resolve the parent post type from the live DB. $attachment = get_post($attachment_id); if (!$attachment || !$attachment->post_parent) { return false; } $parent_post_type = get_post_type($attachment->post_parent); if (!$parent_post_type) { return false; } return in_array($parent_post_type, $excluded_post_types, true); } /** * Log media addition event. * * @param int $attachment_id Attachment ID. */ public function media_added($attachment_id) { // Check if media event tracking is enabled if (!$this->is_tracking_enabled('media')) { return; } // Check if attachment should be excluded based on parent post type if ($this->should_exclude_attachment($attachment_id)) { return; } $attachment = get_post($attachment_id); if (!$attachment) { return; } // Get file information $file_path = get_attached_file($attachment_id); $file_name = basename($file_path); $file_type = wp_check_filetype($file_path)['type']; $file_size = filesize($file_path); // Format file size $formatted_size = size_format($file_size); ACTLOGPRO_Logger::log_activity( 'media', 'attachment', $attachment_id, $attachment->post_title, 'added', sprintf( /* translators: %s: attachment title */ __('Media file "%s" uploaded', 'activity-log-pro'), $attachment->post_title ), array( 'file_name' => $file_name, 'file_type' => $file_type, 'file_size' => $formatted_size, 'url' => wp_get_attachment_url($attachment_id) ), 'low' ); } /** * Log media update event. * * @param int $attachment_id Attachment ID. */ public function media_updated($attachment_id) { // Check if media event tracking is enabled if (!$this->is_tracking_enabled('media')) { return; } // Check if attachment should be excluded based on parent post type if ($this->should_exclude_attachment($attachment_id)) { return; } $attachment = get_post($attachment_id); if (!$attachment) { return; } ACTLOGPRO_Logger::log_activity( 'media', 'attachment', $attachment_id, $attachment->post_title, 'updated', sprintf( /* translators: %s: attachment title */ __('Media file "%s" updated', 'activity-log-pro'), $attachment->post_title ), array( 'file_name' => basename(get_attached_file($attachment_id)), 'url' => wp_get_attachment_url($attachment_id) ), 'low' ); } /** * Log media deletion event. * * @param int $attachment_id Attachment ID. */ public function media_deleted($attachment_id) { // Check if media event tracking is enabled if (!$this->is_tracking_enabled('media')) { return; } // Only apply the deletion-specific exclusion; the upload exclusion setting is intentionally // not checked here so that deletion logging remains independent of upload logging. if ($this->should_exclude_deleted_attachment($attachment_id)) { return; } $attachment = get_post($attachment_id); if (!$attachment) { return; } ACTLOGPRO_Logger::log_activity( 'media', 'attachment', $attachment_id, $attachment->post_title, 'deleted', sprintf( /* translators: %s: attachment title */ __('Media file "%s" deleted', 'activity-log-pro'), $attachment->post_title ), array( 'file_name' => basename(get_attached_file($attachment_id)) ), 'low' ); } /** * Truncate long metadata text for safe storage in log entries. * * @param string $text Text to truncate. * @param int $max_length Maximum character length. * @return string */ private function truncate_metadata_text($text, $max_length = 300) { $text = trim((string) $text); if ($text === '') { return ''; } if (function_exists('mb_strlen') && function_exists('mb_substr')) { if (mb_strlen($text) > $max_length) { return mb_substr($text, 0, $max_length) . '...'; } return $text; } if (strlen($text) > $max_length) { return substr($text, 0, $max_length) . '...'; } return $text; } /** * Build sanitized plugin author, URI, and description fields for log metadata. * * @param array $plugin_data Plugin data from get_plugins(). * @return array */ private function get_plugin_metadata_fields($plugin_data) { $fields = array(); if (!is_array($plugin_data)) { return $fields; } if (!empty($plugin_data['Author'])) { $fields['plugin_author'] = wp_strip_all_tags($plugin_data['Author']); } if (!empty($plugin_data['PluginURI'])) { $fields['plugin_uri'] = esc_url_raw($plugin_data['PluginURI']); } if (!empty($plugin_data['Description'])) { $fields['plugin_description'] = $this->truncate_metadata_text( wp_strip_all_tags($plugin_data['Description']) ); } return $fields; } /** * Build sanitized theme author, URI, and description fields for log metadata. * * @param WP_Theme $theme Theme object. * @return array */ private function get_theme_metadata_fields($theme) { $fields = array(); if (!$theme instanceof WP_Theme) { return $fields; } $author = $theme->get('Author'); if (!empty($author)) { $fields['theme_author'] = wp_strip_all_tags($author); } $uri = $theme->get('ThemeURI'); if (!empty($uri)) { $fields['theme_uri'] = esc_url_raw($uri); } $description = $theme->get('Description'); if (!empty($description)) { $fields['theme_description'] = $this->truncate_metadata_text( wp_strip_all_tags($description) ); } return $fields; } /** * Build nested theme context for switch events (name, version, author, URI, description). * * @param WP_Theme $theme Theme object. * @return array */ private function get_theme_nested_context($theme) { if (!$theme instanceof WP_Theme) { return array(); } $context = array( 'name' => $theme->get('Name'), 'version' => $theme->get('Version'), ); $author = $theme->get('Author'); if (!empty($author)) { $context['author'] = wp_strip_all_tags($author); } $uri = $theme->get('ThemeURI'); if (!empty($uri)) { $context['uri'] = esc_url_raw($uri); } $description = $theme->get('Description'); if (!empty($description)) { $context['description'] = $this->truncate_metadata_text( wp_strip_all_tags($description) ); } return $context; } /** * Map stored theme data (e.g. from a pre-delete transient) to log metadata fields. * * @param array|false $stored_theme_data Stored theme data array. * @return array */ private function get_theme_metadata_fields_from_stored($stored_theme_data) { $fields = array(); if (!is_array($stored_theme_data)) { return $fields; } if (!empty($stored_theme_data['author'])) { $fields['theme_author'] = $stored_theme_data['author']; } if (!empty($stored_theme_data['uri'])) { $fields['theme_uri'] = $stored_theme_data['uri']; } if (!empty($stored_theme_data['description'])) { $fields['theme_description'] = $stored_theme_data['description']; } return $fields; } /** * Map stored plugin data (e.g. from a pre-delete transient) to log metadata fields. * * @param array|false $stored_plugin_data Stored plugin data array. * @return array */ private function get_plugin_metadata_fields_from_stored($stored_plugin_data) { $fields = array(); if (!is_array($stored_plugin_data)) { return $fields; } if (!empty($stored_plugin_data['author'])) { $fields['plugin_author'] = $stored_plugin_data['author']; } if (!empty($stored_plugin_data['uri'])) { $fields['plugin_uri'] = $stored_plugin_data['uri']; } if (!empty($stored_plugin_data['description'])) { $fields['plugin_description'] = $stored_plugin_data['description']; } return $fields; } /** * Build sanitized plugin data for pre-delete transient storage. * * @param array $plugin_data Plugin data from get_plugins(). * @return array */ private function get_plugin_stored_data($plugin_data) { $stored = array( 'name' => !empty($plugin_data['Name']) ? $plugin_data['Name'] : '', 'version' => isset($plugin_data['Version']) ? $plugin_data['Version'] : '', ); $metadata = $this->get_plugin_metadata_fields($plugin_data); if (!empty($metadata['plugin_author'])) { $stored['author'] = $metadata['plugin_author']; } if (!empty($metadata['plugin_uri'])) { $stored['uri'] = $metadata['plugin_uri']; } if (!empty($metadata['plugin_description'])) { $stored['description'] = $metadata['plugin_description']; } return $stored; } /** * Build sanitized theme data for pre-delete transient storage. * * @param WP_Theme $theme Theme object. * @return array */ private function get_theme_stored_data($theme) { $stored = array( 'name' => $theme->get('Name'), 'version' => $theme->get('Version'), ); $metadata = $this->get_theme_metadata_fields($theme); if (!empty($metadata['theme_author'])) { $stored['author'] = $metadata['theme_author']; } if (!empty($metadata['theme_uri'])) { $stored['uri'] = $metadata['theme_uri']; } if (!empty($metadata['theme_description'])) { $stored['description'] = $metadata['theme_description']; } return $stored; } /** * Log plugin activation event. * * @param string $plugin Plugin path. */ public function plugin_activated($plugin) { // Check if plugin event tracking is enabled if (get_option('actlogpro_track_plugin_events', '1') !== '1') { return; } // Make sure get_plugins is available if (!function_exists('get_plugins')) { require_once(ABSPATH . 'wp-admin/includes/plugin.php'); } $all_plugins = get_plugins(); $plugin_data = isset($all_plugins[$plugin]) ? $all_plugins[$plugin] : array(); $plugin_name = !empty($plugin_data['Name']) ? $plugin_data['Name'] : $plugin; // Check if we've already logged this plugin activation // This prevents duplicate entries when the hook fires multiple times $transient_key = 'actlogpro_plugin_activated_' . sanitize_title($plugin); if (get_transient($transient_key)) { // We already logged this plugin activation, so don't log it again return; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_key, true, 30); ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'activated', sprintf( /* translators: %s: plugin name */ __('Plugin Activated: %s', 'activity-log-pro'), $plugin_name ), array_merge( array( 'plugin_file' => $plugin, 'plugin_path' => plugins_url('', $plugin), 'plugin_version' => isset($plugin_data['Version']) ? $plugin_data['Version'] : '', 'object_status' => 'active', 'previous_status' => 'inactive', ), $this->get_plugin_metadata_fields($plugin_data) ), 'high' ); } /** * Log plugin deactivation event. * * @param string $plugin Plugin path. */ public function plugin_deactivated($plugin) { // Check if plugin event tracking is enabled if (get_option('actlogpro_track_plugin_events', '1') !== '1') { return; } // Make sure get_plugins is available if (!function_exists('get_plugins')) { require_once(ABSPATH . 'wp-admin/includes/plugin.php'); } $all_plugins = get_plugins(); $plugin_data = isset($all_plugins[$plugin]) ? $all_plugins[$plugin] : array(); $plugin_name = !empty($plugin_data['Name']) ? $plugin_data['Name'] : $plugin; ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'deactivated', sprintf( /* translators: %s: plugin name */ __('Plugin Deactivated: %s', 'activity-log-pro'), $plugin_name ), array_merge( array( 'plugin_file' => $plugin, 'plugin_path' => plugins_url('', $plugin), 'plugin_version' => isset($plugin_data['Version']) ? $plugin_data['Version'] : '', 'object_status' => 'inactive', 'previous_status' => 'active', ), $this->get_plugin_metadata_fields($plugin_data) ), 'high' ); } /** * Log plugin installation or update event. * * @param WP_Upgrader $upgrader WP_Upgrader instance. * @param array $hook_extra Array of bulk item update data. */ public function plugin_updated($upgrader, $hook_extra) { // Check if plugin event tracking is enabled if (get_option('actlogpro_track_plugin_events', '1') !== '1') { return; } if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') { return; } // Get action type (install or update) $action = isset($hook_extra['action']) ? $hook_extra['action'] : 'update'; // Get plugins list if (!isset($hook_extra['plugins']) || !is_array($hook_extra['plugins'])) { // For single plugin installation, the plugin info might be in the upgrader if ($action === 'install' && method_exists($upgrader, 'plugin_info') && $upgrader->plugin_info()) { $plugins = array($upgrader->plugin_info()); } else { return; } } else { $plugins = $hook_extra['plugins']; } if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all_plugins = get_plugins(); foreach ($plugins as $plugin) { // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_plugin_' . $action . '_' . sanitize_title($plugin); if (get_transient($transient_name)) { continue; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); $plugin_data = isset($all_plugins[$plugin]) ? $all_plugins[$plugin] : array(); $plugin_name = !empty($plugin_data['Name']) ? $plugin_data['Name'] : $plugin; $plugin_version = isset($plugin_data['Version']) ? $plugin_data['Version'] : ''; $plugin_metadata = $this->get_plugin_metadata_fields($plugin_data); // Log based on action type if ($action === 'install') { ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'installed', sprintf( /* translators: %s: plugin name */ __('Plugin Installed: %s', 'activity-log-pro'), $plugin_name ), array_merge( array( 'plugin_file' => $plugin, 'plugin_path' => plugins_url('', $plugin), 'plugin_version' => $plugin_version, 'install_location' => plugins_url('', ACTLOGPRO_PLUGIN_FILE), 'object_status' => 'installed', 'previous_status' => 'not_installed', ), $plugin_metadata ), 'critical' ); } else { // Default to 'update' action ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'updated', sprintf( /* translators: %1$s: plugin name, %2$s: plugin version */ __('Plugin Updated: %1$s to version %2$s', 'activity-log-pro'), $plugin_name, $plugin_version ), array_merge( array( 'plugin_file' => $plugin, 'plugin_path' => plugins_url('', $plugin), 'plugin_version' => $plugin_version, 'object_status' => 'updated', 'previous_status' => 'previous_version_installed', ), $plugin_metadata ), 'low' ); } } } /** * Log plugin deletion event. * * @param string $plugin_file Path to the plugin file relative to the plugins directory. * @param bool $deleted Whether the plugin deletion was successful. */ public function plugin_deleted($plugin_file, $deleted) { // Check if plugin event tracking is enabled if (get_option('actlogpro_track_plugin_events', '1') !== '1') { return; } // If deletion wasn't successful, don't log it if (!$deleted) { return; } // Check if we have cached plugin data from the upgrader $transient_name = 'actlogpro_deleted_plugin_' . sanitize_title($plugin_file); $cached_data = get_transient($transient_name); $plugin_metadata = array(); $plugin_data = array(); if ($cached_data) { // Use cached data $plugin_name = $cached_data['name']; $plugin_version = $cached_data['version']; $plugin_metadata = $this->get_plugin_metadata_fields_from_stored($cached_data); // Delete the transient delete_transient($transient_name); } else { // Fallback to the old method if no cached data // Extract plugin name from the file path $plugin_name = dirname($plugin_file); if ($plugin_name === '.') { // Single file plugin $plugin_name = basename($plugin_file, '.php'); } // Format the plugin name for better readability $plugin_name = str_replace('-', ' ', $plugin_name); $plugin_name = ucwords($plugin_name); // Get plugin version from the WordPress plugin list $plugin_version = ''; if (function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $all_plugins = get_plugins(); if (isset($all_plugins[$plugin_file])) { $plugin_data = $all_plugins[$plugin_file]; if (!empty($plugin_data['Name'])) { $plugin_name = $plugin_data['Name']; } if (isset($plugin_data['Version'])) { $plugin_version = $plugin_data['Version']; } } } $plugin_metadata = $this->get_plugin_metadata_fields($plugin_data); } ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'deleted', sprintf( /* translators: %s: plugin name */ __('Plugin Deleted: %s', 'activity-log-pro'), $plugin_name ), array_merge( array( 'plugin_file' => $plugin_file, 'plugin_path' => plugins_url('', $plugin_file), 'plugin_version' => $plugin_version, 'object_status' => 'deleted', 'previous_status' => 'inactive', ), $plugin_metadata ), 'critical' ); } /** * Log theme switch events for audit trail purposes * * IMPORTANT: This method only LOGS theme switches - it does NOT change or interfere * with theme activation. It listens to the WordPress 'switch_theme' action which * fires AFTER a theme has already been successfully switched by WordPress core. * * @param string $new_name New theme name * @param WP_Theme $new_theme New theme object * @param WP_Theme $old_theme Previous theme object */ public function theme_switched($new_name, $new_theme, $old_theme) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } $old_theme_name = $old_theme->get('Name'); $new_theme_name = $new_theme->get('Name'); ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $new_theme_name, 'switched', sprintf( /* translators: %1$s: old theme name, %2$s: new theme name */ __('Theme switched from "%1$s" to "%2$s"', 'activity-log-pro'), $old_theme_name, $new_theme_name ), array( 'old_theme' => $this->get_theme_nested_context($old_theme), 'new_theme' => $this->get_theme_nested_context($new_theme), 'object_status' => 'active', 'previous_status' => 'inactive', ), 'high' ); } /** * Log theme activation event. * * @param string $theme_name Theme name. * @param mixed $theme Theme object. */ public function theme_activated($theme_name, $theme) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } // Check if we've already logged this theme activation via switch_theme hook // This prevents duplicate entries when both hooks fire for the same activation $transient_key = 'actlogpro_theme_switched_' . sanitize_title($theme_name); if (get_transient($transient_key)) { // We already logged this theme activation, so don't log it again return; } ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $theme_name, 'activated', sprintf( /* translators: %s: theme name */ __('Theme "%s" activated', 'activity-log-pro'), $theme_name ), array_merge( array( 'theme_version' => $theme->get('Version'), 'object_status' => 'active', 'previous_status' => 'inactive', ), $this->get_theme_metadata_fields($theme) ), 'high' ); } /** * Log theme installation or update event. * * @param WP_Upgrader $upgrader WP_Upgrader instance. * @param array $hook_extra Array of bulk item update data. */ public function theme_installed_or_updated($upgrader, $hook_extra) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } // Check if this is a theme update/install if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'theme') { return; } // Get theme data if (isset($hook_extra['theme'])) { // Single theme update $theme_slug = $hook_extra['theme']; $themes = array($theme_slug); } elseif (isset($hook_extra['themes'])) { // Bulk theme update $themes = $hook_extra['themes']; } else { // New theme installation $theme_info = $upgrader->theme_info(); if ($theme_info) { $themes = array($theme_info->get_stylesheet()); } else { return; // Unable to determine theme } } // Action type (install or update) $action = isset($hook_extra['action']) ? $hook_extra['action'] : ''; // Log each theme foreach ($themes as $theme_slug) { // Get theme data $theme = wp_get_theme($theme_slug); if (!$theme->exists()) { continue; } $theme_name = $theme->get('Name'); $theme_version = $theme->get('Version'); $theme_metadata = $this->get_theme_metadata_fields($theme); // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_theme_' . $action . '_' . $theme_slug; if (get_transient($transient_name)) { continue; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); // Log based on action type if ($action === 'install') { ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $theme_name, 'installed', sprintf( /* translators: %1$s: theme name, %2$s: theme version */ __('Theme "%1$s" installed (Version: %2$s)', 'activity-log-pro'), $theme_name, $theme_version ), array_merge( array( 'theme_slug' => $theme_slug, 'theme_version' => $theme_version, 'object_status' => 'installed', 'previous_status' => 'not_installed', ), $theme_metadata ), 'high' ); } else { // Default to 'update' action ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $theme_name, 'updated', sprintf( /* translators: %1$s: theme name, %2$s: theme version */ __('Theme "%1$s" updated to version %2$s', 'activity-log-pro'), $theme_name, $theme_version ), array_merge( array( 'theme_slug' => $theme_slug, 'theme_version' => $theme_version, 'object_status' => 'updated', 'previous_status' => 'previous_version_installed', ), $theme_metadata ), 'low' ); } } } /** * Store theme data before deletion. * * @param string $theme_slug Theme slug. */ public function theme_before_delete($theme_slug) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } // Get theme data before it's deleted $theme = wp_get_theme($theme_slug); if ($theme->exists()) { // Store theme data in a transient for use in the deletion log set_transient( 'actlogpro_theme_data_' . $theme_slug, $this->get_theme_stored_data($theme), 60 ); } } /** * Log theme deletion event. * * @param string $theme_slug Theme slug. * @param bool $deleted Whether the theme deletion was successful. */ public function theme_deleted($theme_slug, $deleted) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } if (!$deleted) { return; } // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_theme_deleted_' . $theme_slug; if (get_transient($transient_name)) { return; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); // Get theme data from the transient if available $theme_data = get_transient('actlogpro_theme_data_' . $theme_slug); $theme_name = isset($theme_data['name']) ? $theme_data['name'] : $theme_slug; $theme_version = isset($theme_data['version']) ? $theme_data['version'] : ''; $theme_metadata = $this->get_theme_metadata_fields_from_stored($theme_data); ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $theme_name, 'deleted', sprintf( /* translators: %s: theme name */ __('Theme "%s" deleted', 'activity-log-pro'), $theme_name ), array_merge( array( 'theme_slug' => $theme_slug, 'theme_version' => $theme_version, 'object_status' => 'deleted', 'previous_status' => 'installed', ), $theme_metadata ), 'high' ); // Clean up the theme data transient delete_transient('actlogpro_theme_data_' . $theme_slug); } /** * Log theme customizer updates. * * @param WP_Customize_Manager $wp_customize WP_Customize_Manager instance. */ public function theme_customizer_updated($wp_customize) { // Check if theme event tracking is enabled if (!$this->is_tracking_enabled('theme')) { return; } $theme = $wp_customize->theme(); $theme_name = $theme->display('Name'); // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_theme_customizer_' . $theme->get_stylesheet(); if (get_transient($transient_name)) { return; } // Set transient to prevent duplicate logging (expires after 10 seconds) set_transient($transient_name, true, 10); ACTLOGPRO_Logger::log_activity( 'theme', 'theme', 0, $theme_name, 'customized', sprintf( /* translators: %s: theme name */ __('Theme "%s" customized via Customizer', 'activity-log-pro'), $theme_name ), array( 'theme_slug' => $theme->get_stylesheet(), 'theme_version' => $theme->get('Version'), 'object_status' => 'customized', 'previous_status' => 'default_configuration' ), 'low' ); } /** * Log option update event. * * @param string $option Option name. * @param mixed $old_value Old option value. * @param mixed $value New option value. */ public function option_updated($option, $old_value, $value) { // Check if option event tracking is enabled if (!$this->is_tracking_enabled('option')) { return; } // Safety check: ensure WordPress user functions are available if (!function_exists('wp_get_current_user')) { return; } // Check if this option is in the user-defined exclusion list if ($this->is_option_excluded($option)) { return; } if ( $this->should_skip_credential_option_logging( $option ) ) { return; } // Skip some internal options and widget-related options (now handled by ACTLOGPRO_Widget_Logger) $skip_options = array( '_transient_', '_site_transient_', 'cron', 'actlogpro_', 'widget_', 'theme_mods_', 'doing_cron', 'auto_core_update_notified', 'auto_update', 'recovery_keys', 'recently_activated', 'acf_version', 'rewrite_rules', 'update_', 'active_plugins', 'wc_pending_batch_processes', 'wc_regenerate_images_batch', 'wp_widget_', 'freemius_', 'fs_', '_fs_' ); // Skip exact widget-related options (now handled by ACTLOGPRO_Widget_Logger) $exact_skip_options = array( 'sidebars_widgets', 'wp_user_roles', // WordPress user roles - internal 'wp_page_for_privacy_policy' // WordPress privacy policy page - usually set programmatically ); // Check prefix-based skips foreach ($skip_options as $skip_option) { if (strpos($option, $skip_option) === 0) { return; } } // Check exact match skips if (in_array($option, $exact_skip_options)) { return; } // Skip WooCommerce options regardless of whether the integration is active // This ensures WooCommerce events are only logged by the WooCommerce integration if (strpos($option, 'woocommerce') !== false) { return; // Let the WooCommerce integration handle this if it's active } // Skip Yoast SEO options - these are handled by the Yoast integration $yoast_options = array('wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_sitemap_cache_validator_global'); if (in_array($option, $yoast_options) || strpos($option, 'wpseo_') === 0) { return; // Let the Yoast integration handle this if it's active } // Don't log if values are the same if ($old_value === $value) { return; } // Identify special option types that need enhanced duplicate prevention $is_user_roles_option = (strpos($option, '_user_roles') !== false); $is_theme_option = $this->is_theme_option($option); // Set appropriate expiration times based on option type // Theme options often update in rapid succession, so we use a longer expiration if ($is_theme_option) { $transient_expiration = 180; // 3 minutes for theme options } elseif ($is_user_roles_option) { $transient_expiration = 120; // 2 minutes for user roles options } else { $transient_expiration = 60; // 1 minute for standard options } // Create a unique transient key based on option type if ($is_theme_option) { // For theme options, use a simpler key without the value hash // This prevents multiple logs for theme options that update frequently $transient_base = 'actlogpro_option_updated_theme_' . md5($option); // Get the current microtime to track rapid updates $current_time = microtime(true); // Check if we have a record of recent updates for this theme option $recent_updates = get_transient('actlogpro_theme_option_updates_' . md5($option)); if ($recent_updates) { // If the last update was less than 2 seconds ago, skip this one if (($current_time - $recent_updates['last_time']) < 2) { return; } // If we've logged more than 3 updates for this option in the last minute, skip this one if ($recent_updates['count'] > 3 && ($current_time - $recent_updates['first_time']) < 60) { return; } // Update the record $recent_updates['count']++; $recent_updates['last_time'] = $current_time; } else { // Create a new record $recent_updates = array( 'count' => 1, 'first_time' => $current_time, 'last_time' => $current_time ); } // Save the updated record set_transient('actlogpro_theme_option_updates_' . md5($option), $recent_updates, $transient_expiration); } else { // Standard approach for non-theme options $value_hash = md5(maybe_serialize($value)); $transient_base = 'actlogpro_option_updated_' . md5($option . '_' . $value_hash); // For user roles options, use a simpler transient key that doesn't include the value hash if ($is_user_roles_option) { $transient_base = 'actlogpro_option_updated_' . md5($option); } // Check if we've logged this option recently if (get_transient($transient_base)) { // We've already logged this option recently return; } // Set transient to prevent duplicate logging set_transient($transient_base, true, $transient_expiration); } // Also store the last value for this option for additional checking $last_value_transient = 'actlogpro_option_last_value_' . md5($option); set_transient($last_value_transient, $value, $transient_expiration); // Prepare values for logging $old_value_for_log = $this->prepare_option_value_for_log($old_value); $new_value_for_log = $this->prepare_option_value_for_log($value); ACTLOGPRO_Logger::log_activity( 'option', 'option', 0, $option, 'updated', sprintf( /* translators: 1: option name, 2: old value, 3: new value */ __('Option "%1$s" changed from "%2$s" to "%3$s"', 'activity-log-pro'), $option, $old_value_for_log, $new_value_for_log ), array( 'old_value' => $old_value_for_log, 'new_value' => $new_value_for_log ), 'low' ); } /** * Prepare option value for logging. * * @param mixed $value Option value. * @return mixed Prepared value. */ private function prepare_option_value_for_log($value) { if (is_array($value)) { return $this->format_array_for_log($value); } elseif (is_object($value)) { return $this->format_object_for_log($value); } elseif (is_bool($value)) { return $value ? 'true' : 'false'; } elseif (is_null($value)) { return 'null'; } else { // For simple values, return as is, but truncate if too long $value_str = (string) $value; if (strlen($value_str) > 500) { return substr($value_str, 0, 500) . '... (truncated)'; } return $value_str; } } /** * Format array for logging display. * * @param array $array Array to format. * @return string Formatted array representation. */ private function format_array_for_log($array) { $count = count($array); // Empty array if ($count === 0) { return '[empty array]'; } // Try to create a readable representation $json = json_encode($array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // If JSON encoding failed, fall back to basic info if ($json === false) { return sprintf('[array with %d item%s]', $count, $count !== 1 ? 's' : ''); } // For small arrays with short content, show the full JSON if ($count <= 5 && strlen($json) <= 200) { return $json; } // For larger or longer arrays, show count and preview if (strlen($json) <= 500) { return $json; } // For very large arrays, show count and truncated preview return sprintf('[array with %d item%s]: %s... (truncated)', $count, $count !== 1 ? 's' : '', substr($json, 0, 200) ); } /** * Format object for logging display. * * @param object $object Object to format. * @return string Formatted object representation. */ private function format_object_for_log($object) { $class_name = get_class($object); // Try to get object properties $json = json_encode($object, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false || $json === '{}' || $json === 'null') { return sprintf('[object: %s]', $class_name); } // For small objects, show the JSON if (strlen($json) <= 200) { return sprintf('[object: %s] %s', $class_name, $json); } // For larger objects, show class and truncated content return sprintf('[object: %s] %s... (truncated)', $class_name, substr($json, 0, 150) ); } /** * Determine if an option is a theme-related option. * * @param string $option Option name. * @return bool True if it's a theme option, false otherwise. */ private function is_theme_option($option) { // List of known theme option prefixes and exact matches $theme_option_patterns = array( 'theme_mods_', // WordPress theme modifications 'et_', // Divi theme options 'astra_', // Astra theme 'generate_', // GeneratePress 'oceanwp_', // OceanWP 'avada_', // Avada 'fusion_', // Avada Fusion Builder 'elementor_', // Elementor 'wp_custom_css', // WordPress custom CSS 'customizer_', // Customizer ); // Check if the option starts with any of the theme option patterns foreach ($theme_option_patterns as $pattern) { if (strpos($option, $pattern) === 0) { return true; } } // Check for specific theme options that don't follow the pattern $specific_theme_options = array( 'et_divi', // Divi theme 'stylesheet', // Active theme 'template', // Parent theme 'current_theme', // Current theme name ); if (in_array($option, $specific_theme_options)) { return true; } return false; } /** * Set up widget event tracking. * * @deprecated Widget logging is now handled by ACTLOGPRO_Widget_Logger class */ public function widget_events() { // Widget logging is now exclusively handled by ACTLOGPRO_Widget_Logger class // This method is kept for backward compatibility but no longer registers hooks return; } /** * Log widget update event. * * @deprecated Widget logging is now handled by ACTLOGPRO_Widget_Logger class * @param array $instance Widget instance. * @param array $new_instance New widget instance. * @param array $old_instance Old widget instance. * @param WP_Widget $widget_object Widget object. * @return array Widget instance. */ public function widget_updated($instance, $new_instance, $old_instance, $widget_object) { // Widget logging is now exclusively handled by ACTLOGPRO_Widget_Logger class // This method is kept for backward compatibility but no longer logs return $instance; } /** * Track widget additions and removals. * * @deprecated Widget logging is now handled by ACTLOGPRO_Widget_Logger class */ public function track_widget_changes() { // Widget logging is now exclusively handled by ACTLOGPRO_Widget_Logger class // This method is kept for backward compatibility but no longer functions return; } /** * Log menu update event. * * @param int $menu_id Menu ID. * @param array $menu_data Menu data. */ public function menu_updated($menu_id, $menu_data = array()) { // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return; } $menu = wp_get_nav_menu_object($menu_id); if (!$menu) { return; } // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_menu_updated_' . $menu_id; if (get_transient($transient_name)) { return; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); ACTLOGPRO_Logger::log_activity( 'menu', 'menu', $menu_id, $menu->name, 'updated', sprintf( /* translators: %s: menu name */ __('Menu "%s" updated', 'activity-log-pro'), $menu->name ), array(), 'low' ); } /** * Log menu creation event. * * @param int $menu_id Menu ID. * @param array $menu_data Menu data. */ public function menu_created($menu_id, $menu_data = array()) { // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return; } $menu = wp_get_nav_menu_object($menu_id); if (!$menu) { return; } ACTLOGPRO_Logger::log_activity( 'menu', 'menu', $menu_id, $menu->name, 'created', sprintf( /* translators: %s: menu name */ __('Menu "%s" created', 'activity-log-pro'), $menu->name ), array(), 'low' ); } /** * Store menu details before deletion to use in the deletion log. * * @param int $menu_id Menu ID. */ public function menu_before_delete($menu_id) { // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return; } // Get menu details before it's deleted $menu = wp_get_nav_menu_object($menu_id); if (!$menu) { return; } // Store menu details in a transient for use in the deletion log set_transient('actlogpro_menu_data_' . $menu_id, array( 'name' => $menu->name, 'slug' => $menu->slug, 'term_id' => $menu->term_id ), 60); // Keep for 1 minute } /** * Log menu deletion event. * * @param int $menu_id Menu ID. */ public function menu_deleted($menu_id) { // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return; } // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_menu_deleted_' . $menu_id; if (get_transient($transient_name)) { return; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); // Get menu data from the transient if available $menu_data = get_transient('actlogpro_menu_data_' . $menu_id); if ($menu_data) { $menu_name = $menu_data['name']; } else { $menu_name = 'Menu #' . $menu_id; } // Log the activity ACTLOGPRO_Logger::log_activity( 'menu', 'menu', $menu_id, $menu_name, 'deleted', sprintf( /* translators: %s: menu name */ __('Menu "%s" deleted', 'activity-log-pro'), $menu_name ), array(), 'low' ); // Clean up the menu data transient delete_transient('actlogpro_menu_data_' . $menu_id); } /** * Capture term deletion for nav_menu taxonomy. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. */ public function term_before_delete($term_id, $taxonomy) { // Only proceed if this is a menu term being deleted if ($taxonomy !== 'nav_menu') { return; } // Get term data before it's deleted $term = get_term($term_id, 'nav_menu'); if (!$term || is_wp_error($term)) { return; } // Store term data for later use set_transient('actlogpro_menu_term_data_' . $term_id, array( 'name' => $term->name, 'slug' => $term->slug, 'term_id' => $term->term_id ), 60); // Keep for 1 minute } /** * Log term deletion for nav_menu taxonomy. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy name. * @param mixed $deleted_term The deleted term object. */ public function term_deleted($term_id, $tt_id, $taxonomy, $deleted_term) { // Only proceed if this is a menu term that was deleted if ($taxonomy !== 'nav_menu') { return; } // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return; } // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_menu_term_deleted_' . $term_id; if (get_transient($transient_name)) { return; } // Set transient to prevent duplicate logging (expires after 30 seconds) set_transient($transient_name, true, 30); // Get menu data from either the deleted_term or from our transient if (is_object($deleted_term) && isset($deleted_term->name)) { $menu_name = $deleted_term->name; } else { $menu_data = get_transient('actlogpro_menu_term_data_' . $term_id); $menu_name = isset($menu_data['name']) ? $menu_data['name'] : 'Menu #' . $term_id; } // Log the deletion ACTLOGPRO_Logger::log_activity( 'menu', 'menu', $term_id, $menu_name, 'deleted', sprintf( /* translators: %s: menu name */ __('Menu "%s" deleted (via term deletion)', 'activity-log-pro'), $menu_name ), array(), 'low' ); // Clean up transients delete_transient('actlogpro_menu_term_data_' . $term_id); } /** * Capture menu term deletion using a direct filter. * This is our most reliable method for capturing menu deletion. * * @param mixed $delete Whether to go forward with term deletion. * @param int $term Term ID. * @param string $taxonomy Taxonomy name (optional). * @param object $object Term object (optional). * @param array $args Array of arguments to delete_term function (optional). * @return mixed Whether to go forward with term deletion. */ public function capture_deleting_term($delete, $term = null, $taxonomy = '', $object = null, $args = array()) { // If taxonomy is not provided, return early if (empty($taxonomy)) { return $delete; } // Only proceed if this is a menu term being deleted if ($taxonomy !== 'nav_menu') { return $delete; } // Check if menu event tracking is enabled if (!$this->is_tracking_enabled('menu')) { return $delete; } // Use the object parameter which contains the term data // This is more reliable than trying to get the term when it might be in process of deletion $term_name = ''; $term_slug = ''; if (is_object($object) && isset($object->name)) { $term_name = $object->name; $term_slug = isset($object->slug) ? $object->slug : ''; } else { // Fallback to trying get_term() one more time $term_object = get_term($term, $taxonomy); if (!is_wp_error($term_object) && is_object($term_object)) { $term_name = $term_object->name; $term_slug = $term_object->slug; } else { // Last resort fallback - use the stored transient if available $menu_data = get_transient('actlogpro_menu_term_data_' . $term); if ($menu_data && isset($menu_data['name'])) { $term_name = $menu_data['name']; $term_slug = isset($menu_data['slug']) ? $menu_data['slug'] : ''; } else { // Absolute last resort - use a generic name $term_name = 'Menu #' . $term; } } } // Log the deletion immediately since this is our most reliable hook ACTLOGPRO_Logger::log_activity( 'menu', 'menu', $term, $term_name, 'deleted', sprintf( /* translators: %s: menu name */ __('Menu "%s" deleted', 'activity-log-pro'), $term_name ), array(), 'low' ); // Store the data in a transient as well for our other handlers set_transient('actlogpro_menu_term_direct_' . $term, array( 'name' => $term_name, 'slug' => $term_slug, 'term_id' => $term ), 60); // Keep for 1 minute // Continue with the deletion process return $delete; } /** * Log category creation. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ public function category_created($term_id, $tt_id) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } $term = get_term($term_id, 'category'); if (!$term || is_wp_error($term)) { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'category', $term_id, $term->name, 'created', sprintf( /* translators: %s: category name */ __('Category Created: %s', 'activity-log-pro'), $term->name ), array( 'term_slug' => $term->slug, 'term_description' => $term->description, 'parent_id' => $term->parent, 'post_count' => $term->count, 'taxonomy' => $term->taxonomy ), 'low' ); } /** * Log category update. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ public function category_updated($term_id, $tt_id) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } $term = get_term($term_id, 'category'); if (!$term || is_wp_error($term)) { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'category', $term_id, $term->name, 'updated', sprintf( /* translators: %s: category name */ __('Category Updated: %s', 'activity-log-pro'), $term->name ), array( 'term_slug' => $term->slug, 'term_description' => $term->description, 'parent_id' => $term->parent, 'post_count' => $term->count, 'taxonomy' => $term->taxonomy ), 'low' ); } /** * Log category deletion. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param WP_Term $deleted_term Deleted term object. * @param array $object_ids Object IDs. */ public function category_deleted($term_id, $tt_id, $deleted_term, $object_ids) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } if (!$deleted_term || $deleted_term->taxonomy !== 'category') { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'category', $term_id, $deleted_term->name, 'deleted', sprintf( /* translators: %s: category name */ __('Category Deleted: %s', 'activity-log-pro'), $deleted_term->name ), array( 'term_slug' => $deleted_term->slug, 'term_description' => $deleted_term->description, 'parent_id' => $deleted_term->parent, 'post_count' => $deleted_term->count, 'taxonomy' => $deleted_term->taxonomy ), 'medium' ); } /** * Log tag creation. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ public function tag_created($term_id, $tt_id) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } $term = get_term($term_id, 'post_tag'); if (!$term || is_wp_error($term)) { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'post_tag', $term_id, $term->name, 'created', sprintf( /* translators: %s: tag name */ __('Tag Created: %s', 'activity-log-pro'), $term->name ), array( 'term_slug' => $term->slug, 'term_description' => $term->description, 'post_count' => $term->count, 'taxonomy' => $term->taxonomy ), 'low' ); } /** * Log tag update. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ public function tag_updated($term_id, $tt_id) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } $term = get_term($term_id, 'post_tag'); if (!$term || is_wp_error($term)) { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'post_tag', $term_id, $term->name, 'updated', sprintf( /* translators: %s: tag name */ __('Tag Updated: %s', 'activity-log-pro'), $term->name ), array( 'term_slug' => $term->slug, 'term_description' => $term->description, 'post_count' => $term->count, 'taxonomy' => $term->taxonomy ), 'low' ); } /** * Log tag deletion. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param WP_Term $deleted_term Deleted term object. * @param array $object_ids Object IDs. */ public function tag_deleted($term_id, $tt_id, $deleted_term, $object_ids) { // Validate term ID $term_id = absint($term_id); if (empty($term_id)) { return; } if (!$deleted_term || $deleted_term->taxonomy !== 'post_tag') { return; } ACTLOGPRO_Logger::log_activity( 'taxonomy', 'post_tag', $term_id, $deleted_term->name, 'deleted', sprintf( /* translators: %s: tag name */ __('Tag Deleted: %s', 'activity-log-pro'), $deleted_term->name ), array( 'term_slug' => $deleted_term->slug, 'term_description' => $deleted_term->description, 'post_count' => $deleted_term->count, 'taxonomy' => $deleted_term->taxonomy ), 'low' ); } /** * Log failed login attempt. * * @param string $username Username or email address. */ public function user_login_failed($username) { // Check if security event tracking is enabled if (!$this->is_tracking_enabled('security')) { return; } // Login Flood Guard: suppress excessive failed login logging if (get_option('actlogpro_flood_guard_enabled', '1') === '1') { $threshold = (int) get_option('actlogpro_flood_guard_limit', 50); $count = (int) get_transient('actlogpro_flood_guard_count'); $new_count = $count + 1; set_transient('actlogpro_flood_guard_count', $new_count, DAY_IN_SECONDS); if ($new_count > $threshold) { $suppressed = (int) get_transient('actlogpro_flood_guard_suppressed'); set_transient('actlogpro_flood_guard_suppressed', $suppressed + 1, DAY_IN_SECONDS); return; } } // Check if the user exists $user_exists = username_exists($username) || email_exists($username); if ($user_exists) { $description = sprintf( /* translators: %s: username */ __('Failed Login Attempt: %s', 'activity-log-pro'), $username ); $action = 'login_failed'; } else { $description = sprintf( /* translators: %s: username */ __('Failed Login Attempt (non-existing user): %s', 'activity-log-pro'), $username ); $action = 'login_failed_nonexistent'; } ACTLOGPRO_Logger::log_activity( 'security', 'user', 0, // No user ID since login failed $username, $action, $description, array(), 'medium' ); } /** * Skip generic option logs that would expose credentials (AI loggers record governance separately). * * @param string $option_name Option name. */ private function should_skip_credential_option_logging( string $option_name ): bool { if ( strpos( $option_name, 'wpai_' ) === 0 ) { return true; } if ( strpos( $option_name, 'connectors_' ) === 0 && substr( $option_name, -strlen( '_api_key' ) ) === '_api_key' ) { return true; } $lower = strtolower( $option_name ); $markers = array( '_api_key', '_api_secret', '_access_token', '_auth_token', '_private_key' ); foreach ( $markers as $marker ) { if ( strpos( $lower, $marker ) !== false ) { return true; } } return false; } /** * Check if an option should be excluded from logging. * * @param string $option_name The option name to check. * @return bool True if the option should be excluded, false otherwise. */ private function is_option_excluded($option_name) { // Get the excluded options from settings $excluded_options = get_option('actlogpro_excluded_option_names', ''); if (empty($excluded_options)) { return false; } // Split the excluded options by lines and clean them up $patterns = array_filter(array_map('trim', explode("\n", $excluded_options))); foreach ($patterns as $pattern) { // Convert wildcard pattern to regex if (strpos($pattern, '*') !== false) { // Escape special regex characters except * $regex_pattern = preg_quote($pattern, '/'); // Convert * to .* $regex_pattern = str_replace('\\*', '.*', $regex_pattern); // Add anchors $regex_pattern = '/^' . $regex_pattern . '$/i'; if (preg_match($regex_pattern, $option_name)) { return true; } } else { // Exact match (case insensitive) if (strcasecmp($pattern, $option_name) === 0) { return true; } } } return false; } /** * Check if a specific event type tracking is enabled. * * @param string $event_type The event type to check (user, post, etc.) * @return bool True if tracking is enabled, false otherwise */ public function is_tracking_enabled($event_type) { $option_name = 'actlogpro_track_' . $event_type . '_events'; return get_option($option_name, '1') === '1'; } /** * Refresh tracking settings cache. * This should be called when settings are saved. */ public function refresh_tracking_settings() { $event_types = array('user', 'post', 'media', 'comment', 'plugin', 'theme', 'widget', 'menu', 'option'); $settings = array(); foreach ($event_types as $type) { $settings[$type] = $this->is_tracking_enabled($type); } // Store settings in a transient for quick access set_transient('actlogpro_tracking_settings', $settings, HOUR_IN_SECONDS); return $settings; } /** * Get the current tracking settings. * * @return array Associative array of event types and their tracking status */ public function get_tracking_settings() { $settings = get_transient('actlogpro_tracking_settings'); if (false === $settings) { $settings = $this->refresh_tracking_settings(); } return $settings; } /** * Reset all tracking settings to their default values. * This can be used for troubleshooting. * * @return array The reset settings */ public function reset_tracking_settings() { $event_types = array('user', 'post', 'media', 'comment', 'plugin', 'theme', 'widget', 'menu', 'option'); $settings = array(); foreach ($event_types as $type) { $option_name = 'actlogpro_track_' . $type . '_events'; update_option($option_name, '1'); $settings[$type] = true; } // Update the cache set_transient('actlogpro_tracking_settings', $settings, HOUR_IN_SECONDS); return $settings; } public function log_product_attribute_created($attribute_id, $attribute) { // Rest of the method... } /** * Log WordPress core update event. * * @param WP_Upgrader $upgrader WP_Upgrader instance. * @param array $hook_extra Array of bulk item update data. */ public function wordpress_core_updated($upgrader, $hook_extra) { // Check if WordPress core event tracking is enabled if (!$this->is_tracking_enabled('wordpress')) { return; } // Check if this is a core update if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'core') { return; } // Get the previous version from our stored value $previous_version = get_option('actlogpro_wp_version_before_update', ''); // Get the NEW version - prioritize our stored target version $new_version = get_option('actlogpro_wp_target_version', ''); // If we don't have a stored target version, try other methods if (empty($new_version)) { // Method 1: Try to get target version from upgrader result if (is_object($upgrader) && isset($upgrader->result) && is_array($upgrader->result)) { if (isset($upgrader->result['source'])) { // Extract version from source path $source = $upgrader->result['source']; if (preg_match('/wordpress-([0-9.]+)/', $source, $matches)) { $new_version = $matches[1]; } } } } // Method 2: Try to get it from the update transient if (empty($new_version)) { $update = get_site_transient('update_core'); if (isset($update->updates[0]->version)) { $new_version = $update->updates[0]->version; } } // Method 3: As last resort, use the current WordPress version if (empty($new_version)) { global $wp_version; $new_version = $wp_version; } // Fallback if we don't have a previous version if (empty($previous_version)) { $previous_version = 'Unknown'; } // Defer cleanup so other hooks (e.g. premium alerts) can read stored versions. $this->schedule_wp_core_version_option_cleanup(); $is_reinstall = ( $previous_version !== 'Unknown' && $previous_version === $new_version ); // Log the WordPress core update ACTLOGPRO_Logger::log_activity( 'wordpress', 'core', 0, 'WordPress', 'updated', $is_reinstall ? sprintf( /* translators: %s: WordPress version */ __('WordPress Core Reinstalled: Successfully reinstalled version %s', 'activity-log-pro'), $new_version ) : sprintf( /* translators: %1$s: previous WordPress version, %2$s: new WordPress version */ __('WordPress Core Updated: Successfully updated from version %1$s to version %2$s', 'activity-log-pro'), $previous_version, $new_version ), array( 'from_version' => $previous_version, 'to_version' => $new_version, 'action' => isset($hook_extra['action']) ? $hook_extra['action'] : 'update', 'upgrader_class' => get_class($upgrader) ), 'high' ); } /** * Log WordPress core database upgrade event. * This fires after the WordPress database has been upgraded. * * @param int $wp_db_version The new $wp_db_version. * @param int $wp_current_db_version The old (current) $wp_db_version. */ public function wordpress_core_upgraded($wp_db_version, $wp_current_db_version) { // Get current WordPress version global $wp_version; // Get the previous WordPress version that we stored before the update $previous_version = get_option('actlogpro_previous_wp_version', ''); // If we don't have a previous version stored, this might be a database upgrade only if (empty($previous_version)) { $previous_version = $wp_version; // Use current version as fallback } ACTLOGPRO_Logger::log_activity( 'database', 'core', 0, 'WordPress', 'database_upgraded', sprintf( /* translators: %1$s: previous database version, %2$s: new database version, %3$s: WordPress version */ __('WordPress Database Upgraded: Database updated from version %1$s to %2$s (WordPress %3$s)', 'activity-log-pro'), $wp_current_db_version, $wp_db_version, $wp_version ), array( 'from_db_version' => $wp_current_db_version, 'to_db_version' => $wp_db_version, 'wp_version' => $wp_version, 'previous_wp_version' => $previous_version ), 'high' ); // Clean up the stored version if it exists delete_option('actlogpro_previous_wp_version'); delete_option('actlogpro_target_wp_version'); } /** * Log successful WordPress core update event. */ public function wordpress_core_updated_successfully() { // Check if WordPress core event tracking is enabled if (!$this->is_tracking_enabled('wordpress')) { return; } // Get the previous WordPress version that we stored before the update $previous_version = get_option('actlogpro_previous_wp_version', ''); // Get current WordPress version after update global $wp_version; $current_version = $wp_version; // Get the target version we stored in wordpress_core_updated $target_version = get_option('actlogpro_target_wp_version', ''); // If we don't have a previous version stored, try to get it from update information if (empty($previous_version)) { $update = get_site_transient('update_core'); if (isset($update->updates[0]->current)) { $previous_version = $update->updates[0]->current; } else { $previous_version = $current_version; // Fallback } } // If target version is empty, use current version as fallback if (empty($target_version)) { // Try to get the target version from the update information $update = get_site_transient('update_core'); if (isset($update->updates[0]->version)) { $target_version = $update->updates[0]->version; } else { // As a last resort, use the current version // But only if it's different from the previous version if ($current_version !== $previous_version) { $target_version = $current_version; } else { // If they're the same, try to determine a reasonable target version $version_parts = explode('.', $current_version); if (count($version_parts) >= 3) { // Increment the patch version $version_parts[2] = intval($version_parts[2]) + 1; $target_version = implode('.', $version_parts); } else { // Add .1 if we can't parse the version properly $target_version = $current_version . '.1'; } } } } // We don't want to reset target_version if it equals previous_version // as this would cause inconsistency with the update started message ACTLOGPRO_Logger::log_activity( 'wordpress', 'core', 0, 'WordPress', 'updated_successfully', sprintf( /* translators: %1$s: previous WordPress version, %2$s: new WordPress version */ __('WordPress Core Update Completed: Successfully updated from version %1$s to version %2$s', 'activity-log-pro'), $previous_version, $target_version ), array( 'from_version' => $previous_version, 'to_version' => $target_version ), 'high' ); // Clean up the stored versions delete_option('actlogpro_previous_wp_version'); delete_option('actlogpro_target_wp_version'); } /** * Log plugin deletion event via upgrader. * This method captures plugin information before deletion. * * @param WP_Upgrader $upgrader WP_Upgrader instance. * @param array $hook_extra Array of bulk item update data. */ public function plugin_deleted_via_upgrader($upgrader, $hook_extra) { // Check if plugin event tracking is enabled if (get_option('actlogpro_track_plugin_events', '1') !== '1') { return; } if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') { return; } // Check if this is a deletion action if (!isset($hook_extra['action']) || $hook_extra['action'] !== 'delete') { return; } // Get plugins list if (!isset($hook_extra['plugins']) || !is_array($hook_extra['plugins'])) { return; } if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all_plugins = get_plugins(); foreach ($hook_extra['plugins'] as $plugin) { $plugin_data = isset($all_plugins[$plugin]) ? $all_plugins[$plugin] : array(); $plugin_name = !empty($plugin_data['Name']) ? $plugin_data['Name'] : $plugin; $plugin_version = isset($plugin_data['Version']) ? $plugin_data['Version'] : ''; $plugin_metadata = $this->get_plugin_metadata_fields($plugin_data); $stored_plugin_data = $this->get_plugin_stored_data($plugin_data); // Check for transient to avoid duplicate logging $transient_name = 'actlogpro_deleted_plugin_' . sanitize_title($plugin); if (get_transient($transient_name)) { continue; } // Store plugin data in a transient for the deleted_plugin hook set_transient($transient_name, $stored_plugin_data, 60); // Log the deletion ACTLOGPRO_Logger::log_activity( 'plugin', 'plugin', 0, $plugin_name, 'deleted', sprintf( /* translators: %s: plugin name */ __('Plugin Deleted: %s', 'activity-log-pro'), $plugin_name ), array_merge( array( 'plugin_file' => $plugin, 'plugin_path' => plugins_url('', $plugin), 'plugin_version' => $plugin_version, 'object_status' => 'deleted', 'previous_status' => 'inactive', ), $plugin_metadata ), 'critical' ); } } /** * Safely capture WordPress version on admin pages. */ public function maybe_capture_wp_version() { // Check if WordPress core event tracking is enabled if (!$this->is_tracking_enabled('wordpress')) { return; } // Only capture on update-core.php page global $pagenow; if ($pagenow === 'update-core.php') { global $wp_version; $current_version = $wp_version; // Store the current version (this will be the old version when update happens) update_option('actlogpro_wp_version_before_update', $current_version); // Also capture the target version from update data $update = get_site_transient('update_core'); if (isset($update->updates[0]->version)) { $target_version = $update->updates[0]->version; update_option('actlogpro_wp_target_version', $target_version); } else { // Reinstall uses the same version as the running site. update_option('actlogpro_wp_target_version', $current_version); } } } /** * Capture WordPress version before AJAX core update. */ public function actlogpro_capture_wp_version_before_ajax_update() { // Check if WordPress core event tracking is enabled if (!$this->is_tracking_enabled('wordpress')) { return; } // Verify nonce first before processing any POST data if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'update-core')) { return; } // Verify this is a core update request if (isset($_POST['action']) && sanitize_text_field(wp_unslash($_POST['action'])) === 'update-core') { global $wp_version; $current_version = $wp_version; // Store the current version as the "before update" version update_option('actlogpro_wp_version_before_update', $current_version); // Also capture the target version from update data $update = get_site_transient('update_core'); if (isset($update->updates[0]->version)) { $target_version = $update->updates[0]->version; update_option('actlogpro_wp_target_version', $target_version); } else { update_option('actlogpro_wp_target_version', $current_version); } } } /** * Remove temporary core version options at the end of the request. */ public function cleanup_wp_core_version_capture_options() { delete_option('actlogpro_wp_version_before_update'); delete_option('actlogpro_wp_target_version'); } /** * Schedule one shutdown cleanup for temporary core version options. */ private function schedule_wp_core_version_option_cleanup() { if (!has_action('shutdown', array($this, 'cleanup_wp_core_version_capture_options'))) { add_action('shutdown', array($this, 'cleanup_wp_core_version_capture_options'), 999); } } }