10) { $bulk_data = array( 'start_time' => $current_time, 'widget_count' => 1, 'widgets' => array($widget_id) ); } else { // Continue the current bulk operation $bulk_data['widget_count']++; $bulk_data['widgets'][] = $widget_id; // If this is not the first widget in the bulk operation, skip logging if ($bulk_data['widget_count'] > 1) { set_transient($bulk_key, $bulk_data, 15); return true; // Skip this individual log } } set_transient($bulk_key, $bulk_data, 15); } else { // Start a new bulk operation $bulk_data = array( 'start_time' => $current_time, 'widget_count' => 1, 'widgets' => array($widget_id) ); set_transient($bulk_key, $bulk_data, 15); } // For the first widget in a bulk operation, check individual duplicate prevention $transient_key = 'actlogpro_widget_log_' . $widget_id . '_' . $action; $logged = get_transient($transient_key); if ($logged) { return true; } // Set transient to prevent duplicate logging (expires in 30 seconds for bulk operations) set_transient($transient_key, true, 30); // If this is the first widget in a potential bulk operation, schedule a bulk summary if ($bulk_data['widget_count'] === 1) { // Schedule a delayed action to log bulk summary if more widgets follow wp_schedule_single_event(time() + 3, 'actlogpro_log_widget_bulk_summary', array($user_id, $current_time)); } return false; } else { // For non-update actions, use simple duplicate prevention $transient_key = 'actlogpro_widget_log_' . $widget_id . '_' . $action; $logged = get_transient($transient_key); if ($logged) { return true; } // Set transient to prevent duplicate logging (expires in 5 seconds) set_transient($transient_key, true, 5); return false; } } /** * Get widget name from widget object or ID. * * @since 1.0.0 * @param string $widget_id Widget ID. * @return string Widget name or fallback to widget ID. */ private function get_widget_name($widget_id) { $widget_name = $widget_id; // Try to get widget name from registered widgets if (isset($GLOBALS['wp_registered_widgets'][$widget_id])) { $widget_obj = $GLOBALS['wp_registered_widgets'][$widget_id]; if (isset($widget_obj['callback'][0]) && is_object($widget_obj['callback'][0])) { if (property_exists($widget_obj['callback'][0], 'name')) { $widget_name = $widget_obj['callback'][0]->name; } } elseif (isset($widget_obj['name'])) { $widget_name = $widget_obj['name']; } } return $widget_name; } /** * Get sidebar name from sidebar ID. * * @since 1.0.0 * @param string $sidebar_id Sidebar ID. * @return string Sidebar name or fallback to sidebar ID. */ private function get_sidebar_name($sidebar_id) { return isset($GLOBALS['wp_registered_sidebars'][$sidebar_id]['name']) ? $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] : $sidebar_id; } /** * Capture sidebars state before widget operations. * * @since 1.0.0 */ public function capture_sidebars_before() { if (!$this->is_tracking_enabled()) { return; } $this->sidebars_before = get_option('sidebars_widgets', array()); } /** * Log widget update event. * * @since 1.0.0 * @param array $instance Widget instance. * @param array $new_instance New widget instance. * @param array $old_instance Old widget instance. * @param WP_Widget $widget Widget object. * @return array Widget instance. */ public function widget_updated($instance, $new_instance, $old_instance, $widget) { if (!$this->is_tracking_enabled() || !$instance) { return $instance; } $widget_name = $widget->name; $widget_id = $widget->id; // Check for duplicate logs (includes bulk operation detection) if ($this->is_duplicate_log($widget_id, 'updated')) { return $instance; } // Get current bulk operation status $user_id = get_current_user_id(); $bulk_key = 'actlogpro_widget_bulk_update_' . $user_id; $bulk_data = get_transient($bulk_key); if ($bulk_data && $bulk_data['widget_count'] === 1) { // This is the first widget in a potential bulk operation // Log individual widget update (will be supplemented by bulk summary if more widgets follow) ACTLOGPRO_Logger::log_activity( 'widget', 'widget', $widget_id, $widget_name, 'updated', sprintf( /* translators: %s: widget name */ __('Widget "%s" was updated', 'activity-log-pro'), $widget_name ), array( 'widget_id' => $widget_id, 'widget_type' => get_class($widget), 'old_instance' => $old_instance, 'new_instance' => $new_instance, 'operation_type' => 'individual_or_first_in_bulk', 'object_status' => 'active', 'previous_status' => 'configured' ) ); } else { // This should not happen with our new logic, but fallback ACTLOGPRO_Logger::log_activity( 'widget', 'widget', $widget_id, $widget_name, 'updated', sprintf( /* translators: %s: widget name */ __('Widget "%s" was updated', 'activity-log-pro'), $widget_name ), array( 'widget_id' => $widget_id, 'widget_type' => get_class($widget), 'old_instance' => $old_instance, 'new_instance' => $new_instance, 'object_status' => 'active', 'previous_status' => 'configured' ) ); } return $instance; } /** * Track widget save operations (add/remove). * * @since 1.0.0 */ public function actlogpro_track_widget_save() { if (!$this->is_tracking_enabled()) { return; } // Check for valid nonce and user capabilities if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'save-sidebar-widgets')) { return; } if (!current_user_can('edit_theme_options')) { return; } // Get widget and sidebar information $widget_id = isset($_POST['widget-id']) ? sanitize_text_field(wp_unslash($_POST['widget-id'])) : ''; $sidebar_id = isset($_POST['sidebar']) ? sanitize_text_field(wp_unslash($_POST['sidebar'])) : ''; if (!$widget_id || !$sidebar_id) { return; } $widget_name = $this->get_widget_name($widget_id); $sidebar_name = $this->get_sidebar_name($sidebar_id); // Check if widget is being added or removed $is_widget_added = isset($_POST['add_new']) && sanitize_text_field(wp_unslash($_POST['add_new'])) === 'multi'; $is_widget_removed = isset($_POST['delete_widget']) && intval(sanitize_text_field(wp_unslash($_POST['delete_widget']))) === 1; if ($is_widget_added) { // Check for duplicate logs if (!$this->is_duplicate_log($widget_id, 'added')) { // Log widget addition ACTLOGPRO_Logger::log_activity( 'widget', 'widget', $widget_id, $widget_name, 'added', sprintf( /* translators: %1$s: widget name, %2$s: sidebar name */ __('Widget "%1$s" was added to sidebar "%2$s"', 'activity-log-pro'), $widget_name, $sidebar_name ), array( 'widget_id' => $widget_id, 'sidebar_id' => $sidebar_id, 'sidebar_name' => $sidebar_name, 'object_status' => 'active', 'previous_status' => 'not_added' ) ); } } if ($is_widget_removed) { // Check for duplicate logs if (!$this->is_duplicate_log($widget_id, 'removed')) { // Log widget removal ACTLOGPRO_Logger::log_activity( 'widget', 'widget', $widget_id, $widget_name, 'removed', sprintf( /* translators: %1$s: widget name, %2$s: sidebar name */ __('Widget "%1$s" was removed from sidebar "%2$s"', 'activity-log-pro'), $widget_name, $sidebar_name ), array( 'widget_id' => $widget_id, 'sidebar_id' => $sidebar_id, 'sidebar_name' => $sidebar_name, 'object_status' => 'removed', 'previous_status' => 'active' ) ); } } } /** * Track widget deletion via AJAX. * * @since 1.0.0 */ public function actlogpro_track_widget_deletion() { if (!$this->is_tracking_enabled()) { return; } // Check for valid nonce and user capabilities if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'save-sidebar-widgets')) { return; } if (!current_user_can('edit_theme_options')) { return; } // Get widget information $widget_id = isset($_POST['widget-id']) ? sanitize_text_field(wp_unslash($_POST['widget-id'])) : ''; if (!$widget_id) { return; } // Find which sidebar this widget was in $sidebars = get_option('sidebars_widgets', array()); $sidebar_id = ''; $sidebar_name = ''; foreach ($sidebars as $sid => $widgets) { if (is_array($widgets) && in_array($widget_id, $widgets)) { $sidebar_id = $sid; $sidebar_name = $this->get_sidebar_name($sid); break; } } $widget_name = $this->get_widget_name($widget_id); // Check for duplicate logs if (!$this->is_duplicate_log($widget_id, 'deleted')) { // Log widget deletion ACTLOGPRO_Logger::log_activity( 'widget', 'widget', $widget_id, $widget_name, 'deleted', sprintf( /* translators: %1$s: widget name, %2$s: sidebar name */ __('Widget "%1$s" was deleted from sidebar "%2$s"', 'activity-log-pro'), $widget_name, $sidebar_name ), array( 'widget_id' => $widget_id, 'sidebar_id' => $sidebar_id, 'sidebar_name' => $sidebar_name, 'object_status' => 'deleted', 'previous_status' => 'active' ) ); } } /** * Track widget order changes. * * @since 1.0.0 */ public function actlogpro_track_widget_order_changes() { if (!$this->is_tracking_enabled()) { return; } // Check for valid nonce and user capabilities if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'save-sidebar-widgets')) { return; } if (!current_user_can('edit_theme_options')) { return; } // Get the sidebars data with proper sanitization // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with map_deep below $sidebars_raw = isset($_POST['sidebars']) ? wp_unslash($_POST['sidebars']) : array(); $sidebars = array(); // Validate that we have an array if (!is_array($sidebars_raw)) { return; } // Sanitize the multidimensional array properly using map_deep $sidebars = map_deep($sidebars_raw, 'sanitize_text_field'); // Compare with previous state to detect changes if (empty($this->sidebars_before)) { return; } foreach ($sidebars as $sidebar_id => $widgets) { $sidebar_name = $this->get_sidebar_name($sidebar_id); // Check if order changed for this sidebar $old_widgets = isset($this->sidebars_before[$sidebar_id]) ? $this->sidebars_before[$sidebar_id] : array(); // Only log if widgets are actually different (order changed) if (array_values($old_widgets) !== array_values($widgets)) { // Check for duplicate logs if (!$this->is_duplicate_log($sidebar_id, 'reordered')) { // Log the order change ACTLOGPRO_Logger::log_activity( 'widget', 'sidebar', $sidebar_id, $sidebar_name, 'reordered', sprintf( /* translators: %s: sidebar name */ __('Widgets in sidebar "%s" were reordered', 'activity-log-pro'), $sidebar_name ), array( 'sidebar_id' => $sidebar_id, 'old_order' => $old_widgets, 'new_order' => $widgets ) ); } } } } /** * Log widget bulk summary. * * @since 1.0.0 * @param int $user_id User ID. * @param int $start_time Start time of the bulk operation. */ public function log_widget_bulk_summary($user_id, $start_time) { if (!$this->is_tracking_enabled()) { return; } // Get the bulk operation data $bulk_key = 'actlogpro_widget_bulk_update_' . $user_id; $bulk_data = get_transient($bulk_key); if (!$bulk_data || $bulk_data['widget_count'] <= 1 || $bulk_data['start_time'] !== $start_time) { // No bulk operation or only one widget, nothing to summarize return; } $widget_count = $bulk_data['widget_count']; $widgets = $bulk_data['widgets']; // Create a summary log entry for the bulk operation ACTLOGPRO_Logger::log_activity( 'widget', 'bulk', 0, 'Multiple Widgets', 'bulk_updated', sprintf( /* translators: %d: number of widgets */ __('%d widgets were updated together', 'activity-log-pro'), $widget_count ), array( 'widget_count' => $widget_count, 'widgets' => $widgets, 'operation_type' => 'bulk_update' ) ); // Clean up the transient delete_transient($bulk_key); } }