using_index_permalinks() && $rss_feed_enabled) { add_rewrite_rule( 'activity-log-feed/([^/]+)/rss/?$', 'index.php?feed=activity-log&actlogpro_token=$matches[1]', 'top' ); } // If rules have changed, flush them if (get_option('actlogpro_feed_token_updated', false)) { flush_rewrite_rules(); update_option('actlogpro_feed_token_updated', false); } } /** * Generate a new RSS feed access token * * @return string The generated token */ public function generate_token() { $token = wp_generate_password(32, false, false); // 32-character alphanumeric token update_option('actlogpro_rss_feed_token', $token); update_option('actlogpro_feed_token_updated', true); // Clear WordPress object cache for these options if (function_exists('wp_cache_delete')) { wp_cache_delete('actlogpro_rss_feed_token', 'options'); wp_cache_delete('actlogpro_feed_token_updated', 'options'); } // Clear any plugin-specific cache groups if (function_exists('wp_cache_flush_group')) { wp_cache_flush_group('activity-log-pro'); } // Force cache plugins to clear relevant caches if (function_exists('wp_cache_flush')) { wp_cache_flush(); } return $token; } /** * Get the current RSS feed token * * @return string The current token or an empty string if not set */ public function get_token() { $token = get_option('actlogpro_rss_feed_token', ''); if (empty($token)) { $token = $this->generate_token(); } return $token; } /** * Get the secure RSS feed URL * * @return string The secure RSS feed URL */ public function get_secure_feed_url() { $token = $this->get_token(); // Check permalink structure if ($this->using_index_permalinks()) { return home_url('/?feed=activity-log&actlogpro_token=' . $token); } else { return home_url('activity-log-feed/' . $token . '/rss/'); } } /** * Check if WordPress is using index permalinks (not mod_rewrite) * * @return bool True if using index permalinks, false otherwise */ private function using_index_permalinks() { $permalink_structure = get_option('permalink_structure'); // If permalink structure is empty, we're using Plain permalinks (index.php) // If it starts with /index.php, we're using "Almost Pretty" permalinks return empty($permalink_structure) || strpos($permalink_structure, '/index.php') === 0; } /** * Handle permalink structure changes * * @param string $old_value The old permalink structure * @param string $new_value The new permalink structure */ public function on_permalink_structure_change($old_value, $new_value) { // Mark that we need to flush rewrite rules update_option('actlogpro_feed_token_updated', true); // Flush rewrite rules immediately to ensure feed URLs work with new structure flush_rewrite_rules(); } /** * Generate invalid token response for RSS feed */ private function generate_invalid_rss_response($message = '') { // Set default message if none provided if (empty($message)) { $message = __('Access Denied: Invalid or expired token', 'activity-log-pro'); } // Set XML content type header('Content-Type: application/xml; charset=UTF-8'); // Generate properly formatted RSS XML $xml = '' . "\n"; $xml .= '' . "\n"; $xml .= '' . "\n"; $xml .= '' . esc_html__('Activity Log Access Error', 'activity-log-pro') . '' . "\n"; $xml .= '' . esc_html($message) . '' . "\n"; $xml .= '' . esc_url(home_url()) . '' . "\n"; $xml .= '' . "\n"; $xml .= '' . esc_html__('Access Denied', 'activity-log-pro') . '' . "\n"; $xml .= '' . esc_html($message) . '

'; $xml .= '

' . esc_html__('If you believe this is an error, please contact your site administrator for assistance.', 'activity-log-pro') . '

'; $xml .= '

' . esc_html__('Additional Information:', 'activity-log-pro') . '

'; $xml .= '
    '; $xml .= '
  • ' . esc_html__('Ensure you are using the correct feed URL', 'activity-log-pro') . '
  • '; $xml .= '
  • ' . esc_html__('Verify that your access token is valid and has not expired', 'activity-log-pro') . '
  • '; $xml .= '
  • ' . esc_html__('Check if the feed feature is enabled in the Activity Log settings', 'activity-log-pro') . '
  • '; $xml .= '
