diff --git a/wp-content/plugins/wp-debug/assets/css/debug-panel.css b/wp-content/plugins/wp-debug/assets/css/debug-panel.css new file mode 100644 index 00000000..d9dcc8dc --- /dev/null +++ b/wp-content/plugins/wp-debug/assets/css/debug-panel.css @@ -0,0 +1,457 @@ +/** + * WP Debug Frontend Panel Styles + * + * @package WP_Debug + * @since 1.0.0 + */ + +/* Toggle Button */ +.wp-debug-toggle { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 99998; + background: #2271b1; + color: #fff; + border: none; + border-radius: 4px; + padding: 10px 15px; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; +} + +.wp-debug-toggle:hover { + background: #135e96; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.wp-debug-toggle .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +.wp-debug-toggle .label { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +/* Main Panel */ +.wp-debug-panel { + position: fixed; + bottom: 80px; + right: 20px; + width: 800px; + max-width: calc(100vw - 40px); + max-height: calc(100vh - 120px); + background: #fff; + border-radius: 8px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); + z-index: 99999; + overflow: hidden; + display: flex; + flex-direction: column; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +/* Panel Header */ +.wp-debug-panel-header { + background: #2271b1; + color: #fff; + padding: 15px 20px; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + border-bottom: 3px solid #135e96; +} + +.wp-debug-panel-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #fff; +} + +/* Tabs */ +.wp-debug-panel-tabs { + display: flex; + gap: 5px; + margin: 0 auto; +} + +.wp-debug-tab { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + border: none; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; + font-size: 13px; + transition: all 0.2s ease; +} + +.wp-debug-tab:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +.wp-debug-tab.active { + background: #fff; + color: #2271b1; + font-weight: 600; +} + +/* Close Button */ +.wp-debug-panel-close { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border: none; + width: 32px; + height: 32px; + border-radius: 4px; + cursor: pointer; + font-size: 24px; + line-height: 1; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.wp-debug-panel-close:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Panel Content */ +.wp-debug-panel-content { + padding: 20px; + overflow-y: auto; + flex: 1; + background: #f9f9f9; +} + +.wp-debug-tab-content { + display: none; +} + +.wp-debug-tab-content.active { + display: block; +} + +/* Typography */ +.wp-debug-panel-content h4 { + margin: 0 0 15px 0; + font-size: 16px; + font-weight: 600; + color: #1d2327; + border-bottom: 2px solid #2271b1; + padding-bottom: 8px; +} + +.wp-debug-panel-content h5 { + margin: 20px 0 10px 0; + font-size: 14px; + font-weight: 600; + color: #1d2327; +} + +.wp-debug-panel-content p { + margin: 10px 0; + color: #50575e; + line-height: 1.6; +} + +/* Tables */ +.wp-debug-table { + width: 100%; + border-collapse: collapse; + background: #fff; + border-radius: 4px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin: 10px 0 20px 0; +} + +.wp-debug-table thead { + background: #f0f0f1; +} + +.wp-debug-table th { + padding: 12px; + text-align: left; + font-weight: 600; + font-size: 13px; + color: #1d2327; + border-bottom: 2px solid #dcdcde; +} + +.wp-debug-table td { + padding: 10px 12px; + border-bottom: 1px solid #f0f0f1; + font-size: 13px; + color: #50575e; +} + +.wp-debug-table tr:last-child td { + border-bottom: none; +} + +.wp-debug-table tr:hover { + background: #f9f9f9; +} + +.wp-debug-table code { + background: #f0f0f1; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; + color: #d63638; + font-family: "Courier New", Courier, monospace; +} + +/* Lists */ +.wp-debug-list { + list-style: none; + padding: 0; + margin: 10px 0; +} + +.wp-debug-list li { + background: #fff; + padding: 10px 12px; + margin-bottom: 5px; + border-radius: 4px; + border-left: 3px solid #2271b1; + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.wp-debug-list li.missing { + border-left-color: #d63638; +} + +.wp-debug-list li.duplicate { + border-left-color: #dba617; +} + +.wp-debug-list code { + background: #f0f0f1; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; + font-family: "Courier New", Courier, monospace; +} + +/* Module List */ +.wp-debug-module-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; +} + +.wp-debug-module-list li { + background: #fff; + padding: 10px; + border-radius: 4px; + font-size: 13px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +/* Status Badges */ +.status-active { + display: inline-block; + padding: 2px 8px; + background: #00a32a; + color: #fff; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.status-inactive { + display: inline-block; + padding: 2px 8px; + background: #dcdcde; + color: #50575e; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.status-error { + display: inline-block; + padding: 2px 8px; + background: #d63638; + color: #fff; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.template-type { + display: inline-block; + padding: 2px 8px; + background: #f0f0f1; + color: #2271b1; + border-radius: 3px; + font-size: 11px; + font-weight: 600; +} + +/* Query Items */ +.wp-debug-queries { + display: flex; + flex-direction: column; + gap: 10px; + margin: 10px 0; +} + +.query-item { + background: #fff; + padding: 12px; + border-radius: 4px; + border-left: 3px solid #2271b1; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.query-item.n-plus-one { + border-left-color: #d63638; +} + +.query-time { + display: inline-block; + padding: 2px 6px; + background: #f0f0f1; + color: #d63638; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; +} + +.query-count { + display: inline-block; + padding: 2px 6px; + background: #d63638; + color: #fff; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; + margin-right: 5px; +} + +.query-sql { + display: block; + background: #f9f9f9; + padding: 8px; + border-radius: 3px; + font-size: 12px; + color: #1d2327; + font-family: "Courier New", Courier, monospace; + word-break: break-all; +} + +.count { + background: #dba617; + color: #fff; + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; +} + +/* Scrollbar */ +.wp-debug-panel-content::-webkit-scrollbar { + width: 8px; +} + +.wp-debug-panel-content::-webkit-scrollbar-track { + background: #f0f0f1; +} + +.wp-debug-panel-content::-webkit-scrollbar-thumb { + background: #c3c4c7; + border-radius: 4px; +} + +.wp-debug-panel-content::-webkit-scrollbar-thumb:hover { + background: #a7aaad; +} + +/* Responsive */ +@media (max-width: 768px) { + .wp-debug-panel { + width: calc(100vw - 40px); + bottom: 80px; + right: 20px; + max-height: calc(100vh - 120px); + } + + .wp-debug-panel-header { + flex-direction: column; + gap: 10px; + align-items: flex-start; + } + + .wp-debug-panel-tabs { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .wp-debug-tab { + white-space: nowrap; + } + + .wp-debug-panel-close { + position: absolute; + top: 15px; + right: 15px; + } + + .wp-debug-module-list { + grid-template-columns: 1fr; + } + + .wp-debug-table { + font-size: 12px; + } + + .wp-debug-table th, + .wp-debug-table td { + padding: 8px; + } +} + +/* Animation */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.wp-debug-panel[data-visible="true"] { + animation: slideUp 0.3s ease; +} diff --git a/wp-content/plugins/wp-debug/assets/js/debug-panel.js b/wp-content/plugins/wp-debug/assets/js/debug-panel.js new file mode 100644 index 00000000..e96fc96b --- /dev/null +++ b/wp-content/plugins/wp-debug/assets/js/debug-panel.js @@ -0,0 +1,229 @@ +/** + * WP Debug Frontend Panel JavaScript + * + * @package WP_Debug + * @since 1.0.0 + */ + +(function($) { + 'use strict'; + + /** + * WP Debug Panel + */ + const WPDebugPanel = { + + /** + * Initialize + */ + init: function() { + this.toggleButton = $('#wp-debug-toggle'); + this.panel = $('#wp-debug-panel'); + this.closeButton = $('.wp-debug-panel-close'); + this.tabs = $('.wp-debug-tab'); + + // Restore panel state from localStorage + this.restoreState(); + + // Bind events + this.bindEvents(); + + // Log initialization + if (window.console && console.log) { + console.log('[WP Debug] Panel initialized'); + } + }, + + /** + * Bind events + */ + bindEvents: function() { + const self = this; + + // Toggle button click + this.toggleButton.on('click', function(e) { + e.preventDefault(); + self.togglePanel(); + }); + + // Close button click + this.closeButton.on('click', function(e) { + e.preventDefault(); + self.hidePanel(); + }); + + // Tab clicks + this.tabs.on('click', function(e) { + e.preventDefault(); + const tabName = $(this).data('tab'); + self.switchTab(tabName); + }); + + // Press Escape to close + $(document).on('keydown', function(e) { + if (e.key === 'Escape' && self.panel.is(':visible')) { + self.hidePanel(); + } + }); + + // Click outside to close + $(document).on('click', function(e) { + if (self.panel.is(':visible') && + !self.panel.is(e.target) && + self.panel.has(e.target).length === 0 && + !self.toggleButton.is(e.target) && + self.toggleButton.has(e.target).length === 0) { + self.hidePanel(); + } + }); + }, + + /** + * Toggle panel visibility + */ + togglePanel: function() { + if (this.panel.is(':visible')) { + this.hidePanel(); + } else { + this.showPanel(); + } + }, + + /** + * Show panel + */ + showPanel: function() { + this.panel.attr('data-visible', 'true').fadeIn(300); + this.saveState('visible', true); + + // Log + if (window.console && console.log) { + console.log('[WP Debug] Panel opened'); + } + }, + + /** + * Hide panel + */ + hidePanel: function() { + this.panel.fadeOut(300, function() { + $(this).attr('data-visible', 'false'); + }); + this.saveState('visible', false); + + // Log + if (window.console && console.log) { + console.log('[WP Debug] Panel closed'); + } + }, + + /** + * Switch tab + */ + switchTab: function(tabName) { + // Update tab buttons + this.tabs.removeClass('active'); + this.tabs.filter('[data-tab="' + tabName + '"]').addClass('active'); + + // Update tab content + $('.wp-debug-tab-content').removeClass('active'); + $('.wp-debug-tab-content[data-tab="' + tabName + '"]').addClass('active'); + + // Save active tab + this.saveState('activeTab', tabName); + + // Log + if (window.console && console.log) { + console.log('[WP Debug] Switched to tab: ' + tabName); + } + }, + + /** + * Save state to localStorage + */ + saveState: function(key, value) { + if (typeof Storage !== 'undefined') { + try { + localStorage.setItem('wpDebugPanel_' + key, JSON.stringify(value)); + } catch (e) { + // Ignore storage errors + } + } + }, + + /** + * Get state from localStorage + */ + getState: function(key, defaultValue) { + if (typeof Storage !== 'undefined') { + try { + const value = localStorage.getItem('wpDebugPanel_' + key); + return value !== null ? JSON.parse(value) : defaultValue; + } catch (e) { + return defaultValue; + } + } + return defaultValue; + }, + + /** + * Restore panel state + */ + restoreState: function() { + // Restore visibility + const isVisible = this.getState('visible', false); + if (isVisible) { + this.panel.show().attr('data-visible', 'true'); + } + + // Restore active tab + const activeTab = this.getState('activeTab', 'overview'); + this.switchTab(activeTab); + }, + + /** + * Refresh panel data (for future AJAX updates) + */ + refresh: function() { + if (window.console && console.log) { + console.log('[WP Debug] Refreshing panel data...'); + } + + // This could be extended to fetch fresh data via AJAX + // For now, it just logs + if (typeof wpDebugData !== 'undefined' && wpDebugData.ajaxUrl) { + $.ajax({ + url: wpDebugData.ajaxUrl, + type: 'POST', + data: { + action: 'wp_debug_refresh', + nonce: wpDebugData.nonce + }, + success: function(response) { + if (window.console && console.log) { + console.log('[WP Debug] Panel data refreshed', response); + } + }, + error: function(xhr, status, error) { + if (window.console && console.error) { + console.error('[WP Debug] Refresh failed:', error); + } + } + }); + } + } + }; + + /** + * Initialize on document ready + */ + $(document).ready(function() { + WPDebugPanel.init(); + }); + + /** + * Expose to window for external access + */ + window.WPDebugPanel = WPDebugPanel; + +})(jQuery); diff --git a/wp-content/plugins/wp-debug/inc/activation.php b/wp-content/plugins/wp-debug/inc/activation.php new file mode 100644 index 00000000..3b96e5ba --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/activation.php @@ -0,0 +1,145 @@ +1000ms + + // Frontend panel settings + add_option('wp_debug_panel_enabled', true); + add_option('wp_debug_panel_position', 'bottom-right'); // bottom-right, bottom-left, top-right, top-left + + // Query analyzer settings + add_option('wp_debug_query_threshold', 100); // Log queries taking >100ms + add_option('wp_debug_detect_n_plus_one', true); + } + + /** + * Create logs directory + */ + private static function create_logs_directory() { + $logs_dir = WP_CONTENT_DIR . '/logs/wp-debug'; + + if (!file_exists($logs_dir)) { + wp_mkdir_p($logs_dir); + + // Create .htaccess to protect logs + $htaccess_file = $logs_dir . '/.htaccess'; + $htaccess_content = "Order deny,allow\nDeny from all"; + file_put_contents($htaccess_file, $htaccess_content); + + // Create index.php to prevent directory listing + $index_file = $logs_dir . '/index.php'; + $index_content = "get_charset_collate(); + + // Logs table + $table_logs = $wpdb->prefix . 'wp_debug_logs'; + $sql_logs = "CREATE TABLE IF NOT EXISTS $table_logs ( + id bigint(20) NOT NULL AUTO_INCREMENT, + timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + level varchar(20) DEFAULT 'INFO' NOT NULL, + message text NOT NULL, + context text, + user_id bigint(20), + url varchar(255), + ip_address varchar(45), + PRIMARY KEY (id), + KEY level (level), + KEY timestamp (timestamp), + KEY user_id (user_id) + ) $charset_collate;"; + + // Hooks table + $table_hooks = $wpdb->prefix . 'wp_debug_hooks'; + $sql_hooks = "CREATE TABLE IF NOT EXISTS $table_hooks ( + id bigint(20) NOT NULL AUTO_INCREMENT, + timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + hook_name varchar(255) NOT NULL, + hook_type varchar(20) NOT NULL, + execution_time float, + args_count int, + PRIMARY KEY (id), + KEY hook_name (hook_name), + KEY timestamp (timestamp), + KEY hook_type (hook_type) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql_logs); + dbDelta($sql_hooks); + } +} diff --git a/wp-content/plugins/wp-debug/inc/admin-menu.php b/wp-content/plugins/wp-debug/inc/admin-menu.php new file mode 100644 index 00000000..47d931f7 --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/admin-menu.php @@ -0,0 +1,224 @@ + +
+

+ +
+

+

+
+ +
+ +
+

+

+ + ✓ Activo' : '✗ Inactivo'; ?> +

+

+ + +

+

+ + +

+

+ + get('Name')); ?> +

