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);
}
}
}