logged_events[ $event_hash ] ) ) { return; // Already logged this exact event in this request } $this->logged_events[ $event_hash ] = true; $table_name_activity = $wpdb->prefix . 'user_id_activity'; // Skip logging if table doesn't exist yet (e.g., initial activation issues) // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name_activity ) ) !== $table_name_activity ) { return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $table_name_activity, array( 'user_id' => $user_id, 'event_type' => sanitize_text_field( $event_type ), 'event_details' => sanitize_text_field( wp_unslash( $event_details ) ), ), array( '%d', '%s', '%s' ) ); } // --- Hook Handlers --- public function log_login( $user_login, $user ) { // Distinguish from impersonation: If currently impersonating during wp_login hook? // Note wp_login usually fires for standard logins, but wp_set_auth_cookie also handles impersonation. // Actually, our impersonation logic clears cookies, sets them, but doesn't call `wp_login` natively, handled via standard flows if required. // So this is isolated standard login correctly. $this->log_activity( $user->ID, 'login', 'User logged in' ); } public function log_logout() { $user_id = get_current_user_id(); if ( $user_id ) { $this->log_activity( $user_id, 'logout', 'User logged out' ); } } public function log_profile_update( $user_id, $old_userdata ) { $this->log_activity( $user_id, 'profile_update', 'User profile was updated' ); } public function log_set_user_role( $user_id, $role, $old_roles ) { $this->log_activity( $user_id, 'role_change', sprintf( 'Role changed from %s to %s', implode( ',', $old_roles ), $role ) ); } public function log_password_reset( $user ) { $this->log_activity( $user->ID, 'password_reset', 'User password was reset' ); } /** * Deletes activity logs older than 30 days. */ public function cleanup_old_logs() { global $wpdb; $table_name_activity = $wpdb->prefix . 'user_id_activity'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name_activity ) ) === $table_name_activity ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query( "DELETE FROM " . $table_name_activity . " WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)" ); } } // --- Comprehensive Hook Handlers --- public function log_post_transition( $new_status, $old_status, $post ) { $user_id = get_current_user_id(); if ( ! $user_id || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || 'revision' === $post->post_type ) { return; } if ( $new_status === $old_status ) { if ( 'publish' === $new_status ) { $this->log_activity( $user_id, 'post_updated', sprintf( 'Updated %s: "%s" (ID: %d)', $post->post_type, $post->post_title, $post->ID ) ); } return; } if ( 'publish' === $new_status && 'publish' !== $old_status ) { $this->log_activity( $user_id, 'post_published', sprintf( 'Published %s: "%s" (ID: %d)', $post->post_type, $post->post_title, $post->ID ) ); } elseif ( 'trash' === $new_status ) { $this->log_activity( $user_id, 'post_trashed', sprintf( 'Trashed %s: "%s" (ID: %d)', $post->post_type, $post->post_title, $post->ID ) ); } elseif ( 'trash' === $old_status && 'publish' === $new_status ) { $this->log_activity( $user_id, 'post_restored', sprintf( 'Restored %s: "%s" (ID: %d)', $post->post_type, $post->post_title, $post->ID ) ); } } public function log_post_deleted( $post_id, $post ) { $user_id = get_current_user_id(); if ( ! $user_id || 'revision' === $post->post_type ) { return; } $this->log_activity( $user_id, 'post_deleted', sprintf( 'Permanently deleted %s: "%s" (ID: %d)', $post->post_type, $post->post_title, $post_id ) ); } public function log_attachment_added( $post_id ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; $title = get_the_title( $post_id ); $this->log_activity( $user_id, 'attachment_uploaded', sprintf( 'Uploaded media: "%s" (ID: %d)', $title, $post_id ) ); } public function log_attachment_deleted( $post_id ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; $post = get_post( $post_id ); $title = $post ? $post->post_title : 'Unknown'; $this->log_activity( $user_id, 'attachment_deleted', sprintf( 'Deleted media: "%s" (ID: %d)', $title, $post_id ) ); } public function log_plugin_activated( $plugin, $network_wide ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; $this->log_activity( $user_id, 'plugin_activated', sprintf( 'Activated plugin: %s%s', $plugin, $network_wide ? ' (Network wide)' : '' ) ); } public function log_plugin_deactivated( $plugin, $network_wide ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; $this->log_activity( $user_id, 'plugin_deactivated', sprintf( 'Deactivated plugin: %s%s', $plugin, $network_wide ? ' (Network wide)' : '' ) ); } public function log_theme_switched( $new_name, $new_theme, $old_theme ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; $this->log_activity( $user_id, 'theme_switched', sprintf( 'Switched theme to: %s', $new_name ) ); } public function log_upgrader_process( $upgrader_object, $options ) { $user_id = get_current_user_id(); if ( ! $user_id || empty( $options['action'] ) || empty( $options['type'] ) ) return; $action = $options['action']; $type = $options['type']; $details = sprintf( 'Performed %s on %s', $action, $type ); if ( isset( $options['plugins'] ) && is_array( $options['plugins'] ) ) { $details .= ': ' . implode( ', ', $options['plugins'] ); } elseif ( isset( $options['plugin'] ) ) { $details .= ': ' . $options['plugin']; } elseif ( isset( $options['themes'] ) && is_array( $options['themes'] ) ) { $details .= ': ' . implode( ', ', $options['themes'] ); } elseif ( isset( $options['theme'] ) ) { $details .= ': ' . $options['theme']; } $this->log_activity( $user_id, 'system_update', $details ); } public function log_option_updated( $option, $old_value, $value ) { $user_id = get_current_user_id(); if ( ! $user_id ) return; // Filter out noisy, temporary, or plugin-internal options if ( strpos( $option, '_transient' ) === 0 || strpos( $option, '_site_transient' ) === 0 || strpos( $option, 'cron' ) === 0 || strpos( $option, 'auto_updater' ) === 0 || strpos( $option, 'recently_edited' ) === 0 || strpos( $option, 'wdp_un_' ) === 0 || $option === 'rewrite_rules' || $option === 'user_id_db_version' || $option === 'active_plugins' || $option === 'recently_activated' || $option === 'uninstall_plugins' ) { return; } $this->log_activity( $user_id, 'setting_updated', sprintf( 'Updated setting: %s', $option ) ); } // --- UI Display --- /** * Render timeline pane for user. * * @param int $user_id The user ID. */ public function render_activity_log_pane( $user_id ) { global $wpdb; $table_name_activity = $wpdb->prefix . 'user_id_activity'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name_activity ) ) !== $table_name_activity ) { return; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $activities = $wpdb->get_results( $wpdb->prepare( " SELECT * FROM " . $table_name_activity . " WHERE user_id = %d ORDER BY created_at DESC LIMIT 50 ", $user_id ) ); // phpcs:enable ?>