From 1f3098fb0b43965ac06544d57e2a5ca2db964eaf Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 14:42:30 -0400 Subject: [PATCH 1/6] save --- classes/Dispatcher.php | 3 + collectors/hooks_discovered.php | 168 +++++++++++++++++++++++++++++++ data/hooks_discovered.php | 35 +++++++ dispatchers/Html.php | 8 ++ output/html/hooks_discovered.php | 168 +++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+) create mode 100644 collectors/hooks_discovered.php create mode 100644 data/hooks_discovered.php create mode 100644 output/html/hooks_discovered.php diff --git a/classes/Dispatcher.php b/classes/Dispatcher.php index b24ce39cf..bc1bd4356 100644 --- a/classes/Dispatcher.php +++ b/classes/Dispatcher.php @@ -41,6 +41,9 @@ public function __construct( QM_Plugin $qm ) { if ( ! defined( 'QM_EDITOR_COOKIE' ) ) { define( 'QM_EDITOR_COOKIE', 'wp-query_monitor_editor_' . COOKIEHASH ); } + if ( ! defined( 'QM_MAX_DISCOVERED_HOOKS' ) ) { + define( 'QM_MAX_DISCOVERED_HOOKS', 100 ); + } add_action( 'init', array( $this, 'init' ) ); diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php new file mode 100644 index 000000000..8bab6f9c8 --- /dev/null +++ b/collectors/hooks_discovered.php @@ -0,0 +1,168 @@ +is_active( $id ) ) { + return; + } + + if ( is_array( $this->data->bounds ) && array_key_exists( $id, $this->data->bounds ) ) { + trigger_error( sprintf( + /* translators: %s: Hook discovery ID */ + esc_html__( 'Hook discovery ID `%s` already exists', 'query-monitor' ), + $id, + ), E_USER_NOTICE ); + + return; + } + + $this->maybe_add_all_callback(); + + if ( ! is_array( $this->data->active ) ) { + $this->data->active = array(); + } + + if ( ! is_array( $this->data->hooks ) ) { + $this->data->hooks = array(); + } + + if ( ! is_array( $this->data->counts ) ) { + $this->data->counts = array(); + } + + if ( ! is_array( $this->data->bounds ) ) { + $this->data->bounds = array(); + } + + $this->data->active[ $id ] = 1; + $this->data->hooks[ $id ] = array(); + $this->data->counts[ $id ] = 0; + $this->data->bounds[ $id ] = array( + 'start' => new QM_Backtrace(), + 'stop' => null, + ); + } + + public function action_listener_stop( $id ) { + if ( ! $this->is_active( $id ) && ! array_key_exists( $id, $this->data->hooks ) ) { + trigger_error( sprintf( + /* translators: %s: Hook discovery ID */ + esc_html__( 'Hook discovery starting bound for `%s` has not been set', 'query-monitor' ), + $id + ), E_USER_NOTICE ); + + return; + } + + unset( $this->data->active[ $id ] ); + $this->data->bounds[ $id ]['stop'] = new QM_Backtrace(); + + if ( $this->is_active() ) { + return; + } + + remove_action( 'all', array( $this, 'action_all' ) ); + } + + public function action_all( $var ) { + if ( ! $this->is_active() ) { + remove_action( 'all', array( $this, 'action_all' ) ); + + return $var; + } + + if ( in_array( current_action(), array( + 'qm/listen/start', + 'qm/listen/stop', + ) ) ) { + return $var; + } + + global $wp_actions; + + foreach ( array_keys( $this->data->active ) as $id ) { + end( $this->data->hooks[ $id ] ); + $last = current( $this->data->hooks[ $id ] ); + + if ( ! empty( $last ) && current_action() === $last['hook'] ) { + $i = key( $this->data->hooks[ $id ] ); + $this->data->hooks[ $id ][ $i ]['fires']++; + } else { + $this->data->hooks[ $id ][] = array( + 'hook' => current_action(), + 'is_action' => array_key_exists( current_action(), $wp_actions ), + 'fires' => 1, + ); + } + + if ( constant( 'QM_MAX_DISCOVERED_HOOKS' ) < ++$this->data->counts[ $id ] ) { + $this->action_listener_stop( $id ); + $this->data->bounds[ $id ]['terminated'] = true; + } + + return $var; + } + } + + protected function is_active( $id = false ) { + if ( false === $id ) { + return ! empty( $this->data->active ); + } + + return is_array( $this->data->active ) && array_key_exists( $id, $this->data->active ); + } + + protected function maybe_add_all_callback() { + if ( $this->is_active() ) { + return; + } + + add_action( 'all', array( $this, 'action_all' ) ); + } + +} + +# Load early to catch all hooks +QM_Collectors::add( new QM_Collector_Hooks_Discovered() ); diff --git a/data/hooks_discovered.php b/data/hooks_discovered.php new file mode 100644 index 000000000..07deb7766 --- /dev/null +++ b/data/hooks_discovered.php @@ -0,0 +1,35 @@ + + */ + public $active; + + /** + * @var array> + */ + public $bounds; + + /** + * @var array + */ + public $counts; + + /** + * @var array> + * @phpstan-var list + */ + public $hooks; + +} diff --git a/dispatchers/Html.php b/dispatchers/Html.php index 17a848a7e..a0a3cb62d 100644 --- a/dispatchers/Html.php +++ b/dispatchers/Html.php @@ -651,6 +651,14 @@ protected function after_output() { 'label' => __( 'Allow the wp-content/db.php file symlink to be put into place during activation. Set to false to prevent the symlink creation.', 'query-monitor' ), 'default' => true, ), + 'QM_DISABLED_HOOK_DISCOVERY' => array( + 'label' => __( 'Prevent hook discovery, to safeguard against performance impact in production.', 'query-monitor' ), + 'default' => false, + ), + 'QM_MAX_DISCOVERED_HOOKS' => array( + 'label' => __( 'Maximum number of hooks to discover before terminating.', 'query-monitor' ), + 'default' => 100, + ), ); /** diff --git a/output/html/hooks_discovered.php b/output/html/hooks_discovered.php new file mode 100644 index 000000000..baca0a58e --- /dev/null +++ b/output/html/hooks_discovered.php @@ -0,0 +1,168 @@ +menu( array( + 'title' => $this->name(), + ) ); + + return $menu; + } + + /** + * @return void + */ + public function output() { + /** @var QM_Data_Discovered_Hooks */ + $data = $this->collector->get_data(); + + $types = array( + 'action' => __( 'Action', 'query-monitor' ), + 'filter' => __( 'Filter', 'query-monitor' ), + ); + + if ( empty( $data->hooks ) ) { + $notice = __( 'No discovered hooks.', 'query-monitor' ); + + if ( defined( 'QM_DISABLED_HOOK_DISCOVERY' ) && constant( 'QM_DISABLED_HOOK_DISCOVERY' ) ) { + $notice = __( 'Hook discovery disabled.', 'query-monitor' ); + } + + $this->before_non_tabular_output(); + echo $this->build_notice( $notice ); // WPCS: XSS ok. + echo $this->after_non_tabular_output(); + + return; + } + + printf( '
', esc_attr( $this->collector->id() ) ); + + echo '
'; + + printf( '', esc_attr( $this->collector->id() ), esc_html( $this->name() ) ); + + echo ''; + echo ''; + printf( '', esc_html__( 'Label', 'query-monitor' ) ); + echo ''; + printf( '', esc_html__( 'Hook', 'query-monitor' ) ); + printf( '', esc_html__( 'Type', 'query-monitor' ) ); + echo ''; + echo ''; + + echo ''; + + foreach ( $data->hooks as $id => $hooks ) { + $trace_file_start = ''; + $trace_file_stop = ''; + + $bounds = $data->bounds[ $id ]; + + if ( is_a( $bounds['start'], QM_Backtrace::class ) ) { + $trace__start = $bounds['start']->get_trace(); + $trace_text__start = self::output_filename( '', $trace__start[0]['file'], $trace__start[0]['line'] ); + } + + $trace_text__stop = sprintf( '
Limit reached', constant( 'QM_MAX_DISCOVERED_HOOKS' ) ); + + if ( empty( $bounds['terminated'] ) && is_a( $bounds['stop'], QM_Backtrace::class ) ) { + $trace__stop = $bounds['stop']->get_trace(); + $trace_text__stop = self::output_filename( '', $trace__stop[0]['file'], $trace__stop[0]['line'] ); + } + + $first = true; + + foreach ( $hooks as $i => $hook ) { + $type = $types['filter']; + + if ( $hook['is_action'] ) { + $type = $types['action']; + } + + printf( '', strtolower( $type ) ); + + if ( $first ) { + $first = false; + + printf( + '', esc_html( ++$i ) ); + + echo ''; + + printf( '', $type ); + + echo ''; + } + } + + echo ''; + + echo '

