Changeset 3448917
- Timestamp:
- 01/28/2026 05:49:05 PM (3 weeks ago)
- Location:
- 0-day-analytics
- Files:
-
- 18 edited
- 1 copied
-
tags/4.6.0 (copied) (copied from 0-day-analytics/trunk)
-
tags/4.6.0/advanced-analytics.php (modified) (2 diffs)
-
tags/4.6.0/classes/class-advanced-analytics.php (modified) (2 diffs)
-
tags/4.6.0/classes/migration/class-migration.php (modified) (1 diff)
-
tags/4.6.0/classes/vendor/controllers/class-hooks-capture.php (modified) (29 diffs)
-
tags/4.6.0/classes/vendor/controllers/class-wp-mail-log.php (modified) (1 diff)
-
tags/4.6.0/classes/vendor/entities/class-hooks-capture-entity.php (modified) (10 diffs)
-
tags/4.6.0/classes/vendor/helpers/class-system-analytics.php (modified) (1 diff)
-
tags/4.6.0/classes/vendor/lists/class-hooks-capture-list.php (modified) (5 diffs)
-
tags/4.6.0/readme.txt (modified) (2 diffs)
-
trunk/advanced-analytics.php (modified) (2 diffs)
-
trunk/classes/class-advanced-analytics.php (modified) (2 diffs)
-
trunk/classes/migration/class-migration.php (modified) (1 diff)
-
trunk/classes/vendor/controllers/class-hooks-capture.php (modified) (29 diffs)
-
trunk/classes/vendor/controllers/class-wp-mail-log.php (modified) (1 diff)
-
trunk/classes/vendor/entities/class-hooks-capture-entity.php (modified) (10 diffs)
-
trunk/classes/vendor/helpers/class-system-analytics.php (modified) (1 diff)
-
trunk/classes/vendor/lists/class-hooks-capture-list.php (modified) (5 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
0-day-analytics/tags/4.6.0/advanced-analytics.php
r3442473 r3448917 11 11 * Plugin Name: 0 Day Analytics 12 12 * Description: Take full control of error log, crons, transients, plugins, requests, mails and DB tables. 13 * Version: 4. 5.213 * Version: 4.6.0 14 14 * Author: Stoil Dobrev 15 15 * Author URI: https://github.com/sdobreff/ … … 39 39 // Constants. 40 40 if ( ! defined( 'ADVAN_VERSION' ) ) { 41 define( 'ADVAN_VERSION', '4. 5.2' );41 define( 'ADVAN_VERSION', '4.6.0' ); 42 42 define( 'ADVAN_TEXTDOMAIN', '0-day-analytics' ); 43 43 define( 'ADVAN_NAME', '0 Day Analytics' ); -
0-day-analytics/tags/4.6.0/classes/class-advanced-analytics.php
r3442115 r3448917 31 31 use ADVAN\Lists\Hooks_Capture_List; 32 32 use ADVAN\Lists\Hooks_Management_List; 33 use ADVAN\Controllers\Hooks_Capture;34 33 use ADVAN\Migration\Migration; 35 34 use ADVAN\Controllers\Pointers; 36 use ADVAN\Controllers\Cron_Jobs;37 35 use ADVAN\Helpers\Miscellaneous; 38 36 use ADVAN\Lists\Transients_List; … … 71 69 72 70 \add_action( 'admin_init', array( __CLASS__, 'plugin_redirect' ) ); 71 72 // Initialize hooks capture list admin features. 73 Hooks_Capture_List::init_admin_hooks(); 73 74 74 75 // Setup screen options. Needs to be here as admin_init hook is too late. Per page set is below. -
0-day-analytics/tags/4.6.0/classes/migration/class-migration.php
r3413453 r3448917 252 252 } 253 253 } 254 255 /** 256 * Migrates the plugin up-to version 4.6.0 (adds request_id column to hooks capture). 257 * 258 * @return void 259 * 260 * @since 4.6.0 261 */ 262 public static function migrate_up_to_460() { 263 if ( \class_exists( '\\ADVAN\\Entities\\Hooks_Capture_Entity' ) ) { 264 if ( Common_Table::check_table_exists( \ADVAN\Entities\Hooks_Capture_Entity::get_table_name() ) && ! Common_Table::check_column( 'request_id', 'varchar(50)', \ADVAN\Entities\Hooks_Capture_Entity::get_table_name() ) ) { 265 \ADVAN\Entities\Hooks_Capture_Entity::alter_table_460(); 266 } 267 } 268 } 254 269 } 255 270 } -
0-day-analytics/tags/4.6.0/classes/vendor/controllers/class-hooks-capture.php
r3442473 r3448917 31 31 32 32 /** 33 * Maximum depth for capturing nested hooks. 34 * 35 * @var int 36 * 37 * @since 4.5.0 38 */ 39 private const MAX_CAPTURE_DEPTH = 3; 40 41 /** 42 * Maximum number of hook logs to store in memory before forcing a commit. 43 * 44 * @var int 45 * 46 * @since 4.6.1 47 */ 48 private const MAX_MEMORY_LOGS = 1000; 49 50 /** 51 * Maximum size for parameter/output JSON strings (64KB). 52 * 53 * @var int 54 * 55 * @since 4.5.0 56 */ 57 private const MAX_JSON_SIZE = 65536; 58 59 /** 60 * Maximum length for string parameters before truncation. 61 * 62 * @var int 63 * 64 * @since 4.5.0 65 */ 66 private const MAX_STRING_LENGTH = 255; 67 68 /** 69 * Maximum depth for recursive array/object sanitization. 70 * 71 * @var int 72 * 73 * @since 4.5.0 74 */ 75 private const MAX_SANITIZE_DEPTH = 2; 76 77 /** 78 * Maximum number of backtrace frames to capture. 79 * 80 * @var int 81 * 82 * @since 4.5.0 83 */ 84 private const MAX_BACKTRACE_FRAMES = 3; 85 86 /** 87 * Maximum number of properties to capture from objects. 88 * 89 * @var int 90 * 91 * @since 4.5.0 92 */ 93 private const MAX_OBJECT_PROPERTIES = 50; 94 95 /** 96 * Maximum length for hook names. 97 * 98 * @var int 99 * 100 * @since 4.6.1 101 */ 102 private const MAX_HOOK_NAME_LENGTH = 255; 103 104 /** 33 105 * Array of hooks currently being captured to prevent infinite loops. 34 106 * … … 83 155 */ 84 156 private static $cache_dir_path = null; 157 158 /** 159 * Unique request ID for grouping hook calls per request. 160 * 161 * @var string|null 162 * 163 * @since 4.5.0 164 */ 165 private static $request_id = null; 166 167 /** 168 * In-memory storage for hook logs to deduplicate per request. 169 * 170 * @var array 171 * 172 * @since 4.6.0 173 */ 174 private static $hook_logs = array(); 85 175 86 176 /** … … 96 186 } 97 187 98 // In WP-CLI context, ensure hooks are attached properly 188 // Generate unique request ID for this execution. 189 self::$request_id = uniqid( 'req_', true ); 190 191 // self::debug_log( 'Initializing hooks capture', array( 'request_id' => self::$request_id ) ); 192 193 // In WP-CLI context, ensure hooks are attached properly. 99 194 if ( defined( 'WP_CLI' ) && WP_CLI ) { 100 195 self::attach_hooks_cli(); … … 109 204 // Re-attach hooks after cache clear to pick up changes. 110 205 \add_action( 'advan_hooks_management_updated', array( __CLASS__, 'detach_and_reattach_hooks' ) ); 206 207 // Commit hook logs at the end of the request. 208 \add_action( 'shutdown', array( __CLASS__, 'commit_hook_logs' ) ); 209 210 // ======================================================================= 211 // MEMORY POOL: Initialize memory pool for reusing structures 212 // ======================================================================= 213 self::init_memory_pool(); 214 215 // ======================================================================= 216 // ERROR RECOVERY: Setup error recovery for serialization failures 217 // ======================================================================= 218 \add_filter( 'advan_serialize_hook_data', array( __CLASS__, 'safe_json_encode' ), 10, 2 ); 219 \add_filter( 'advan_unserialize_hook_data', array( __CLASS__, 'safe_json_decode' ), 10, 2 ); 220 221 // Cleanup memory pool on shutdown. 222 \add_action( 'shutdown', array( __CLASS__, 'cleanup_memory_pool' ), 999 ); 111 223 } 112 224 … … 123 235 124 236 // Regenerate cache file with latest hooks configuration. 125 self::regenerate_cache_file(); 237 // Defer regeneration if WordPress isn't fully loaded yet. 238 if ( ! \did_action( 'init' ) ) { 239 \add_action( 'init', array( __CLASS__, 'regenerate_cache_file' ), 1 ); 240 } else { 241 self::regenerate_cache_file(); 242 } 126 243 } 127 244 … … 167 284 */ 168 285 public static function attach_hooks_cli() { 169 // In CLI context, always load from DB to ensure hooks are attached 286 // In CLI context, always load from DB to ensure hooks are attached. 170 287 self::$enabled_hooks = Hooks_Management_Entity::get_enabled_hooks(); 171 288 … … 176 293 // Attach monitoring to each enabled hook. 177 294 foreach ( self::$enabled_hooks as $hook_config ) { 178 if ( empty( $hook_config['hook_name'] ) ) {295 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 179 296 continue; 180 297 } … … 186 303 // Use a high number of accepted args to capture all parameters. 187 304 if ( 'action' === $hook_type ) { 188 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, 10);305 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, self::get_accepted_args() ); 189 306 } else { 190 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, 10);307 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, self::get_accepted_args() ); 191 308 } 192 309 } … … 224 341 // Attach monitoring to each enabled hook. 225 342 foreach ( self::$enabled_hooks as $hook_config ) { 226 if ( empty( $hook_config['hook_name'] ) ) {343 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 227 344 continue; 228 345 } … … 234 351 // Use a high number of accepted args to capture all parameters. 235 352 if ( 'action' === $hook_type ) { 236 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, 10);353 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, self::get_accepted_args() ); 237 354 } else { 238 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, 10);355 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, self::get_accepted_args() ); 239 356 } 240 357 } … … 286 403 */ 287 404 private static function log_hook( string $hook_name, string $hook_type, array $args, $output ) { 405 // Validate hook name for security. 406 if ( ! self::is_valid_hook_name( $hook_name ) ) { 407 return; 408 } 409 410 // ======================================================================= 411 // EARLY FILTERING: Filter out unwanted hooks before processing 412 // ======================================================================= 413 if ( ! self::should_capture_hook_early( $hook_name ) ) { 414 return; 415 } 416 417 // ======================================================================= 418 // SAMPLING: Apply sampling for high-frequency hooks to reduce storage 419 // ======================================================================= 420 if ( ! self::should_sample_hook( $hook_name ) ) { 421 return; 422 } 423 288 424 // Prevent infinite loops. 289 425 if ( isset( self::$capturing_hooks[ $hook_name ] ) ) { … … 292 428 293 429 // Prevent excessive nesting. 294 if ( self::$current_depth >= self:: $max_depth) {430 if ( self::$current_depth >= self::MAX_CAPTURE_DEPTH ) { 295 431 return; 296 432 } … … 335 471 336 472 // Limit parameter size (max 64KB). 337 if ( strlen( $parameters_json ) > 65536) {338 $parameters_json = substr( $parameters_json, 0, 65536) . '... [truncated]';473 if ( strlen( $parameters_json ) > self::MAX_JSON_SIZE ) { 474 $parameters_json = substr( $parameters_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 339 475 } 340 476 } … … 346 482 347 483 // Limit output size (max 64KB). 348 if ( strlen( $output_json ) > 65536) {349 $output_json = substr( $output_json, 0, 65536) . '... [truncated]';484 if ( strlen( $output_json ) > self::MAX_JSON_SIZE ) { 485 $output_json = substr( $output_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 350 486 } 351 487 } … … 357 493 $memory_usage = memory_get_usage() - $start_memory; 358 494 359 // Prepare log entry. 360 $log_entry = array( 361 'blog_id' => \is_multisite() ? \get_current_blog_id() : 0, 362 'user_id' => $user_id, 363 'user_login' => $user_login, 364 'trigger_source' => $trigger_source, 365 'hook_name' => $hook_name, 366 'hook_type' => $hook_type, 367 'parameters' => $parameters_json, 368 'output' => $output_json, 369 'backtrace' => \wp_json_encode( $backtrace ), 370 'execution_time' => $execution_time, 371 'memory_usage' => $memory_usage, 372 'is_cli' => (int) self::is_cli(), 373 'hooks_management_id' => self::get_hook_management_id( $hook_name ), 374 'date_added' => microtime( true ), 375 ); 376 377 // Insert asynchronously if possible, synchronously as fallback. 378 if ( function_exists( 'wp_schedule_single_event' ) ) { 379 // For very high-traffic hooks, consider batching. 380 Hooks_Capture_Entity::insert( $log_entry ); 495 // Collect performance metrics for monitoring. 496 self::collect_performance_metrics( $execution_time, $memory_usage, count( self::$hook_logs ) ); 497 498 // Create unique key for deduplication based on hook name and args. 499 // Use optimized key generation for better performance. 500 $key = self::generate_deduplication_key( $hook_name, $args ); 501 502 if ( ! isset( self::$hook_logs[ $key ] ) ) { 503 // Check if we've exceeded memory limits and force a commit if needed. 504 if ( count( self::$hook_logs ) >= self::MAX_MEMORY_LOGS ) { 505 self::commit_hook_logs(); 506 } 507 508 // Prepare log data. 509 $log_data = self::prepare_hook_log_data( $hook_name, $hook_type, $args, $output, $capture_args, $capture_output, $trigger_source, $user_id, $user_login, $backtrace, $execution_time, $memory_usage ); 510 511 // Store the log data in memory. 512 self::$hook_logs[ $key ] = $log_data; 381 513 } else { 382 Hooks_Capture_Entity::insert( $log_entry ); 514 // Increment count for duplicate hook calls. 515 ++self::$hook_logs[ $key ]['count']; 383 516 } 384 517 } finally { … … 465 598 */ 466 599 private static function get_backtrace(): array { 467 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace468 $trace = \debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 5);600 // Use Exception backtrace for better performance (faster than debug_backtrace). 601 $trace = ( new \Exception( '' ) )->getTrace(); 469 602 470 603 $simplified = array(); … … 484 617 ); 485 618 486 // Limit to 3frames for performance.487 if ( count( $simplified ) >= 3) {619 // Limit to configured number of frames for performance. 620 if ( count( $simplified ) >= self::MAX_BACKTRACE_FRAMES ) { 488 621 break; 489 622 } … … 491 624 492 625 return $simplified; 626 } 627 628 /** 629 * Generate optimized deduplication key for hook calls. 630 * 631 * @param string $hook_name Hook name. 632 * @param array $args Hook arguments. 633 * 634 * @return string Deduplication key. 635 * 636 * @since 4.6.1 637 */ 638 private static function generate_deduplication_key( string $hook_name, array $args ): string { 639 // For performance, use a simplified approach for common cases. 640 $arg_signature = ''; 641 642 // Limit to first few arguments to avoid expensive serialization. 643 $max_args = 3; 644 $arg_count = 0; 645 646 foreach ( $args as $arg ) { 647 if ( $arg_count >= $max_args ) { 648 break; 649 } 650 651 if ( is_scalar( $arg ) ) { 652 $arg_signature .= (string) $arg . '|'; 653 } elseif ( is_array( $arg ) ) { 654 $arg_signature .= 'array(' . count( $arg ) . ')|'; 655 } elseif ( is_object( $arg ) ) { 656 $arg_signature .= 'object(' . get_class( $arg ) . ')|'; 657 } else { 658 $arg_signature .= gettype( $arg ) . '|'; 659 } 660 661 ++$arg_count; 662 } 663 664 // Add count of remaining args if any. 665 if ( count( $args ) > $max_args ) { 666 $arg_signature .= '+' . ( count( $args ) - $max_args ) . 'more'; 667 } 668 669 return $hook_name . '_' . md5( $arg_signature ); 670 } 671 672 /** 673 * Get module health status. 674 * 675 * @return array Health status information. 676 * 677 * @since 4.6.1 678 */ 679 public static function get_health_status(): array { 680 return self::health_check(); 681 } 682 683 /** 684 * Force commit of pending hook logs. 685 * 686 * @return bool True on success. 687 * 688 * @since 4.6.1 689 */ 690 public static function force_commit(): bool { 691 if ( empty( self::$hook_logs ) ) { 692 return true; 693 } 694 695 self::commit_hook_logs(); 696 return empty( self::$hook_logs ); 697 } 698 699 /** 700 * Get current performance metrics. 701 * 702 * @return array Performance metrics. 703 * 704 * @since 4.6.1 705 */ 706 public static function get_performance_metrics(): array { 707 return array( 708 'memory_usage' => memory_get_usage( true ), 709 'peak_memory' => memory_get_peak_usage( true ), 710 'queued_logs' => count( self::$hook_logs ), 711 'max_logs' => self::MAX_MEMORY_LOGS, 712 'cache_enabled' => ! empty( self::get_cache_file_path() ), 713 'request_id' => self::$request_id, 714 ); 715 } 716 717 /** 718 * Perform health check for the hooks capture module. 719 * 720 * @return array Health check results. 721 * 722 * @since 4.6.1 723 */ 724 public static function health_check(): array { 725 $health = array( 726 'status' => 'healthy', 727 'issues' => array(), 728 'metrics' => array(), 729 'timestamp' => time(), 730 ); 731 732 // Check memory usage. 733 $memory_usage = memory_get_usage( true ); 734 $memory_limit = self::get_memory_limit_bytes(); 735 736 if ( $memory_limit > 0 && $memory_usage > $memory_limit * 0.8 ) { 737 $health['issues'][] = 'High memory usage detected'; 738 $health['status'] = 'warning'; 739 } 740 741 $health['metrics']['memory_usage'] = $memory_usage; 742 $health['metrics']['memory_limit'] = $memory_limit; 743 744 // Check hook logs count. 745 $log_count = count( self::$hook_logs ); 746 $health['metrics']['queued_logs'] = $log_count; 747 748 if ( $log_count > self::MAX_MEMORY_LOGS * 0.9 ) { 749 $health['issues'][] = 'Approaching memory log limit'; 750 $health['status'] = 'warning'; 751 } 752 753 // Check cache file status. 754 $cache_file = self::get_cache_file_path(); 755 if ( $cache_file ) { 756 $health['metrics']['cache_file_exists'] = file_exists( $cache_file ); 757 $health['metrics']['cache_file_readable'] = is_readable( $cache_file ); 758 759 if ( file_exists( $cache_file ) && is_readable( $cache_file ) ) { 760 $cache_content = file_get_contents( $cache_file ); 761 $health['metrics']['cache_file_valid'] = self::is_valid_cache_content( $cache_content ); 762 } 763 } 764 765 // Check database connectivity. 766 try { 767 $test_query = Hooks_Capture_Entity::load( '1=0' ); // Should return empty array. 768 $health['metrics']['database_connected'] = true; 769 } catch ( \Exception $e ) { 770 $health['issues'][] = 'Database connectivity issue: ' . $e->getMessage(); 771 $health['status'] = 'error'; 772 $health['metrics']['database_connected'] = false; 773 } 774 775 return $health; 776 } 777 778 /** 779 * Get memory limit in bytes. 780 * 781 * @return int Memory limit in bytes, or 0 if unlimited. 782 * 783 * @since 4.6.1 784 */ 785 private static function get_memory_limit_bytes(): int { 786 $memory_limit = ini_get( 'memory_limit' ); 787 788 if ( empty( $memory_limit ) || $memory_limit === '-1' ) { 789 return 0; // Unlimited. 790 } 791 792 $unit = strtolower( substr( $memory_limit, -1 ) ); 793 $value = (int) substr( $memory_limit, 0, -1 ); 794 795 switch ( $unit ) { 796 case 'g': 797 return $value * 1024 * 1024 * 1024; 798 case 'm': 799 return $value * 1024 * 1024; 800 case 'k': 801 return $value * 1024; 802 default: 803 return (int) $memory_limit; 804 } 805 } 806 807 /** 808 * Prepare hook log data for storage. 809 * 810 * @param string $hook_name Hook name. 811 * @param string $hook_type Hook type. 812 * @param array $args Hook arguments. 813 * @param mixed $output Hook output. 814 * @param bool $capture_args Whether to capture arguments. 815 * @param bool $capture_output Whether to capture output. 816 * @param string $trigger_source Trigger source. 817 * @param int $user_id User ID. 818 * @param string $user_login User login. 819 * @param array $backtrace Backtrace data. 820 * @param float $execution_time Execution time. 821 * @param int $memory_usage Memory usage. 822 * 823 * @return array Prepared log data. 824 * 825 * @since 4.6.1 826 */ 827 private static function prepare_hook_log_data( string $hook_name, string $hook_type, array $args, $output, bool $capture_args, bool $capture_output, string $trigger_source, int $user_id, string $user_login, array $backtrace, float $execution_time, int $memory_usage ): array { 828 // Capture parameters (with size limit for performance). 829 $parameters_json = ''; 830 if ( $capture_args && ! empty( $args ) ) { 831 $sanitized_args = self::sanitize_args( $args, $hook_name ); 832 $parameters_json = \wp_json_encode( $sanitized_args ); 833 834 // Limit parameter size (max 64KB). 835 if ( strlen( $parameters_json ) > self::MAX_JSON_SIZE ) { 836 $parameters_json = substr( $parameters_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 837 } 838 } 839 840 // Capture output (with size limit). 841 $output_json = ''; 842 if ( $capture_output && null !== $output ) { 843 $output_json = \wp_json_encode( self::sanitize_args( array( $output ) ) ); 844 845 // Limit output size (max 64KB). 846 if ( strlen( $output_json ) > self::MAX_JSON_SIZE ) { 847 $output_json = substr( $output_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 848 } 849 } 850 851 return array( 852 'blog_id' => \is_multisite() ? \get_current_blog_id() : 0, 853 'user_id' => $user_id, 854 'user_login' => $user_login, 855 'trigger_source' => $trigger_source, 856 'hook_name' => $hook_name, 857 'hook_type' => $hook_type, 858 'parameters' => $parameters_json, 859 'output' => $output_json, 860 'backtrace' => \wp_json_encode( $backtrace ), 861 'execution_time' => $execution_time, 862 'memory_usage' => $memory_usage, 863 'is_cli' => (int) self::is_cli(), 864 'hooks_management_id' => self::get_hook_management_id( $hook_name ), 865 'count' => 1, 866 'date_added' => microtime( true ), 867 ); 493 868 } 494 869 … … 612 987 if ( self::is_sensitive_key( (string) $key ) ) { 613 988 $sanitized[ $key ] = '[REDACTED - Sensitive Data]'; 614 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {615 $sanitized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';989 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 990 $sanitized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 616 991 } else { 617 992 $sanitized[ $key ] = $value; … … 643 1018 */ 644 1019 private static function sanitize_args_recursive( array $args, int $depth ) { 645 if ( $depth > 2) {1020 if ( $depth > self::MAX_SANITIZE_DEPTH ) { 646 1021 return '[nested array]'; 647 1022 } … … 654 1029 if ( self::is_sensitive_key( (string) $key ) ) { 655 1030 $sanitized[ $key ] = '[REDACTED - Sensitive Data]'; 656 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {657 $sanitized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';1031 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 1032 $sanitized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 658 1033 } else { 659 1034 $sanitized[ $key ] = $value; … … 682 1057 */ 683 1058 private static function normalize_object( $object, int $depth ) { 684 if ( $depth > 2) {1059 if ( $depth > self::MAX_SANITIZE_DEPTH ) { 685 1060 return '[nested object]'; 686 1061 } … … 708 1083 709 1084 // Limit to reasonable number of properties. 710 if ( \count( $properties ) > 50) {711 $properties = \array_slice( $properties, 0, 50, true );1085 if ( \count( $properties ) > self::MAX_OBJECT_PROPERTIES ) { 1086 $properties = \array_slice( $properties, 0, self::MAX_OBJECT_PROPERTIES, true ); 712 1087 $normalized['__truncated__'] = true; 713 1088 } … … 719 1094 if ( self::is_sensitive_key( (string) $key ) ) { 720 1095 $normalized[ $key ] = '[REDACTED - Sensitive Data]'; 721 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {722 $normalized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';1096 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 1097 $normalized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 723 1098 } else { 724 1099 $normalized[ $key ] = $value; … … 869 1244 870 1245 foreach ( $enabled_hooks as $hook_config ) { 871 if ( empty( $hook_config['hook_name'] ) ) {1246 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 872 1247 continue; 873 1248 } … … 884 1259 885 1260 if ( 'action' === $hook_type ) { 886 $content .= "add_action( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_action' ), {$escaped_priority}, 10);\n";1261 $content .= "add_action( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_action' ), {$escaped_priority}, " . self::get_accepted_args() . " );\n"; 887 1262 } else { 888 $content .= "add_filter( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_filter' ), {$escaped_priority}, 10);\n";1263 $content .= "add_filter( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_filter' ), {$escaped_priority}, " . self::get_accepted_args() . " );\n"; 889 1264 } 890 1265 … … 954 1329 } 955 1330 956 // Check user capabilities (only if function is available).1331 // Check user capabilities (only if WordPress is fully loaded and function is available). 957 1332 if ( ! self::is_cli() ) { 958 if ( ! \function_exists( 'current_user_can' ) || ! \current_user_can( 'manage_options' ) ) { 1333 // During early WordPress loading, skip capability checks to avoid fatal errors. 1334 if ( ! \did_action( 'init' ) || ! \function_exists( 'current_user_can' ) || ! \function_exists( 'wp_get_current_user' ) ) { 959 1335 return false; 960 1336 } 1337 1338 if ( ! \current_user_can( 'manage_options' ) ) { 1339 return false; 1340 } 961 1341 } 962 1342 963 1343 return self::generate_cache_file(); 1344 } 1345 1346 /** 1347 * Get the number of arguments accepted by hook capture callbacks. 1348 * 1349 * @return int Number of accepted arguments. 1350 * 1351 * @since 4.5.0 1352 */ 1353 private static function get_accepted_args(): int { 1354 // Use a high number to capture all possible hook arguments. 1355 return 99; 964 1356 } 965 1357 … … 983 1375 984 1376 /** 985 * Delete cache file. 1377 * Maximum number of hook logs to store in memory before forcing a commit. 1378 * This prevents excessive memory usage on long-running requests. 1379 * 1380 * @var int 1381 * 1382 * @since 4.6.1 1383 */ 1384 private static $max_memory_logs = 1000; 1385 1386 /** 1387 * Commit accumulated hook logs to the database at the end of the request. 1388 * 1389 * @return void 1390 * 1391 * @since 4.6.0 1392 */ 1393 public static function commit_hook_logs() { 1394 if ( empty( self::$hook_logs ) ) { 1395 // self::debug_log( 'No hook logs to commit' ); 1396 return; 1397 } 1398 1399 $log_count = count( self::$hook_logs ); 1400 // self::debug_log( 'Committing hook logs', array( 'count' => $log_count, 'request_id' => self::$request_id ) ); 1401 1402 try { 1403 foreach ( self::$hook_logs as $log ) { 1404 $log_entry = array( 1405 'blog_id' => $log['blog_id'], 1406 'user_id' => $log['user_id'], 1407 'user_login' => $log['user_login'], 1408 'trigger_source' => $log['trigger_source'], 1409 'request_id' => self::$request_id, 1410 'hook_name' => $log['hook_name'], 1411 'hook_type' => $log['hook_type'], 1412 'parameters' => $log['parameters'], 1413 'output' => $log['output'], 1414 'backtrace' => $log['backtrace'], 1415 'execution_time' => $log['execution_time'], 1416 'memory_usage' => $log['memory_usage'], 1417 'is_cli' => $log['is_cli'], 1418 'hooks_management_id' => $log['hooks_management_id'], 1419 'count' => $log['count'], 1420 'date_added' => $log['date_added'], 1421 ); 1422 1423 Hooks_Capture_Entity::insert( $log_entry ); 1424 } 1425 } catch ( \Exception $e ) { 1426 // Log the error but don't let it break the request. 1427 self::debug_log( 'Failed to commit hook logs', array( 'error' => $e->getMessage() ) ); 1428 if ( function_exists( 'error_log' ) ) { 1429 \error_log( 'Hooks Capture: Failed to commit logs - ' . $e->getMessage() ); 1430 } 1431 } finally { 1432 // Always clear the logs after attempting to commit. 1433 self::$hook_logs = array(); 1434 } 1435 } 1436 1437 /** 1438 * Validate hook name for security. 1439 * 1440 * @param string $hook_name The hook name to validate. 1441 * 1442 * @return bool True if valid, false otherwise. 1443 * 1444 * @since 4.6.1 1445 */ 1446 private static function is_valid_hook_name( string $hook_name ): bool { 1447 // Basic validation: not empty, reasonable length, no dangerous characters. 1448 if ( empty( $hook_name ) || strlen( $hook_name ) > self::MAX_HOOK_NAME_LENGTH ) { 1449 return false; 1450 } 1451 1452 // Allow alphanumeric, underscores, hyphens, slashes, and dots. 1453 if ( ! preg_match( '/^[a-zA-Z0-9_\/\-\.]+$/', $hook_name ) ) { 1454 return false; 1455 } 1456 1457 return true; 1458 } 1459 1460 /** 1461 * Validate cache file content for security before evaluation. 1462 * 1463 * @param string $content Cache file content. 1464 * 1465 * @return bool True if content is safe to evaluate. 1466 * 1467 * @since 4.6.1 1468 */ 1469 private static function is_valid_cache_content( string $content ): bool { 1470 // Basic validation: check for expected PHP structure. 1471 if ( empty( $content ) ) { 1472 return false; 1473 } 1474 1475 // Check for HTML content (indicates corrupted cache file). 1476 if ( preg_match( '/<html|<head|<body|<div|<p|<span/i', $content ) ) { 1477 self::debug_log( 'Cache content contains HTML - file is corrupted' ); 1478 return false; 1479 } 1480 1481 // Check for dangerous PHP functions that shouldn't be in cache. 1482 $dangerous_patterns = array( 1483 '/exec\(/i', 1484 '/system\(/i', 1485 '/shell_exec\(/i', 1486 '/passthru\(/i', 1487 '/eval\(/i', 1488 '/include\(/i', 1489 '/require\(/i', 1490 '/file_get_contents\(/i', 1491 '/fopen\(/i', 1492 '/\$\w+\s*\(/', // Variable function calls. 1493 ); 1494 1495 foreach ( $dangerous_patterns as $pattern ) { 1496 if ( preg_match( $pattern, $content ) ) { 1497 self::debug_log( 'Cache content contains dangerous pattern', array( 'pattern' => $pattern ) ); 1498 return false; 1499 } 1500 } 1501 1502 // Check that content contains expected PHP structure. 1503 if ( ! preg_match( '/^<\?php/', $content ) ) { 1504 self::debug_log( 'Cache content does not start with PHP opening tag' ); 1505 return false; 1506 } 1507 1508 // Check for ABSPATH check (security measure). 1509 if ( ! preg_match( '/defined\s*\(\s*[\'"]ABSPATH[\'"]\s*\)/', $content ) ) { 1510 self::debug_log( 'Cache content missing ABSPATH security check' ); 1511 return false; 1512 } 1513 1514 // Check that it contains our class reference (ensures it's our cache file). 1515 if ( ! preg_match( '/ADVAN\\\\Controllers\\\\Hooks_Capture/', $content ) ) { 1516 self::debug_log( 'Cache content does not reference our class' ); 1517 return false; 1518 } 1519 1520 // Check that it contains hook registration calls. 1521 if ( ! preg_match( '/add_(?:action|filter)\s*\(/', $content ) ) { 1522 self::debug_log( 'Cache content does not contain hook registrations' ); 1523 return false; 1524 } 1525 1526 // Basic PHP syntax check - try to parse the content. 1527 try { 1528 // Remove PHP opening tag for tokenization. 1529 $code = preg_replace( '/^<\?php\s*/', '', $content ); 1530 if ( false === $code ) { 1531 self::debug_log( 'Failed to prepare code for syntax check' ); 1532 return false; 1533 } 1534 1535 // Use token_get_all to check for basic syntax validity. 1536 $tokens = token_get_all( '<?php ' . $code ); 1537 if ( empty( $tokens ) ) { 1538 self::debug_log( 'Cache content tokenization failed' ); 1539 return false; 1540 } 1541 } catch ( \Throwable $e ) { 1542 self::debug_log( 'Cache content syntax check failed', array( 'error' => $e->getMessage() ) ); 1543 return false; 1544 } 1545 1546 return true; 1547 } 1548 1549 /** 1550 * Collect performance metrics for monitoring. 1551 * 1552 * @param float $execution_time Hook execution time in seconds. 1553 * @param int $memory_usage Memory usage in bytes. 1554 * @param int $log_count Current number of logs in memory. 1555 * 1556 * @return void 1557 * 1558 * @since 4.6.1 1559 */ 1560 private static function collect_performance_metrics( float $execution_time, int $memory_usage, int $log_count ) { 1561 static $metrics = array( 1562 'total_execution_time' => 0.0, 1563 'total_memory_usage' => 0, 1564 'hook_count' => 0, 1565 'max_execution_time' => 0.0, 1566 'max_memory_usage' => 0, 1567 'avg_execution_time' => 0.0, 1568 'avg_memory_usage' => 0, 1569 ); 1570 1571 $metrics['total_execution_time'] += $execution_time; 1572 $metrics['total_memory_usage'] += $memory_usage; 1573 ++$metrics['hook_count']; 1574 1575 if ( $execution_time > $metrics['max_execution_time'] ) { 1576 $metrics['max_execution_time'] = $execution_time; 1577 } 1578 1579 if ( $memory_usage > $metrics['max_memory_usage'] ) { 1580 $metrics['max_memory_usage'] = $memory_usage; 1581 } 1582 1583 $metrics['avg_execution_time'] = $metrics['total_execution_time'] / $metrics['hook_count']; 1584 $metrics['avg_memory_usage'] = $metrics['total_memory_usage'] / $metrics['hook_count']; 1585 1586 // Log performance warnings if thresholds are exceeded. 1587 if ( $execution_time > 0.1 ) { // More than 100ms. 1588 self::debug_log( 1589 'Slow hook execution detected', 1590 array( 1591 'execution_time' => $execution_time, 1592 'memory_usage' => $memory_usage, 1593 'log_count' => $log_count, 1594 ) 1595 ); 1596 } 1597 1598 if ( $memory_usage > 1048576 ) { // More than 1MB. 1599 self::debug_log( 1600 'High memory usage detected', 1601 array( 1602 'execution_time' => $execution_time, 1603 'memory_usage' => $memory_usage, 1604 'log_count' => $log_count, 1605 ) 1606 ); 1607 } 1608 1609 if ( $log_count > self::MAX_MEMORY_LOGS * 0.8 ) { // 80% of max capacity. 1610 self::debug_log( 1611 'Approaching memory log limit', 1612 array( 1613 'log_count' => $log_count, 1614 'max_logs' => self::MAX_MEMORY_LOGS, 1615 ) 1616 ); 1617 } 1618 } 1619 1620 /** 1621 * Log debug information for troubleshooting. 1622 * 1623 * @param string $message Debug message. 1624 * @param array $context Additional context data. 1625 * 1626 * @return void 1627 * 1628 * @since 4.6.1 1629 */ 1630 private static function debug_log( string $message, array $context = array() ) { 1631 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { 1632 $log_message = '[Hooks Capture] ' . $message; 1633 if ( ! empty( $context ) ) { 1634 $log_message .= ' ' . wp_json_encode( $context ); 1635 } 1636 error_log( $log_message ); 1637 } 1638 } 1639 1640 /** 1641 * Delete the existing cache file. 986 1642 * 987 1643 * @return bool True on success, false on failure. … … 998 1654 return @unlink( $cache_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.WP.AlternativeFunctions.unlink_unlink 999 1655 } 1656 1657 /** 1658 * ======================================================================= 1659 * NEW FEATURES: Early Filtering, Memory Pool, Error Recovery, Sampling 1660 * ======================================================================= 1661 */ 1662 1663 /** 1664 * Initialize memory pool for reusing structures. 1665 * 1666 * @return void 1667 * 1668 * @since latest 1669 */ 1670 private static function init_memory_pool() { 1671 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1672 $GLOBALS['advan_memory_pool'] = array( 1673 'backtrace_cache' => array(), 1674 'sanitized_data' => array(), 1675 'json_cache' => array(), 1676 'pool_size' => 0, 1677 'max_pool_size' => 100 * 1024 * 1024, // 100MB limit. 1678 ); 1679 } 1680 } 1681 1682 /** 1683 * Get cached backtrace. 1684 * 1685 * @param string $key Cache key. 1686 * 1687 * @return array|null Cached backtrace or null if not found. 1688 * 1689 * @since latest 1690 */ 1691 private static function get_cached_backtrace( string $key ) { 1692 if ( isset( $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ] ) ) { 1693 return $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ]; 1694 } 1695 return null; 1696 } 1697 1698 /** 1699 * Set cached backtrace. 1700 * 1701 * @param string $key Cache key. 1702 * @param array $backtrace Backtrace data. 1703 * 1704 * @return void 1705 * 1706 * @since latest 1707 */ 1708 private static function set_cached_backtrace( string $key, array $backtrace ) { 1709 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1710 return; 1711 } 1712 1713 // Check pool size limit. 1714 $backtrace_size = strlen( \wp_json_encode( $backtrace ) ); 1715 if ( $GLOBALS['advan_memory_pool']['pool_size'] + $backtrace_size > $GLOBALS['advan_memory_pool']['max_pool_size'] ) { 1716 // Clear oldest entries if pool is full. 1717 array_shift( $GLOBALS['advan_memory_pool']['backtrace_cache'] ); 1718 } 1719 1720 $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ] = $backtrace; 1721 $GLOBALS['advan_memory_pool']['pool_size'] += $backtrace_size; 1722 } 1723 1724 /** 1725 * Get cached JSON. 1726 * 1727 * @param string $key Cache key. 1728 * 1729 * @return string|null Cached JSON or null if not found. 1730 * 1731 * @since latest 1732 */ 1733 private static function get_cached_json( string $key ) { 1734 if ( isset( $GLOBALS['advan_memory_pool']['json_cache'][ $key ] ) ) { 1735 return $GLOBALS['advan_memory_pool']['json_cache'][ $key ]; 1736 } 1737 return null; 1738 } 1739 1740 /** 1741 * Set cached JSON. 1742 * 1743 * @param string $key Cache key. 1744 * @param string $json JSON data. 1745 * 1746 * @return void 1747 * 1748 * @since latest 1749 */ 1750 private static function set_cached_json( string $key, string $json ) { 1751 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1752 return; 1753 } 1754 1755 $json_size = strlen( $json ); 1756 if ( $GLOBALS['advan_memory_pool']['pool_size'] + $json_size > $GLOBALS['advan_memory_pool']['max_pool_size'] ) { 1757 array_shift( $GLOBALS['advan_memory_pool']['json_cache'] ); 1758 } 1759 1760 $GLOBALS['advan_memory_pool']['json_cache'][ $key ] = $json; 1761 $GLOBALS['advan_memory_pool']['pool_size'] += $json_size; 1762 } 1763 1764 /** 1765 * Cleanup memory pool on shutdown. 1766 * 1767 * @return void 1768 * 1769 * @since latest 1770 */ 1771 public static function cleanup_memory_pool() { 1772 unset( $GLOBALS['advan_memory_pool'] ); 1773 } 1774 1775 /** 1776 * Early filtering: Check if hook should be captured before processing. 1777 * 1778 * @param string $hook_name The hook name to check. 1779 * @return bool True if hook should be captured, false otherwise. 1780 */ 1781 private static function should_capture_hook_early( string $hook_name ): bool { 1782 static $filtered_hooks = null; 1783 1784 // Initialize filtered hooks list on first call 1785 if ( null === $filtered_hooks ) { 1786 $filtered_hooks = apply_filters( 1787 'advan_excluded_hooks', 1788 array( 1789 // WordPress core hooks that are too frequent/noisy. 1790 'gettext', 1791 'gettext_with_context', 1792 'ngettext', 1793 'ngettext_with_context', 1794 'locale', 1795 'override_load_textdomain', 1796 'load_textdomain', 1797 'unload_textdomain', 1798 1799 // Option hooks (very frequent). 1800 'pre_option_*', 1801 'default_option_*', 1802 'option_*', 1803 1804 // Transient hooks (very frequent). 1805 'pre_transient_*', 1806 'transient_*', 1807 'set_transient_*', 1808 'delete_transient_*', 1809 1810 // Cache hooks (very frequent). 1811 'pre_cache_*', 1812 'cache_*', 1813 1814 // WP_Query hooks (can be very frequent). 1815 'pre_get_posts', 1816 'posts_where', 1817 'posts_join', 1818 'posts_orderby', 1819 'posts_fields', 1820 'posts_clauses', 1821 'posts_request', 1822 'posts_results', 1823 'posts_pre_query', 1824 1825 // Meta hooks (very frequent). 1826 'get_*_metadata', 1827 'update_*_metadata', 1828 'add_*_metadata', 1829 'delete_*_metadata', 1830 1831 // User hooks (frequent in some contexts). 1832 'get_user_metadata', 1833 'update_user_metadata', 1834 1835 // Comment hooks (can be frequent). 1836 'wp_update_comment_count', 1837 'pre_get_comments', 1838 'comments_clauses', 1839 1840 // Taxonomy hooks. 1841 'get_terms', 1842 'get_term', 1843 'get_*_terms', 1844 ) 1845 ); 1846 } 1847 1848 // Check if hook matches any filtered pattern. 1849 foreach ( $filtered_hooks as $pattern ) { 1850 if ( fnmatch( $pattern, $hook_name ) ) { 1851 return false; // Don't capture this hook. 1852 } 1853 } 1854 1855 return true; 1856 } 1857 1858 /** 1859 * Sampling: Check if hook should be sampled (for high-frequency hooks). 1860 * 1861 * @param string $hook_name The hook name to check. 1862 * 1863 * @return bool True if hook should be captured, false if skipped for sampling. 1864 * 1865 * @since latest 1866 */ 1867 private static function should_sample_hook( string $hook_name ): bool { 1868 static $hook_counters = array(); 1869 static $sampling_rates = null; 1870 1871 // Initialize sampling rates on first call. 1872 if ( null === $sampling_rates ) { 1873 $sampling_rates = apply_filters( 1874 'advan_hook_sampling_rates', 1875 array( 1876 // Sample every Nth call for these hooks. 1877 'option_*' => 100, // Sample 1% of option hooks. 1878 'transient_*' => 50, // Sample 2% of transient hooks. 1879 'gettext*' => 20, // Sample 5% of gettext hooks. 1880 'get_*_metadata' => 10, // Sample 10% of metadata hooks. 1881 'pre_get_posts' => 5, // Sample 20% of query hooks. 1882 ) 1883 ); 1884 } 1885 1886 // Check if this hook should be sampled. 1887 foreach ( $sampling_rates as $pattern => $rate ) { 1888 if ( fnmatch( $pattern, $hook_name ) ) { 1889 // Initialize counter for this hook. 1890 if ( ! isset( $hook_counters[ $hook_name ] ) ) { 1891 $hook_counters[ $hook_name ] = 0; 1892 } 1893 1894 ++$hook_counters[ $hook_name ]; 1895 1896 // Only capture if counter is multiple of rate. 1897 if ( $hook_counters[ $hook_name ] % 0 !== $rate ) { 1898 return false; // Skip this call. 1899 } 1900 1901 break; // Found matching pattern, no need to check others. 1902 } 1903 } 1904 1905 return true; 1906 } 1907 1908 /** 1909 * Safe JSON encoding with fallback for error recovery. 1910 * 1911 * @param mixed $data Data to encode. 1912 * @param mixed $fallback Fallback data if encoding fails. 1913 * 1914 * @return string JSON encoded data or fallback. 1915 * 1916 * @since latest 1917 */ 1918 public static function safe_json_encode( $data, $fallback = null ) { 1919 try { 1920 $encoded = \wp_json_encode( $data ); 1921 if ( false === $encoded ) { 1922 throw new \Exception( 'JSON encoding failed' ); 1923 } 1924 return $encoded; 1925 } catch ( \Exception $e ) { 1926 // Log the error. 1927 if ( function_exists( 'error_log' ) ) { 1928 error_log( 'Hooks Capture: JSON encoding failed - ' . $e->getMessage() ); 1929 } 1930 1931 // Return fallback or simplified data. 1932 if ( null !== $fallback ) { 1933 return \wp_json_encode( $fallback ); 1934 } 1935 1936 // Create a simplified version. 1937 if ( is_array( $data ) ) { 1938 return \wp_json_encode( 1939 array( 1940 'error' => 'JSON encoding failed', 1941 'type' => 'array', 1942 'count' => count( $data ), 1943 ) 1944 ); 1945 } elseif ( is_object( $data ) ) { 1946 return \wp_json_encode( 1947 array( 1948 'error' => 'JSON encoding failed', 1949 'type' => 'object', 1950 'class' => get_class( $data ), 1951 ) 1952 ); 1953 } else { 1954 return \wp_json_encode( 1955 array( 1956 'error' => 'JSON encoding failed', 1957 'type' => gettype( $data ), 1958 'content' => substr( (string) $data, 0, 100 ), 1959 ) 1960 ); 1961 } 1962 } 1963 } 1964 1965 /** 1966 * Safe JSON decoding with error handling for error recovery. 1967 * 1968 * @param string $data Data to decode. 1969 * @param mixed $fallback Fallback data if decoding fails. 1970 * 1971 * @return mixed Decoded data or fallback. 1972 * 1973 * @since latest 1974 */ 1975 public static function safe_json_decode( string $data, $fallback = null ) { 1976 if ( empty( $data ) ) { 1977 return $fallback ?: array(); 1978 } 1979 1980 try { 1981 $decoded = \wp_json_decode( $data, true ); 1982 if ( null === $decoded && 'null' !== $data ) { 1983 throw new \Exception( 'JSON decoding failed' ); 1984 } 1985 return $decoded; 1986 } catch ( \Exception $e ) { 1987 // Log the error. 1988 if ( function_exists( 'error_log' ) ) { 1989 error_log( 'Hooks Capture: JSON decoding failed - ' . $e->getMessage() ); 1990 } 1991 1992 // Return fallback. 1993 return $fallback ?: array( 'error' => 'JSON decoding failed' ); 1994 } 1995 } 1996 1997 /** 1998 * Safe serialization with fallback for error recovery. 1999 * DEPRECATED: Use safe_json_encode() instead for security. 2000 * 2001 * @param mixed $data Data to serialize. 2002 * @param mixed $fallback Fallback data if serialization fails. 2003 * 2004 * @return string Serialized data or fallback. 2005 * 2006 * @throws \Exception 2007 * 2008 * @deprecated Use safe_json_encode() instead 2009 * @since latest 2010 */ 2011 public static function safe_serialize( $data, $fallback = null ) { 2012 // Log deprecation warning. 2013 if ( function_exists( 'error_log' ) ) { 2014 error_log( 'Hooks Capture: safe_serialize() is deprecated. Use safe_json_encode() instead.' ); 2015 } 2016 2017 // Fall back to JSON encoding for security. 2018 return self::safe_json_encode( $data, $fallback ); 2019 } 2020 2021 /** 2022 * Safe unserialization with error handling for error recovery. 2023 * DEPRECATED: Use safe_json_decode() instead for security. 2024 * 2025 * @param string $data Data to unserialize. 2026 * @param mixed $fallback Fallback data if unserialization fails. 2027 * 2028 * @return mixed Unserialized data or fallback. 2029 * 2030 * @throws \Exception 2031 * 2032 * @deprecated Use safe_json_decode() instead 2033 * @since latest 2034 */ 2035 public static function safe_unserialize( string $data, $fallback = null ) { 2036 // Log deprecation warning. 2037 if ( function_exists( 'error_log' ) ) { 2038 error_log( 'Hooks Capture: safe_unserialize() is deprecated. Use safe_json_decode() instead.' ); 2039 } 2040 2041 // Fall back to JSON decoding for security. 2042 return self::safe_json_decode( $data, $fallback ); 2043 } 1000 2044 } 1001 2045 } -
0-day-analytics/tags/4.6.0/classes/vendor/controllers/class-wp-mail-log.php
r3413453 r3448917 417 417 * @since 3.0.0 418 418 */ 419 private static function get_backtrace( $function_name = 'wp_mail' ) : ?array{419 private static function get_backtrace( $function_name = 'wp_mail' ) { 420 420 $backtrace_segment = null; 421 421 -
0-day-analytics/tags/4.6.0/classes/vendor/entities/class-hooks-capture-entity.php
r3442115 r3448917 12 12 namespace ADVAN\Entities; 13 13 14 use ADVAN\Entities_Global\Common_Table; 15 14 16 // Exit if accessed directly. 15 17 if ( ! defined( 'ABSPATH' ) ) { … … 44 46 'hook_type' => 'string', 45 47 'trigger_source' => 'string', 48 'request_id' => 'string', 46 49 'user_id' => 'int', 47 50 'user_login' => 'string', … … 53 56 'is_cli' => 'int', 54 57 'hooks_management_id' => 'int', 58 'count' => 'int', 55 59 'date_added' => 'float', 56 60 ); … … 69 73 'hook_type' => 'action', 70 74 'trigger_source' => '', 75 'request_id' => '', 71 76 'user_id' => 0, 72 77 'user_login' => '', … … 78 83 'is_cli' => 0, 79 84 'hooks_management_id' => 0, 85 'count' => 1, 80 86 'date_added' => 0.0, 81 87 ); … … 110 116 hook_type VARCHAR(10) NOT NULL DEFAULT "action", 111 117 trigger_source VARCHAR(50) NOT NULL DEFAULT "", 118 request_id VARCHAR(50) NOT NULL DEFAULT "", 112 119 user_id BIGINT unsigned NOT NULL DEFAULT 0, 113 120 user_login VARCHAR(60) NOT NULL DEFAULT "", … … 119 126 is_cli TINYINT(1) NOT NULL DEFAULT 0, 120 127 hooks_management_id BIGINT unsigned NOT NULL DEFAULT 0, 128 count INT NOT NULL DEFAULT 1, 121 129 date_added DOUBLE NOT NULL DEFAULT 0, 122 130 PRIMARY KEY (id), … … 125 133 KEY `hook_type` (`hook_type`), 126 134 KEY `trigger_source` (`trigger_source`), 135 KEY `request_id` (`request_id`), 127 136 KEY `user_id` (`user_id`), 128 137 KEY `date_added` (`date_added`), … … 152 161 'memory_usage' => __( 'Memory', '0-day-analytics' ), 153 162 'is_cli' => __( 'CLI', '0-day-analytics' ), 163 'count' => __( 'Count', '0-day-analytics' ), 154 164 'parameters' => __( 'Parameters', '0-day-analytics' ), 155 165 ); … … 165 175 return $columns; 166 176 } 177 178 /** 179 * Alters the table to add request_id column for version 4.6.0. 180 * 181 * @return void 182 * 183 * @since 4.6.0 184 */ 185 public static function alter_table_460() { 186 $table_name = self::get_table_name(); 187 188 if ( ! Common_Table::check_table_exists( $table_name ) ) { 189 return; 190 } 191 192 $connection = self::get_connection(); 193 194 // Check if request_id column already exists. 195 $columns = $connection->get_results( "SHOW COLUMNS FROM `{$table_name}` LIKE 'request_id'" ); 196 197 if ( empty( $columns ) ) { 198 // Add the request_id column. 199 $alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `request_id` VARCHAR(50) NOT NULL DEFAULT '' AFTER `trigger_source`"; 200 $connection->query( $alter_sql ); 201 202 // Add index for the new column. 203 $index_sql = "ALTER TABLE `{$table_name}` ADD KEY `request_id` (`request_id`)"; 204 $connection->query( $index_sql ); 205 } 206 207 // Check if count column already exists. 208 $columns_count = $connection->get_results( "SHOW COLUMNS FROM `{$table_name}` LIKE 'count'" ); 209 210 if ( empty( $columns_count ) ) { 211 // Add the count column. 212 $alter_sql_count = "ALTER TABLE `{$table_name}` ADD COLUMN `count` INT NOT NULL DEFAULT 1 AFTER `hooks_management_id`"; 213 $connection->query( $alter_sql_count ); 214 } 215 } 167 216 } 168 217 } -
0-day-analytics/tags/4.6.0/classes/vendor/helpers/class-system-analytics.php
r3442115 r3448917 462 462 .advan-stat-label {font-weight:600;} 463 463 .advan-stat-value {font-size:16px;margin-top:3px;} 464 .advan-info-section {margin-top:20px; padding:10px; b ackground:#f9f9f9; border-radius:5px;}464 .advan-info-section {margin-top:20px; padding:10px; border-radius:5px;} 465 465 .advan-info-item {margin-bottom:5px;} 466 466 </style> -
0-day-analytics/tags/4.6.0/classes/vendor/lists/class-hooks-capture-list.php
r3442473 r3448917 411 411 if ( ! empty( $hooks_management_results ) ) { 412 412 $hooks_management_ids = array_column( $hooks_management_results, 'id' ); 413 $placeholders = implode( ',', array_fill( 0, count( $hooks_management_ids ), '%d' ) );414 $where_sql_parts[] = 'hooks_management_id IN (' . $placeholders . ')';415 $where_args = array_merge( $where_args, $hooks_management_ids );413 $placeholders = implode( ',', array_fill( 0, count( $hooks_management_ids ), '%d' ) ); 414 $where_sql_parts[] = 'hooks_management_id IN (' . $placeholders . ')'; 415 $where_args = array_merge( $where_args, $hooks_management_ids ); 416 416 } 417 417 } … … 745 745 $hook_label = Hooks_Management_Entity::get_hook_label( $item[ $column_name ] ); 746 746 $hook_name = '<code>' . \esc_html( $item[ $column_name ] ) . '</code>'; 747 $display = $hook_label ? '<strong>' . \esc_html( $hook_label ) . '</strong> ' . $hook_name : $hook_name; 747 748 // Make hook name a link to hooks management if hooks_management_id is available 749 if ( ! empty( $item['hooks_management_id'] ) ) { 750 $edit_url = \network_admin_url( 'admin.php?page=advan_hooks_management&action=edit&id=' . absint( $item['hooks_management_id'] ) ); 751 $hook_name = '<code><a href="' . \esc_url( $edit_url ) . '" title="' . \esc_attr__( 'Edit hook in Hooks Management', '0-day-analytics' ) . '">' . \esc_html( $item[ $column_name ] ) . '</a></code>'; 752 } 753 754 // Check for post-related hooks and add post type information. 755 $post_type_info = ''; 756 if ( self::is_post_related_hook( $item[ $column_name ] ) && ! empty( $item['parameters'] ) ) { 757 $post_type = self::extract_post_type_from_parameters( $item['parameters'] ); 758 if ( $post_type ) { 759 $post_type_info = ' <code style="font-weight: normal;">(<b>' . \esc_html( $post_type ) . '</b>)</code>'; 760 } 761 } 762 763 $display = $hook_label ? '<strong>' . \esc_html( $hook_label ) . $post_type_info . '</strong> ' . $hook_name : $hook_name; 748 764 749 765 // Add row actions. … … 764 780 ); 765 781 782 // Add disable hook action if applicable. 783 $actions = self::add_disable_hook_action( $actions, $item ); 784 766 785 return sprintf( 767 786 '%s %s', … … 781 800 case 'is_cli': 782 801 return ! empty( $item[ $column_name ] ) ? '<span class="dashicons dashicons-yes"></span>' : '<span class="dashicons dashicons-no"></span>'; 802 803 case 'count': 804 $count = isset( $item[ $column_name ] ) ? (int) $item[ $column_name ] : 1; 805 if ( $count > 1 ) { 806 return '<span class="badge badge-warning">' . \esc_html( $count ) . '</span>'; 807 } 808 return \esc_html( $count ); 783 809 784 810 case 'parameters': … … 864 890 echo '</tr>'; 865 891 } 892 893 /** 894 * Check if a hook is post-related. 895 * 896 * @param string $hook_name The hook name to check. 897 * 898 * @return bool True if post-related, false otherwise. 899 * 900 * @since 4.6.1 901 */ 902 private static function is_post_related_hook( string $hook_name ): bool { 903 $post_related_hooks = array( 904 'wp_insert_post', 905 'wp_update_post', 906 'wp_delete_post', 907 'save_post', 908 'publish_post', 909 'transition_post_status', 910 'before_delete_post', 911 'after_delete_post', 912 'post_updated', 913 'edit_post', 914 'delete_post', 915 ); 916 917 return in_array( $hook_name, $post_related_hooks, true ); 918 } 919 920 /** 921 * Extract post type from hook parameters. 922 * 923 * @param string $parameters_json JSON-encoded parameters. 924 * 925 * @return string|null Post type if found, null otherwise. 926 * 927 * @since 4.6.1 928 */ 929 private static function extract_post_type_from_parameters( string $parameters_json ): ?string { 930 if ( empty( $parameters_json ) ) { 931 return null; 932 } 933 934 $parameters = json_decode( $parameters_json, true ); 935 if ( ! is_array( $parameters ) || empty( $parameters ) ) { 936 return null; 937 } 938 939 // Try different parameter positions and structures. 940 foreach ( $parameters as $param ) { 941 // Check if parameter is an array/object with post_type. 942 if ( is_array( $param ) && isset( $param['post_type'] ) ) { 943 return $param['post_type']; 944 } 945 946 // Check if parameter is an object with post_type property. 947 if ( is_array( $param ) && isset( $param['__class__'] ) && isset( $param['post_type'] ) ) { 948 return $param['post_type']; 949 } 950 951 // Check if parameter is a post ID and try to get post type from database. 952 if ( is_numeric( $param ) && $param > 0 ) { 953 $post = \get_post( (int) $param ); 954 if ( $post && isset( $post->post_type ) ) { 955 return $post->post_type; 956 } 957 } 958 } 959 960 return null; 961 } 962 963 /** 964 * ======================================================================= 965 * NEW FEATURES: Clear All Logs Button & Disable Hook Actions 966 * ======================================================================= 967 */ 968 969 /** 970 * Initialize admin hooks for new features. 971 * 972 * @return void 973 */ 974 public static function init_admin_hooks() { 975 // Clear logs functionality. 976 \add_action( 'admin_notices', array( __CLASS__, 'render_clear_logs_button' ) ); 977 \add_action( 'admin_post_clear_hooks_logs', array( __CLASS__, 'handle_clear_logs' ) ); 978 979 // Disable/enable hook functionality. 980 \add_action( 'admin_post_disable_hook_capture', array( __CLASS__, 'handle_disable_hook' ) ); 981 \add_action( 'admin_post_enable_hook_capture', array( __CLASS__, 'handle_enable_hook' ) ); 982 } 983 984 /** 985 * Render the clear logs button in admin notices. 986 * 987 * @return void 988 */ 989 public static function render_clear_logs_button() { 990 $screen = \get_current_screen(); 991 992 if ( ! $screen || ! \in_array( $screen->id, array( '0-day_page_advan_hooks_capture' ), true ) ) { 993 return; 994 } 995 996 if ( ! \current_user_can( 'manage_options' ) ) { 997 return; 998 } 999 1000 $logs_count = self::get_logs_count(); 1001 if ( 0 === $logs_count ) { 1002 return; 1003 } 1004 1005 ?> 1006 <div class="notice"> 1007 <p> 1008 <strong><?php \esc_html_e( 'Hooks Capture', '0-day-analytics' ); ?></strong> 1009 <?php 1010 printf( 1011 /* translators: %d: number of logs */ 1012 \esc_html__( 'Currently tracking %d hook executions.', '0-day-analytics' ), 1013 \number_format_i18n( $logs_count ) 1014 ); 1015 ?> 1016 <a href="<?php echo \esc_url( \wp_nonce_url( \network_admin_url( 'admin-post.php?action=clear_hooks_logs' ), 'clear_hooks_logs' ) ); ?>" 1017 class="button button-secondary" 1018 onclick="return confirm('<?php \esc_attr_e( 'Are you sure you want to clear all hook logs? This action cannot be undone.', '0-day-analytics' ); ?>')"> 1019 <?php \esc_html_e( 'Clear All Logs', '0-day-analytics' ); ?> 1020 </a> 1021 </p> 1022 </div> 1023 <?php 1024 } 1025 1026 /** 1027 * Handle the clear logs action. 1028 * 1029 * @return void 1030 */ 1031 public static function handle_clear_logs() { 1032 if ( ! \current_user_can( 'manage_options' ) ) { 1033 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1034 } 1035 1036 \check_admin_referer( 'clear_hooks_logs' ); 1037 1038 try { 1039 // Use the proper architectural method to truncate the table. 1040 Common_Table::truncate_table( null, Hooks_Capture_Entity::get_table_name() ); 1041 1042 // Clear any cached data. 1043 \wp_cache_flush(); 1044 1045 // Add success message. 1046 \add_action( 1047 'admin_notices', 1048 function() { 1049 ?> 1050 <div class="notice notice-success is-dismissible"> 1051 <p><?php \esc_html_e( 'All hook logs have been cleared successfully.', '0-day-analytics' ); ?></p> 1052 </div> 1053 <?php 1054 } 1055 ); 1056 } catch ( \Exception $e ) { 1057 // Add error message. 1058 \add_action( 1059 'admin_notices', 1060 function() { 1061 ?> 1062 <div class="notice notice-error is-dismissible"> 1063 <p><?php \esc_html_e( 'Failed to clear hook logs. Please try again.', '0-day-analytics' ); ?></p> 1064 </div> 1065 <?php 1066 } 1067 ); 1068 } 1069 1070 // Redirect back to the hooks capture page. 1071 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1072 exit; 1073 } 1074 1075 /** 1076 * Get the total count of logs. 1077 * 1078 * @return int 1079 */ 1080 private static function get_logs_count() { 1081 // Use the proper architectural method through the entity class. 1082 return Hooks_Capture_Entity::count( '1=%d', array( 1 ) ); 1083 } 1084 1085 /** 1086 * Add disable/enable hook action to row actions. 1087 * 1088 * @param array $actions Existing actions. 1089 * @param array $item The current item. 1090 * @return array Modified actions. 1091 */ 1092 public static function add_disable_hook_action( $actions, $item ) { 1093 if ( ! empty( $item['hooks_management_id'] ) && \current_user_can( 'manage_options' ) ) { 1094 // Load the hook configuration to check if it's enabled or disabled. 1095 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $item['hooks_management_id'] ) ); 1096 if ( ! $hook_config ) { 1097 return $actions; 1098 } 1099 1100 $is_enabled = isset( $hook_config['enabled'] ) ? (bool) $hook_config['enabled'] : true; 1101 1102 if ( $is_enabled ) { 1103 // Hook is enabled, show disable action. 1104 $action_url = \wp_nonce_url( 1105 \network_admin_url( 'admin-post.php?action=disable_hook_capture&id=' . \absint( $item['hooks_management_id'] ) ), 1106 'disable_hook_capture_' . $item['hooks_management_id'] 1107 ); 1108 1109 $actions['disable_hook'] = \sprintf( 1110 '<a href="%s" onclick="return confirm(\'%s\')" style="color: #dc3232;">%s</a>', 1111 \esc_url( $action_url ), 1112 \esc_js( __( 'Are you sure you want to disable this hook? It will stop being captured.', '0-day-analytics' ) ), 1113 __( 'Disable Hook', '0-day-analytics' ) 1114 ); 1115 } else { 1116 // Hook is disabled, show enable action. 1117 $action_url = \wp_nonce_url( 1118 \network_admin_url( 'admin-post.php?action=enable_hook_capture&id=' . \absint( $item['hooks_management_id'] ) ), 1119 'enable_hook_capture_' . $item['hooks_management_id'] 1120 ); 1121 1122 $actions['enable_hook'] = \sprintf( 1123 '<a href="%s" onclick="return confirm(\'%s\')" style="color: #007cba;">%s</a>', 1124 \esc_url( $action_url ), 1125 \esc_js( __( 'Are you sure you want to enable this hook? It will start being captured again.', '0-day-analytics' ) ), 1126 __( 'Enable Hook', '0-day-analytics' ) 1127 ); 1128 } 1129 } 1130 1131 return $actions; 1132 } 1133 1134 /** 1135 * Handle disable hook action. 1136 * 1137 * @return void 1138 */ 1139 public static function handle_disable_hook() { 1140 if ( ! \current_user_can( 'manage_options' ) ) { 1141 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1142 } 1143 1144 $hook_id = isset( $_GET['id'] ) ? \absint( $_GET['id'] ) : 0; 1145 if ( ! $hook_id ) { 1146 \wp_die( \esc_html__( 'Invalid hook ID.', '0-day-analytics' ) ); 1147 } 1148 1149 \check_admin_referer( 'disable_hook_capture_' . $hook_id ); 1150 1151 // Load the hook configuration. 1152 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $hook_id ) ); 1153 if ( ! $hook_config ) { 1154 \wp_die( \esc_html__( 'Hook not found.', '0-day-analytics' ) ); 1155 } 1156 1157 // Disable the hook by setting enabled to 0. 1158 $result = Hooks_Management_Entity::insert( \array_merge( $hook_config, array( 'enabled' => 0 ) ) ); 1159 1160 if ( $result ) { 1161 // Clear cache to reflect changes. 1162 \do_action( 'advan_hooks_management_updated' ); 1163 1164 \add_action( 1165 'admin_notices', 1166 function() use ( $hook_config ) { 1167 ?> 1168 <div class="notice notice-success is-dismissible"> 1169 <p> 1170 <?php 1171 printf( 1172 /* translators: %s: hook name */ 1173 \esc_html__( 'Hook "%s" has been disabled successfully.', '0-day-analytics' ), 1174 \esc_html( $hook_config['hook_name'] ) 1175 ); 1176 ?> 1177 </p> 1178 </div> 1179 <?php 1180 } 1181 ); 1182 } else { 1183 \add_action( 1184 'admin_notices', 1185 function() { 1186 ?> 1187 <div class="notice notice-error is-dismissible"> 1188 <p><?php \esc_html_e( 'Failed to disable hook. Please try again.', '0-day-analytics' ); ?></p> 1189 </div> 1190 <?php 1191 } 1192 ); 1193 } 1194 1195 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1196 exit; 1197 } 1198 1199 /** 1200 * Handle enable hook action. 1201 * 1202 * @return void 1203 */ 1204 public static function handle_enable_hook() { 1205 if ( ! \current_user_can( 'manage_options' ) ) { 1206 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1207 } 1208 1209 $hook_id = isset( $_GET['id'] ) ? \absint( $_GET['id'] ) : 0; 1210 if ( ! $hook_id ) { 1211 \wp_die( \esc_html__( 'Invalid hook ID.', '0-day-analytics' ) ); 1212 } 1213 1214 \check_admin_referer( 'enable_hook_capture_' . $hook_id ); 1215 1216 // Load the hook configuration. 1217 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $hook_id ) ); 1218 if ( ! $hook_config ) { 1219 \wp_die( \esc_html__( 'Hook not found.', '0-day-analytics' ) ); 1220 } 1221 1222 // Enable the hook by setting enabled to 1. 1223 $result = Hooks_Management_Entity::insert( \array_merge( $hook_config, array( 'enabled' => 1 ) ) ); 1224 1225 if ( $result ) { 1226 // Clear cache to reflect changes. 1227 \do_action( 'advan_hooks_management_updated' ); 1228 1229 \add_action( 1230 'admin_notices', 1231 function() use ( $hook_config ) { 1232 ?> 1233 <div class="notice notice-success is-dismissible"> 1234 <p> 1235 <?php 1236 printf( 1237 /* translators: %s: hook name */ 1238 \esc_html__( 'Hook "%s" has been enabled successfully.', '0-day-analytics' ), 1239 \esc_html( $hook_config['hook_name'] ) 1240 ); 1241 ?> 1242 </p> 1243 </div> 1244 <?php 1245 } 1246 ); 1247 } else { 1248 \add_action( 1249 'admin_notices', 1250 function() { 1251 ?> 1252 <div class="notice notice-error is-dismissible"> 1253 <p><?php \esc_html_e( 'Failed to enable hook. Please try again.', '0-day-analytics' ); ?></p> 1254 </div> 1255 <?php 1256 } 1257 ); 1258 } 1259 1260 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1261 exit; 1262 } 866 1263 } 867 1264 } -
0-day-analytics/tags/4.6.0/readme.txt
r3442473 r3448917 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 4. 5.27 Stable tag: 4.6.0 8 8 License: GPLv3 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.txt … … 93 93 == Changelog == 94 94 95 = 4.6.0 = 96 * Hooks module improvements and small styling issues fixed. 97 95 98 = 4.5.2 = 96 99 * Fixes problems with hooks quick actions - enable/disable. Fixed problem with showing human-readable data, when core object is captured, but only its ID is present. -
0-day-analytics/trunk/advanced-analytics.php
r3442473 r3448917 11 11 * Plugin Name: 0 Day Analytics 12 12 * Description: Take full control of error log, crons, transients, plugins, requests, mails and DB tables. 13 * Version: 4. 5.213 * Version: 4.6.0 14 14 * Author: Stoil Dobrev 15 15 * Author URI: https://github.com/sdobreff/ … … 39 39 // Constants. 40 40 if ( ! defined( 'ADVAN_VERSION' ) ) { 41 define( 'ADVAN_VERSION', '4. 5.2' );41 define( 'ADVAN_VERSION', '4.6.0' ); 42 42 define( 'ADVAN_TEXTDOMAIN', '0-day-analytics' ); 43 43 define( 'ADVAN_NAME', '0 Day Analytics' ); -
0-day-analytics/trunk/classes/class-advanced-analytics.php
r3442115 r3448917 31 31 use ADVAN\Lists\Hooks_Capture_List; 32 32 use ADVAN\Lists\Hooks_Management_List; 33 use ADVAN\Controllers\Hooks_Capture;34 33 use ADVAN\Migration\Migration; 35 34 use ADVAN\Controllers\Pointers; 36 use ADVAN\Controllers\Cron_Jobs;37 35 use ADVAN\Helpers\Miscellaneous; 38 36 use ADVAN\Lists\Transients_List; … … 71 69 72 70 \add_action( 'admin_init', array( __CLASS__, 'plugin_redirect' ) ); 71 72 // Initialize hooks capture list admin features. 73 Hooks_Capture_List::init_admin_hooks(); 73 74 74 75 // Setup screen options. Needs to be here as admin_init hook is too late. Per page set is below. -
0-day-analytics/trunk/classes/migration/class-migration.php
r3413453 r3448917 252 252 } 253 253 } 254 255 /** 256 * Migrates the plugin up-to version 4.6.0 (adds request_id column to hooks capture). 257 * 258 * @return void 259 * 260 * @since 4.6.0 261 */ 262 public static function migrate_up_to_460() { 263 if ( \class_exists( '\\ADVAN\\Entities\\Hooks_Capture_Entity' ) ) { 264 if ( Common_Table::check_table_exists( \ADVAN\Entities\Hooks_Capture_Entity::get_table_name() ) && ! Common_Table::check_column( 'request_id', 'varchar(50)', \ADVAN\Entities\Hooks_Capture_Entity::get_table_name() ) ) { 265 \ADVAN\Entities\Hooks_Capture_Entity::alter_table_460(); 266 } 267 } 268 } 254 269 } 255 270 } -
0-day-analytics/trunk/classes/vendor/controllers/class-hooks-capture.php
r3442473 r3448917 31 31 32 32 /** 33 * Maximum depth for capturing nested hooks. 34 * 35 * @var int 36 * 37 * @since 4.5.0 38 */ 39 private const MAX_CAPTURE_DEPTH = 3; 40 41 /** 42 * Maximum number of hook logs to store in memory before forcing a commit. 43 * 44 * @var int 45 * 46 * @since 4.6.1 47 */ 48 private const MAX_MEMORY_LOGS = 1000; 49 50 /** 51 * Maximum size for parameter/output JSON strings (64KB). 52 * 53 * @var int 54 * 55 * @since 4.5.0 56 */ 57 private const MAX_JSON_SIZE = 65536; 58 59 /** 60 * Maximum length for string parameters before truncation. 61 * 62 * @var int 63 * 64 * @since 4.5.0 65 */ 66 private const MAX_STRING_LENGTH = 255; 67 68 /** 69 * Maximum depth for recursive array/object sanitization. 70 * 71 * @var int 72 * 73 * @since 4.5.0 74 */ 75 private const MAX_SANITIZE_DEPTH = 2; 76 77 /** 78 * Maximum number of backtrace frames to capture. 79 * 80 * @var int 81 * 82 * @since 4.5.0 83 */ 84 private const MAX_BACKTRACE_FRAMES = 3; 85 86 /** 87 * Maximum number of properties to capture from objects. 88 * 89 * @var int 90 * 91 * @since 4.5.0 92 */ 93 private const MAX_OBJECT_PROPERTIES = 50; 94 95 /** 96 * Maximum length for hook names. 97 * 98 * @var int 99 * 100 * @since 4.6.1 101 */ 102 private const MAX_HOOK_NAME_LENGTH = 255; 103 104 /** 33 105 * Array of hooks currently being captured to prevent infinite loops. 34 106 * … … 83 155 */ 84 156 private static $cache_dir_path = null; 157 158 /** 159 * Unique request ID for grouping hook calls per request. 160 * 161 * @var string|null 162 * 163 * @since 4.5.0 164 */ 165 private static $request_id = null; 166 167 /** 168 * In-memory storage for hook logs to deduplicate per request. 169 * 170 * @var array 171 * 172 * @since 4.6.0 173 */ 174 private static $hook_logs = array(); 85 175 86 176 /** … … 96 186 } 97 187 98 // In WP-CLI context, ensure hooks are attached properly 188 // Generate unique request ID for this execution. 189 self::$request_id = uniqid( 'req_', true ); 190 191 // self::debug_log( 'Initializing hooks capture', array( 'request_id' => self::$request_id ) ); 192 193 // In WP-CLI context, ensure hooks are attached properly. 99 194 if ( defined( 'WP_CLI' ) && WP_CLI ) { 100 195 self::attach_hooks_cli(); … … 109 204 // Re-attach hooks after cache clear to pick up changes. 110 205 \add_action( 'advan_hooks_management_updated', array( __CLASS__, 'detach_and_reattach_hooks' ) ); 206 207 // Commit hook logs at the end of the request. 208 \add_action( 'shutdown', array( __CLASS__, 'commit_hook_logs' ) ); 209 210 // ======================================================================= 211 // MEMORY POOL: Initialize memory pool for reusing structures 212 // ======================================================================= 213 self::init_memory_pool(); 214 215 // ======================================================================= 216 // ERROR RECOVERY: Setup error recovery for serialization failures 217 // ======================================================================= 218 \add_filter( 'advan_serialize_hook_data', array( __CLASS__, 'safe_json_encode' ), 10, 2 ); 219 \add_filter( 'advan_unserialize_hook_data', array( __CLASS__, 'safe_json_decode' ), 10, 2 ); 220 221 // Cleanup memory pool on shutdown. 222 \add_action( 'shutdown', array( __CLASS__, 'cleanup_memory_pool' ), 999 ); 111 223 } 112 224 … … 123 235 124 236 // Regenerate cache file with latest hooks configuration. 125 self::regenerate_cache_file(); 237 // Defer regeneration if WordPress isn't fully loaded yet. 238 if ( ! \did_action( 'init' ) ) { 239 \add_action( 'init', array( __CLASS__, 'regenerate_cache_file' ), 1 ); 240 } else { 241 self::regenerate_cache_file(); 242 } 126 243 } 127 244 … … 167 284 */ 168 285 public static function attach_hooks_cli() { 169 // In CLI context, always load from DB to ensure hooks are attached 286 // In CLI context, always load from DB to ensure hooks are attached. 170 287 self::$enabled_hooks = Hooks_Management_Entity::get_enabled_hooks(); 171 288 … … 176 293 // Attach monitoring to each enabled hook. 177 294 foreach ( self::$enabled_hooks as $hook_config ) { 178 if ( empty( $hook_config['hook_name'] ) ) {295 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 179 296 continue; 180 297 } … … 186 303 // Use a high number of accepted args to capture all parameters. 187 304 if ( 'action' === $hook_type ) { 188 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, 10);305 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, self::get_accepted_args() ); 189 306 } else { 190 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, 10);307 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, self::get_accepted_args() ); 191 308 } 192 309 } … … 224 341 // Attach monitoring to each enabled hook. 225 342 foreach ( self::$enabled_hooks as $hook_config ) { 226 if ( empty( $hook_config['hook_name'] ) ) {343 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 227 344 continue; 228 345 } … … 234 351 // Use a high number of accepted args to capture all parameters. 235 352 if ( 'action' === $hook_type ) { 236 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, 10);353 \add_action( $hook_name, array( __CLASS__, 'capture_action' ), $priority, self::get_accepted_args() ); 237 354 } else { 238 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, 10);355 \add_filter( $hook_name, array( __CLASS__, 'capture_filter' ), $priority, self::get_accepted_args() ); 239 356 } 240 357 } … … 286 403 */ 287 404 private static function log_hook( string $hook_name, string $hook_type, array $args, $output ) { 405 // Validate hook name for security. 406 if ( ! self::is_valid_hook_name( $hook_name ) ) { 407 return; 408 } 409 410 // ======================================================================= 411 // EARLY FILTERING: Filter out unwanted hooks before processing 412 // ======================================================================= 413 if ( ! self::should_capture_hook_early( $hook_name ) ) { 414 return; 415 } 416 417 // ======================================================================= 418 // SAMPLING: Apply sampling for high-frequency hooks to reduce storage 419 // ======================================================================= 420 if ( ! self::should_sample_hook( $hook_name ) ) { 421 return; 422 } 423 288 424 // Prevent infinite loops. 289 425 if ( isset( self::$capturing_hooks[ $hook_name ] ) ) { … … 292 428 293 429 // Prevent excessive nesting. 294 if ( self::$current_depth >= self:: $max_depth) {430 if ( self::$current_depth >= self::MAX_CAPTURE_DEPTH ) { 295 431 return; 296 432 } … … 335 471 336 472 // Limit parameter size (max 64KB). 337 if ( strlen( $parameters_json ) > 65536) {338 $parameters_json = substr( $parameters_json, 0, 65536) . '... [truncated]';473 if ( strlen( $parameters_json ) > self::MAX_JSON_SIZE ) { 474 $parameters_json = substr( $parameters_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 339 475 } 340 476 } … … 346 482 347 483 // Limit output size (max 64KB). 348 if ( strlen( $output_json ) > 65536) {349 $output_json = substr( $output_json, 0, 65536) . '... [truncated]';484 if ( strlen( $output_json ) > self::MAX_JSON_SIZE ) { 485 $output_json = substr( $output_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 350 486 } 351 487 } … … 357 493 $memory_usage = memory_get_usage() - $start_memory; 358 494 359 // Prepare log entry. 360 $log_entry = array( 361 'blog_id' => \is_multisite() ? \get_current_blog_id() : 0, 362 'user_id' => $user_id, 363 'user_login' => $user_login, 364 'trigger_source' => $trigger_source, 365 'hook_name' => $hook_name, 366 'hook_type' => $hook_type, 367 'parameters' => $parameters_json, 368 'output' => $output_json, 369 'backtrace' => \wp_json_encode( $backtrace ), 370 'execution_time' => $execution_time, 371 'memory_usage' => $memory_usage, 372 'is_cli' => (int) self::is_cli(), 373 'hooks_management_id' => self::get_hook_management_id( $hook_name ), 374 'date_added' => microtime( true ), 375 ); 376 377 // Insert asynchronously if possible, synchronously as fallback. 378 if ( function_exists( 'wp_schedule_single_event' ) ) { 379 // For very high-traffic hooks, consider batching. 380 Hooks_Capture_Entity::insert( $log_entry ); 495 // Collect performance metrics for monitoring. 496 self::collect_performance_metrics( $execution_time, $memory_usage, count( self::$hook_logs ) ); 497 498 // Create unique key for deduplication based on hook name and args. 499 // Use optimized key generation for better performance. 500 $key = self::generate_deduplication_key( $hook_name, $args ); 501 502 if ( ! isset( self::$hook_logs[ $key ] ) ) { 503 // Check if we've exceeded memory limits and force a commit if needed. 504 if ( count( self::$hook_logs ) >= self::MAX_MEMORY_LOGS ) { 505 self::commit_hook_logs(); 506 } 507 508 // Prepare log data. 509 $log_data = self::prepare_hook_log_data( $hook_name, $hook_type, $args, $output, $capture_args, $capture_output, $trigger_source, $user_id, $user_login, $backtrace, $execution_time, $memory_usage ); 510 511 // Store the log data in memory. 512 self::$hook_logs[ $key ] = $log_data; 381 513 } else { 382 Hooks_Capture_Entity::insert( $log_entry ); 514 // Increment count for duplicate hook calls. 515 ++self::$hook_logs[ $key ]['count']; 383 516 } 384 517 } finally { … … 465 598 */ 466 599 private static function get_backtrace(): array { 467 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace468 $trace = \debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 5);600 // Use Exception backtrace for better performance (faster than debug_backtrace). 601 $trace = ( new \Exception( '' ) )->getTrace(); 469 602 470 603 $simplified = array(); … … 484 617 ); 485 618 486 // Limit to 3frames for performance.487 if ( count( $simplified ) >= 3) {619 // Limit to configured number of frames for performance. 620 if ( count( $simplified ) >= self::MAX_BACKTRACE_FRAMES ) { 488 621 break; 489 622 } … … 491 624 492 625 return $simplified; 626 } 627 628 /** 629 * Generate optimized deduplication key for hook calls. 630 * 631 * @param string $hook_name Hook name. 632 * @param array $args Hook arguments. 633 * 634 * @return string Deduplication key. 635 * 636 * @since 4.6.1 637 */ 638 private static function generate_deduplication_key( string $hook_name, array $args ): string { 639 // For performance, use a simplified approach for common cases. 640 $arg_signature = ''; 641 642 // Limit to first few arguments to avoid expensive serialization. 643 $max_args = 3; 644 $arg_count = 0; 645 646 foreach ( $args as $arg ) { 647 if ( $arg_count >= $max_args ) { 648 break; 649 } 650 651 if ( is_scalar( $arg ) ) { 652 $arg_signature .= (string) $arg . '|'; 653 } elseif ( is_array( $arg ) ) { 654 $arg_signature .= 'array(' . count( $arg ) . ')|'; 655 } elseif ( is_object( $arg ) ) { 656 $arg_signature .= 'object(' . get_class( $arg ) . ')|'; 657 } else { 658 $arg_signature .= gettype( $arg ) . '|'; 659 } 660 661 ++$arg_count; 662 } 663 664 // Add count of remaining args if any. 665 if ( count( $args ) > $max_args ) { 666 $arg_signature .= '+' . ( count( $args ) - $max_args ) . 'more'; 667 } 668 669 return $hook_name . '_' . md5( $arg_signature ); 670 } 671 672 /** 673 * Get module health status. 674 * 675 * @return array Health status information. 676 * 677 * @since 4.6.1 678 */ 679 public static function get_health_status(): array { 680 return self::health_check(); 681 } 682 683 /** 684 * Force commit of pending hook logs. 685 * 686 * @return bool True on success. 687 * 688 * @since 4.6.1 689 */ 690 public static function force_commit(): bool { 691 if ( empty( self::$hook_logs ) ) { 692 return true; 693 } 694 695 self::commit_hook_logs(); 696 return empty( self::$hook_logs ); 697 } 698 699 /** 700 * Get current performance metrics. 701 * 702 * @return array Performance metrics. 703 * 704 * @since 4.6.1 705 */ 706 public static function get_performance_metrics(): array { 707 return array( 708 'memory_usage' => memory_get_usage( true ), 709 'peak_memory' => memory_get_peak_usage( true ), 710 'queued_logs' => count( self::$hook_logs ), 711 'max_logs' => self::MAX_MEMORY_LOGS, 712 'cache_enabled' => ! empty( self::get_cache_file_path() ), 713 'request_id' => self::$request_id, 714 ); 715 } 716 717 /** 718 * Perform health check for the hooks capture module. 719 * 720 * @return array Health check results. 721 * 722 * @since 4.6.1 723 */ 724 public static function health_check(): array { 725 $health = array( 726 'status' => 'healthy', 727 'issues' => array(), 728 'metrics' => array(), 729 'timestamp' => time(), 730 ); 731 732 // Check memory usage. 733 $memory_usage = memory_get_usage( true ); 734 $memory_limit = self::get_memory_limit_bytes(); 735 736 if ( $memory_limit > 0 && $memory_usage > $memory_limit * 0.8 ) { 737 $health['issues'][] = 'High memory usage detected'; 738 $health['status'] = 'warning'; 739 } 740 741 $health['metrics']['memory_usage'] = $memory_usage; 742 $health['metrics']['memory_limit'] = $memory_limit; 743 744 // Check hook logs count. 745 $log_count = count( self::$hook_logs ); 746 $health['metrics']['queued_logs'] = $log_count; 747 748 if ( $log_count > self::MAX_MEMORY_LOGS * 0.9 ) { 749 $health['issues'][] = 'Approaching memory log limit'; 750 $health['status'] = 'warning'; 751 } 752 753 // Check cache file status. 754 $cache_file = self::get_cache_file_path(); 755 if ( $cache_file ) { 756 $health['metrics']['cache_file_exists'] = file_exists( $cache_file ); 757 $health['metrics']['cache_file_readable'] = is_readable( $cache_file ); 758 759 if ( file_exists( $cache_file ) && is_readable( $cache_file ) ) { 760 $cache_content = file_get_contents( $cache_file ); 761 $health['metrics']['cache_file_valid'] = self::is_valid_cache_content( $cache_content ); 762 } 763 } 764 765 // Check database connectivity. 766 try { 767 $test_query = Hooks_Capture_Entity::load( '1=0' ); // Should return empty array. 768 $health['metrics']['database_connected'] = true; 769 } catch ( \Exception $e ) { 770 $health['issues'][] = 'Database connectivity issue: ' . $e->getMessage(); 771 $health['status'] = 'error'; 772 $health['metrics']['database_connected'] = false; 773 } 774 775 return $health; 776 } 777 778 /** 779 * Get memory limit in bytes. 780 * 781 * @return int Memory limit in bytes, or 0 if unlimited. 782 * 783 * @since 4.6.1 784 */ 785 private static function get_memory_limit_bytes(): int { 786 $memory_limit = ini_get( 'memory_limit' ); 787 788 if ( empty( $memory_limit ) || $memory_limit === '-1' ) { 789 return 0; // Unlimited. 790 } 791 792 $unit = strtolower( substr( $memory_limit, -1 ) ); 793 $value = (int) substr( $memory_limit, 0, -1 ); 794 795 switch ( $unit ) { 796 case 'g': 797 return $value * 1024 * 1024 * 1024; 798 case 'm': 799 return $value * 1024 * 1024; 800 case 'k': 801 return $value * 1024; 802 default: 803 return (int) $memory_limit; 804 } 805 } 806 807 /** 808 * Prepare hook log data for storage. 809 * 810 * @param string $hook_name Hook name. 811 * @param string $hook_type Hook type. 812 * @param array $args Hook arguments. 813 * @param mixed $output Hook output. 814 * @param bool $capture_args Whether to capture arguments. 815 * @param bool $capture_output Whether to capture output. 816 * @param string $trigger_source Trigger source. 817 * @param int $user_id User ID. 818 * @param string $user_login User login. 819 * @param array $backtrace Backtrace data. 820 * @param float $execution_time Execution time. 821 * @param int $memory_usage Memory usage. 822 * 823 * @return array Prepared log data. 824 * 825 * @since 4.6.1 826 */ 827 private static function prepare_hook_log_data( string $hook_name, string $hook_type, array $args, $output, bool $capture_args, bool $capture_output, string $trigger_source, int $user_id, string $user_login, array $backtrace, float $execution_time, int $memory_usage ): array { 828 // Capture parameters (with size limit for performance). 829 $parameters_json = ''; 830 if ( $capture_args && ! empty( $args ) ) { 831 $sanitized_args = self::sanitize_args( $args, $hook_name ); 832 $parameters_json = \wp_json_encode( $sanitized_args ); 833 834 // Limit parameter size (max 64KB). 835 if ( strlen( $parameters_json ) > self::MAX_JSON_SIZE ) { 836 $parameters_json = substr( $parameters_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 837 } 838 } 839 840 // Capture output (with size limit). 841 $output_json = ''; 842 if ( $capture_output && null !== $output ) { 843 $output_json = \wp_json_encode( self::sanitize_args( array( $output ) ) ); 844 845 // Limit output size (max 64KB). 846 if ( strlen( $output_json ) > self::MAX_JSON_SIZE ) { 847 $output_json = substr( $output_json, 0, self::MAX_JSON_SIZE ) . '... [truncated]'; 848 } 849 } 850 851 return array( 852 'blog_id' => \is_multisite() ? \get_current_blog_id() : 0, 853 'user_id' => $user_id, 854 'user_login' => $user_login, 855 'trigger_source' => $trigger_source, 856 'hook_name' => $hook_name, 857 'hook_type' => $hook_type, 858 'parameters' => $parameters_json, 859 'output' => $output_json, 860 'backtrace' => \wp_json_encode( $backtrace ), 861 'execution_time' => $execution_time, 862 'memory_usage' => $memory_usage, 863 'is_cli' => (int) self::is_cli(), 864 'hooks_management_id' => self::get_hook_management_id( $hook_name ), 865 'count' => 1, 866 'date_added' => microtime( true ), 867 ); 493 868 } 494 869 … … 612 987 if ( self::is_sensitive_key( (string) $key ) ) { 613 988 $sanitized[ $key ] = '[REDACTED - Sensitive Data]'; 614 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {615 $sanitized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';989 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 990 $sanitized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 616 991 } else { 617 992 $sanitized[ $key ] = $value; … … 643 1018 */ 644 1019 private static function sanitize_args_recursive( array $args, int $depth ) { 645 if ( $depth > 2) {1020 if ( $depth > self::MAX_SANITIZE_DEPTH ) { 646 1021 return '[nested array]'; 647 1022 } … … 654 1029 if ( self::is_sensitive_key( (string) $key ) ) { 655 1030 $sanitized[ $key ] = '[REDACTED - Sensitive Data]'; 656 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {657 $sanitized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';1031 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 1032 $sanitized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 658 1033 } else { 659 1034 $sanitized[ $key ] = $value; … … 682 1057 */ 683 1058 private static function normalize_object( $object, int $depth ) { 684 if ( $depth > 2) {1059 if ( $depth > self::MAX_SANITIZE_DEPTH ) { 685 1060 return '[nested object]'; 686 1061 } … … 708 1083 709 1084 // Limit to reasonable number of properties. 710 if ( \count( $properties ) > 50) {711 $properties = \array_slice( $properties, 0, 50, true );1085 if ( \count( $properties ) > self::MAX_OBJECT_PROPERTIES ) { 1086 $properties = \array_slice( $properties, 0, self::MAX_OBJECT_PROPERTIES, true ); 712 1087 $normalized['__truncated__'] = true; 713 1088 } … … 719 1094 if ( self::is_sensitive_key( (string) $key ) ) { 720 1095 $normalized[ $key ] = '[REDACTED - Sensitive Data]'; 721 } elseif ( is_string( $value ) && mb_strlen( $value ) > 255) {722 $normalized[ $key ] = mb_substr( $value, 0, 255) . '... (truncated)';1096 } elseif ( is_string( $value ) && mb_strlen( $value ) > self::MAX_STRING_LENGTH ) { 1097 $normalized[ $key ] = mb_substr( $value, 0, self::MAX_STRING_LENGTH ) . '... (truncated)'; 723 1098 } else { 724 1099 $normalized[ $key ] = $value; … … 869 1244 870 1245 foreach ( $enabled_hooks as $hook_config ) { 871 if ( empty( $hook_config['hook_name'] ) ) {1246 if ( empty( $hook_config['hook_name'] ) || ! self::is_valid_hook_name( $hook_config['hook_name'] ) ) { 872 1247 continue; 873 1248 } … … 884 1259 885 1260 if ( 'action' === $hook_type ) { 886 $content .= "add_action( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_action' ), {$escaped_priority}, 10);\n";1261 $content .= "add_action( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_action' ), {$escaped_priority}, " . self::get_accepted_args() . " );\n"; 887 1262 } else { 888 $content .= "add_filter( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_filter' ), {$escaped_priority}, 10);\n";1263 $content .= "add_filter( '{$escaped_hook_name}', array( '\\ADVAN\\Controllers\\Hooks_Capture', 'capture_filter' ), {$escaped_priority}, " . self::get_accepted_args() . " );\n"; 889 1264 } 890 1265 … … 954 1329 } 955 1330 956 // Check user capabilities (only if function is available).1331 // Check user capabilities (only if WordPress is fully loaded and function is available). 957 1332 if ( ! self::is_cli() ) { 958 if ( ! \function_exists( 'current_user_can' ) || ! \current_user_can( 'manage_options' ) ) { 1333 // During early WordPress loading, skip capability checks to avoid fatal errors. 1334 if ( ! \did_action( 'init' ) || ! \function_exists( 'current_user_can' ) || ! \function_exists( 'wp_get_current_user' ) ) { 959 1335 return false; 960 1336 } 1337 1338 if ( ! \current_user_can( 'manage_options' ) ) { 1339 return false; 1340 } 961 1341 } 962 1342 963 1343 return self::generate_cache_file(); 1344 } 1345 1346 /** 1347 * Get the number of arguments accepted by hook capture callbacks. 1348 * 1349 * @return int Number of accepted arguments. 1350 * 1351 * @since 4.5.0 1352 */ 1353 private static function get_accepted_args(): int { 1354 // Use a high number to capture all possible hook arguments. 1355 return 99; 964 1356 } 965 1357 … … 983 1375 984 1376 /** 985 * Delete cache file. 1377 * Maximum number of hook logs to store in memory before forcing a commit. 1378 * This prevents excessive memory usage on long-running requests. 1379 * 1380 * @var int 1381 * 1382 * @since 4.6.1 1383 */ 1384 private static $max_memory_logs = 1000; 1385 1386 /** 1387 * Commit accumulated hook logs to the database at the end of the request. 1388 * 1389 * @return void 1390 * 1391 * @since 4.6.0 1392 */ 1393 public static function commit_hook_logs() { 1394 if ( empty( self::$hook_logs ) ) { 1395 // self::debug_log( 'No hook logs to commit' ); 1396 return; 1397 } 1398 1399 $log_count = count( self::$hook_logs ); 1400 // self::debug_log( 'Committing hook logs', array( 'count' => $log_count, 'request_id' => self::$request_id ) ); 1401 1402 try { 1403 foreach ( self::$hook_logs as $log ) { 1404 $log_entry = array( 1405 'blog_id' => $log['blog_id'], 1406 'user_id' => $log['user_id'], 1407 'user_login' => $log['user_login'], 1408 'trigger_source' => $log['trigger_source'], 1409 'request_id' => self::$request_id, 1410 'hook_name' => $log['hook_name'], 1411 'hook_type' => $log['hook_type'], 1412 'parameters' => $log['parameters'], 1413 'output' => $log['output'], 1414 'backtrace' => $log['backtrace'], 1415 'execution_time' => $log['execution_time'], 1416 'memory_usage' => $log['memory_usage'], 1417 'is_cli' => $log['is_cli'], 1418 'hooks_management_id' => $log['hooks_management_id'], 1419 'count' => $log['count'], 1420 'date_added' => $log['date_added'], 1421 ); 1422 1423 Hooks_Capture_Entity::insert( $log_entry ); 1424 } 1425 } catch ( \Exception $e ) { 1426 // Log the error but don't let it break the request. 1427 self::debug_log( 'Failed to commit hook logs', array( 'error' => $e->getMessage() ) ); 1428 if ( function_exists( 'error_log' ) ) { 1429 \error_log( 'Hooks Capture: Failed to commit logs - ' . $e->getMessage() ); 1430 } 1431 } finally { 1432 // Always clear the logs after attempting to commit. 1433 self::$hook_logs = array(); 1434 } 1435 } 1436 1437 /** 1438 * Validate hook name for security. 1439 * 1440 * @param string $hook_name The hook name to validate. 1441 * 1442 * @return bool True if valid, false otherwise. 1443 * 1444 * @since 4.6.1 1445 */ 1446 private static function is_valid_hook_name( string $hook_name ): bool { 1447 // Basic validation: not empty, reasonable length, no dangerous characters. 1448 if ( empty( $hook_name ) || strlen( $hook_name ) > self::MAX_HOOK_NAME_LENGTH ) { 1449 return false; 1450 } 1451 1452 // Allow alphanumeric, underscores, hyphens, slashes, and dots. 1453 if ( ! preg_match( '/^[a-zA-Z0-9_\/\-\.]+$/', $hook_name ) ) { 1454 return false; 1455 } 1456 1457 return true; 1458 } 1459 1460 /** 1461 * Validate cache file content for security before evaluation. 1462 * 1463 * @param string $content Cache file content. 1464 * 1465 * @return bool True if content is safe to evaluate. 1466 * 1467 * @since 4.6.1 1468 */ 1469 private static function is_valid_cache_content( string $content ): bool { 1470 // Basic validation: check for expected PHP structure. 1471 if ( empty( $content ) ) { 1472 return false; 1473 } 1474 1475 // Check for HTML content (indicates corrupted cache file). 1476 if ( preg_match( '/<html|<head|<body|<div|<p|<span/i', $content ) ) { 1477 self::debug_log( 'Cache content contains HTML - file is corrupted' ); 1478 return false; 1479 } 1480 1481 // Check for dangerous PHP functions that shouldn't be in cache. 1482 $dangerous_patterns = array( 1483 '/exec\(/i', 1484 '/system\(/i', 1485 '/shell_exec\(/i', 1486 '/passthru\(/i', 1487 '/eval\(/i', 1488 '/include\(/i', 1489 '/require\(/i', 1490 '/file_get_contents\(/i', 1491 '/fopen\(/i', 1492 '/\$\w+\s*\(/', // Variable function calls. 1493 ); 1494 1495 foreach ( $dangerous_patterns as $pattern ) { 1496 if ( preg_match( $pattern, $content ) ) { 1497 self::debug_log( 'Cache content contains dangerous pattern', array( 'pattern' => $pattern ) ); 1498 return false; 1499 } 1500 } 1501 1502 // Check that content contains expected PHP structure. 1503 if ( ! preg_match( '/^<\?php/', $content ) ) { 1504 self::debug_log( 'Cache content does not start with PHP opening tag' ); 1505 return false; 1506 } 1507 1508 // Check for ABSPATH check (security measure). 1509 if ( ! preg_match( '/defined\s*\(\s*[\'"]ABSPATH[\'"]\s*\)/', $content ) ) { 1510 self::debug_log( 'Cache content missing ABSPATH security check' ); 1511 return false; 1512 } 1513 1514 // Check that it contains our class reference (ensures it's our cache file). 1515 if ( ! preg_match( '/ADVAN\\\\Controllers\\\\Hooks_Capture/', $content ) ) { 1516 self::debug_log( 'Cache content does not reference our class' ); 1517 return false; 1518 } 1519 1520 // Check that it contains hook registration calls. 1521 if ( ! preg_match( '/add_(?:action|filter)\s*\(/', $content ) ) { 1522 self::debug_log( 'Cache content does not contain hook registrations' ); 1523 return false; 1524 } 1525 1526 // Basic PHP syntax check - try to parse the content. 1527 try { 1528 // Remove PHP opening tag for tokenization. 1529 $code = preg_replace( '/^<\?php\s*/', '', $content ); 1530 if ( false === $code ) { 1531 self::debug_log( 'Failed to prepare code for syntax check' ); 1532 return false; 1533 } 1534 1535 // Use token_get_all to check for basic syntax validity. 1536 $tokens = token_get_all( '<?php ' . $code ); 1537 if ( empty( $tokens ) ) { 1538 self::debug_log( 'Cache content tokenization failed' ); 1539 return false; 1540 } 1541 } catch ( \Throwable $e ) { 1542 self::debug_log( 'Cache content syntax check failed', array( 'error' => $e->getMessage() ) ); 1543 return false; 1544 } 1545 1546 return true; 1547 } 1548 1549 /** 1550 * Collect performance metrics for monitoring. 1551 * 1552 * @param float $execution_time Hook execution time in seconds. 1553 * @param int $memory_usage Memory usage in bytes. 1554 * @param int $log_count Current number of logs in memory. 1555 * 1556 * @return void 1557 * 1558 * @since 4.6.1 1559 */ 1560 private static function collect_performance_metrics( float $execution_time, int $memory_usage, int $log_count ) { 1561 static $metrics = array( 1562 'total_execution_time' => 0.0, 1563 'total_memory_usage' => 0, 1564 'hook_count' => 0, 1565 'max_execution_time' => 0.0, 1566 'max_memory_usage' => 0, 1567 'avg_execution_time' => 0.0, 1568 'avg_memory_usage' => 0, 1569 ); 1570 1571 $metrics['total_execution_time'] += $execution_time; 1572 $metrics['total_memory_usage'] += $memory_usage; 1573 ++$metrics['hook_count']; 1574 1575 if ( $execution_time > $metrics['max_execution_time'] ) { 1576 $metrics['max_execution_time'] = $execution_time; 1577 } 1578 1579 if ( $memory_usage > $metrics['max_memory_usage'] ) { 1580 $metrics['max_memory_usage'] = $memory_usage; 1581 } 1582 1583 $metrics['avg_execution_time'] = $metrics['total_execution_time'] / $metrics['hook_count']; 1584 $metrics['avg_memory_usage'] = $metrics['total_memory_usage'] / $metrics['hook_count']; 1585 1586 // Log performance warnings if thresholds are exceeded. 1587 if ( $execution_time > 0.1 ) { // More than 100ms. 1588 self::debug_log( 1589 'Slow hook execution detected', 1590 array( 1591 'execution_time' => $execution_time, 1592 'memory_usage' => $memory_usage, 1593 'log_count' => $log_count, 1594 ) 1595 ); 1596 } 1597 1598 if ( $memory_usage > 1048576 ) { // More than 1MB. 1599 self::debug_log( 1600 'High memory usage detected', 1601 array( 1602 'execution_time' => $execution_time, 1603 'memory_usage' => $memory_usage, 1604 'log_count' => $log_count, 1605 ) 1606 ); 1607 } 1608 1609 if ( $log_count > self::MAX_MEMORY_LOGS * 0.8 ) { // 80% of max capacity. 1610 self::debug_log( 1611 'Approaching memory log limit', 1612 array( 1613 'log_count' => $log_count, 1614 'max_logs' => self::MAX_MEMORY_LOGS, 1615 ) 1616 ); 1617 } 1618 } 1619 1620 /** 1621 * Log debug information for troubleshooting. 1622 * 1623 * @param string $message Debug message. 1624 * @param array $context Additional context data. 1625 * 1626 * @return void 1627 * 1628 * @since 4.6.1 1629 */ 1630 private static function debug_log( string $message, array $context = array() ) { 1631 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { 1632 $log_message = '[Hooks Capture] ' . $message; 1633 if ( ! empty( $context ) ) { 1634 $log_message .= ' ' . wp_json_encode( $context ); 1635 } 1636 error_log( $log_message ); 1637 } 1638 } 1639 1640 /** 1641 * Delete the existing cache file. 986 1642 * 987 1643 * @return bool True on success, false on failure. … … 998 1654 return @unlink( $cache_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.WP.AlternativeFunctions.unlink_unlink 999 1655 } 1656 1657 /** 1658 * ======================================================================= 1659 * NEW FEATURES: Early Filtering, Memory Pool, Error Recovery, Sampling 1660 * ======================================================================= 1661 */ 1662 1663 /** 1664 * Initialize memory pool for reusing structures. 1665 * 1666 * @return void 1667 * 1668 * @since latest 1669 */ 1670 private static function init_memory_pool() { 1671 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1672 $GLOBALS['advan_memory_pool'] = array( 1673 'backtrace_cache' => array(), 1674 'sanitized_data' => array(), 1675 'json_cache' => array(), 1676 'pool_size' => 0, 1677 'max_pool_size' => 100 * 1024 * 1024, // 100MB limit. 1678 ); 1679 } 1680 } 1681 1682 /** 1683 * Get cached backtrace. 1684 * 1685 * @param string $key Cache key. 1686 * 1687 * @return array|null Cached backtrace or null if not found. 1688 * 1689 * @since latest 1690 */ 1691 private static function get_cached_backtrace( string $key ) { 1692 if ( isset( $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ] ) ) { 1693 return $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ]; 1694 } 1695 return null; 1696 } 1697 1698 /** 1699 * Set cached backtrace. 1700 * 1701 * @param string $key Cache key. 1702 * @param array $backtrace Backtrace data. 1703 * 1704 * @return void 1705 * 1706 * @since latest 1707 */ 1708 private static function set_cached_backtrace( string $key, array $backtrace ) { 1709 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1710 return; 1711 } 1712 1713 // Check pool size limit. 1714 $backtrace_size = strlen( \wp_json_encode( $backtrace ) ); 1715 if ( $GLOBALS['advan_memory_pool']['pool_size'] + $backtrace_size > $GLOBALS['advan_memory_pool']['max_pool_size'] ) { 1716 // Clear oldest entries if pool is full. 1717 array_shift( $GLOBALS['advan_memory_pool']['backtrace_cache'] ); 1718 } 1719 1720 $GLOBALS['advan_memory_pool']['backtrace_cache'][ $key ] = $backtrace; 1721 $GLOBALS['advan_memory_pool']['pool_size'] += $backtrace_size; 1722 } 1723 1724 /** 1725 * Get cached JSON. 1726 * 1727 * @param string $key Cache key. 1728 * 1729 * @return string|null Cached JSON or null if not found. 1730 * 1731 * @since latest 1732 */ 1733 private static function get_cached_json( string $key ) { 1734 if ( isset( $GLOBALS['advan_memory_pool']['json_cache'][ $key ] ) ) { 1735 return $GLOBALS['advan_memory_pool']['json_cache'][ $key ]; 1736 } 1737 return null; 1738 } 1739 1740 /** 1741 * Set cached JSON. 1742 * 1743 * @param string $key Cache key. 1744 * @param string $json JSON data. 1745 * 1746 * @return void 1747 * 1748 * @since latest 1749 */ 1750 private static function set_cached_json( string $key, string $json ) { 1751 if ( ! isset( $GLOBALS['advan_memory_pool'] ) ) { 1752 return; 1753 } 1754 1755 $json_size = strlen( $json ); 1756 if ( $GLOBALS['advan_memory_pool']['pool_size'] + $json_size > $GLOBALS['advan_memory_pool']['max_pool_size'] ) { 1757 array_shift( $GLOBALS['advan_memory_pool']['json_cache'] ); 1758 } 1759 1760 $GLOBALS['advan_memory_pool']['json_cache'][ $key ] = $json; 1761 $GLOBALS['advan_memory_pool']['pool_size'] += $json_size; 1762 } 1763 1764 /** 1765 * Cleanup memory pool on shutdown. 1766 * 1767 * @return void 1768 * 1769 * @since latest 1770 */ 1771 public static function cleanup_memory_pool() { 1772 unset( $GLOBALS['advan_memory_pool'] ); 1773 } 1774 1775 /** 1776 * Early filtering: Check if hook should be captured before processing. 1777 * 1778 * @param string $hook_name The hook name to check. 1779 * @return bool True if hook should be captured, false otherwise. 1780 */ 1781 private static function should_capture_hook_early( string $hook_name ): bool { 1782 static $filtered_hooks = null; 1783 1784 // Initialize filtered hooks list on first call 1785 if ( null === $filtered_hooks ) { 1786 $filtered_hooks = apply_filters( 1787 'advan_excluded_hooks', 1788 array( 1789 // WordPress core hooks that are too frequent/noisy. 1790 'gettext', 1791 'gettext_with_context', 1792 'ngettext', 1793 'ngettext_with_context', 1794 'locale', 1795 'override_load_textdomain', 1796 'load_textdomain', 1797 'unload_textdomain', 1798 1799 // Option hooks (very frequent). 1800 'pre_option_*', 1801 'default_option_*', 1802 'option_*', 1803 1804 // Transient hooks (very frequent). 1805 'pre_transient_*', 1806 'transient_*', 1807 'set_transient_*', 1808 'delete_transient_*', 1809 1810 // Cache hooks (very frequent). 1811 'pre_cache_*', 1812 'cache_*', 1813 1814 // WP_Query hooks (can be very frequent). 1815 'pre_get_posts', 1816 'posts_where', 1817 'posts_join', 1818 'posts_orderby', 1819 'posts_fields', 1820 'posts_clauses', 1821 'posts_request', 1822 'posts_results', 1823 'posts_pre_query', 1824 1825 // Meta hooks (very frequent). 1826 'get_*_metadata', 1827 'update_*_metadata', 1828 'add_*_metadata', 1829 'delete_*_metadata', 1830 1831 // User hooks (frequent in some contexts). 1832 'get_user_metadata', 1833 'update_user_metadata', 1834 1835 // Comment hooks (can be frequent). 1836 'wp_update_comment_count', 1837 'pre_get_comments', 1838 'comments_clauses', 1839 1840 // Taxonomy hooks. 1841 'get_terms', 1842 'get_term', 1843 'get_*_terms', 1844 ) 1845 ); 1846 } 1847 1848 // Check if hook matches any filtered pattern. 1849 foreach ( $filtered_hooks as $pattern ) { 1850 if ( fnmatch( $pattern, $hook_name ) ) { 1851 return false; // Don't capture this hook. 1852 } 1853 } 1854 1855 return true; 1856 } 1857 1858 /** 1859 * Sampling: Check if hook should be sampled (for high-frequency hooks). 1860 * 1861 * @param string $hook_name The hook name to check. 1862 * 1863 * @return bool True if hook should be captured, false if skipped for sampling. 1864 * 1865 * @since latest 1866 */ 1867 private static function should_sample_hook( string $hook_name ): bool { 1868 static $hook_counters = array(); 1869 static $sampling_rates = null; 1870 1871 // Initialize sampling rates on first call. 1872 if ( null === $sampling_rates ) { 1873 $sampling_rates = apply_filters( 1874 'advan_hook_sampling_rates', 1875 array( 1876 // Sample every Nth call for these hooks. 1877 'option_*' => 100, // Sample 1% of option hooks. 1878 'transient_*' => 50, // Sample 2% of transient hooks. 1879 'gettext*' => 20, // Sample 5% of gettext hooks. 1880 'get_*_metadata' => 10, // Sample 10% of metadata hooks. 1881 'pre_get_posts' => 5, // Sample 20% of query hooks. 1882 ) 1883 ); 1884 } 1885 1886 // Check if this hook should be sampled. 1887 foreach ( $sampling_rates as $pattern => $rate ) { 1888 if ( fnmatch( $pattern, $hook_name ) ) { 1889 // Initialize counter for this hook. 1890 if ( ! isset( $hook_counters[ $hook_name ] ) ) { 1891 $hook_counters[ $hook_name ] = 0; 1892 } 1893 1894 ++$hook_counters[ $hook_name ]; 1895 1896 // Only capture if counter is multiple of rate. 1897 if ( $hook_counters[ $hook_name ] % 0 !== $rate ) { 1898 return false; // Skip this call. 1899 } 1900 1901 break; // Found matching pattern, no need to check others. 1902 } 1903 } 1904 1905 return true; 1906 } 1907 1908 /** 1909 * Safe JSON encoding with fallback for error recovery. 1910 * 1911 * @param mixed $data Data to encode. 1912 * @param mixed $fallback Fallback data if encoding fails. 1913 * 1914 * @return string JSON encoded data or fallback. 1915 * 1916 * @since latest 1917 */ 1918 public static function safe_json_encode( $data, $fallback = null ) { 1919 try { 1920 $encoded = \wp_json_encode( $data ); 1921 if ( false === $encoded ) { 1922 throw new \Exception( 'JSON encoding failed' ); 1923 } 1924 return $encoded; 1925 } catch ( \Exception $e ) { 1926 // Log the error. 1927 if ( function_exists( 'error_log' ) ) { 1928 error_log( 'Hooks Capture: JSON encoding failed - ' . $e->getMessage() ); 1929 } 1930 1931 // Return fallback or simplified data. 1932 if ( null !== $fallback ) { 1933 return \wp_json_encode( $fallback ); 1934 } 1935 1936 // Create a simplified version. 1937 if ( is_array( $data ) ) { 1938 return \wp_json_encode( 1939 array( 1940 'error' => 'JSON encoding failed', 1941 'type' => 'array', 1942 'count' => count( $data ), 1943 ) 1944 ); 1945 } elseif ( is_object( $data ) ) { 1946 return \wp_json_encode( 1947 array( 1948 'error' => 'JSON encoding failed', 1949 'type' => 'object', 1950 'class' => get_class( $data ), 1951 ) 1952 ); 1953 } else { 1954 return \wp_json_encode( 1955 array( 1956 'error' => 'JSON encoding failed', 1957 'type' => gettype( $data ), 1958 'content' => substr( (string) $data, 0, 100 ), 1959 ) 1960 ); 1961 } 1962 } 1963 } 1964 1965 /** 1966 * Safe JSON decoding with error handling for error recovery. 1967 * 1968 * @param string $data Data to decode. 1969 * @param mixed $fallback Fallback data if decoding fails. 1970 * 1971 * @return mixed Decoded data or fallback. 1972 * 1973 * @since latest 1974 */ 1975 public static function safe_json_decode( string $data, $fallback = null ) { 1976 if ( empty( $data ) ) { 1977 return $fallback ?: array(); 1978 } 1979 1980 try { 1981 $decoded = \wp_json_decode( $data, true ); 1982 if ( null === $decoded && 'null' !== $data ) { 1983 throw new \Exception( 'JSON decoding failed' ); 1984 } 1985 return $decoded; 1986 } catch ( \Exception $e ) { 1987 // Log the error. 1988 if ( function_exists( 'error_log' ) ) { 1989 error_log( 'Hooks Capture: JSON decoding failed - ' . $e->getMessage() ); 1990 } 1991 1992 // Return fallback. 1993 return $fallback ?: array( 'error' => 'JSON decoding failed' ); 1994 } 1995 } 1996 1997 /** 1998 * Safe serialization with fallback for error recovery. 1999 * DEPRECATED: Use safe_json_encode() instead for security. 2000 * 2001 * @param mixed $data Data to serialize. 2002 * @param mixed $fallback Fallback data if serialization fails. 2003 * 2004 * @return string Serialized data or fallback. 2005 * 2006 * @throws \Exception 2007 * 2008 * @deprecated Use safe_json_encode() instead 2009 * @since latest 2010 */ 2011 public static function safe_serialize( $data, $fallback = null ) { 2012 // Log deprecation warning. 2013 if ( function_exists( 'error_log' ) ) { 2014 error_log( 'Hooks Capture: safe_serialize() is deprecated. Use safe_json_encode() instead.' ); 2015 } 2016 2017 // Fall back to JSON encoding for security. 2018 return self::safe_json_encode( $data, $fallback ); 2019 } 2020 2021 /** 2022 * Safe unserialization with error handling for error recovery. 2023 * DEPRECATED: Use safe_json_decode() instead for security. 2024 * 2025 * @param string $data Data to unserialize. 2026 * @param mixed $fallback Fallback data if unserialization fails. 2027 * 2028 * @return mixed Unserialized data or fallback. 2029 * 2030 * @throws \Exception 2031 * 2032 * @deprecated Use safe_json_decode() instead 2033 * @since latest 2034 */ 2035 public static function safe_unserialize( string $data, $fallback = null ) { 2036 // Log deprecation warning. 2037 if ( function_exists( 'error_log' ) ) { 2038 error_log( 'Hooks Capture: safe_unserialize() is deprecated. Use safe_json_decode() instead.' ); 2039 } 2040 2041 // Fall back to JSON decoding for security. 2042 return self::safe_json_decode( $data, $fallback ); 2043 } 1000 2044 } 1001 2045 } -
0-day-analytics/trunk/classes/vendor/controllers/class-wp-mail-log.php
r3413453 r3448917 417 417 * @since 3.0.0 418 418 */ 419 private static function get_backtrace( $function_name = 'wp_mail' ) : ?array{419 private static function get_backtrace( $function_name = 'wp_mail' ) { 420 420 $backtrace_segment = null; 421 421 -
0-day-analytics/trunk/classes/vendor/entities/class-hooks-capture-entity.php
r3442115 r3448917 12 12 namespace ADVAN\Entities; 13 13 14 use ADVAN\Entities_Global\Common_Table; 15 14 16 // Exit if accessed directly. 15 17 if ( ! defined( 'ABSPATH' ) ) { … … 44 46 'hook_type' => 'string', 45 47 'trigger_source' => 'string', 48 'request_id' => 'string', 46 49 'user_id' => 'int', 47 50 'user_login' => 'string', … … 53 56 'is_cli' => 'int', 54 57 'hooks_management_id' => 'int', 58 'count' => 'int', 55 59 'date_added' => 'float', 56 60 ); … … 69 73 'hook_type' => 'action', 70 74 'trigger_source' => '', 75 'request_id' => '', 71 76 'user_id' => 0, 72 77 'user_login' => '', … … 78 83 'is_cli' => 0, 79 84 'hooks_management_id' => 0, 85 'count' => 1, 80 86 'date_added' => 0.0, 81 87 ); … … 110 116 hook_type VARCHAR(10) NOT NULL DEFAULT "action", 111 117 trigger_source VARCHAR(50) NOT NULL DEFAULT "", 118 request_id VARCHAR(50) NOT NULL DEFAULT "", 112 119 user_id BIGINT unsigned NOT NULL DEFAULT 0, 113 120 user_login VARCHAR(60) NOT NULL DEFAULT "", … … 119 126 is_cli TINYINT(1) NOT NULL DEFAULT 0, 120 127 hooks_management_id BIGINT unsigned NOT NULL DEFAULT 0, 128 count INT NOT NULL DEFAULT 1, 121 129 date_added DOUBLE NOT NULL DEFAULT 0, 122 130 PRIMARY KEY (id), … … 125 133 KEY `hook_type` (`hook_type`), 126 134 KEY `trigger_source` (`trigger_source`), 135 KEY `request_id` (`request_id`), 127 136 KEY `user_id` (`user_id`), 128 137 KEY `date_added` (`date_added`), … … 152 161 'memory_usage' => __( 'Memory', '0-day-analytics' ), 153 162 'is_cli' => __( 'CLI', '0-day-analytics' ), 163 'count' => __( 'Count', '0-day-analytics' ), 154 164 'parameters' => __( 'Parameters', '0-day-analytics' ), 155 165 ); … … 165 175 return $columns; 166 176 } 177 178 /** 179 * Alters the table to add request_id column for version 4.6.0. 180 * 181 * @return void 182 * 183 * @since 4.6.0 184 */ 185 public static function alter_table_460() { 186 $table_name = self::get_table_name(); 187 188 if ( ! Common_Table::check_table_exists( $table_name ) ) { 189 return; 190 } 191 192 $connection = self::get_connection(); 193 194 // Check if request_id column already exists. 195 $columns = $connection->get_results( "SHOW COLUMNS FROM `{$table_name}` LIKE 'request_id'" ); 196 197 if ( empty( $columns ) ) { 198 // Add the request_id column. 199 $alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `request_id` VARCHAR(50) NOT NULL DEFAULT '' AFTER `trigger_source`"; 200 $connection->query( $alter_sql ); 201 202 // Add index for the new column. 203 $index_sql = "ALTER TABLE `{$table_name}` ADD KEY `request_id` (`request_id`)"; 204 $connection->query( $index_sql ); 205 } 206 207 // Check if count column already exists. 208 $columns_count = $connection->get_results( "SHOW COLUMNS FROM `{$table_name}` LIKE 'count'" ); 209 210 if ( empty( $columns_count ) ) { 211 // Add the count column. 212 $alter_sql_count = "ALTER TABLE `{$table_name}` ADD COLUMN `count` INT NOT NULL DEFAULT 1 AFTER `hooks_management_id`"; 213 $connection->query( $alter_sql_count ); 214 } 215 } 167 216 } 168 217 } -
0-day-analytics/trunk/classes/vendor/helpers/class-system-analytics.php
r3442115 r3448917 462 462 .advan-stat-label {font-weight:600;} 463 463 .advan-stat-value {font-size:16px;margin-top:3px;} 464 .advan-info-section {margin-top:20px; padding:10px; b ackground:#f9f9f9; border-radius:5px;}464 .advan-info-section {margin-top:20px; padding:10px; border-radius:5px;} 465 465 .advan-info-item {margin-bottom:5px;} 466 466 </style> -
0-day-analytics/trunk/classes/vendor/lists/class-hooks-capture-list.php
r3442473 r3448917 411 411 if ( ! empty( $hooks_management_results ) ) { 412 412 $hooks_management_ids = array_column( $hooks_management_results, 'id' ); 413 $placeholders = implode( ',', array_fill( 0, count( $hooks_management_ids ), '%d' ) );414 $where_sql_parts[] = 'hooks_management_id IN (' . $placeholders . ')';415 $where_args = array_merge( $where_args, $hooks_management_ids );413 $placeholders = implode( ',', array_fill( 0, count( $hooks_management_ids ), '%d' ) ); 414 $where_sql_parts[] = 'hooks_management_id IN (' . $placeholders . ')'; 415 $where_args = array_merge( $where_args, $hooks_management_ids ); 416 416 } 417 417 } … … 745 745 $hook_label = Hooks_Management_Entity::get_hook_label( $item[ $column_name ] ); 746 746 $hook_name = '<code>' . \esc_html( $item[ $column_name ] ) . '</code>'; 747 $display = $hook_label ? '<strong>' . \esc_html( $hook_label ) . '</strong> ' . $hook_name : $hook_name; 747 748 // Make hook name a link to hooks management if hooks_management_id is available 749 if ( ! empty( $item['hooks_management_id'] ) ) { 750 $edit_url = \network_admin_url( 'admin.php?page=advan_hooks_management&action=edit&id=' . absint( $item['hooks_management_id'] ) ); 751 $hook_name = '<code><a href="' . \esc_url( $edit_url ) . '" title="' . \esc_attr__( 'Edit hook in Hooks Management', '0-day-analytics' ) . '">' . \esc_html( $item[ $column_name ] ) . '</a></code>'; 752 } 753 754 // Check for post-related hooks and add post type information. 755 $post_type_info = ''; 756 if ( self::is_post_related_hook( $item[ $column_name ] ) && ! empty( $item['parameters'] ) ) { 757 $post_type = self::extract_post_type_from_parameters( $item['parameters'] ); 758 if ( $post_type ) { 759 $post_type_info = ' <code style="font-weight: normal;">(<b>' . \esc_html( $post_type ) . '</b>)</code>'; 760 } 761 } 762 763 $display = $hook_label ? '<strong>' . \esc_html( $hook_label ) . $post_type_info . '</strong> ' . $hook_name : $hook_name; 748 764 749 765 // Add row actions. … … 764 780 ); 765 781 782 // Add disable hook action if applicable. 783 $actions = self::add_disable_hook_action( $actions, $item ); 784 766 785 return sprintf( 767 786 '%s %s', … … 781 800 case 'is_cli': 782 801 return ! empty( $item[ $column_name ] ) ? '<span class="dashicons dashicons-yes"></span>' : '<span class="dashicons dashicons-no"></span>'; 802 803 case 'count': 804 $count = isset( $item[ $column_name ] ) ? (int) $item[ $column_name ] : 1; 805 if ( $count > 1 ) { 806 return '<span class="badge badge-warning">' . \esc_html( $count ) . '</span>'; 807 } 808 return \esc_html( $count ); 783 809 784 810 case 'parameters': … … 864 890 echo '</tr>'; 865 891 } 892 893 /** 894 * Check if a hook is post-related. 895 * 896 * @param string $hook_name The hook name to check. 897 * 898 * @return bool True if post-related, false otherwise. 899 * 900 * @since 4.6.1 901 */ 902 private static function is_post_related_hook( string $hook_name ): bool { 903 $post_related_hooks = array( 904 'wp_insert_post', 905 'wp_update_post', 906 'wp_delete_post', 907 'save_post', 908 'publish_post', 909 'transition_post_status', 910 'before_delete_post', 911 'after_delete_post', 912 'post_updated', 913 'edit_post', 914 'delete_post', 915 ); 916 917 return in_array( $hook_name, $post_related_hooks, true ); 918 } 919 920 /** 921 * Extract post type from hook parameters. 922 * 923 * @param string $parameters_json JSON-encoded parameters. 924 * 925 * @return string|null Post type if found, null otherwise. 926 * 927 * @since 4.6.1 928 */ 929 private static function extract_post_type_from_parameters( string $parameters_json ): ?string { 930 if ( empty( $parameters_json ) ) { 931 return null; 932 } 933 934 $parameters = json_decode( $parameters_json, true ); 935 if ( ! is_array( $parameters ) || empty( $parameters ) ) { 936 return null; 937 } 938 939 // Try different parameter positions and structures. 940 foreach ( $parameters as $param ) { 941 // Check if parameter is an array/object with post_type. 942 if ( is_array( $param ) && isset( $param['post_type'] ) ) { 943 return $param['post_type']; 944 } 945 946 // Check if parameter is an object with post_type property. 947 if ( is_array( $param ) && isset( $param['__class__'] ) && isset( $param['post_type'] ) ) { 948 return $param['post_type']; 949 } 950 951 // Check if parameter is a post ID and try to get post type from database. 952 if ( is_numeric( $param ) && $param > 0 ) { 953 $post = \get_post( (int) $param ); 954 if ( $post && isset( $post->post_type ) ) { 955 return $post->post_type; 956 } 957 } 958 } 959 960 return null; 961 } 962 963 /** 964 * ======================================================================= 965 * NEW FEATURES: Clear All Logs Button & Disable Hook Actions 966 * ======================================================================= 967 */ 968 969 /** 970 * Initialize admin hooks for new features. 971 * 972 * @return void 973 */ 974 public static function init_admin_hooks() { 975 // Clear logs functionality. 976 \add_action( 'admin_notices', array( __CLASS__, 'render_clear_logs_button' ) ); 977 \add_action( 'admin_post_clear_hooks_logs', array( __CLASS__, 'handle_clear_logs' ) ); 978 979 // Disable/enable hook functionality. 980 \add_action( 'admin_post_disable_hook_capture', array( __CLASS__, 'handle_disable_hook' ) ); 981 \add_action( 'admin_post_enable_hook_capture', array( __CLASS__, 'handle_enable_hook' ) ); 982 } 983 984 /** 985 * Render the clear logs button in admin notices. 986 * 987 * @return void 988 */ 989 public static function render_clear_logs_button() { 990 $screen = \get_current_screen(); 991 992 if ( ! $screen || ! \in_array( $screen->id, array( '0-day_page_advan_hooks_capture' ), true ) ) { 993 return; 994 } 995 996 if ( ! \current_user_can( 'manage_options' ) ) { 997 return; 998 } 999 1000 $logs_count = self::get_logs_count(); 1001 if ( 0 === $logs_count ) { 1002 return; 1003 } 1004 1005 ?> 1006 <div class="notice"> 1007 <p> 1008 <strong><?php \esc_html_e( 'Hooks Capture', '0-day-analytics' ); ?></strong> 1009 <?php 1010 printf( 1011 /* translators: %d: number of logs */ 1012 \esc_html__( 'Currently tracking %d hook executions.', '0-day-analytics' ), 1013 \number_format_i18n( $logs_count ) 1014 ); 1015 ?> 1016 <a href="<?php echo \esc_url( \wp_nonce_url( \network_admin_url( 'admin-post.php?action=clear_hooks_logs' ), 'clear_hooks_logs' ) ); ?>" 1017 class="button button-secondary" 1018 onclick="return confirm('<?php \esc_attr_e( 'Are you sure you want to clear all hook logs? This action cannot be undone.', '0-day-analytics' ); ?>')"> 1019 <?php \esc_html_e( 'Clear All Logs', '0-day-analytics' ); ?> 1020 </a> 1021 </p> 1022 </div> 1023 <?php 1024 } 1025 1026 /** 1027 * Handle the clear logs action. 1028 * 1029 * @return void 1030 */ 1031 public static function handle_clear_logs() { 1032 if ( ! \current_user_can( 'manage_options' ) ) { 1033 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1034 } 1035 1036 \check_admin_referer( 'clear_hooks_logs' ); 1037 1038 try { 1039 // Use the proper architectural method to truncate the table. 1040 Common_Table::truncate_table( null, Hooks_Capture_Entity::get_table_name() ); 1041 1042 // Clear any cached data. 1043 \wp_cache_flush(); 1044 1045 // Add success message. 1046 \add_action( 1047 'admin_notices', 1048 function() { 1049 ?> 1050 <div class="notice notice-success is-dismissible"> 1051 <p><?php \esc_html_e( 'All hook logs have been cleared successfully.', '0-day-analytics' ); ?></p> 1052 </div> 1053 <?php 1054 } 1055 ); 1056 } catch ( \Exception $e ) { 1057 // Add error message. 1058 \add_action( 1059 'admin_notices', 1060 function() { 1061 ?> 1062 <div class="notice notice-error is-dismissible"> 1063 <p><?php \esc_html_e( 'Failed to clear hook logs. Please try again.', '0-day-analytics' ); ?></p> 1064 </div> 1065 <?php 1066 } 1067 ); 1068 } 1069 1070 // Redirect back to the hooks capture page. 1071 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1072 exit; 1073 } 1074 1075 /** 1076 * Get the total count of logs. 1077 * 1078 * @return int 1079 */ 1080 private static function get_logs_count() { 1081 // Use the proper architectural method through the entity class. 1082 return Hooks_Capture_Entity::count( '1=%d', array( 1 ) ); 1083 } 1084 1085 /** 1086 * Add disable/enable hook action to row actions. 1087 * 1088 * @param array $actions Existing actions. 1089 * @param array $item The current item. 1090 * @return array Modified actions. 1091 */ 1092 public static function add_disable_hook_action( $actions, $item ) { 1093 if ( ! empty( $item['hooks_management_id'] ) && \current_user_can( 'manage_options' ) ) { 1094 // Load the hook configuration to check if it's enabled or disabled. 1095 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $item['hooks_management_id'] ) ); 1096 if ( ! $hook_config ) { 1097 return $actions; 1098 } 1099 1100 $is_enabled = isset( $hook_config['enabled'] ) ? (bool) $hook_config['enabled'] : true; 1101 1102 if ( $is_enabled ) { 1103 // Hook is enabled, show disable action. 1104 $action_url = \wp_nonce_url( 1105 \network_admin_url( 'admin-post.php?action=disable_hook_capture&id=' . \absint( $item['hooks_management_id'] ) ), 1106 'disable_hook_capture_' . $item['hooks_management_id'] 1107 ); 1108 1109 $actions['disable_hook'] = \sprintf( 1110 '<a href="%s" onclick="return confirm(\'%s\')" style="color: #dc3232;">%s</a>', 1111 \esc_url( $action_url ), 1112 \esc_js( __( 'Are you sure you want to disable this hook? It will stop being captured.', '0-day-analytics' ) ), 1113 __( 'Disable Hook', '0-day-analytics' ) 1114 ); 1115 } else { 1116 // Hook is disabled, show enable action. 1117 $action_url = \wp_nonce_url( 1118 \network_admin_url( 'admin-post.php?action=enable_hook_capture&id=' . \absint( $item['hooks_management_id'] ) ), 1119 'enable_hook_capture_' . $item['hooks_management_id'] 1120 ); 1121 1122 $actions['enable_hook'] = \sprintf( 1123 '<a href="%s" onclick="return confirm(\'%s\')" style="color: #007cba;">%s</a>', 1124 \esc_url( $action_url ), 1125 \esc_js( __( 'Are you sure you want to enable this hook? It will start being captured again.', '0-day-analytics' ) ), 1126 __( 'Enable Hook', '0-day-analytics' ) 1127 ); 1128 } 1129 } 1130 1131 return $actions; 1132 } 1133 1134 /** 1135 * Handle disable hook action. 1136 * 1137 * @return void 1138 */ 1139 public static function handle_disable_hook() { 1140 if ( ! \current_user_can( 'manage_options' ) ) { 1141 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1142 } 1143 1144 $hook_id = isset( $_GET['id'] ) ? \absint( $_GET['id'] ) : 0; 1145 if ( ! $hook_id ) { 1146 \wp_die( \esc_html__( 'Invalid hook ID.', '0-day-analytics' ) ); 1147 } 1148 1149 \check_admin_referer( 'disable_hook_capture_' . $hook_id ); 1150 1151 // Load the hook configuration. 1152 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $hook_id ) ); 1153 if ( ! $hook_config ) { 1154 \wp_die( \esc_html__( 'Hook not found.', '0-day-analytics' ) ); 1155 } 1156 1157 // Disable the hook by setting enabled to 0. 1158 $result = Hooks_Management_Entity::insert( \array_merge( $hook_config, array( 'enabled' => 0 ) ) ); 1159 1160 if ( $result ) { 1161 // Clear cache to reflect changes. 1162 \do_action( 'advan_hooks_management_updated' ); 1163 1164 \add_action( 1165 'admin_notices', 1166 function() use ( $hook_config ) { 1167 ?> 1168 <div class="notice notice-success is-dismissible"> 1169 <p> 1170 <?php 1171 printf( 1172 /* translators: %s: hook name */ 1173 \esc_html__( 'Hook "%s" has been disabled successfully.', '0-day-analytics' ), 1174 \esc_html( $hook_config['hook_name'] ) 1175 ); 1176 ?> 1177 </p> 1178 </div> 1179 <?php 1180 } 1181 ); 1182 } else { 1183 \add_action( 1184 'admin_notices', 1185 function() { 1186 ?> 1187 <div class="notice notice-error is-dismissible"> 1188 <p><?php \esc_html_e( 'Failed to disable hook. Please try again.', '0-day-analytics' ); ?></p> 1189 </div> 1190 <?php 1191 } 1192 ); 1193 } 1194 1195 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1196 exit; 1197 } 1198 1199 /** 1200 * Handle enable hook action. 1201 * 1202 * @return void 1203 */ 1204 public static function handle_enable_hook() { 1205 if ( ! \current_user_can( 'manage_options' ) ) { 1206 \wp_die( \esc_html__( 'Insufficient permissions.', '0-day-analytics' ) ); 1207 } 1208 1209 $hook_id = isset( $_GET['id'] ) ? \absint( $_GET['id'] ) : 0; 1210 if ( ! $hook_id ) { 1211 \wp_die( \esc_html__( 'Invalid hook ID.', '0-day-analytics' ) ); 1212 } 1213 1214 \check_admin_referer( 'enable_hook_capture_' . $hook_id ); 1215 1216 // Load the hook configuration. 1217 $hook_config = Hooks_Management_Entity::load( 'id=%d', array( $hook_id ) ); 1218 if ( ! $hook_config ) { 1219 \wp_die( \esc_html__( 'Hook not found.', '0-day-analytics' ) ); 1220 } 1221 1222 // Enable the hook by setting enabled to 1. 1223 $result = Hooks_Management_Entity::insert( \array_merge( $hook_config, array( 'enabled' => 1 ) ) ); 1224 1225 if ( $result ) { 1226 // Clear cache to reflect changes. 1227 \do_action( 'advan_hooks_management_updated' ); 1228 1229 \add_action( 1230 'admin_notices', 1231 function() use ( $hook_config ) { 1232 ?> 1233 <div class="notice notice-success is-dismissible"> 1234 <p> 1235 <?php 1236 printf( 1237 /* translators: %s: hook name */ 1238 \esc_html__( 'Hook "%s" has been enabled successfully.', '0-day-analytics' ), 1239 \esc_html( $hook_config['hook_name'] ) 1240 ); 1241 ?> 1242 </p> 1243 </div> 1244 <?php 1245 } 1246 ); 1247 } else { 1248 \add_action( 1249 'admin_notices', 1250 function() { 1251 ?> 1252 <div class="notice notice-error is-dismissible"> 1253 <p><?php \esc_html_e( 'Failed to enable hook. Please try again.', '0-day-analytics' ); ?></p> 1254 </div> 1255 <?php 1256 } 1257 ); 1258 } 1259 1260 \wp_redirect( \network_admin_url( 'admin.php?page=advan_hooks_capture' ) ); 1261 exit; 1262 } 866 1263 } 867 1264 } -
0-day-analytics/trunk/readme.txt
r3442473 r3448917 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 4. 5.27 Stable tag: 4.6.0 8 8 License: GPLv3 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.txt … … 93 93 == Changelog == 94 94 95 = 4.6.0 = 96 * Hooks module improvements and small styling issues fixed. 97 95 98 = 4.5.2 = 96 99 * Fixes problems with hooks quick actions - enable/disable. Fixed problem with showing human-readable data, when core object is captured, but only its ID is present.
Note: See TracChangeset
for help on using the changeset viewer.