'; $xml .= ']]>
' . "\n"; $xml .= '' . gmdate('r') . '' . "\n"; $xml .= '' . esc_url(home_url('?feed=activity-log&error=invalid_token')) . '' . "\n"; $xml .= '
' . "\n"; $xml .= '
' . "\n"; $xml .= '
'; // Output XML with allowed tags for RSS feed format $allowed_xml_tags = array( 'rss' => array('version' => array(), 'xmlns:atom' => array()), 'channel' => array(), 'title' => array(), 'description' => array(), 'link' => array(), 'item' => array(), 'pubDate' => array(), 'guid' => array('isPermaLink' => array()), 'p' => array(), 'ul' => array(), 'li' => array() ); echo wp_kses($xml, $allowed_xml_tags); die(); } /** * Validate an RSS feed token * * @param string $token The token to validate * @return bool Whether the token is valid */ private function validate_feed_token($token) { if (empty($token)) { return false; } $stored_token = $this->get_token(); return !empty($stored_token) && hash_equals($stored_token, $token); } /** * Check if RSS feed is enabled * * @return bool Whether the RSS feed is enabled */ private function is_feed_enabled() { return get_option('actlogpro_enable_rss_feed', '0') === '1'; } /** * Register RSS feed */ public function register_rss_feed() { // Register the feed with WordPress regardless of enabled status // We'll check the status when generating the feed add_feed('activity-log', array($this, 'generate_rss_feed')); } /** * Generate RSS feed */ public function generate_rss_feed() { // First, check if RSS feed is enabled at all $rss_feed_enabled = get_option('actlogpro_enable_rss_feed', '0') === '1'; if (!$rss_feed_enabled) { $this->generate_invalid_rss_response(__('The RSS feed feature is currently disabled. Please contact your site administrator.', 'activity-log-pro')); return; } // Get the token from the request $token = get_query_var('actlogpro_token', ''); // Determine access method and validate accordingly if (!empty($token)) { // Token-based access - token must be valid if (!$this->validate_feed_token($token)) { $this->generate_invalid_rss_response(__('Access Denied: The provided token is invalid or has expired. Please contact your site administrator for a new token.', 'activity-log-pro')); return; } } else { // Direct feed access without token - only allow for logged-in admins if (!is_user_logged_in() || !current_user_can('manage_options')) { $this->generate_invalid_rss_response(__('Access Denied: A valid access token is required to view this feed. Please contact your site administrator for the secure feed URL.', 'activity-log-pro')); return; } } // Include the database class require_once plugin_dir_path(__FILE__) . 'class-alp-database.php'; // Get logs using the database class with caching (limit to 50 items) $logs = ACTLOGPRO_Database::get_feed_logs(50, 0); // Set the content type header('Content-Type: application/rss+xml; charset=' . get_option('blog_charset'), true); // Start output buffering ob_start(); // Output the RSS header // Use esc_attr to properly escape the blog charset echo ''; ?> <?php echo esc_html(get_bloginfo('name') . ' - ' . __('Activity Log', 'activity-log-pro')); ?> format_log_for_display($log); ?> <?php echo esc_html($formatted_log['event_type'] . ': ' . $formatted_log['action']); ?> ]]> is_feed_enabled()) { $this->generate_invalid_rss_response(__('The RSS feed feature is currently disabled. Please contact your site administrator.', 'activity-log-pro')); return; } // Get token from query var $token = get_query_var('actlogpro_token', ''); // Validate token if (!$this->validate_feed_token($token)) { $this->generate_invalid_rss_response(__('Access Denied: The provided token is invalid or has expired. Please contact your site administrator for a new token.', 'activity-log-pro')); return; } // Generate the feed $this->generate_rss_feed(); } /** * Format log data for display */ private function format_log_for_display($log) { // Format user data $user_data = []; if (!empty($log['user_id']) && $log['user_id'] != 0) { // Regular user - get data from WordPress user system $user = get_userdata($log['user_id']); if ($user) { $user_data = [ 'id' => $user->ID, 'login' => $user->user_login, 'display_name' => $user->display_name ]; } } elseif ($log['user_id'] == 0 || !empty($log['user_login'])) { // System user or user with stored login data - use stored data from log $user_data = [ 'id' => $log['user_id'], 'login' => $log['user_login'] ?? 'system', 'display_name' => $log['user_display_name'] ?? 'System' ]; } // Format object data $object_data = []; if (!empty($log['object_type'])) { $object_data = [ 'type' => $log['object_type'], 'name' => $log['object_name'] ]; } $severity = isset( $log['severity'] ) && ACTLOGPRO_Severity::is_valid( $log['severity'] ) ? $log['severity'] : ACTLOGPRO_Severity::LEVEL_DEFAULT; return [ 'id' => $log['id'], 'created_at' => $log['created_at'], 'user_id' => $log['user_id'], 'user' => $user_data, 'ip_address' => $log['ip_address'], 'event_type' => $log['event_type'], 'object' => $object_data, 'action' => $log['action'], 'description' => $log['description'], 'severity' => $severity, 'severity_label' => ACTLOGPRO_Severity::get_label( $severity ), ]; } /** * Register fallback JSON routes when premium is not active */ public function register_json_fallback_routes() { // Check if premium is active by looking for the actlogpro_Feed_JSON class if (class_exists('ACTLOGPRO_Feed_JSON')) { // Premium is active, don't register fallback routes return; } // Register fallback routes that return proper error messages // Main logs endpoint - requires admin access for security register_rest_route('activity-log-pro/v1', '/logs', array( 'methods' => 'GET', 'callback' => array($this, 'json_fallback_response'), 'permission_callback' => function() { return current_user_can('manage_options'); // Admin required: Activity logs contain sensitive data }, 'show_in_index' => false )); // Token-based endpoint - validate token format and check if feature is enabled register_rest_route('activity-log-pro/v1', '/logs/token/(?P[^/]+)', array( 'methods' => 'GET', 'callback' => array($this, 'json_fallback_response'), 'permission_callback' => array($this, 'check_fallback_token_permission'), 'show_in_index' => false )); } /** * Check permissions for token-based fallback endpoint * Even though this is a fallback that returns errors, we should validate the request format */ public function check_fallback_token_permission($request) { // Validate that token parameter exists and has reasonable format $token = $request->get_param('token'); if (empty($token) || !is_string($token) || strlen($token) < 10) { return new WP_Error( 'invalid_token_format', __('Invalid token format provided.', 'activity-log-pro'), array('status' => 400) ); } // Allow the request to proceed (it will get the fallback error response) return true; } /** * Return proper error message for JSON feed requests when premium is not active */ public function json_fallback_response($request) { return new WP_Error( 'invalid_token', __('Access Denied: The provided token is invalid or has expired. Please contact your site administrator for a new token.', 'activity-log-pro'), array('status' => 403) ); } }