%2$s

%s#%s%s
%s%s%s', + esc_attr( count( $hooks ) ), + esc_html( $id ), + $trace_text__start, + $trace_text__stop + ); + } + + printf( '%d'; + printf( '%s', esc_html( $hook['hook'] ) ); + if ( 1 < $hook['fires'] ) { + printf( '
Fired %d times', esc_html( $hook['fires'] ) ); + } + echo '
%s
'; + + echo '
'; + } + +} + +/** + * @param array $output + * @param QM_Collectors $collectors + * @return array + */ +function register_qm_output_html_discovered_hooks( array $output, QM_Collectors $collectors ) { + $collector = QM_Collectors::get( 'hooks_discovered' ); + if ( $collector ) { + $output['hooks_discovered'] = new QM_Output_Html_Hooks_Discovered( $collector ); + } + return $output; +} + +add_filter( 'qm/outputter/html', 'register_qm_output_html_discovered_hooks', 80, 2 ); From d8224a51c362849dd6b165edadfdd8a26f9f3e1c Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 14:51:15 -0400 Subject: [PATCH 2/6] save --- collectors/hooks_discovered.php | 23 +++++++++++++++++++++-- output/html/hooks_discovered.php | 16 ++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php index 8bab6f9c8..5137421d9 100644 --- a/collectors/hooks_discovered.php +++ b/collectors/hooks_discovered.php @@ -43,6 +43,10 @@ public function tear_down() { remove_action( 'shutdown', array( $this, 'action_shutdown' ) ); } + /** + * @param string $id + * @return void + */ public function action_listener_start( $id ) { if ( $this->is_active( $id ) ) { return; @@ -85,6 +89,10 @@ public function action_listener_start( $id ) { ); } + /** + * @param string $id + * @return void + */ public function action_listener_stop( $id ) { if ( ! $this->is_active( $id ) && ! array_key_exists( $id, $this->data->hooks ) ) { trigger_error( sprintf( @@ -106,6 +114,10 @@ public function action_listener_stop( $id ) { remove_action( 'all', array( $this, 'action_all' ) ); } + /** + * @param mixed $var + * @return void + */ public function action_all( $var ) { if ( ! $this->is_active() ) { remove_action( 'all', array( $this, 'action_all' ) ); @@ -146,14 +158,21 @@ public function action_all( $var ) { } } - protected function is_active( $id = false ) { - if ( false === $id ) { + /** + * @param string $id + * @return bool + */ + protected function is_active( $id = '' ) { + if ( empty( $id ) ) { return ! empty( $this->data->active ); } return is_array( $this->data->active ) && array_key_exists( $id, $this->data->active ); } + /** + * @return void + */ protected function maybe_add_all_callback() { if ( $this->is_active() ) { return; diff --git a/output/html/hooks_discovered.php b/output/html/hooks_discovered.php index baca0a58e..57ec27802 100644 --- a/output/html/hooks_discovered.php +++ b/output/html/hooks_discovered.php @@ -30,6 +30,10 @@ public function name() { return __( 'Discovered Hooks', 'query-monitor' ); } + /** + * @param array $menu + * @return array + */ public function action_output_menus( array $menu ) { $hooks = QM_Collectors::get( 'hooks' ); @@ -48,7 +52,7 @@ public function action_output_menus( array $menu ) { * @return void */ public function output() { - /** @var QM_Data_Discovered_Hooks */ + /** @var QM_Data_Hooks_Discovered */ $data = $this->collector->get_data(); $types = array( @@ -65,7 +69,7 @@ public function output() { $this->before_non_tabular_output(); echo $this->build_notice( $notice ); // WPCS: XSS ok. - echo $this->after_non_tabular_output(); + $this->after_non_tabular_output(); return; } @@ -88,8 +92,8 @@ public function output() { echo ''; foreach ( $data->hooks as $id => $hooks ) { - $trace_file_start = ''; - $trace_file_stop = ''; + $trace_text__start = ''; + $trace_text__stop = ''; $bounds = $data->bounds[ $id ]; @@ -121,14 +125,14 @@ public function output() { printf( '%s%s%s', - esc_attr( count( $hooks ) ), + absint( count( $hooks ) ), esc_html( $id ), $trace_text__start, $trace_text__stop ); } - printf( '%d', esc_html( ++$i ) ); + printf( '%d', absint( ++$i ) ); echo ''; printf( '%s', esc_html( $hook['hook'] ) ); From 080e629517d74cbb34a5f40e1d3fd27d55823eb8 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 15:20:45 -0400 Subject: [PATCH 3/6] save --- collectors/hooks_discovered.php | 19 ++++++++++++++++--- data/hooks_discovered.php | 4 ++-- output/html/hooks_discovered.php | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php index 5137421d9..bc315c2a7 100644 --- a/collectors/hooks_discovered.php +++ b/collectors/hooks_discovered.php @@ -116,7 +116,7 @@ public function action_listener_stop( $id ) { /** * @param mixed $var - * @return void + * @return mixed */ public function action_all( $var ) { if ( ! $this->is_active() ) { @@ -138,12 +138,12 @@ public function action_all( $var ) { end( $this->data->hooks[ $id ] ); $last = current( $this->data->hooks[ $id ] ); - if ( ! empty( $last ) && current_action() === $last['hook'] ) { + if ( ! empty( $last ) && current_action() === $last['name'] ) { $i = key( $this->data->hooks[ $id ] ); $this->data->hooks[ $id ][ $i ]['fires']++; } else { $this->data->hooks[ $id ][] = array( - 'hook' => current_action(), + 'name' => current_action(), 'is_action' => array_key_exists( current_action(), $wp_actions ), 'fires' => 1, ); @@ -158,6 +158,19 @@ public function action_all( $var ) { } } + /** + * @return void + */ + public function action_shutdown() { + if ( ! $this->is_active() ) { + return; + } + + foreach ( array_keys( $this->data->active ) as $id ) { + $this->action_listener_stop( $id ); + } + } + /** * @param string $id * @return bool diff --git a/data/hooks_discovered.php b/data/hooks_discovered.php index 07deb7766..9e15de6b6 100644 --- a/data/hooks_discovered.php +++ b/data/hooks_discovered.php @@ -23,9 +23,9 @@ class QM_Data_Hooks_Discovered extends QM_Data { public $counts; /** - * @var array> + * @var array> * @phpstan-var list diff --git a/output/html/hooks_discovered.php b/output/html/hooks_discovered.php index 57ec27802..4c4216e95 100644 --- a/output/html/hooks_discovered.php +++ b/output/html/hooks_discovered.php @@ -135,9 +135,9 @@ public function output() { printf( '%d', absint( ++$i ) ); echo ''; - printf( '%s', esc_html( $hook['hook'] ) ); + printf( '%s', esc_html( $hook['name'] ) ); if ( 1 < $hook['fires'] ) { - printf( '
Fired %d times', esc_html( $hook['fires'] ) ); + printf( '
Fired %d times', absint( $hook['fires'] ) ); } echo ''; From 14fe5b5936c898151772b56ab624a039a00857d4 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 15:29:39 -0400 Subject: [PATCH 4/6] save --- collectors/hooks_discovered.php | 10 +++++----- data/hooks_discovered.php | 7 +------ output/html/hooks_discovered.php | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php index bc315c2a7..15db9ecc0 100644 --- a/collectors/hooks_discovered.php +++ b/collectors/hooks_discovered.php @@ -111,16 +111,16 @@ public function action_listener_stop( $id ) { return; } - remove_action( 'all', array( $this, 'action_all' ) ); + remove_filter( 'all', array( $this, 'filter_all' ) ); } /** * @param mixed $var * @return mixed */ - public function action_all( $var ) { + public function filter_all( $var ) { if ( ! $this->is_active() ) { - remove_action( 'all', array( $this, 'action_all' ) ); + remove_filter( 'all', array( $this, 'filter_all' ) ); return $var; } @@ -166,7 +166,7 @@ public function action_shutdown() { return; } - foreach ( array_keys( $this->data->active ) as $id ) { + foreach ( $this->data->active as $id ) { $this->action_listener_stop( $id ); } } @@ -191,7 +191,7 @@ protected function maybe_add_all_callback() { return; } - add_action( 'all', array( $this, 'action_all' ) ); + add_filter( 'all', array( $this, 'filter_all' ) ); } } diff --git a/data/hooks_discovered.php b/data/hooks_discovered.php index 9e15de6b6..ee2938402 100644 --- a/data/hooks_discovered.php +++ b/data/hooks_discovered.php @@ -23,12 +23,7 @@ class QM_Data_Hooks_Discovered extends QM_Data { public $counts; /** - * @var array> - * @phpstan-var list + * @var array>> */ public $hooks; diff --git a/output/html/hooks_discovered.php b/output/html/hooks_discovered.php index 4c4216e95..96f0e72db 100644 --- a/output/html/hooks_discovered.php +++ b/output/html/hooks_discovered.php @@ -90,7 +90,7 @@ public function output() { echo ''; echo ''; - +error_log( print_r( $data->hooks, true ) ); foreach ( $data->hooks as $id => $hooks ) { $trace_text__start = ''; $trace_text__stop = ''; From 72238c19fefdb04ad0284b82306007161c24d419 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 15:36:23 -0400 Subject: [PATCH 5/6] save --- collectors/hooks_discovered.php | 6 ++++++ data/hooks_discovered.php | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php index 15db9ecc0..edf371bb1 100644 --- a/collectors/hooks_discovered.php +++ b/collectors/hooks_discovered.php @@ -5,6 +5,9 @@ * @package query-monitor */ +/** + * @extends QM_DataCollector + */ class QM_Collector_Hooks_Discovered extends QM_DataCollector { public $id = 'hooks_discovered'; @@ -13,6 +16,9 @@ public function get_storage(): QM_Data { return new QM_Data_Hooks_Discovered(); } + /** + * @return string + */ public function name() { return __( 'Discovered Hooks', 'query-monitor' ); } diff --git a/data/hooks_discovered.php b/data/hooks_discovered.php index ee2938402..d94aeb97d 100644 --- a/data/hooks_discovered.php +++ b/data/hooks_discovered.php @@ -6,7 +6,6 @@ */ class QM_Data_Hooks_Discovered extends QM_Data { - /** * @var array */ @@ -26,5 +25,4 @@ class QM_Data_Hooks_Discovered extends QM_Data { * @var array>> */ public $hooks; - } From e9ec8a35f0952f8063022b89e1dedf9286c5c9e3 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 12 Jul 2023 15:55:23 -0400 Subject: [PATCH 6/6] save --- collectors/hooks_discovered.php | 14 +++++++------- output/html/hooks_discovered.php | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/collectors/hooks_discovered.php b/collectors/hooks_discovered.php index edf371bb1..5fd62ce1b 100644 --- a/collectors/hooks_discovered.php +++ b/collectors/hooks_discovered.php @@ -53,7 +53,7 @@ public function tear_down() { * @param string $id * @return void */ - public function action_listener_start( $id ) { + public function action_listener_start( string $id ) { if ( $this->is_active( $id ) ) { return; } @@ -62,7 +62,7 @@ public function action_listener_start( $id ) { trigger_error( sprintf( /* translators: %s: Hook discovery ID */ esc_html__( 'Hook discovery ID `%s` already exists', 'query-monitor' ), - $id, + esc_html( $id ), ), E_USER_NOTICE ); return; @@ -99,12 +99,12 @@ public function action_listener_start( $id ) { * @param string $id * @return void */ - public function action_listener_stop( $id ) { + public function action_listener_stop( string $id ) { if ( ! $this->is_active( $id ) && ! array_key_exists( $id, $this->data->hooks ) ) { trigger_error( sprintf( /* translators: %s: Hook discovery ID */ esc_html__( 'Hook discovery starting bound for `%s` has not been set', 'query-monitor' ), - $id + esc_html( $id ) ), E_USER_NOTICE ); return; @@ -145,7 +145,7 @@ public function filter_all( $var ) { $last = current( $this->data->hooks[ $id ] ); if ( ! empty( $last ) && current_action() === $last['name'] ) { - $i = key( $this->data->hooks[ $id ] ); + $i = absint( key( $this->data->hooks[ $id ] ) ); $this->data->hooks[ $id ][ $i ]['fires']++; } else { $this->data->hooks[ $id ][] = array( @@ -172,7 +172,7 @@ public function action_shutdown() { return; } - foreach ( $this->data->active as $id ) { + foreach ( array_keys( $this->data->active ) as $id ) { $this->action_listener_stop( $id ); } } @@ -181,7 +181,7 @@ public function action_shutdown() { * @param string $id * @return bool */ - protected function is_active( $id = '' ) { + protected function is_active( string $id = '' ) { if ( empty( $id ) ) { return ! empty( $this->data->active ); } diff --git a/output/html/hooks_discovered.php b/output/html/hooks_discovered.php index 96f0e72db..e2a7a6b0a 100644 --- a/output/html/hooks_discovered.php +++ b/output/html/hooks_discovered.php @@ -90,7 +90,7 @@ public function output() { echo ''; echo ''; -error_log( print_r( $data->hooks, true ) ); + foreach ( $data->hooks as $id => $hooks ) { $trace_text__start = ''; $trace_text__stop = ''; @@ -118,7 +118,7 @@ public function output() { $type = $types['action']; } - printf( '', strtolower( $type ) ); + printf( '', esc_attr( strtolower( $type ) ) ); if ( $first ) { $first = false; @@ -129,7 +129,7 @@ public function output() { esc_html( $id ), $trace_text__start, $trace_text__stop - ); + ); // WPCS: XSS ok. } printf( '%d', absint( ++$i ) ); @@ -141,7 +141,7 @@ public function output() { } echo ''; - printf( '%s', $type ); + printf( '%s', esc_html( $type ) ); echo ''; }