+
+ + +
+

+ $module_data) { + $enabled = get_option('wp_debug_module_' . $module_name, $module_data['enabled']); + $status_class = $enabled ? 'status-active' : 'status-inactive'; + $status_text = $enabled ? '✓ Activo' : '✗ Inactivo'; + echo '

' . esc_html(ucwords(str_replace('_', ' ', $module_name))) . ': ' . esc_html($status_text) . '

'; + } + ?> +
+ + + + + +
+

+ wp debug status
+ wp debug hooks
+ wp debug templates
+ wp debug export
+ wp debug clear +

+
+
+ +
+

+

+ +
+
+ + + array(), + 'scripts' => array() + ); + + /** + * Initialize the asset tracker + * + * @return void + */ + public static function init() { + // Hook into wp_enqueue_scripts with high priority to catch all assets + add_action('wp_enqueue_scripts', array(__CLASS__, 'track_assets'), PHP_INT_MAX); + + // Hook into admin_enqueue_scripts for admin assets + add_action('admin_enqueue_scripts', array(__CLASS__, 'track_assets'), PHP_INT_MAX); + + // Record assets on shutdown + add_action('shutdown', array(__CLASS__, 'record_assets'), 1); + + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug('Asset Tracker initialized'); + } + } + + /** + * Track enqueued assets + * + * @return void + */ + public static function track_assets() { + global $wp_styles, $wp_scripts; + + // Track styles + if (isset($wp_styles) && is_object($wp_styles)) { + self::track_styles($wp_styles); + } + + // Track scripts + if (isset($wp_scripts) && is_object($wp_scripts)) { + self::track_scripts($wp_scripts); + } + } + + /** + * Track stylesheet assets + * + * @param WP_Styles $wp_styles WordPress styles object + * @return void + */ + private static function track_styles($wp_styles) { + if (empty($wp_styles->queue)) { + return; + } + + foreach ($wp_styles->queue as $handle) { + if (!isset($wp_styles->registered[$handle])) { + continue; + } + + $style = $wp_styles->registered[$handle]; + + $asset_data = array( + 'handle' => $handle, + 'src' => $style->src, + 'dependencies' => $style->deps, + 'version' => $style->ver, + 'media' => $style->args, + 'type' => 'style', + 'size' => self::get_file_size($style->src), + 'local' => self::is_local_asset($style->src), + 'has_version' => !empty($style->ver), + 'extra' => isset($style->extra) ? $style->extra : array() + ); + + self::$assets_enqueued['styles'][$handle] = $asset_data; + } + } + + /** + * Track script assets + * + * @param WP_Scripts $wp_scripts WordPress scripts object + * @return void + */ + private static function track_scripts($wp_scripts) { + if (empty($wp_scripts->queue)) { + return; + } + + foreach ($wp_scripts->queue as $handle) { + if (!isset($wp_scripts->registered[$handle])) { + continue; + } + + $script = $wp_scripts->registered[$handle]; + + $asset_data = array( + 'handle' => $handle, + 'src' => $script->src, + 'dependencies' => $script->deps, + 'version' => $script->ver, + 'in_footer' => isset($script->extra['group']) && $script->extra['group'] === 1, + 'type' => 'script', + 'size' => self::get_file_size($script->src), + 'local' => self::is_local_asset($script->src), + 'has_version' => !empty($script->ver), + 'extra' => isset($script->extra) ? $script->extra : array() + ); + + self::$assets_enqueued['scripts'][$handle] = $asset_data; + } + } + + /** + * Check if asset is local + * + * @param string $src Asset source URL + * @return bool + */ + private static function is_local_asset($src) { + if (empty($src)) { + return false; + } + + $home_url = home_url(); + $site_url = site_url(); + + // Check if it starts with home or site URL + if (strpos($src, $home_url) === 0 || strpos($src, $site_url) === 0) { + return true; + } + + // Check if it starts with relative path + if (strpos($src, '/') === 0 && strpos($src, '//') !== 0) { + return true; + } + + return false; + } + + /** + * Get file size for local assets + * + * @param string $src Asset source URL + * @return int|null File size in bytes or null if not available + */ + private static function get_file_size($src) { + if (empty($src) || !self::is_local_asset($src)) { + return null; + } + + // Convert URL to file path + $file_path = self::url_to_path($src); + + if (!$file_path || !file_exists($file_path)) { + return null; + } + + $size = @filesize($file_path); + return $size !== false ? $size : null; + } + + /** + * Convert asset URL to file system path + * + * @param string $src Asset source URL + * @return string|false File path or false if not convertible + */ + private static function url_to_path($src) { + // Remove query string + $src = strtok($src, '?'); + + // Handle absolute URLs + $home_url = home_url(); + $site_url = site_url(); + + if (strpos($src, $home_url) === 0) { + $relative_path = str_replace($home_url, '', $src); + return ABSPATH . ltrim($relative_path, '/'); + } + + if (strpos($src, $site_url) === 0) { + $relative_path = str_replace($site_url, '', $src); + return ABSPATH . ltrim($relative_path, '/'); + } + + // Handle relative URLs + if (strpos($src, '/') === 0 && strpos($src, '//') !== 0) { + return ABSPATH . ltrim($src, '/'); + } + + return false; + } + + /** + * Get all tracked assets + * + * @return array All tracked assets + */ + public static function get_assets() { + return self::$assets_enqueued; + } + + /** + * Detect duplicate assets (same src, different handles) + * + * @return array Detected duplicates + */ + public static function get_duplicates() { + $duplicates = array(); + $src_map = array(); + + // Check styles + foreach (self::$assets_enqueued['styles'] as $handle => $asset) { + if (empty($asset['src'])) { + continue; + } + + // Normalize source (remove query strings for comparison) + $normalized_src = strtok($asset['src'], '?'); + + if (!isset($src_map['styles'][$normalized_src])) { + $src_map['styles'][$normalized_src] = array(); + } + + $src_map['styles'][$normalized_src][] = $handle; + } + + // Check scripts + foreach (self::$assets_enqueued['scripts'] as $handle => $asset) { + if (empty($asset['src'])) { + continue; + } + + // Normalize source (remove query strings for comparison) + $normalized_src = strtok($asset['src'], '?'); + + if (!isset($src_map['scripts'][$normalized_src])) { + $src_map['scripts'][$normalized_src] = array(); + } + + $src_map['scripts'][$normalized_src][] = $handle; + } + + // Find duplicates + foreach ($src_map as $type => $sources) { + foreach ($sources as $src => $handles) { + if (count($handles) > 1) { + $duplicates[] = array( + 'type' => $type, + 'src' => $src, + 'handles' => $handles, + 'count' => count($handles) + ); + } + } + } + + return $duplicates; + } + + /** + * Get statistics about tracked assets + * + * @return array Asset statistics + */ + public static function get_statistics() { + $total_styles = count(self::$assets_enqueued['styles']); + $total_scripts = count(self::$assets_enqueued['scripts']); + $total_size = 0; + $local_count = 0; + $external_count = 0; + + // Calculate statistics for styles + foreach (self::$assets_enqueued['styles'] as $asset) { + if ($asset['size'] !== null) { + $total_size += $asset['size']; + } + if ($asset['local']) { + $local_count++; + } else { + $external_count++; + } + } + + // Calculate statistics for scripts + foreach (self::$assets_enqueued['scripts'] as $asset) { + if ($asset['size'] !== null) { + $total_size += $asset['size']; + } + if ($asset['local']) { + $local_count++; + } else { + $external_count++; + } + } + + return array( + 'total_assets' => $total_styles + $total_scripts, + 'total_styles' => $total_styles, + 'total_scripts' => $total_scripts, + 'total_size' => $total_size, + 'total_size_formatted' => size_format($total_size), + 'local_assets' => $local_count, + 'external_assets' => $external_count, + 'duplicates_count' => count(self::get_duplicates()), + ); + } + + /** + * Record assets and save to transient + * + * @return void + */ + public static function record_assets() { + if (empty(self::$assets_enqueued['styles']) && empty(self::$assets_enqueued['scripts'])) { + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug('No assets to record'); + } + return; + } + + $assets_data = array( + 'assets' => self::$assets_enqueued, + 'duplicates' => self::get_duplicates(), + 'statistics' => self::get_statistics(), + 'timestamp' => current_time('timestamp'), + ); + + // Save to transient (expires in 1 hour) + set_transient('wp_debug_assets_data', $assets_data, HOUR_IN_SECONDS); + + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug('Asset Tracker recorded assets', array( + 'total_styles' => count(self::$assets_enqueued['styles']), + 'total_scripts' => count(self::$assets_enqueued['scripts']), + 'duplicates' => count($assets_data['duplicates']), + )); + } + } +} diff --git a/wp-content/plugins/wp-debug/inc/autoloader.php b/wp-content/plugins/wp-debug/inc/autoloader.php new file mode 100644 index 00000000..0e7b024e --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/autoloader.php @@ -0,0 +1,154 @@ + array( + 'file' => 'logger.php', + 'class' => 'WP_Debug_Logger', + 'enabled' => true, + ), + 'hook_monitor' => array( + 'file' => 'hook-monitor.php', + 'class' => 'WP_Debug_Hook_Monitor', + 'enabled' => true, + ), + 'template_tracer' => array( + 'file' => 'template-tracer.php', + 'class' => 'WP_Debug_Template_Tracer', + 'enabled' => true, + ), + 'asset_tracker' => array( + 'file' => 'asset-tracker.php', + 'class' => 'WP_Debug_Asset_Tracker', + 'enabled' => true, + ), + 'profiler' => array( + 'file' => 'profiler.php', + 'class' => 'WP_Debug_Profiler', + 'enabled' => true, + ), + 'query_analyzer' => array( + 'file' => 'query-analyzer.php', + 'class' => 'WP_Debug_Query_Analyzer', + 'enabled' => true, + ), + 'frontend_panel' => array( + 'file' => 'frontend-panel.php', + 'class' => 'WP_Debug_Frontend_Panel', + 'enabled' => true, + ), + 'cli_commands' => array( + 'file' => 'cli-commands.php', + 'class' => 'WP_Debug_CLI_Commands', + 'enabled' => true, + ), + ); + + /** + * Initialize autoloader + */ + public static function init() { + // Check if debug is enabled globally + $debug_enabled = get_option('wp_debug_enabled', true); + + if (!$debug_enabled) { + return; + } + + // Load each module + foreach (self::$modules as $module_name => $module_data) { + self::load_module($module_name, $module_data); + } + } + + /** + * Load a single module + * + * @param string $module_name Module name + * @param array $module_data Module data + */ + private static function load_module($module_name, $module_data) { + // Check if module is enabled + $module_enabled = get_option('wp_debug_module_' . $module_name, $module_data['enabled']); + + if (!$module_enabled) { + return; + } + + // Build file path + $file_path = WP_DEBUG_PLUGIN_DIR . 'inc/' . $module_data['file']; + + // Check if file exists + if (!file_exists($file_path)) { + if (WP_DEBUG) { + error_log(sprintf('WP Debug: Module file not found: %s', $file_path)); + } + return; + } + + // Load the file + require_once $file_path; + + // Initialize the class if it has an init method + if (class_exists($module_data['class']) && method_exists($module_data['class'], 'init')) { + call_user_func(array($module_data['class'], 'init')); + } + } + + /** + * Get list of available modules + * + * @return array List of modules + */ + public static function get_modules() { + return self::$modules; + } + + /** + * Enable a module + * + * @param string $module_name Module name + * @return bool True on success + */ + public static function enable_module($module_name) { + if (!isset(self::$modules[$module_name])) { + return false; + } + + return update_option('wp_debug_module_' . $module_name, true); + } + + /** + * Disable a module + * + * @param string $module_name Module name + * @return bool True on success + */ + public static function disable_module($module_name) { + if (!isset(self::$modules[$module_name])) { + return false; + } + + return update_option('wp_debug_module_' . $module_name, false); + } +} diff --git a/wp-content/plugins/wp-debug/inc/cli-commands.php b/wp-content/plugins/wp-debug/inc/cli-commands.php new file mode 100644 index 00000000..9e06e155 --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/cli-commands.php @@ -0,0 +1,543 @@ + $module_data) { + $module_enabled = get_option('wp_debug_module_' . $module_name, $module_data['enabled']); + $class_exists = class_exists($module_data['class']); + + $items[] = array( + 'module' => $module_name, + 'status' => $module_enabled ? 'enabled' : 'disabled', + 'loaded' => $class_exists ? 'yes' : 'no', + 'class' => $module_data['class'], + ); + } + + WP_CLI\Utils\format_items('table', $items, array('module', 'status', 'loaded', 'class')); + + // Database info + global $wpdb; + $table = $wpdb->prefix . 'wp_debug_logs'; + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); + + WP_CLI::log(''); + WP_CLI::log('Database Logs: ' . number_format($count) . ' entries'); + + // File logs + $logs_dir = WP_CONTENT_DIR . '/logs/wp-debug'; + if (file_exists($logs_dir)) { + $files = glob($logs_dir . '/debug-*.log'); + WP_CLI::log('Log Files: ' . count($files)); + } + + WP_CLI::success('Status check complete'); + } + + /** + * List hooks executed + * + * ## OPTIONS + * + * [--search=] + * : Filter hooks by pattern + * + * [--limit=] + * : Limit number of results + * --- + * default: 50 + * --- + * + * [--format=] + * : Output format + * --- + * default: table + * options: + * - table + * - json + * - csv + * --- + * + * ## EXAMPLES + * + * wp debug hooks + * wp debug hooks --search=init + * wp debug hooks --limit=100 --format=json + * + * @when after_wp_load + */ + public function hooks($args, $assoc_args) { + if (!class_exists('WP_Debug_Hook_Monitor')) { + WP_CLI::error('Hook Monitor module is not loaded'); + return; + } + + $search = isset($assoc_args['search']) ? $assoc_args['search'] : ''; + $limit = isset($assoc_args['limit']) ? intval($assoc_args['limit']) : 50; + $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table'; + + // Get hooks from transient or current execution + $transient_data = get_transient('wp_debug_hooks_data'); + + if (!$transient_data) { + WP_CLI::warning('No hook data available. Run a page request first.'); + return; + } + + $hooks = $transient_data['hooks']; + + // Filter by search + if ($search) { + $hooks = array_filter($hooks, function($hook) use ($search) { + return strpos($hook['hook'], $search) !== false; + }); + WP_CLI::log('Filtered by: ' . $search); + } + + // Limit results + if ($limit > 0) { + $hooks = array_slice($hooks, 0, $limit); + } + + if (empty($hooks)) { + WP_CLI::warning('No hooks found'); + return; + } + + // Prepare items for display + $items = array(); + foreach ($hooks as $hook) { + $items[] = array( + 'hook' => $hook['hook'], + 'type' => $hook['type'], + 'caller' => isset($hook['caller']) ? $hook['caller'] : 'unknown', + 'args' => isset($hook['args_count']) ? $hook['args_count'] : 0, + ); + } + + WP_CLI\Utils\format_items($format, $items, array('hook', 'type', 'caller', 'args')); + + WP_CLI::log(''); + WP_CLI::log('Total hooks: ' . $transient_data['total']); + WP_CLI::log('Unique hooks: ' . $transient_data['unique']); + WP_CLI::success('Retrieved ' . count($items) . ' hooks'); + } + + /** + * List templates loaded + * + * ## OPTIONS + * + * [--format=] + * : Output format + * --- + * default: table + * options: + * - table + * - json + * - csv + * --- + * + * [--parts] + * : Show template parts instead of main templates + * + * [--missing] + * : Show only missing template parts + * + * ## EXAMPLES + * + * wp debug templates + * wp debug templates --parts + * wp debug templates --missing + * + * @when after_wp_load + */ + public function templates($args, $assoc_args) { + if (!class_exists('WP_Debug_Template_Tracer')) { + WP_CLI::error('Template Tracer module is not loaded'); + return; + } + + $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table'; + $show_parts = isset($assoc_args['parts']); + $show_missing = isset($assoc_args['missing']); + + // Get templates from transient + $transient_data = get_transient('wp_debug_templates_data'); + + if (!$transient_data) { + WP_CLI::warning('No template data available. Run a page request first.'); + return; + } + + if ($show_missing) { + // Show missing template parts + $items = array(); + foreach ($transient_data['missing'] as $part) { + $items[] = array( + 'slug' => $part['slug'], + 'name' => $part['name'] ? $part['name'] : 'N/A', + 'file' => $part['file'], + ); + } + + if (empty($items)) { + WP_CLI::success('No missing templates found'); + return; + } + + WP_CLI\Utils\format_items($format, $items, array('slug', 'name', 'file')); + WP_CLI::warning('Found ' . count($items) . ' missing templates'); + + } elseif ($show_parts) { + // Show template parts + $items = array(); + foreach ($transient_data['parts'] as $part) { + $items[] = array( + 'slug' => $part['slug'], + 'name' => $part['name'] ? $part['name'] : 'N/A', + 'file' => $part['file'], + 'found' => $part['found'] ? 'yes' : 'no', + ); + } + + if (empty($items)) { + WP_CLI::warning('No template parts found'); + return; + } + + WP_CLI\Utils\format_items($format, $items, array('slug', 'name', 'file', 'found')); + WP_CLI::success('Retrieved ' . count($items) . ' template parts'); + + } else { + // Show main templates + $items = array(); + foreach ($transient_data['templates'] as $template) { + $items[] = array( + 'name' => $template['name'], + 'type' => $template['type'], + 'path' => $template['path'], + ); + } + + if (empty($items)) { + WP_CLI::warning('No templates found'); + return; + } + + WP_CLI\Utils\format_items($format, $items, array('name', 'type', 'path')); + WP_CLI::success('Retrieved ' . count($items) . ' templates'); + } + } + + /** + * Show performance metrics + * + * ## OPTIONS + * + * [--format=] + * : Output format + * --- + * default: table + * options: + * - table + * - json + * --- + * + * ## EXAMPLES + * + * wp debug performance + * wp debug performance --format=json + * + * @when after_wp_load + */ + public function performance($args, $assoc_args) { + if (!class_exists('WP_Debug_Profiler')) { + WP_CLI::error('Profiler module is not loaded'); + return; + } + + $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table'; + + // Get performance report + $report = WP_Debug_Profiler::get_performance_report(); + + if (isset($report['error'])) { + WP_CLI::warning($report['error']); + return; + } + + if ($format === 'json') { + WP_CLI::log(json_encode($report, JSON_PRETTY_PRINT)); + return; + } + + WP_CLI::log('Performance Metrics'); + WP_CLI::log('==================='); + WP_CLI::log(''); + + // Display formatted metrics + WP_CLI::log('Execution Time: ' . $report['formatted']['execution_time']); + WP_CLI::log('Memory Current: ' . $report['formatted']['memory_current']); + WP_CLI::log('Memory Peak: ' . $report['formatted']['memory_peak']); + if ($report['formatted']['server_load']) { + WP_CLI::log('Server Load: ' . $report['formatted']['server_load']); + } + WP_CLI::log(''); + + // Display timers if available + if (!empty($report['metrics']['timers'])) { + WP_CLI::log('Function Timers:'); + WP_CLI::log('----------------'); + + $items = array(); + foreach ($report['metrics']['timers'] as $name => $timer) { + $items[] = array( + 'name' => $name, + 'time' => round($timer['elapsed'] * 1000, 2) . 'ms', + 'memory' => WP_Debug_Profiler::format_bytes($timer['memory_used']), + ); + } + + WP_CLI\Utils\format_items('table', $items, array('name', 'time', 'memory')); + WP_CLI::log(''); + } + + // Display suggestions + if (!empty($report['suggestions'])) { + WP_CLI::log('Suggestions:'); + WP_CLI::log('------------'); + foreach ($report['suggestions'] as $suggestion) { + WP_CLI::warning($suggestion); + } + } else { + WP_CLI::success('Performance looks good!'); + } + } + + /** + * Export complete debug report in JSON + * + * ## OPTIONS + * + * [] + * : Output file path (optional, defaults to stdout) + * + * ## EXAMPLES + * + * wp debug export + * wp debug export report.json + * + * @when after_wp_load + */ + public function export($args, $assoc_args) { + WP_CLI::log('Generating debug report...'); + + // Collect all data + $report = array( + 'timestamp' => current_time('mysql'), + 'site_url' => get_site_url(), + 'wp_version' => get_bloginfo('version'), + 'plugin_version' => WP_DEBUG_VERSION, + ); + + // Add hooks data + if (class_exists('WP_Debug_Hook_Monitor')) { + $hooks_data = get_transient('wp_debug_hooks_data'); + if ($hooks_data) { + $report['hooks'] = array( + 'total' => $hooks_data['total'], + 'unique' => $hooks_data['unique'], + 'top_10' => array_slice($hooks_data['counts'], 0, 10, true), + ); + } + } + + // Add templates data + if (class_exists('WP_Debug_Template_Tracer')) { + $templates_data = get_transient('wp_debug_templates_data'); + if ($templates_data) { + $report['templates'] = array( + 'templates' => $templates_data['templates'], + 'parts_count' => count($templates_data['parts']), + 'missing_count' => count($templates_data['missing']), + ); + } + } + + // Add performance data + if (class_exists('WP_Debug_Profiler')) { + $perf_report = WP_Debug_Profiler::get_performance_report(); + if (!isset($perf_report['error'])) { + $report['performance'] = $perf_report; + } + } + + // Add recent logs + if (class_exists('WP_Debug_Logger')) { + $logs = WP_Debug_Logger::get_logs(array('limit' => 50)); + $report['recent_logs'] = $logs; + } + + // Add module status + $modules = WP_Debug_Autoloader::get_modules(); + $modules_status = array(); + foreach ($modules as $module_name => $module_data) { + $modules_status[$module_name] = array( + 'enabled' => get_option('wp_debug_module_' . $module_name, $module_data['enabled']), + 'loaded' => class_exists($module_data['class']), + ); + } + $report['modules'] = $modules_status; + + // Convert to JSON + $json = json_encode($report, JSON_PRETTY_PRINT); + + // Output to file or stdout + if (isset($args[0])) { + $file = $args[0]; + $result = file_put_contents($file, $json); + + if ($result === false) { + WP_CLI::error('Failed to write to file: ' . $file); + } else { + WP_CLI::success('Report exported to: ' . $file); + } + } else { + WP_CLI::log($json); + WP_CLI::success('Report generated successfully'); + } + } + + /** + * Clear old logs + * + * ## OPTIONS + * + * [--days=] + * : Number of days to keep logs + * --- + * default: 7 + * --- + * + * [--yes] + * : Skip confirmation + * + * ## EXAMPLES + * + * wp debug clear + * wp debug clear --days=30 + * wp debug clear --yes + * + * @when after_wp_load + */ + public function clear($args, $assoc_args) { + if (!class_exists('WP_Debug_Logger')) { + WP_CLI::error('Logger module is not loaded'); + return; + } + + $days = isset($assoc_args['days']) ? intval($assoc_args['days']) : 7; + $yes = isset($assoc_args['yes']); + + // Count logs to be deleted + global $wpdb; + $table = $wpdb->prefix . 'wp_debug_logs'; + $date = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE timestamp < %s", $date)); + + // Count log files to be deleted + $logs_dir = WP_CONTENT_DIR . '/logs/wp-debug'; + $files_count = 0; + if (file_exists($logs_dir)) { + $files = glob($logs_dir . '/debug-*.log'); + $cutoff = strtotime("-{$days} days"); + foreach ($files as $file) { + if (filemtime($file) < $cutoff) { + $files_count++; + } + } + } + + WP_CLI::log("Logs older than {$days} days:"); + WP_CLI::log(" Database entries: {$count}"); + WP_CLI::log(" Log files: {$files_count}"); + + if ($count == 0 && $files_count == 0) { + WP_CLI::success('No old logs to clear'); + return; + } + + // Ask for confirmation + if (!$yes) { + WP_CLI::confirm('Are you sure you want to delete these logs?'); + } + + // Clear logs + WP_CLI::log('Clearing logs...'); + WP_Debug_Logger::clear_old_logs($days); + + WP_CLI::success("Cleared {$count} database entries and {$files_count} log files"); + } +} diff --git a/wp-content/plugins/wp-debug/inc/deactivation.php b/wp-content/plugins/wp-debug/inc/deactivation.php new file mode 100644 index 00000000..26c0b37b --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/deactivation.php @@ -0,0 +1,47 @@ + WP Debug if desired + } + + /** + * Clean up temporary transients + */ + private static function cleanup_transients() { + // Delete temporary transients only + delete_transient('wp_debug_temp_data'); + delete_transient('wp_debug_cache'); + + // Clean up expired transients + global $wpdb; + $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_wp_debug_%' AND option_value < UNIX_TIMESTAMP()"); + $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_wp_debug_%' AND option_name NOT IN (SELECT CONCAT('_transient_', SUBSTRING(option_name, 19)) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_wp_debug_%')"); + } +} diff --git a/wp-content/plugins/wp-debug/inc/frontend-panel.php b/wp-content/plugins/wp-debug/inc/frontend-panel.php new file mode 100644 index 00000000..fb492753 --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/frontend-panel.php @@ -0,0 +1,373 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wp_debug_panel'), + )); + } + + /** + * Render panel + */ + public static function render_panel() { + // Load data from transients + $metrics = get_transient('wp_debug_last_metrics'); + $hooks_data = get_transient('wp_debug_hooks_data'); + $templates_data = get_transient('wp_debug_templates_data'); + $assets_data = get_transient('wp_debug_assets_data'); + $queries_data = get_transient('wp_debug_queries_data'); + + ?> + + + + + $hook, + 'type' => current_filter() === $hook ? 'action' : 'filter', + 'timestamp' => microtime(true), + 'caller' => $caller, + 'args_count' => func_num_args() - 1, + ); + + self::$hooks_executed[] = $hook_info; + + // Count executions + if (!isset(self::$hook_counts[$hook])) { + self::$hook_counts[$hook] = 0; + } + self::$hook_counts[$hook]++; + } + + /** + * Get executed hooks + * + * @param array $args Query arguments + * @return array Hooks + */ + public static function get_hooks($args = array()) { + $defaults = array( + 'search' => '', + 'limit' => 100, + ); + + $args = wp_parse_args($args, $defaults); + + $hooks = self::$hooks_executed; + + // Filter by search + if ($args['search']) { + $hooks = array_filter($hooks, function($hook) use ($args) { + return strpos($hook['hook'], $args['search']) !== false; + }); + } + + // Limit results + if ($args['limit'] > 0) { + $hooks = array_slice($hooks, 0, $args['limit']); + } + + return $hooks; + } + + /** + * Get hook counts + * + * @return array Hook counts + */ + public static function get_hook_counts() { + // Sort by count descending + arsort(self::$hook_counts); + return self::$hook_counts; + } + + /** + * Get hooks called multiple times + * + * @param int $threshold Minimum count + * @return array Hooks + */ + public static function get_frequent_hooks($threshold = 10) { + return array_filter(self::$hook_counts, function($count) use ($threshold) { + return $count >= $threshold; + }); + } + + /** + * Record hooks on shutdown + */ + public static function record_hooks() { + $total_hooks = count(self::$hooks_executed); + $unique_hooks = count(self::$hook_counts); + + // Log summary + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug("Hooks executed", array( + 'total' => $total_hooks, + 'unique' => $unique_hooks, + 'top_5' => array_slice(self::$hook_counts, 0, 5, true), + )); + } + + // Store for frontend panel + set_transient('wp_debug_hooks_data', array( + 'total' => $total_hooks, + 'unique' => $unique_hooks, + 'hooks' => self::$hooks_executed, + 'counts' => self::$hook_counts, + ), 3600); + } + + /** + * Check if hook was executed + * + * @param string $hook Hook name + * @return bool True if executed + */ + public static function was_hook_executed($hook) { + return isset(self::$hook_counts[$hook]); + } + + /** + * Get hook execution time + * + * @param string $hook Hook name + * @return float|null Execution time in seconds + */ + public static function get_hook_execution_time($hook) { + foreach (self::$hooks_executed as $hook_info) { + if ($hook_info['hook'] === $hook && isset($hook_info['execution_time'])) { + return $hook_info['execution_time']; + } + } + return null; + } +} diff --git a/wp-content/plugins/wp-debug/inc/logger.php b/wp-content/plugins/wp-debug/inc/logger.php new file mode 100644 index 00000000..5c26c0e4 --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/logger.php @@ -0,0 +1,335 @@ + 0, + self::LEVEL_INFO => 1, + self::LEVEL_WARNING => 2, + self::LEVEL_ERROR => 3, + ); + + return $levels[$level] >= $levels[$min_level]; + } + + /** + * Prepare log entry with all context + * + * @param string $message Message + * @param string $level Level + * @param array $context Context + * @return array Log entry + */ + private static function prepare_log_entry($message, $level, $context) { + $user = wp_get_current_user(); + + return array( + 'timestamp' => current_time('mysql'), + 'level' => $level, + 'message' => $message, + 'context' => !empty($context) ? json_encode($context) : null, + 'user_id' => $user->ID, + 'url' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw($_SERVER['REQUEST_URI']) : '', + 'ip_address' => self::get_client_ip(), + 'backtrace' => self::get_backtrace(), + ); + } + + /** + * Log to file + * + * @param array $log_entry Log entry + * @return bool True on success + */ + private static function log_to_file($log_entry) { + $logs_dir = WP_CONTENT_DIR . '/logs/wp-debug'; + $log_file = $logs_dir . '/debug-' . date('Y-m-d') . '.log'; + + // Ensure directory exists + if (!file_exists($logs_dir)) { + wp_mkdir_p($logs_dir); + } + + // Prepare log line + $log_line = sprintf( + "[%s] [%s] %s | User: %s | URL: %s | IP: %s\n", + $log_entry['timestamp'], + $log_entry['level'], + $log_entry['message'], + $log_entry['user_id'], + $log_entry['url'], + $log_entry['ip_address'] + ); + + // Add context if exists + if ($log_entry['context']) { + $log_line .= sprintf(" Context: %s\n", $log_entry['context']); + } + + // Add backtrace if exists + if ($log_entry['backtrace']) { + $log_line .= sprintf(" Backtrace: %s\n", $log_entry['backtrace']); + } + + // Write to file + return file_put_contents($log_file, $log_line, FILE_APPEND | LOCK_EX) !== false; + } + + /** + * Log to database + * + * @param array $log_entry Log entry + * @return bool True on success + */ + private static function log_to_database($log_entry) { + global $wpdb; + + $table = $wpdb->prefix . 'wp_debug_logs'; + + return $wpdb->insert( + $table, + array( + 'timestamp' => $log_entry['timestamp'], + 'level' => $log_entry['level'], + 'message' => $log_entry['message'], + 'context' => $log_entry['context'], + 'user_id' => $log_entry['user_id'], + 'url' => $log_entry['url'], + 'ip_address' => $log_entry['ip_address'], + ), + array('%s', '%s', '%s', '%s', '%d', '%s', '%s') + ) !== false; + } + + /** + * Get client IP address + * + * @return string IP address + */ + private static function get_client_ip() { + $ip = ''; + + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + + return sanitize_text_field($ip); + } + + /** + * Get backtrace + * + * @return string Backtrace string + */ + private static function get_backtrace() { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5); + $trace_string = ''; + + foreach ($backtrace as $index => $trace) { + if (isset($trace['file']) && isset($trace['line'])) { + $trace_string .= sprintf( + "%s:%s", + basename($trace['file']), + $trace['line'] + ); + if ($index < count($backtrace) - 1) { + $trace_string .= ' -> '; + } + } + } + + return $trace_string; + } + + /** + * Get logs from database + * + * @param array $args Query arguments + * @return array Logs + */ + public static function get_logs($args = array()) { + global $wpdb; + + $defaults = array( + 'level' => null, + 'limit' => 100, + 'offset' => 0, + 'orderby' => 'timestamp', + 'order' => 'DESC', + ); + + $args = wp_parse_args($args, $defaults); + + $table = $wpdb->prefix . 'wp_debug_logs'; + $where = '1=1'; + + if ($args['level']) { + $where .= $wpdb->prepare(' AND level = %s', $args['level']); + } + + $query = $wpdb->prepare( + "SELECT * FROM {$table} WHERE {$where} ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", + $args['limit'], + $args['offset'] + ); + + return $wpdb->get_results($query); + } + + /** + * Clear old logs + * + * @param int $days Days to keep + * @return bool True on success + */ + public static function clear_old_logs($days = null) { + if ($days === null) { + $days = get_option('wp_debug_log_retention_days', 7); + } + + // Clear database logs + global $wpdb; + $table = $wpdb->prefix . 'wp_debug_logs'; + $date = date('Y-m-d H:i:s', strtotime("-{$days} days")); + + $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE timestamp < %s", $date)); + + // Clear file logs + $logs_dir = WP_CONTENT_DIR . '/logs/wp-debug'; + if (file_exists($logs_dir)) { + $files = glob($logs_dir . '/debug-*.log'); + $cutoff = strtotime("-{$days} days"); + + foreach ($files as $file) { + if (filemtime($file) < $cutoff) { + unlink($file); + } + } + } + + return true; + } + + /** + * Close file handle on shutdown + */ + public static function close_file_handle() { + if (self::$file_handle) { + fclose(self::$file_handle); + self::$file_handle = null; + } + } + + /** + * Convenience methods for different log levels + */ + public static function info($message, $context = array()) { + return self::log($message, self::LEVEL_INFO, $context); + } + + public static function warning($message, $context = array()) { + return self::log($message, self::LEVEL_WARNING, $context); + } + + public static function error($message, $context = array()) { + return self::log($message, self::LEVEL_ERROR, $context); + } + + public static function debug($message, $context = array()) { + return self::log($message, self::LEVEL_DEBUG, $context); + } +} diff --git a/wp-content/plugins/wp-debug/inc/profiler.php b/wp-content/plugins/wp-debug/inc/profiler.php new file mode 100644 index 00000000..2fab20ab --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/profiler.php @@ -0,0 +1,266 @@ + isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', + 'memory' => self::format_bytes(memory_get_usage()), + )); + } + } + + /** + * Start a timer + * + * @param string $name Timer name + * @return bool True on success + */ + public static function start_timer($name) { + self::$timers[$name] = array( + 'start' => microtime(true), + 'memory_start' => memory_get_usage(), + ); + + return true; + } + + /** + * End a timer + * + * @param string $name Timer name + * @return float|bool Elapsed time in seconds, or false + */ + public static function end_timer($name) { + if (!isset(self::$timers[$name])) { + return false; + } + + $end_time = microtime(true); + $memory_end = memory_get_usage(); + + $elapsed = $end_time - self::$timers[$name]['start']; + $memory_used = $memory_end - self::$timers[$name]['memory_start']; + + self::$completed_timers[$name] = array( + 'elapsed' => $elapsed, + 'memory_used' => $memory_used, + 'memory_peak' => memory_get_peak_usage(), + ); + + // Log if exceeds threshold + $threshold = get_option('wp_debug_profiler_threshold', 1000); // milliseconds + if ($elapsed * 1000 > $threshold) { + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::warning("Slow execution: {$name}", array( + 'elapsed_ms' => round($elapsed * 1000, 2), + 'memory' => self::format_bytes($memory_used), + )); + } + } + + unset(self::$timers[$name]); + + return $elapsed; + } + + /** + * Get completed timers + * + * @return array Timers + */ + public static function get_timers() { + return self::$completed_timers; + } + + /** + * Track query execution + * + * @param string $query SQL query + * @return string Query + */ + public static function track_query($query) { + // This will be used by query analyzer + return $query; + } + + /** + * Record metrics on shutdown + */ + public static function record_metrics() { + // Calculate total execution time + $total_time = timer_stop(0, 3); + + // Get memory stats + $memory_current = memory_get_usage(); + $memory_peak = memory_get_peak_usage(); + + // Get server load if available + $server_load = self::get_server_load(); + + // Prepare metrics + $metrics = array( + 'execution_time' => $total_time, + 'memory_current' => $memory_current, + 'memory_peak' => $memory_peak, + 'server_load' => $server_load, + 'timers' => self::$completed_timers, + 'url' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', + 'timestamp' => current_time('mysql'), + ); + + // Log metrics + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::info('Page load completed', array( + 'time' => round($total_time, 3) . 's', + 'memory_peak' => self::format_bytes($memory_peak), + 'timers_count' => count(self::$completed_timers), + )); + } + + // Store metrics in transient for frontend panel + set_transient('wp_debug_last_metrics', $metrics, 3600); + } + + /** + * Get server load + * + * @return string|null Server load + */ + private static function get_server_load() { + if (function_exists('sys_getloadavg')) { + $load = sys_getloadavg(); + return implode(', ', array_map(function($val) { + return round($val, 2); + }, $load)); + } + + return null; + } + + /** + * Format bytes to human readable + * + * @param int $bytes Bytes + * @return string Formatted string + */ + public static function format_bytes($bytes) { + $units = array('B', 'KB', 'MB', 'GB'); + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= (1 << (10 * $pow)); + + return round($bytes, 2) . ' ' . $units[$pow]; + } + + /** + * Get performance report + * + * @return array Performance report + */ + public static function get_performance_report() { + $metrics = get_transient('wp_debug_last_metrics'); + + if (!$metrics) { + return array( + 'error' => 'No metrics available', + ); + } + + // Analyze and provide suggestions + $suggestions = array(); + + // Check execution time + if ($metrics['execution_time'] > 3) { + $suggestions[] = 'Page load time exceeds 3 seconds. Consider caching or optimizing queries.'; + } + + // Check memory + if ($metrics['memory_peak'] > 128 * 1024 * 1024) { // 128MB + $suggestions[] = 'Memory usage is high (' . self::format_bytes($metrics['memory_peak']) . '). Check for memory leaks.'; + } + + // Check slow timers + $slow_timers = array(); + foreach ($metrics['timers'] as $name => $timer) { + if ($timer['elapsed'] > 1) { + $slow_timers[$name] = round($timer['elapsed'], 3) . 's'; + } + } + + if (!empty($slow_timers)) { + $suggestions[] = 'Slow functions detected: ' . implode(', ', array_keys($slow_timers)); + } + + return array( + 'metrics' => $metrics, + 'suggestions' => $suggestions, + 'formatted' => array( + 'execution_time' => round($metrics['execution_time'], 3) . 's', + 'memory_current' => self::format_bytes($metrics['memory_current']), + 'memory_peak' => self::format_bytes($metrics['memory_peak']), + 'server_load' => $metrics['server_load'], + ), + ); + } +} diff --git a/wp-content/plugins/wp-debug/inc/query-analyzer.php b/wp-content/plugins/wp-debug/inc/query-analyzer.php new file mode 100644 index 00000000..f03d8bf8 --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/query-analyzer.php @@ -0,0 +1,266 @@ +queries)) { + return; + } + + // Process each query + foreach ($wpdb->queries as $query) { + list($sql, $elapsed, $caller) = $query; + + $query_info = array( + 'sql' => $sql, + 'elapsed' => $elapsed, + 'caller' => $caller, + 'timestamp' => microtime(true), + ); + + self::$queries_executed[] = $query_info; + + // Group by pattern + $normalized = self::normalize_query($sql); + if (!isset(self::$query_patterns[$normalized])) { + self::$query_patterns[$normalized] = array( + 'count' => 0, + 'total_time' => 0, + 'queries' => array(), + ); + } + + self::$query_patterns[$normalized]['count']++; + self::$query_patterns[$normalized]['total_time'] += floatval($elapsed); + self::$query_patterns[$normalized]['queries'][] = $query_info; + } + + // Log statistics + $total_queries = count($wpdb->queries); + $total_time = array_sum(array_column($wpdb->queries, 1)); + $slow_queries = self::get_slow_queries(); + $n_plus_one = self::detect_n_plus_one(); + + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::info('Query Analysis Complete', array( + 'total_queries' => $total_queries, + 'total_time' => round($total_time, 4) . 's', + 'slow_queries' => count($slow_queries), + 'n_plus_one_patterns' => count($n_plus_one), + )); + + // Log slow queries + foreach ($slow_queries as $query) { + WP_Debug_Logger::warning('Slow Query Detected', array( + 'elapsed' => round($query['elapsed'], 4) . 's', + 'sql' => substr($query['sql'], 0, 200), + 'caller' => $query['caller'], + )); + } + + // Log N+1 patterns + foreach ($n_plus_one as $pattern) { + WP_Debug_Logger::warning('N+1 Query Pattern Detected', array( + 'count' => $pattern['count'], + 'total_time' => round($pattern['total_time'], 4) . 's', + 'example' => substr($pattern['example'], 0, 200), + )); + } + } + + // Store for frontend panel + set_transient('wp_debug_queries_data', array( + 'total_queries' => $total_queries, + 'total_time' => $total_time, + 'queries' => self::$queries_executed, + 'slow_queries' => $slow_queries, + 'n_plus_one' => $n_plus_one, + 'patterns' => self::$query_patterns, + ), 3600); + } + + /** + * Normalize query for pattern detection + * + * @param string $query SQL query + * @return string Normalized query + */ + private static function normalize_query($query) { + // Remove numeric literals + $query = preg_replace('/\b\d+\b/', '?', $query); + + // Remove string literals + $query = preg_replace("/'[^']*'/", '?', $query); + + // Remove extra whitespace + $query = preg_replace('/\s+/', ' ', $query); + + return trim($query); + } + + /** + * Detect N+1 query patterns + * + * @return array N+1 patterns + */ + public static function detect_n_plus_one() { + $patterns = array(); + + // Find patterns executed 5+ times + foreach (self::$query_patterns as $normalized => $pattern) { + if ($pattern['count'] >= 5) { + $patterns[] = array( + 'count' => $pattern['count'], + 'total_time' => $pattern['total_time'], + 'avg_time' => $pattern['total_time'] / $pattern['count'], + 'example' => $pattern['queries'][0]['sql'], + 'normalized' => $normalized, + ); + } + } + + // Sort by count descending + usort($patterns, function($a, $b) { + return $b['count'] - $a['count']; + }); + + return $patterns; + } + + /** + * Get slow queries + * + * @param float $threshold Threshold in seconds + * @return array Slow queries + */ + public static function get_slow_queries($threshold = 0.05) { + return array_filter(self::$queries_executed, function($query) use ($threshold) { + return floatval($query['elapsed']) > $threshold; + }); + } + + /** + * Get queries by caller + * + * @param string $caller Caller filter + * @return array Queries + */ + public static function get_queries_by_caller($caller) { + return array_filter(self::$queries_executed, function($query) use ($caller) { + return strpos($query['caller'], $caller) !== false; + }); + } + + /** + * Get query statistics + * + * @return array Statistics + */ + public static function get_statistics() { + global $wpdb; + + if (empty($wpdb->queries)) { + return array( + 'error' => 'SAVEQUERIES not enabled or no queries executed', + ); + } + + $total_queries = count($wpdb->queries); + $total_time = array_sum(array_column($wpdb->queries, 1)); + $avg_time = $total_time / $total_queries; + + return array( + 'total_queries' => $total_queries, + 'total_time' => $total_time, + 'avg_time' => $avg_time, + 'slow_queries' => count(self::get_slow_queries()), + 'n_plus_one_patterns' => count(self::detect_n_plus_one()), + 'unique_patterns' => count(self::$query_patterns), + ); + } + + /** + * Get query report + * + * @return array Query report + */ + public static function get_query_report() { + $stats = self::get_statistics(); + $slow_queries = self::get_slow_queries(); + $n_plus_one = self::detect_n_plus_one(); + + $suggestions = array(); + + // Suggestions based on analysis + if (isset($stats['total_queries']) && $stats['total_queries'] > 100) { + $suggestions[] = 'High number of queries detected (' . $stats['total_queries'] . '). Consider using object caching.'; + } + + if (isset($stats['total_time']) && $stats['total_time'] > 1) { + $suggestions[] = 'Total query time exceeds 1 second. Optimize slow queries.'; + } + + if (!empty($n_plus_one)) { + $suggestions[] = 'N+1 query patterns detected. Consider using WP_Query with proper parameters or caching.'; + } + + if (!empty($slow_queries)) { + $suggestions[] = count($slow_queries) . ' slow queries detected. Add indexes or optimize WHERE clauses.'; + } + + return array( + 'statistics' => $stats, + 'slow_queries' => array_slice($slow_queries, 0, 10), + 'n_plus_one_patterns' => array_slice($n_plus_one, 0, 5), + 'suggestions' => $suggestions, + ); + } +} diff --git a/wp-content/plugins/wp-debug/inc/template-tracer.php b/wp-content/plugins/wp-debug/inc/template-tracer.php new file mode 100644 index 00000000..c4829b5a --- /dev/null +++ b/wp-content/plugins/wp-debug/inc/template-tracer.php @@ -0,0 +1,253 @@ + $template, + 'name' => basename($template), + 'type' => self::get_template_type(), + 'timestamp' => microtime(true), + 'backtrace' => self::get_caller(), + ); + + self::$templates_loaded[] = $template_info; + + // Log template load + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug("Template loaded: " . basename($template), array( + 'type' => $template_info['type'], + 'path' => $template, + )); + } + + return $template; + } + + /** + * Track template part + * + * @param string $slug Template slug + * @param string $name Template name + * @param array $args Template args + */ + public static function track_template_part($slug, $name = null, $args = array()) { + $template_file = $slug; + if ($name) { + $template_file .= '-' . $name; + } + $template_file .= '.php'; + + $template_path = locate_template($template_file); + + $part_info = array( + 'slug' => $slug, + 'name' => $name, + 'file' => $template_file, + 'path' => $template_path, + 'found' => !empty($template_path), + 'args' => $args, + 'timestamp' => microtime(true), + ); + + self::$template_parts[] = $part_info; + + // Log if not found + if (!$part_info['found']) { + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::warning("Template part not found: {$template_file}"); + } + } + } + + /** + * Track template hierarchy + * + * @param array $templates Template hierarchy + * @return array Template hierarchy + */ + public static function track_template_hierarchy($templates) { + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::debug("Template hierarchy", array( + 'templates' => $templates, + )); + } + + return $templates; + } + + /** + * Get template type + * + * @return string Template type + */ + private static function get_template_type() { + if (is_404()) return '404'; + if (is_search()) return 'search'; + if (is_front_page()) return 'front-page'; + if (is_home()) return 'home'; + if (is_post_type_archive()) return 'archive-' . get_post_type(); + if (is_tax()) return 'taxonomy'; + if (is_attachment()) return 'attachment'; + if (is_single()) return 'single-' . get_post_type(); + if (is_page()) return 'page'; + if (is_category()) return 'category'; + if (is_tag()) return 'tag'; + if (is_author()) return 'author'; + if (is_date()) return 'date'; + if (is_archive()) return 'archive'; + + return 'index'; + } + + /** + * Get caller information + * + * @return string Caller info + */ + private static function get_caller() { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5); + $callers = array(); + + foreach ($backtrace as $trace) { + if (isset($trace['file']) && isset($trace['line'])) { + $callers[] = basename($trace['file']) . ':' . $trace['line']; + } + } + + return implode(' -> ', $callers); + } + + /** + * Get loaded templates + * + * @return array Templates + */ + public static function get_templates() { + return self::$templates_loaded; + } + + /** + * Get template parts + * + * @return array Template parts + */ + public static function get_template_parts() { + return self::$template_parts; + } + + /** + * Get missing template parts + * + * @return array Missing template parts + */ + public static function get_missing_parts() { + return array_filter(self::$template_parts, function($part) { + return !$part['found']; + }); + } + + /** + * Record templates on shutdown + */ + public static function record_templates() { + $templates_count = count(self::$templates_loaded); + $parts_count = count(self::$template_parts); + $missing_count = count(self::get_missing_parts()); + + // Log summary + if (class_exists('WP_Debug_Logger')) { + WP_Debug_Logger::info("Templates loaded", array( + 'templates' => $templates_count, + 'parts' => $parts_count, + 'missing' => $missing_count, + )); + } + + // Store for frontend panel + set_transient('wp_debug_templates_data', array( + 'templates' => self::$templates_loaded, + 'parts' => self::$template_parts, + 'missing' => self::get_missing_parts(), + ), 3600); + } + + /** + * Get template load order + * + * @return array Template load order + */ + public static function get_load_order() { + $order = array(); + + // Combine templates and parts + $all = array_merge( + array_map(function($t) { + return array('type' => 'template', 'data' => $t); + }, self::$templates_loaded), + array_map(function($p) { + return array('type' => 'part', 'data' => $p); + }, self::$template_parts) + ); + + // Sort by timestamp + usort($all, function($a, $b) { + return $a['data']['timestamp'] - $b['data']['timestamp']; + }); + + return $all; + } +} diff --git a/wp-content/plugins/wp-debug/readme.txt b/wp-content/plugins/wp-debug/readme.txt new file mode 100644 index 00000000..eb213ead --- /dev/null +++ b/wp-content/plugins/wp-debug/readme.txt @@ -0,0 +1,106 @@ +=== WP Debug === +Contributors: Developer Team +Tags: debug, diagnostics, performance, developer tools, wp-cli +Requires at least: 6.0 +Tested up to: 6.8 +Requires PHP: 7.4 +Stable tag: 1.0.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Sistema avanzado de debug y diagnóstico para WordPress. Monitorea hooks, templates, assets, queries y performance en tiempo real. + +== Description == + +WP Debug es un sistema de diagnóstico avanzado diseñado para desarrolladores de WordPress que necesitan visibilidad completa del ciclo de rendering, hooks, queries y performance. + += Características = + +* **Hook Monitor:** Rastrea todos los hooks de WordPress ejecutados con timestamps +* **Template Tracer:** Identifica qué templates se cargan y en qué orden +* **Asset Tracker:** Monitorea CSS/JS encolados con dependencias y duplicados +* **Performance Profiler:** Mide tiempos de ejecución y detecta bottlenecks +* **Query Analyzer:** Analiza queries SQL y detecta problemas N+1 +* **Frontend Panel:** Panel visual flotante en frontend (solo admins) +* **WP-CLI Commands:** Comandos desde terminal para diagnóstico avanzado + += Casos de Uso = + +* Diagnosticar por qué un sidebar no es sticky +* Identificar hooks que no se ejecutan +* Detectar CSS/JS que se cargan en orden incorrecto +* Encontrar queries lentas o duplicadas +* Medir performance de funciones críticas +* Debugging de templates y template-parts + += Comandos WP-CLI = + +* `wp debug status` - Ver estado del sistema +* `wp debug hooks` - Listar hooks ejecutados +* `wp debug templates` - Listar templates cargados +* `wp debug export` - Exportar reporte completo +* `wp debug clear` - Limpiar logs + += API Programática = + +```php +// Logging +wp_debug_log('Mensaje de debug', 'INFO'); + +// Performance profiling +wp_debug_start_timer('mi_funcion'); +// ... tu código ... +wp_debug_end_timer('mi_funcion'); + +// Obtener datos +$hooks = wp_debug_get_hooks(); +$templates = wp_debug_get_templates(); +``` + +== Installation == + +1. Sube la carpeta `wp-debug` a `/wp-content/plugins/` +2. Activa el plugin desde el menú 'Plugins' de WordPress +3. Ve a Tools → WP Debug para ver el dashboard +4. (Opcional) Usa comandos WP-CLI para diagnóstico avanzado + +== Frequently Asked Questions == + += ¿Afecta el performance del sitio? = + +El plugin está optimizado para mínimo overhead (<5%). Se recomienda usar solo en staging/desarrollo. + += ¿Puedo usarlo en producción? = + +Sí, pero se recomienda desactivar módulos innecesarios y usar solo en modo debug. + += ¿Dónde se guardan los logs? = + +En `/wp-content/logs/wp-debug/` y en tablas de base de datos. + += ¿Cómo lo uso en mi plugin? = + +Simplemente llama a las funciones API: `wp_debug_log()`, `wp_debug_start_timer()`, etc. + +== Screenshots == + +1. Dashboard principal en Tools → WP Debug +2. Frontend panel flotante (solo admins) +3. Hook monitor en tiempo real +4. Template tracer con backtraces +5. Performance profiler con métricas + +== Changelog == + += 1.0.0 = +* Release inicial +* 8 módulos de diagnóstico +* WP-CLI commands +* API programática +* Frontend panel +* Admin dashboard + +== Upgrade Notice == + += 1.0.0 = +Primera versión del plugin. diff --git a/wp-content/plugins/wp-debug/wp-debug.php b/wp-content/plugins/wp-debug/wp-debug.php new file mode 100644 index 00000000..ff54f1da --- /dev/null +++ b/wp-content/plugins/wp-debug/wp-debug.php @@ -0,0 +1,145 @@ +