This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,141 @@
<?php declare(strict_types = 1);
/**
* Admin screen collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Admin>
*/
class QM_Collector_Admin extends QM_DataCollector {
public $id = 'response';
public function get_storage(): QM_Data {
return new QM_Data_Admin();
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
$actions = array(
'current_screen',
'admin_notices',
'all_admin_notices',
'network_admin_notices',
'user_admin_notices',
);
if ( ! empty( $this->data->list_table ) ) {
$actions[] = $this->data->list_table['column_action'];
}
return $actions;
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
$filters = array();
if ( ! empty( $this->data->list_table ) ) {
$filters[] = $this->data->list_table['columns_filter'];
$filters[] = $this->data->list_table['sortables_filter'];
}
return $filters;
}
/**
* @return void
*/
public function process() {
/**
* @var string $pagenow
* @var ?WP_List_Table $wp_list_table
*/
global $pagenow, $wp_list_table;
$this->data->pagenow = $pagenow;
$this->data->typenow = $GLOBALS['typenow'] ?? '';
$this->data->taxnow = $GLOBALS['taxnow'] ?? '';
$this->data->hook_suffix = $GLOBALS['hook_suffix'] ?? '';
$this->data->current_screen = get_current_screen();
$screens = array(
'edit' => true,
'edit-comments' => true,
'edit-tags' => true,
'link-manager' => true,
'plugins' => true,
'plugins-network' => true,
'sites-network' => true,
'themes-network' => true,
'upload' => true,
'users' => true,
'users-network' => true,
);
if ( empty( $this->data->current_screen ) || ! isset( $screens[ $this->data->current_screen->base ] ) ) {
return;
}
# And now, WordPress' legendary inconsistency comes into play:
$columns = $this->data->current_screen->id;
$sortables = $this->data->current_screen->id;
$column = $this->data->current_screen->base;
if ( ! empty( $this->data->current_screen->taxonomy ) ) {
$column = $this->data->current_screen->taxonomy;
} elseif ( ! empty( $this->data->current_screen->post_type ) ) {
$column = $this->data->current_screen->post_type . '_posts';
}
if ( ! empty( $this->data->current_screen->post_type ) && empty( $this->data->current_screen->taxonomy ) ) {
$columns = $this->data->current_screen->post_type . '_posts';
}
if ( 'edit-comments' === $column ) {
$column = 'comments';
} elseif ( 'upload' === $column ) {
$column = 'media';
} elseif ( 'link-manager' === $column ) {
$column = 'link';
}
$list_table_data = array(
'columns_filter' => "manage_{$columns}_columns",
'sortables_filter' => "manage_{$sortables}_sortable_columns",
'column_action' => "manage_{$column}_custom_column",
);
if ( ! empty( $wp_list_table ) ) {
$list_table_data['class_name'] = get_class( $wp_list_table );
}
$this->data->list_table = $list_table_data;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_admin( array $collectors, QueryMonitor $qm ) {
$collectors['response'] = new QM_Collector_Admin();
return $collectors;
}
if ( is_admin() ) {
add_filter( 'qm/collectors', 'register_qm_collector_admin', 10, 2 );
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types = 1);
/**
* Enqueued scripts collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Collector_Assets_Scripts extends QM_Collector_Assets {
public $id = 'assets_scripts';
public function get_dependency_type() {
return 'scripts';
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
if ( is_admin() ) {
return array(
'admin_enqueue_scripts',
'admin_print_footer_scripts',
'admin_print_scripts',
);
} else {
return array(
'wp_enqueue_scripts',
'wp_print_footer_scripts',
'wp_print_scripts',
);
}
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'print_scripts_array',
'script_loader_src',
'script_loader_tag',
);
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_assets_scripts( array $collectors, QueryMonitor $qm ) {
$collectors['assets_scripts'] = new QM_Collector_Assets_Scripts();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_assets_scripts', 10, 2 );

View File

@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
/**
* Enqueued styles collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Collector_Assets_Styles extends QM_Collector_Assets {
public $id = 'assets_styles';
public function get_dependency_type() {
return 'styles';
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'print_styles_array',
'style_loader_src',
'style_loader_tag',
);
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_assets_styles( array $collectors, QueryMonitor $qm ) {
$collectors['assets_styles'] = new QM_Collector_Assets_Styles();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_assets_styles', 10, 2 );

View File

@ -0,0 +1,266 @@
<?php declare(strict_types = 1);
/**
* Block editor (née Gutenberg) collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Block_Editor>
*/
class QM_Collector_Block_Editor extends QM_DataCollector {
public $id = 'block_editor';
/**
* @var array<int, mixed[]>
*/
protected $block_context = array();
/**
* @var array<int, QM_Timer|false>
*/
protected $block_timing = array();
/**
* @var QM_Timer|null
*/
protected $block_timer = null;
public function get_storage(): QM_Data {
return new QM_Data_Block_Editor();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_filter( 'pre_render_block', array( $this, 'filter_pre_render_block' ), 9999, 2 );
add_filter( 'render_block_context', array( $this, 'filter_render_block_context' ), -9999, 2 );
add_filter( 'render_block_data', array( $this, 'filter_render_block_data' ), -9999 );
add_filter( 'render_block', array( $this, 'filter_render_block' ), 9999, 2 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'pre_render_block', array( $this, 'filter_pre_render_block' ), 9999 );
remove_filter( 'render_block_context', array( $this, 'filter_render_block_context' ), -9999 );
remove_filter( 'render_block_data', array( $this, 'filter_render_block_data' ), -9999 );
remove_filter( 'render_block', array( $this, 'filter_render_block' ), 9999 );
parent::tear_down();
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'allowed_block_types',
'allowed_block_types_all',
'block_editor_settings_all',
'block_type_metadata',
'block_type_metadata_settings',
'block_parser_class',
'pre_render_block',
'register_block_type_args',
'render_block_context',
'render_block_data',
'render_block',
'use_widgets_block_editor',
);
}
/**
* @param string|null $pre_render
* @param mixed[] $block
* @return string|null
*/
public function filter_pre_render_block( $pre_render, array $block ) {
if ( null !== $pre_render ) {
$this->block_timing[] = false;
}
return $pre_render;
}
/**
* @param mixed[] $context
* @param mixed[] $block
* @return mixed[]
*/
public function filter_render_block_context( array $context, array $block ) {
$this->block_context[] = $context;
return $context;
}
/**
* @param mixed[] $block
* @return mixed[]
*/
public function filter_render_block_data( array $block ) {
$this->block_timer = new QM_Timer();
$this->block_timer->start();
return $block;
}
/**
* @param string $block_content
* @param mixed[] $block
* @return string
*/
public function filter_render_block( $block_content, array $block ) {
if ( isset( $this->block_timer ) ) {
$this->block_timing[] = $this->block_timer->stop();
}
return $block_content;
}
public function process() {
global $_wp_current_template_content;
$this->data->block_editor_enabled = self::wp_block_editor_enabled();
if ( ! empty( $_wp_current_template_content ) ) {
// Full site editor:
$content = $_wp_current_template_content;
} elseif ( is_singular() ) {
// Post editor:
$content = get_post( get_queried_object_id() )->post_content;
} else {
// Nada:
return;
}
$this->data->post_has_blocks = self::wp_has_blocks( $content );
$this->data->post_blocks = self::wp_parse_blocks( $content );
$this->data->all_dynamic_blocks = self::wp_get_dynamic_block_names();
$this->data->total_blocks = 0;
$this->data->has_block_context = false;
$this->data->has_block_timing = false;
if ( $this->data->post_has_blocks ) {
$this->data->post_blocks = array_values( array_filter( array_map( array( $this, 'process_block' ), $this->data->post_blocks ) ) );
}
}
/**
* @param mixed[] $block
* @return mixed[]|null
*/
protected function process_block( array $block ) {
$context = array_shift( $this->block_context );
$timing = array_shift( $this->block_timing );
// Remove empty blocks caused by two consecutive line breaks in content
if ( ! $block['blockName'] && ! trim( $block['innerHTML'] ) ) {
return null;
}
$this->data->total_blocks++;
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$dynamic = false;
$callback = null;
if ( $block_type && $block_type->is_dynamic() ) {
$dynamic = true;
$callback = QM_Util::populate_callback( array(
'function' => $block_type->render_callback,
) );
}
$timing = array_shift( $this->block_timing );
$block['dynamic'] = $dynamic;
$block['callback'] = $callback;
$block['innerHTML'] = trim( $block['innerHTML'] );
$block['size'] = strlen( $block['innerHTML'] );
if ( $context ) {
$block['context'] = $context;
$this->data->has_block_context = true;
}
if ( $timing ) {
$block['timing'] = $timing->get_time();
$this->data->has_block_timing = true;
}
if ( ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = array_values( array_filter( array_map( array( $this, 'process_block' ), $block['innerBlocks'] ) ) );
}
return $block;
}
/**
* @return bool
*/
protected static function wp_block_editor_enabled() {
return ( function_exists( 'parse_blocks' ) || function_exists( 'gutenberg_parse_blocks' ) );
}
/**
* @param string $content
* @return bool
*/
protected static function wp_has_blocks( $content ) {
if ( function_exists( 'has_blocks' ) ) {
return has_blocks( $content );
} elseif ( function_exists( 'gutenberg_has_blocks' ) ) {
return gutenberg_has_blocks( $content );
}
return false;
}
/**
* @param string $content
* @return array<int, mixed>|null
*/
protected static function wp_parse_blocks( $content ) {
if ( function_exists( 'parse_blocks' ) ) {
return parse_blocks( $content );
} elseif ( function_exists( 'gutenberg_parse_blocks' ) ) {
return gutenberg_parse_blocks( $content );
}
return null;
}
/**
* @return array<int, string>|null
*/
protected static function wp_get_dynamic_block_names() {
if ( function_exists( 'get_dynamic_block_names' ) ) {
return get_dynamic_block_names();
}
return array();
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_block_editor( array $collectors, QueryMonitor $qm ) {
$collectors['block_editor'] = new QM_Collector_Block_Editor();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_block_editor', 10, 2 );

View File

@ -0,0 +1,130 @@
<?php declare(strict_types = 1);
/**
* Object cache collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Cache>
*/
class QM_Collector_Cache extends QM_DataCollector {
public $id = 'cache';
public function get_storage(): QM_Data {
return new QM_Data_Cache();
}
/**
* @return void
*/
public function process() {
global $wp_object_cache;
$this->data->has_object_cache = (bool) wp_using_ext_object_cache();
$this->data->cache_hit_percentage = 0;
if ( is_object( $wp_object_cache ) ) {
$object_vars = get_object_vars( $wp_object_cache );
if ( array_key_exists( 'cache_hits', $object_vars ) ) {
$this->data->stats['cache_hits'] = (int) $object_vars['cache_hits'];
}
if ( array_key_exists( 'cache_misses', $object_vars ) ) {
$this->data->stats['cache_misses'] = (int) $object_vars['cache_misses'];
}
$stats = array();
if ( method_exists( $wp_object_cache, 'getStats' ) ) {
$stats = $wp_object_cache->getStats();
} elseif ( array_key_exists( 'stats', $object_vars ) && is_array( $object_vars['stats'] ) ) {
$stats = $object_vars['stats'];
} elseif ( function_exists( 'wp_cache_get_stats' ) ) {
$stats = wp_cache_get_stats();
}
if ( ! empty( $stats ) ) {
if ( is_array( $stats ) && ! isset( $stats['get_hits'] ) && 1 === count( $stats ) ) {
$first_server = reset( $stats );
if ( isset( $first_server['get_hits'] ) ) {
$stats = $first_server;
}
}
foreach ( $stats as $key => $value ) {
if ( ! is_scalar( $value ) ) {
continue;
}
if ( ! is_string( $key ) ) {
continue;
}
$this->data->stats[ $key ] = $value;
}
}
if ( ! isset( $this->data->stats['cache_hits'] ) ) {
if ( isset( $this->data->stats['get_hits'] ) ) {
$this->data->stats['cache_hits'] = (int) $this->data->stats['get_hits'];
}
}
if ( ! isset( $this->data->stats['cache_misses'] ) ) {
if ( isset( $this->data->stats['get_misses'] ) ) {
$this->data->stats['cache_misses'] = (int) $this->data->stats['get_misses'];
}
}
}
if ( ! empty( $this->data->stats['cache_hits'] ) ) {
$total = $this->data->stats['cache_hits'];
if ( ! empty( $this->data->stats['cache_misses'] ) ) {
$total += $this->data->stats['cache_misses'];
}
$this->data->cache_hit_percentage = ( 100 / $total ) * $this->data->stats['cache_hits'];
}
$this->data->display_hit_rate_warning = ( 100 === $this->data->cache_hit_percentage );
if ( function_exists( 'extension_loaded' ) ) {
$this->data->object_cache_extensions = array_map( 'extension_loaded', array(
'Afterburner' => 'afterburner',
'APCu' => 'apcu',
'Redis' => 'redis',
'Relay' => 'relay',
'Memcache' => 'memcache',
'Memcached' => 'memcached',
) );
$this->data->opcode_cache_extensions = array_map( 'extension_loaded', array(
'APC' => 'APC',
'Zend OPcache' => 'Zend OPcache',
) );
} else {
$this->data->object_cache_extensions = array();
$this->data->opcode_cache_extensions = array();
}
$this->data->has_opcode_cache = array_filter( $this->data->opcode_cache_extensions ) ? true : false;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_cache( array $collectors, QueryMonitor $qm ) {
$collectors['cache'] = new QM_Collector_Cache();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_cache', 20, 2 );

View File

@ -0,0 +1,311 @@
<?php declare(strict_types = 1);
/**
* User capability check collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Caps>
* @phpstan-type CapCheck array{
* args: list<mixed>,
* filtered_trace: list<array<string, mixed>>,
* component: QM_Component,
* result: bool,
* }
*/
class QM_Collector_Caps extends QM_DataCollector {
public $id = 'caps';
/**
* @var array<int, array<string, mixed>>
* @phpstan-var list<CapCheck>
*/
private $cap_checks = array();
public function get_storage(): QM_Data {
return new QM_Data_Caps();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
if ( ! self::enabled() ) {
return;
}
add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 9999, 3 );
add_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 9999, 4 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 9999 );
remove_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 9999 );
parent::tear_down();
}
/**
* @return bool
*/
public static function enabled() {
return ( defined( 'QM_ENABLE_CAPS_PANEL' ) && QM_ENABLE_CAPS_PANEL );
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array(
'wp_roles_init',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'map_meta_cap',
'role_has_cap',
'user_has_cap',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_options() {
$blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
return array(
"{$blog_prefix}user_roles",
);
}
/**
* @return array<int, string>
*/
public function get_concerned_constants() {
return array(
'ALLOW_UNFILTERED_UPLOADS',
'DISALLOW_FILE_EDIT',
'DISALLOW_UNFILTERED_HTML',
);
}
/**
* Logs user capability checks.
*
* This does not get called for Super Admins. See filter_map_meta_cap() below.
*
* @param array<string, bool> $user_caps Concerned user's capabilities.
* @param array<int, string> $caps Required primitive capabilities for the requested capability.
* @param array<int, mixed> $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters.
* }
* @phpstan-param array{
* 0: string,
* 1: int,
* } $args
* @return array<string, bool> Concerned user's capabilities.
*/
public function filter_user_has_cap( array $user_caps, array $caps, array $args ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_func' => array(
'current_user_can' => true,
'map_meta_cap' => true,
'user_can' => true,
),
'ignore_method' => array(
'WP_User' => array(
'has_cap' => true,
),
),
) );
$result = true;
foreach ( $caps as $cap ) {
if ( empty( $user_caps[ $cap ] ) ) {
$result = false;
break;
}
}
$this->cap_checks[] = array(
'args' => $args,
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'result' => $result,
);
return $user_caps;
}
/**
* Logs user capability checks for Super Admins on Multisite.
*
* This is needed because the `user_has_cap` filter doesn't fire for Super Admins.
*
* @param array<int, string> $required_caps Required primitive capabilities for the requested capability.
* @param string $cap Capability or meta capability being checked.
* @param int $user_id Concerned user ID.
* @param mixed[] $args {
* Arguments that accompany the requested capability check.
*
* @type mixed ...$0 Optional second and further parameters.
* }
* @return array<int, string> Required capabilities for the requested action.
*/
public function filter_map_meta_cap( array $required_caps, $cap, $user_id, array $args ) {
if ( ! is_multisite() ) {
return $required_caps;
}
if ( ! is_super_admin( $user_id ) ) {
return $required_caps;
}
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_func' => array(
'current_user_can' => true,
'map_meta_cap' => true,
'user_can' => true,
),
'ignore_method' => array(
'WP_User' => array(
'has_cap' => true,
),
),
) );
$result = ( ! in_array( 'do_not_allow', $required_caps, true ) );
array_unshift( $args, $user_id );
array_unshift( $args, $cap );
$this->cap_checks[] = array(
'args' => $args,
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'result' => $result,
);
return $required_caps;
}
/**
* @return void
*/
public function process() {
if ( empty( $this->cap_checks ) ) {
return;
}
$all_parts = array();
$all_users = array();
$components = array();
$this->data->caps = array();
$this->cap_checks = array_values( array_filter( $this->cap_checks, array( $this, 'filter_remove_noise' ) ) );
if ( self::hide_qm() ) {
$this->cap_checks = array_values( array_filter( $this->cap_checks, array( $this, 'filter_remove_qm' ) ) );
}
foreach ( $this->cap_checks as $cap ) {
$name = $cap['args'][0];
if ( ! is_string( $name ) ) {
$name = '';
}
$component = $cap['component'];
$parts = array();
$pieces = preg_split( '#[_/-]#', $name );
if ( is_array( $pieces ) ) {
$parts = array_values( array_filter( $pieces ) );
}
$capability = array_shift( $cap['args'] );
$user_id = array_shift( $cap['args'] );
$cap['parts'] = $parts;
$cap['name'] = $name;
$cap['user'] = $user_id;
$this->data->caps[] = $cap;
$all_parts = array_merge( $all_parts, $parts );
$all_users[] = (int) $user_id;
$components[ $component->name ] = $component->name;
}
$this->data->parts = array_values( array_unique( array_filter( $all_parts ) ) );
$this->data->users = array_values( array_unique( array_filter( $all_users ) ) );
$this->data->components = $components;
}
/**
* @param array<string, mixed> $cap
* @phpstan-param CapCheck $cap
* @return bool
*/
public function filter_remove_noise( array $cap ) {
$trace = $cap['filtered_trace'];
$exclude_files = array(
ABSPATH . 'wp-admin/menu.php',
ABSPATH . 'wp-admin/includes/menu.php',
);
$exclude_functions = array(
'_wp_menu_output',
'wp_admin_bar_render',
);
foreach ( $trace as $item ) {
if ( isset( $item['file'] ) && in_array( $item['file'], $exclude_files, true ) ) {
return false;
}
if ( isset( $item['function'] ) && in_array( $item['function'], $exclude_functions, true ) ) {
return false;
}
}
return true;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_caps( array $collectors, QueryMonitor $qm ) {
$collectors['caps'] = new QM_Collector_Caps();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_caps', 20, 2 );

View File

@ -0,0 +1,126 @@
<?php declare(strict_types = 1);
/**
* Template conditionals collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Conditionals>
*/
class QM_Collector_Conditionals extends QM_DataCollector {
public $id = 'conditionals';
public function get_storage(): QM_Data {
return new QM_Data_Conditionals();
}
/**
* @return void
*/
public function process() {
/**
* Allows users to filter the names of conditional functions that are exposed by QM.
*
* @since 2.7.0
*
* @param array<int, string> $conditionals The list of conditional function names.
*/
$conds = apply_filters( 'qm/collect/conditionals', array(
'is_404',
'is_admin',
'is_archive',
'is_attachment',
'is_author',
'is_blog_admin',
'is_category',
'is_comment_feed',
'is_customize_preview',
'is_date',
'is_day',
'is_embed',
'is_favicon',
'is_feed',
'is_front_page',
'is_home',
'is_main_network',
'is_main_site',
'is_month',
'is_network_admin',
'is_page',
'is_page_template',
'is_paged',
'is_post_type_archive',
'is_preview',
'is_privacy_policy',
'is_robots',
'is_rtl',
'is_search',
'is_single',
'is_singular',
'is_ssl',
'is_sticky',
'is_tag',
'is_tax',
'is_time',
'is_trackback',
'is_user_admin',
'is_year',
) );
/**
* This filter is deprecated. Please use `qm/collect/conditionals` instead.
*
* @since 2.7.0
*
* @param array<int, string> $conditionals The list of conditional function names.
*/
$conds = apply_filters( 'query_monitor_conditionals', $conds );
$true = array();
$false = array();
$na = array();
foreach ( $conds as $cond ) {
if ( function_exists( $cond ) ) {
$id = null;
if ( ( 'is_sticky' === $cond ) && ! get_post( $id ) ) {
# Special case for is_sticky to prevent PHP notices
$false[] = $cond;
} elseif ( ! is_multisite() && in_array( $cond, array( 'is_main_network', 'is_main_site' ), true ) ) {
# Special case for multisite conditionals to prevent them from being annoying on single site installations
$na[] = $cond;
} else {
if ( call_user_func( $cond ) ) {
$true[] = $cond;
} else {
$false[] = $cond;
}
}
} else {
$na[] = $cond;
}
}
$this->data->conds = compact( 'true', 'false', 'na' );
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_conditionals( array $collectors, QueryMonitor $qm ) {
$collectors['conditionals'] = new QM_Collector_Conditionals();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_conditionals', 10, 2 );

View File

@ -0,0 +1,54 @@
<?php declare(strict_types = 1);
/**
* Database query calling function collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_DB_Callers>
*/
class QM_Collector_DB_Callers extends QM_DataCollector {
public $id = 'db_callers';
public function get_storage(): QM_Data {
return new QM_Data_DB_Callers();
}
/**
* @return void
*/
public function process() {
/** @var QM_Collector_DB_Queries|null $dbq */
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
$this->data->times = $dbq_data->times;
QM_Util::rsort( $this->data->times, 'ltime' );
$this->data->types = $dbq_data->types;
}
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_db_callers( array $collectors, QueryMonitor $qm ) {
$collectors['db_callers'] = new QM_Collector_DB_Callers();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_callers', 20, 2 );

View File

@ -0,0 +1,54 @@
<?php declare(strict_types = 1);
/**
* Database query calling component collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_DB_Components>
*/
class QM_Collector_DB_Components extends QM_DataCollector {
public $id = 'db_components';
public function get_storage(): QM_Data {
return new QM_Data_DB_Components();
}
/**
* @return void
*/
public function process() {
/** @var QM_Collector_DB_Queries|null $dbq */
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
$this->data->times = $dbq_data->component_times;
QM_Util::rsort( $this->data->times, 'ltime' );
$this->data->types = $dbq_data->types;
}
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_db_components( array $collectors, QueryMonitor $qm ) {
$collectors['db_components'] = new QM_Collector_DB_Components();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_components', 20, 2 );

View File

@ -0,0 +1,131 @@
<?php declare(strict_types = 1);
/**
* Duplicate database query collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_DB_Dupes>
*/
class QM_Collector_DB_Dupes extends QM_DataCollector {
public $id = 'db_dupes';
public function get_storage(): QM_Data {
return new QM_Data_DB_Dupes();
}
/**
* @return void
*/
public function process() {
/** @var QM_Collector_DB_Queries|null $dbq */
$dbq = QM_Collectors::get( 'db_queries' );
if ( ! $dbq ) {
return;
}
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
if ( empty( $dbq_data->dupes ) ) {
return;
}
// Filter out SQL queries that do not have dupes
$this->data->dupes = array_filter( $dbq_data->dupes, array( $this, 'filter_dupe_items' ) );
// Ignore dupes from `WP_Query->set_found_posts()`
unset( $this->data->dupes['SELECT FOUND_ROWS()'] );
$stacks = array();
$callers = array();
$components = array();
$times = array();
// Loop over all SQL queries that have dupes
foreach ( $this->data->dupes as $sql => $query_ids ) {
// Loop over each query
foreach ( $query_ids as $query_id ) {
if ( isset( $dbq_data->wpdb->rows[ $query_id ]['trace'] ) ) {
/** @var QM_Backtrace */
$trace = $dbq_data->wpdb->rows[ $query_id ]['trace'];
$stack = array_column( $trace->get_filtered_trace(), 'id' );
$component = $trace->get_component();
// Populate the component counts for this query
if ( isset( $components[ $sql ][ $component->name ] ) ) {
$components[ $sql ][ $component->name ]++;
} else {
$components[ $sql ][ $component->name ] = 1;
}
} else {
/** @var array<int, string> */
$stack = $dbq_data->wpdb->rows[ $query_id ]['stack'];
}
// Populate the caller counts for this query
if ( isset( $callers[ $sql ][ $stack[0] ] ) ) {
$callers[ $sql ][ $stack[0] ]++;
} else {
$callers[ $sql ][ $stack[0] ] = 1;
}
// Populate the stack for this query
$stacks[ $sql ][] = $stack;
// Populate the time for this query
if ( isset( $times[ $sql ] ) ) {
$times[ $sql ] += $dbq->data->wpdb->rows[ $query_id ]['ltime'];
} else {
$times[ $sql ] = $dbq->data->wpdb->rows[ $query_id ]['ltime'];
}
}
// Get the callers which are common to all stacks for this query
$common = call_user_func_array( 'array_intersect', $stacks[ $sql ] );
// Remove callers which are common to all stacks for this query
foreach ( $stacks[ $sql ] as $i => $stack ) {
$stacks[ $sql ][ $i ] = array_values( array_diff( $stack, $common ) );
// No uncommon callers within the stack? Just use the topmost caller.
if ( empty( $stacks[ $sql ][ $i ] ) ) {
$stacks[ $sql ][ $i ] = array_keys( $callers[ $sql ] );
}
}
// Wave a magic wand
$sources[ $sql ] = array_count_values( array_column( $stacks[ $sql ], 0 ) );
}
if ( ! empty( $sources ) ) {
$this->data->dupe_sources = $sources;
$this->data->dupe_callers = $callers;
$this->data->dupe_components = $components;
$this->data->dupe_times = $times;
}
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_db_dupes( array $collectors, QueryMonitor $qm ) {
$collectors['db_dupes'] = new QM_Collector_DB_Dupes();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_dupes', 25, 2 );

View File

@ -0,0 +1,286 @@
<?php declare(strict_types = 1);
/**
* Database query collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
if ( ! defined( 'QM_DB_EXPENSIVE' ) ) {
define( 'QM_DB_EXPENSIVE', 0.05 );
}
if ( SAVEQUERIES && property_exists( $GLOBALS['wpdb'], 'save_queries' ) ) {
$GLOBALS['wpdb']->save_queries = true;
}
/**
* @extends QM_DataCollector<QM_Data_DB_Queries>
*/
class QM_Collector_DB_Queries extends QM_DataCollector {
/**
* @var string
*/
public $id = 'db_queries';
/**
* @var wpdb
*/
public $wpdb;
public function get_storage(): QM_Data {
return new QM_Data_DB_Queries();
}
/**
* @return mixed[]|false
*/
public function get_errors() {
if ( ! empty( $this->data->errors ) ) {
return $this->data->errors;
}
return false;
}
/**
* @return mixed[]|false
*/
public function get_expensive() {
if ( ! empty( $this->data->expensive ) ) {
return $this->data->expensive;
}
return false;
}
/**
* @param array<string, mixed> $row
* @return bool
*/
public static function is_expensive( array $row ) {
return $row['ltime'] > QM_DB_EXPENSIVE;
}
/**
* @return void
*/
public function process() {
$this->data->total_qs = 0;
$this->data->total_time = 0;
$this->data->errors = array();
$this->process_db_object();
}
/**
* @param string $caller
* @param float $ltime
* @param string $type
* @return void
*/
protected function log_caller( $caller, $ltime, $type ) {
if ( ! isset( $this->data->times[ $caller ] ) ) {
$this->data->times[ $caller ] = array(
'caller' => $caller,
'ltime' => 0,
'types' => array(),
);
}
$this->data->times[ $caller ]['ltime'] += $ltime;
if ( isset( $this->data->times[ $caller ]['types'][ $type ] ) ) {
$this->data->times[ $caller ]['types'][ $type ]++;
} else {
$this->data->times[ $caller ]['types'][ $type ] = 1;
}
}
/**
* @return void
*/
public function process_db_object() {
global $wp_the_query, $wpdb;
$this->wpdb = $wpdb;
// With SAVEQUERIES defined as false, `wpdb::queries` is empty but `wpdb::num_queries` is not.
if ( empty( $wpdb->queries ) ) {
$this->data->total_qs += $wpdb->num_queries;
return;
}
$rows = array();
$types = array();
$total_time = 0;
$has_result = false;
$has_trace = false;
$i = 0;
$request = trim( $wp_the_query->request ?: '' );
if ( method_exists( $wpdb, 'remove_placeholder_escape' ) ) {
$request = $wpdb->remove_placeholder_escape( $request );
}
/**
* @phpstan-var array{
* 0: string,
* 1: float,
* 2: string,
* trace?: QM_Backtrace,
* result?: int|bool|WP_Error,
* }|array{
* query: string,
* elapsed: float,
* debug: string,
* } $query
*/
foreach ( $wpdb->queries as $query ) {
$has_trace = false;
$has_result = false;
$callers = array();
if ( isset( $query['query'], $query['elapsed'], $query['debug'] ) ) {
// WordPress.com VIP.
$sql = $query['query'];
$ltime = $query['elapsed'];
$stack = $query['debug'];
} elseif ( isset( $query[0], $query[1], $query[2] ) ) {
// Standard WP.
$sql = $query[0];
$ltime = $query[1];
$stack = $query[2];
// Query Monitor db.php drop-in.
$has_trace = isset( $query['trace'] );
$has_result = isset( $query['result'] );
} else {
// ¯\_(ツ)_/¯
continue;
}
// @TODO: decide what I want to do with this:
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( false !== strpos( $stack, 'wp_admin_bar' ) && ! isset( $_REQUEST['qm_display_admin_bar'] ) ) {
continue;
}
$result = $query['result'] ?? null;
$total_time += $ltime;
if ( isset( $query['trace'] ) ) {
$trace = $query['trace'];
$component = $query['trace']->get_component();
$caller = $query['trace']->get_caller();
$caller_name = $caller['display'] ?? 'Unknown';
$caller = $caller['display'] ?? 'Unknown';
} else {
$trace = null;
$component = null;
$callers = array_reverse( explode( ',', $stack ) );
$callers = array_map( 'trim', $callers );
$callers = QM_Backtrace::get_filtered_stack( $callers );
$caller = reset( $callers );
$caller_name = $caller;
}
$sql = trim( $sql );
$type = QM_Util::get_query_type( $sql );
$this->log_type( $type );
$this->log_caller( $caller_name, $ltime, $type );
$this->maybe_log_dupe( $sql, $i );
if ( $component ) {
$this->log_component( $component, $ltime, $type );
}
if ( ! isset( $types[ $type ]['total'] ) ) {
$types[ $type ]['total'] = 1;
} else {
$types[ $type ]['total']++;
}
if ( ! isset( $types[ $type ]['callers'][ $caller ] ) ) {
$types[ $type ]['callers'][ $caller ] = 1;
} else {
$types[ $type ]['callers'][ $caller ]++;
}
$is_main_query = ( $request === $sql && ( false !== strpos( $stack, ' WP->main,' ) ) );
$row = compact( 'caller', 'caller_name', 'sql', 'ltime', 'result', 'type', 'component', 'trace', 'is_main_query' );
if ( ! isset( $trace ) ) {
$row['stack'] = $callers;
}
// @TODO these should store a reference ($i) instead of the whole row
if ( $result instanceof WP_Error ) {
$this->data->errors[] = $row;
}
// @TODO these should store a reference ($i) instead of the whole row
if ( self::is_expensive( $row ) ) {
$this->data->expensive[] = $row;
}
$rows[ $i ] = $row;
$i++;
}
$total_qs = count( $rows );
$this->data->total_qs += $total_qs;
$this->data->total_time += $total_time;
$has_main_query = wp_list_filter( $rows, array(
'is_main_query' => true,
) );
# @TODO put errors in here too:
# @TODO proper class instead of (object)
$this->data->wpdb = (object) compact( 'rows', 'types', 'has_result', 'has_trace', 'total_time', 'total_qs', 'has_main_query' );
}
/**
* @param string $sql
* @param int $i
* @return void
*/
protected function maybe_log_dupe( $sql, $i ) {
$sql = str_replace( array( "\r\n", "\r", "\n" ), ' ', $sql );
$sql = str_replace( array( "\t", '`' ), '', $sql );
$sql = preg_replace( '/ +/', ' ', $sql );
$sql = trim( $sql );
$sql = rtrim( $sql, ';' );
$this->data->dupes[ $sql ][] = $i;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_db_queries( array $collectors, QueryMonitor $qm ) {
$collectors['db_queries'] = new QM_Collector_DB_Queries();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_queries', 10, 2 );

View File

@ -0,0 +1,139 @@
<?php declare(strict_types = 1);
/**
* Mock 'Debug Bar' data collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class QM_Collector_Debug_Bar extends QM_Collector {
/**
* @var string
*/
public $id = 'debug_bar';
/**
* @var Debug_Bar_Panel|null
*/
private $panel = null;
/**
* @param Debug_Bar_Panel $panel
* @return void
*/
public function set_panel( Debug_Bar_Panel $panel ) {
$this->panel = $panel;
}
/**
* @return Debug_Bar_Panel|null
*/
public function get_panel() {
return $this->panel;
}
/**
* @return void
*/
public function process() {
$this->get_panel()->prerender();
}
/**
* @return bool
*/
public function is_visible() {
return $this->get_panel()->is_visible();
}
/**
* @return void
*/
public function render() {
$this->get_panel()->render();
}
}
/**
* @return void
*/
function register_qm_collectors_debug_bar() {
global $debug_bar;
if ( class_exists( 'Debug_Bar', false ) || qm_debug_bar_being_activated() ) {
return;
}
$collectors = QM_Collectors::init();
$debug_bar = new Debug_Bar();
$redundant = array(
'debug_bar_actions_addon_panel', // Debug Bar Actions and Filters Addon
'debug_bar_remote_requests_panel', // Debug Bar Remote Requests
'debug_bar_screen_info_panel', // Debug Bar Screen Info
'ps_listdeps_debug_bar_panel', // Debug Bar List Script & Style Dependencies
);
foreach ( $debug_bar->panels as $panel ) {
$panel_id = strtolower( sanitize_html_class( get_class( $panel ) ) );
if ( in_array( $panel_id, $redundant, true ) ) {
continue;
}
$collector = new QM_Collector_Debug_Bar();
$collector->set_id( "debug_bar_{$panel_id}" );
$collector->set_panel( $panel );
$collectors->add( $collector );
}
}
/**
* @return bool
*/
function qm_debug_bar_being_activated() {
// phpcs:disable
if ( ! is_admin() ) {
return false;
}
if ( ! isset( $_REQUEST['action'] ) ) {
return false;
}
if ( isset( $_GET['action'] ) ) {
if ( ! isset( $_GET['plugin'] ) || ! isset( $_GET['_wpnonce'] ) ) {
return false;
}
if ( 'activate' === $_GET['action'] && false !== strpos( wp_unslash( $_GET['plugin'] ), 'debug-bar.php' ) ) {
return true;
}
} elseif ( isset( $_POST['action'] ) ) {
if ( ! isset( $_POST['checked'] ) || ! is_array( $_POST['checked'] ) || ! isset( $_POST['_wpnonce'] ) ) {
return false;
}
if ( 'activate-selected' === wp_unslash( $_POST['action'] ) && in_array( 'debug-bar/debug-bar.php', wp_unslash( $_POST['checked'] ), true ) ) {
return true;
}
}
return false;
// phpcs:enable
}
add_action( 'init', 'register_qm_collectors_debug_bar' );

View File

@ -0,0 +1,341 @@
<?php declare(strict_types = 1);
/**
* Doing it Wrong collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Doing_It_Wrong>
*/
class QM_Collector_Doing_It_Wrong extends QM_DataCollector {
public $id = 'doing_it_wrong';
public function get_storage(): QM_Data {
return new QM_Data_Doing_It_Wrong();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_action( 'doing_it_wrong_run', array( $this, 'action_doing_it_wrong_run' ), 10, 3 );
add_action( 'deprecated_function_run', array( $this, 'action_deprecated_function_run' ), 10, 3 );
add_action( 'deprecated_constructor_run', array( $this, 'action_deprecated_constructor_run' ), 10, 3 );
add_action( 'deprecated_file_included', array( $this, 'action_deprecated_file_included' ), 10, 4 );
add_action( 'deprecated_argument_run', array( $this, 'action_deprecated_argument_run' ), 10, 3 );
add_action( 'deprecated_hook_run', array( $this, 'action_deprecated_hook_run' ), 10, 4 );
add_filter( 'deprecated_function_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
add_filter( 'deprecated_constructor_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
add_filter( 'deprecated_file_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
add_filter( 'deprecated_argument_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
add_filter( 'deprecated_hook_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
add_filter( 'doing_it_wrong_trigger_error', array( $this, 'maybe_prevent_error' ), 999 );
}
/**
* @return void
*/
public function tear_down() {
parent::tear_down();
remove_action( 'doing_it_wrong_run', array( $this, 'action_doing_it_wrong_run' ) );
remove_action( 'deprecated_function_run', array( $this, 'action_deprecated_function_run' ) );
remove_action( 'deprecated_constructor_run', array( $this, 'action_deprecated_constructor_run' ) );
remove_action( 'deprecated_file_included', array( $this, 'action_deprecated_file_included' ) );
remove_action( 'deprecated_argument_run', array( $this, 'action_deprecated_argument_run' ) );
remove_action( 'deprecated_hook_run', array( $this, 'action_deprecated_hook_run' ) );
remove_filter( 'deprecated_function_trigger_error', array( $this, 'maybe_prevent_error' ) );
remove_filter( 'deprecated_constructor_trigger_error', array( $this, 'maybe_prevent_error' ) );
remove_filter( 'deprecated_file_trigger_error', array( $this, 'maybe_prevent_error' ) );
remove_filter( 'deprecated_argument_trigger_error', array( $this, 'maybe_prevent_error' ) );
remove_filter( 'deprecated_hook_trigger_error', array( $this, 'maybe_prevent_error' ) );
remove_filter( 'doing_it_wrong_trigger_error', array( $this, 'maybe_prevent_error' ) );
}
/**
* Prevents the PHP error (notice or deprecated) from being triggered for doing it wrong calls when the
* current user can view Query Monitor output.
*
* @param bool $trigger
* @return bool
*/
public function maybe_prevent_error( $trigger ) {
if ( function_exists( 'wp_get_current_user' ) && current_user_can( 'view_query_monitor' ) ) {
return false;
}
return $trigger;
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array(
'doing_it_wrong_run',
'deprecated_function_run',
'deprecated_constructor_run',
'deprecated_file_included',
'deprecated_argument_run',
'deprecated_hook_run',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'deprecated_function_trigger_error',
'deprecated_constructor_trigger_error',
'deprecated_file_trigger_error',
'deprecated_argument_trigger_error',
'deprecated_hook_trigger_error',
'doing_it_wrong_trigger_error',
);
}
/**
* @param string $function_name
* @param string $message
* @param string $version
* @return void
*/
public function action_doing_it_wrong_run( $function_name, $message, $version ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
if ( $version ) {
/* translators: %s: Version number. */
$version = sprintf( __( '(This message was added in version %s.)', 'query-monitor' ), $version );
}
$this->data->actions[] = array(
'hook' => 'doing_it_wrong_run',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => sprintf(
/* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message, 3: WordPress version number. */
__( 'Function %1$s was called incorrectly. %2$s %3$s', 'query-monitor' ),
$function_name,
$message,
$version
),
);
}
/**
* @param string $function_name
* @param string $replacement
* @param string $version
* @return void
*/
public function action_deprecated_function_run( $function_name, $replacement, $version ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number. */
__( 'Function %1$s is deprecated since version %2$s with no alternative available.', 'query-monitor' ),
$function_name,
$version
);
if ( $replacement ) {
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number, 3: Alternative function name. */
__( 'Function %1$s is deprecated since version %2$s! Use %3$s instead.', 'query-monitor' ),
$function_name,
$version,
$replacement
);
}
$this->data->actions[] = array(
'hook' => 'deprecated_function_run',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => $message,
);
}
/**
* @param string $class_name
* @param string $version
* @param string $parent_class
* @return void
*/
public function action_deprecated_constructor_run( $class_name, $version, $parent_class ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
$message = sprintf(
/* translators: 1: PHP class name, 2: Version number, 3: __construct() method. */
__( 'The called constructor method for %1$s class is deprecated since version %2$s! Use %3$s instead.', 'query-monitor' ),
$class_name,
$version,
'<code>__construct()</code>'
);
if ( $parent_class ) {
$message = sprintf(
/* translators: 1: PHP class name, 2: PHP parent class name, 3: Version number, 4: __construct() method. */
__( 'The called constructor method for %1$s class in %2$s is deprecated since version %3$s! Use %4$s instead.', 'query-monitor' ),
$class_name,
$parent_class,
$version,
'<code>__construct()</code>'
);
}
$this->data->actions[] = array(
'hook' => 'deprecated_constructor_run',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => $message,
);
}
/**
* @param string $file
* @param string $replacement
* @param string $version
* @param string $message
* @return void
*/
public function action_deprecated_file_included( $file, $replacement, $version, $message ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
if ( $replacement ) {
$message = sprintf(
/* translators: 1: PHP file name, 2: Version number, 3: Alternative file name, 4: Optional message regarding the change. */
__( 'File %1$s is deprecated since version %2$s! Use %3$s instead. %4$s', 'query-monitor' ),
$file,
$version,
$replacement,
$message
);
} else {
$message = sprintf(
/* translators: 1: PHP file name, 2: Version number, 3: Optional message regarding the change. */
__( 'File %1$s is deprecated since version %2$s with no alternative available. %3$s', 'query-monitor' ),
$file,
$version,
$message
);
}
$this->data->actions[] = array(
'hook' => 'deprecated_file_included',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => $message,
);
}
/**
* @param string $function_name
* @param string $message
* @param string $version
* @return void
*/
public function action_deprecated_argument_run( $function_name, $message, $version ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
if ( $message ) {
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number, 3: Optional message regarding the change. */
__( 'Function %1$s was called with an argument that is deprecated since version %2$s! %3$s', 'query-monitor' ),
$function_name,
$version,
$message
);
} else {
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number. */
__( 'Function %1$s was called with an argument that is deprecated since version %2$s with no alternative available.', 'query-monitor' ),
$function_name,
$version
);
}
$this->data->actions[] = array(
'hook' => 'deprecated_argument_run',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => $message,
);
}
/**
* @param string $hook
* @param string $replacement
* @param string $version
* @param string $message
* @return void
*/
public function action_deprecated_hook_run( $hook, $replacement, $version, $message ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_action() => true,
),
) );
if ( $replacement ) {
$message = sprintf(
/* translators: 1: WordPress hook name, 2: Version number, 3: Alternative hook name, 4: Optional message regarding the change. */
__( 'Hook %1$s is deprecated since version %2$s! Use %3$s instead. %4$s', 'query-monitor' ),
$hook,
$version,
$replacement,
$message
);
} else {
$message = sprintf(
/* translators: 1: WordPress hook name, 2: Version number, 3: Optional message regarding the change. */
__( 'Hook %1$s is deprecated since version %2$s with no alternative available. %3$s', 'query-monitor' ),
$hook,
$version,
$message
);
}
$this->data->actions[] = array(
'hook' => 'deprecated_hook_run',
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'message' => $message,
);
}
}
# Load early to catch early actions
QM_Collectors::add( new QM_Collector_Doing_It_Wrong() );

View File

@ -0,0 +1,338 @@
<?php declare(strict_types = 1);
/**
* Environment data collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Environment>
*/
class QM_Collector_Environment extends QM_DataCollector {
/**
* @var string
*/
public $id = 'environment';
/**
* @var array<int, string>
*/
protected $php_vars = array(
'max_execution_time',
'memory_limit',
'upload_max_filesize',
'post_max_size',
'display_errors',
'log_errors',
);
public function get_storage(): QM_Data {
return new QM_Data_Environment();
}
/**
* @param int $error_reporting
* @return array<string, bool>
*/
protected static function get_error_levels( $error_reporting ) {
$levels = array(
'E_ERROR' => false,
'E_WARNING' => false,
'E_PARSE' => false,
'E_NOTICE' => false,
'E_CORE_ERROR' => false,
'E_CORE_WARNING' => false,
'E_COMPILE_ERROR' => false,
'E_COMPILE_WARNING' => false,
'E_USER_ERROR' => false,
'E_USER_WARNING' => false,
'E_USER_NOTICE' => false,
'E_STRICT' => false,
'E_RECOVERABLE_ERROR' => false,
'E_DEPRECATED' => false,
'E_USER_DEPRECATED' => false,
'E_ALL' => false,
);
foreach ( $levels as $level => $reported ) {
if ( defined( $level ) ) {
$c = constant( $level );
if ( $error_reporting & $c ) {
$levels[ $level ] = true;
}
}
}
return $levels;
}
/**
* @return void
*/
public function process() {
global $wp_version;
$mysql_vars = array(
'key_buffer_size' => true, # Key cache size limit
'max_allowed_packet' => false, # Individual query size limit
'max_connections' => false, # Max number of client connections
'query_cache_limit' => true, # Individual query cache size limit
'query_cache_size' => true, # Total cache size limit
'query_cache_type' => 'ON', # Query cache on or off
'innodb_buffer_pool_size' => false, # The amount of memory allocated to the InnoDB buffer pool
);
/** @var QM_Collector_DB_Queries|null */
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
if ( method_exists( $dbq->wpdb, 'db_version' ) ) {
$server = $dbq->wpdb->db_version();
// query_cache_* deprecated since MySQL 5.7.20
if ( version_compare( $server, '5.7.20', '>=' ) ) {
unset( $mysql_vars['query_cache_limit'], $mysql_vars['query_cache_size'], $mysql_vars['query_cache_type'] );
}
}
// phpcs:disable
/** @var array<int, stdClass>|null */
$variables = $dbq->wpdb->get_results( "
SHOW VARIABLES
WHERE Variable_name IN ( '" . implode( "', '", array_keys( $mysql_vars ) ) . "' )
" );
// phpcs:enable
/** @var mysqli|false|null $dbh */
$dbh = $dbq->wpdb->dbh;
if ( is_object( $dbh ) ) {
# mysqli or PDO
$extension = get_class( $dbh );
} else {
# Who knows?
$extension = null;
}
$client = mysqli_get_client_version();
if ( $client ) {
$client_version = implode( '.', QM_Util::get_client_version( $client ) );
$client_version = sprintf( '%s (%s)', $client, $client_version );
} else {
$client_version = null;
}
$server_version = self::get_server_version( $dbq->wpdb );
$info = array(
'server-version' => $server_version,
'extension' => $extension,
'client-version' => $client_version,
'user' => $dbq->wpdb->dbuser,
'host' => $dbq->wpdb->dbhost,
'database' => $dbq->wpdb->dbname,
);
$this->data->db = array(
'info' => $info,
'vars' => $mysql_vars,
'variables' => $variables ?: array(),
);
}
$php_data = array(
'variables' => array(),
);
$php_data['version'] = phpversion();
$php_data['sapi'] = php_sapi_name();
$php_data['user'] = self::get_current_user();
// https://www.php.net/supported-versions.php
$php_data['old'] = version_compare( $php_data['version'], '7.4', '<' );
foreach ( $this->php_vars as $setting ) {
$php_data['variables'][ $setting ] = ini_get( $setting ) ?: null;
}
if ( function_exists( 'get_loaded_extensions' ) ) {
$extensions = get_loaded_extensions();
sort( $extensions, SORT_STRING | SORT_FLAG_CASE );
$php_data['extensions'] = array_combine( $extensions, array_map( array( $this, 'get_extension_version' ), $extensions ) ) ?: array();
} else {
$php_data['extensions'] = array();
}
$php_data['error_reporting'] = error_reporting();
$php_data['error_levels'] = self::get_error_levels( $php_data['error_reporting'] );
$this->data->wp['version'] = $wp_version;
$constants = array(
'WP_DEBUG' => self::format_bool_constant( 'WP_DEBUG' ),
'WP_DEBUG_DISPLAY' => self::format_bool_constant( 'WP_DEBUG_DISPLAY' ),
'WP_DEBUG_LOG' => self::format_bool_constant( 'WP_DEBUG_LOG' ),
'SCRIPT_DEBUG' => self::format_bool_constant( 'SCRIPT_DEBUG' ),
'WP_CACHE' => self::format_bool_constant( 'WP_CACHE' ),
'CONCATENATE_SCRIPTS' => self::format_bool_constant( 'CONCATENATE_SCRIPTS' ),
'COMPRESS_SCRIPTS' => self::format_bool_constant( 'COMPRESS_SCRIPTS' ),
'COMPRESS_CSS' => self::format_bool_constant( 'COMPRESS_CSS' ),
'WP_ENVIRONMENT_TYPE' => self::format_bool_constant( 'WP_ENVIRONMENT_TYPE' ),
'WP_DEVELOPMENT_MODE' => self::format_bool_constant( 'WP_DEVELOPMENT_MODE' ),
);
if ( function_exists( 'wp_get_environment_type' ) ) {
$this->data->wp['environment_type'] = wp_get_environment_type();
}
if ( function_exists( 'wp_get_development_mode' ) ) {
$this->data->wp['development_mode'] = wp_get_development_mode();
}
$this->data->wp['constants'] = apply_filters( 'qm/environment-constants', $constants );
if ( is_multisite() ) {
$this->data->wp['constants']['SUNRISE'] = self::format_bool_constant( 'SUNRISE' );
}
if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
$server = explode( ' ', wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) );
$server = explode( '/', reset( $server ) );
} else {
$server = array( '' );
}
$server_version = $server[1] ?? null;
if ( isset( $_SERVER['SERVER_ADDR'] ) ) {
$address = wp_unslash( $_SERVER['SERVER_ADDR'] );
} else {
$address = null;
}
$this->data->php = $php_data;
$this->data->server = array(
'name' => $server[0],
'version' => $server_version,
'address' => $address,
'host' => null,
'OS' => null,
'arch' => null,
);
if ( function_exists( 'php_uname' ) ) {
$this->data->server['host'] = php_uname( 'n' );
$this->data->server['OS'] = php_uname( 's' ) . ' ' . php_uname( 'r' );
$this->data->server['arch'] = php_uname( 'm' );
}
}
/**
* @param string $extension
* @return string
*/
public function get_extension_version( $extension ) {
// Nothing is simple in PHP. The exif and mysqlnd extensions (and probably others) add a bunch of
// crap to their version number, so we need to pluck out the first numeric value in the string.
$version = trim( phpversion( $extension ) ?: '' );
if ( ! $version ) {
return $version;
}
$parts = explode( ' ', $version );
foreach ( $parts as $part ) {
if ( $part && is_numeric( $part[0] ) ) {
$version = $part;
break;
}
}
return $version;
}
/**
* @param wpdb $db
* @return string
*/
protected static function get_server_version( wpdb $db ) {
$version = null;
if ( method_exists( $db, 'db_server_info' ) ) {
$version = $db->db_server_info();
}
if ( ! $version ) {
$version = $db->get_var( 'SELECT VERSION()' );
}
if ( ! $version ) {
$version = __( 'Unknown', 'query-monitor' );
}
return $version;
}
/**
* @return string
*/
protected static function get_current_user() {
$php_u = null;
if ( function_exists( 'posix_getpwuid' ) && function_exists( 'posix_getuid' ) && function_exists( 'posix_getgrgid' ) ) {
$u = posix_getpwuid( posix_getuid() );
if ( isset( $u['gid'], $u['name'] ) ) {
$g = posix_getgrgid( $u['gid'] );
if ( isset( $g['name'] ) ) {
$php_u = $u['name'] . ':' . $g['name'];
}
}
}
if ( empty( $php_u ) && isset( $_ENV['APACHE_RUN_USER'] ) ) {
$php_u = $_ENV['APACHE_RUN_USER'];
if ( isset( $_ENV['APACHE_RUN_GROUP'] ) ) {
$php_u .= ':' . $_ENV['APACHE_RUN_GROUP'];
}
}
if ( empty( $php_u ) && isset( $_SERVER['USER'] ) ) {
$php_u = wp_unslash( $_SERVER['USER'] );
}
if ( empty( $php_u ) && function_exists( 'exec' ) ) {
$php_u = exec( 'whoami' ); // phpcs:ignore
}
if ( empty( $php_u ) && function_exists( 'getenv' ) ) {
$php_u = getenv( 'USERNAME' );
}
return $php_u;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_environment( array $collectors, QueryMonitor $qm ) {
$collectors['environment'] = new QM_Collector_Environment();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_environment', 20, 2 );

View File

@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
/**
* Hooks and actions collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Hooks>
*/
class QM_Collector_Hooks extends QM_DataCollector {
/**
* @var string
*/
public $id = 'hooks';
/**
* @var bool
*/
protected static $hide_core;
public function get_storage(): QM_Data {
return new QM_Data_Hooks();
}
/**
* @return void
*/
public function process() {
/**
* @var array<string, int> $wp_actions
* @var array<string, WP_Hook> $wp_filter
*/
global $wp_actions, $wp_filter;
self::$hide_qm = self::hide_qm();
self::$hide_core = ( defined( 'QM_HIDE_CORE_ACTIONS' ) && QM_HIDE_CORE_ACTIONS );
$hooks = array();
$all_parts = array();
$components = array();
if ( has_action( 'all' ) ) {
$hooks[] = QM_Hook::process( 'all', 'action', $wp_filter, self::$hide_qm, self::$hide_core );
}
$this->data->all_hooks = defined( 'QM_SHOW_ALL_HOOKS' ) && QM_SHOW_ALL_HOOKS;
if ( $this->data->all_hooks ) {
// Show all hooks
$hook_names = array_keys( $wp_filter );
} else {
// Only show action hooks that have been called at least once
$hook_names = array_keys( $wp_actions );
}
foreach ( $hook_names as $name ) {
$type = 'action';
if ( $this->data->all_hooks ) {
$type = array_key_exists( $name, $wp_actions ) ? 'action' : 'filter';
}
$hook = QM_Hook::process( $name, $type, $wp_filter, self::$hide_qm, self::$hide_core );
$hooks[] = $hook;
$all_parts = array_merge( $all_parts, $hook['parts'] );
$components = array_merge( $components, $hook['components'] );
}
$this->data->hooks = $hooks;
$this->data->parts = array_unique( array_filter( $all_parts ) );
$this->data->components = array_unique( array_filter( $components ) );
usort( $this->data->parts, 'strcasecmp' );
usort( $this->data->components, 'strcasecmp' );
}
}
# Load early to catch all hooks
QM_Collectors::add( new QM_Collector_Hooks() );

View File

@ -0,0 +1,399 @@
<?php declare(strict_types = 1);
/**
* HTTP API request collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_HTTP>
*/
class QM_Collector_HTTP extends QM_DataCollector {
/**
* @var string
*/
public $id = 'http';
/**
* @var mixed|null
*/
private $info = null;
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* url: string,
* start: float,
* args: array<string, mixed>,
* filtered_trace: list<array<string, mixed>>,
* component: QM_Component,
* }>
*/
private $http_requests = array();
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* end: float,
* args: array<string, mixed>,
* response: mixed[]|WP_Error,
* info: array<string, mixed>|null,
* }>
*/
private $http_responses = array();
public function get_storage(): QM_Data {
return new QM_Data_HTTP();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_filter( 'http_request_args', array( $this, 'filter_http_request_args' ), 9999, 2 );
add_filter( 'pre_http_request', array( $this, 'filter_pre_http_request' ), 9999, 3 );
add_action( 'http_api_debug', array( $this, 'action_http_api_debug' ), 9999, 5 );
add_action( 'requests-curl.after_request', array( $this, 'action_curl_after_request' ), 9999, 2 );
add_action( 'requests-fsockopen.after_request', array( $this, 'action_fsockopen_after_request' ), 9999, 2 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'http_request_args', array( $this, 'filter_http_request_args' ), 9999 );
remove_filter( 'pre_http_request', array( $this, 'filter_pre_http_request' ), 9999 );
remove_action( 'http_api_debug', array( $this, 'action_http_api_debug' ), 9999 );
remove_action( 'requests-curl.before_request', array( $this, 'action_curl_before_request' ), 9999 );
remove_action( 'requests-curl.after_request', array( $this, 'action_curl_after_request' ), 9999 );
remove_action( 'requests-fsockopen.before_request', array( $this, 'action_fsockopen_before_request' ), 9999 );
remove_action( 'requests-fsockopen.after_request', array( $this, 'action_fsockopen_after_request' ), 9999 );
parent::tear_down();
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
$actions = array(
'http_api_curl',
'requests-multiple.request.complete',
'requests-request.progress',
'requests-transport.internal.parse_error',
'requests-transport.internal.parse_response',
);
$transports = array(
'requests',
'curl',
'fsockopen',
);
foreach ( $transports as $transport ) {
$actions[] = "requests-{$transport}.after_headers";
$actions[] = "requests-{$transport}.after_multi_exec";
$actions[] = "requests-{$transport}.after_request";
$actions[] = "requests-{$transport}.after_send";
$actions[] = "requests-{$transport}.before_multi_add";
$actions[] = "requests-{$transport}.before_multi_exec";
$actions[] = "requests-{$transport}.before_parse";
$actions[] = "requests-{$transport}.before_redirect";
$actions[] = "requests-{$transport}.before_redirect_check";
$actions[] = "requests-{$transport}.before_request";
$actions[] = "requests-{$transport}.before_send";
$actions[] = "requests-{$transport}.remote_host_path";
$actions[] = "requests-{$transport}.remote_socket";
}
return $actions;
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'block_local_requests',
'http_request_args',
'http_response',
'https_local_ssl_verify',
'https_ssl_verify',
'pre_http_request',
'use_curl_transport',
'use_streams_transport',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_constants() {
return array(
'WP_PROXY_HOST',
'WP_PROXY_PORT',
'WP_PROXY_USERNAME',
'WP_PROXY_PASSWORD',
'WP_PROXY_BYPASS_HOSTS',
'WP_HTTP_BLOCK_EXTERNAL',
'WP_ACCESSIBLE_HOSTS',
);
}
/**
* Filter the arguments used in an HTTP request.
*
* Used to log the request, and to add the logging key to the arguments array.
*
* @param array<string, mixed> $args HTTP request arguments.
* @param string $url The request URL.
* @return array<string, mixed> HTTP request arguments.
*/
public function filter_http_request_args( array $args, $url ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_class' => array(
'WP_Http' => true,
),
'ignore_func' => array(
'wp_safe_remote_request' => true,
'wp_safe_remote_get' => true,
'wp_safe_remote_post' => true,
'wp_safe_remote_head' => true,
'wp_remote_request' => true,
'wp_remote_get' => true,
'wp_remote_post' => true,
'wp_remote_head' => true,
'wp_remote_fopen' => true,
'download_url' => true,
'vip_safe_wp_remote_get' => true,
'vip_safe_wp_remote_request' => true,
'wpcom_vip_file_get_contents' => true,
),
) );
if ( isset( $args['_qm_key'], $this->http_requests[ $args['_qm_key'] ] ) ) {
// Something has triggered another HTTP request from within the `pre_http_request` filter
// (eg. WordPress Beta Tester does this). This allows for one level of nested queries.
$args['_qm_original_key'] = $args['_qm_key'];
$start = $this->http_requests[ $args['_qm_key'] ]['start'];
} else {
$start = microtime( true );
}
$key = microtime( true ) . $url;
$this->http_requests[ $key ] = array(
'url' => $url,
'args' => $args,
'start' => $start,
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
);
$args['_qm_key'] = $key;
return $args;
}
/**
* Log the HTTP request's response if it's being short-circuited by another plugin.
* This is necessary due to https://core.trac.wordpress.org/ticket/25747
*
* $response should be one of boolean false, an array, or a `WP_Error`, but be aware that plugins
* which short-circuit the request using this filter may (incorrectly) return data of another type.
*
* @param false|mixed[]|WP_Error $response The preemptive HTTP response. Default false.
* @param array<string, mixed> $args HTTP request arguments.
* @param string $url The request URL.
* @return false|mixed[]|WP_Error The preemptive HTTP response.
*/
public function filter_pre_http_request( $response, array $args, $url ) {
// All is well:
if ( false === $response ) {
return $response;
}
// Something's filtering the response, so we'll log it
$this->log_http_response( $response, $args, $url );
return $response;
}
/**
* Debugging action for the HTTP API.
*
* @param mixed $response A parameter which varies depending on $action.
* @param string $action The debug action. Currently one of 'response' or 'transports_list'.
* @param string $class The HTTP transport class name.
* @param array<string, mixed> $args HTTP request arguments.
* @param string $url The request URL.
* @return void
*/
public function action_http_api_debug( $response, $action, $class, $args, $url ) {
switch ( $action ) {
case 'response':
$this->log_http_response( $response, $args, $url );
break;
case 'transports_list':
# Nothing
break;
}
}
/**
* @param mixed $headers
* @param mixed[] $info
* @return void
*/
public function action_curl_after_request( $headers, array $info = null ) {
$this->info = $info;
}
/**
* @param mixed $headers
* @param mixed[] $info
* @return void
*/
public function action_fsockopen_after_request( $headers, array $info = null ) {
$this->info = $info;
}
/**
* Log an HTTP response.
*
* @param mixed[]|WP_Error $response The HTTP response.
* @param array<string, mixed> $args HTTP request arguments.
* @param string $url The request URL.
* @return void
*/
public function log_http_response( $response, array $args, $url ) {
/** @var string */
$key = $args['_qm_key'];
$http_response = array(
'end' => microtime( true ),
'response' => $response,
'args' => $args,
'info' => $this->info,
);
if ( isset( $args['_qm_original_key'] ) ) {
/** @var string */
$original_key = $args['_qm_original_key'];
$this->http_responses[ $original_key ]['end'] = $this->http_requests[ $original_key ]['start'];
$this->http_responses[ $original_key ]['response'] = new WP_Error( 'http_request_not_executed', sprintf(
/* translators: %s: Hook name */
__( 'Request not executed due to a filter on %s', 'query-monitor' ),
'pre_http_request'
) );
}
$this->http_responses[ $key ] = $http_response;
$this->info = null;
}
/**
* @return void
*/
public function process() {
$this->data->ltime = 0;
if ( empty( $this->http_requests ) ) {
return;
}
/**
* List of HTTP API error codes to ignore.
*
* @since 2.7.0
*
* @param array $http_errors Array of HTTP errors.
*/
$silent = apply_filters( 'qm/collect/silent_http_errors', array(
'http_request_not_executed',
'airplane_mode_enabled',
) );
$home_host = (string) parse_url( home_url(), PHP_URL_HOST );
foreach ( $this->http_requests as $key => $request ) {
$response = $this->http_responses[ $key ];
if ( empty( $response['response'] ) ) {
// Timed out
$response['response'] = new WP_Error( 'http_request_timed_out', __( 'Request timed out', 'query-monitor' ) );
$response['end'] = floatval( $request['start'] + $response['args']['timeout'] );
}
if ( $response['response'] instanceof WP_Error ) {
if ( ! in_array( $response['response']->get_error_code(), $silent, true ) ) {
$this->data->errors['alert'][] = $key;
}
$type = 'error';
} elseif ( ! $response['args']['blocking'] ) {
$type = 'non-blocking';
} else {
$code = intval( wp_remote_retrieve_response_code( $response['response'] ) );
$type = "http:{$code}";
if ( ( $code >= 400 ) && ( 'HEAD' !== $request['args']['method'] ) ) {
$this->data->errors['warning'][] = $key;
}
}
$ltime = ( $response['end'] - $request['start'] );
$redirected_to = null;
if ( isset( $response['info'] ) && ! empty( $response['info']['url'] ) && is_string( $response['info']['url'] ) ) {
// Ignore query variables when detecting a redirect.
$from = untrailingslashit( preg_replace( '#\?[^$]+$#', '', $request['url'] ) );
$to = untrailingslashit( preg_replace( '#\?[^$]+$#', '', $response['info']['url'] ) );
if ( $from !== $to ) {
$redirected_to = $response['info']['url'];
}
}
$this->data->ltime += $ltime;
$host = (string) parse_url( $request['url'], PHP_URL_HOST );
$local = ( $host === $home_host );
$this->log_type( $type );
$this->log_component( $request['component'], $ltime, $type );
$this->data->http[ $key ] = array(
'args' => $response['args'],
'component' => $request['component'],
'filtered_trace' => $request['filtered_trace'],
'host' => $host,
'info' => $response['info'],
'local' => $local,
'ltime' => $ltime,
'redirected_to' => $redirected_to,
'response' => $response['response'],
'type' => $type,
'url' => $request['url'],
);
}
}
}
# Load early in case a plugin is doing an HTTP request when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_HTTP() );

View File

@ -0,0 +1,222 @@
<?php declare(strict_types = 1);
/**
* Language and locale collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Languages>
*/
class QM_Collector_Languages extends QM_DataCollector {
public $id = 'languages';
public function get_storage(): QM_Data {
return new QM_Data_Languages();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_filter( 'load_textdomain_mofile', array( $this, 'log_file_load' ), 9999, 2 );
add_filter( 'load_script_translation_file', array( $this, 'log_script_file_load' ), 9999, 3 );
add_action( 'init', array( $this, 'collect_locale_data' ), 9999 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'load_textdomain_mofile', array( $this, 'log_file_load' ), 9999 );
remove_filter( 'load_script_translation_file', array( $this, 'log_script_file_load' ), 9999 );
remove_action( 'init', array( $this, 'collect_locale_data' ), 9999 );
parent::tear_down();
}
/**
* @return void
*/
public function collect_locale_data() {
$this->data->locale = get_locale();
$this->data->user_locale = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale();
$this->data->determined_locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
$this->data->language_attributes = get_language_attributes();
if ( function_exists( '\Inpsyde\MultilingualPress\siteLanguageTag' ) ) {
$this->data->mlp_language = \Inpsyde\MultilingualPress\siteLanguageTag();
}
if ( function_exists( 'pll_current_language' ) ) {
$this->data->pll_language = pll_current_language();
}
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array(
'load_textdomain',
'unload_textdomain',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array(
'determine_locale',
'gettext',
'gettext_with_context',
'language_attributes',
'load_script_textdomain_relative_path',
'load_script_translation_file',
'load_script_translations',
'load_textdomain_mofile',
'locale',
'ngettext',
'ngettext_with_context',
'override_load_textdomain',
'override_unload_textdomain',
'plugin_locale',
'pre_determine_locale',
'pre_load_script_translations',
'theme_locale',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_options() {
return array(
'WPLANG',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_constants() {
return array(
'WPLANG',
);
}
/**
* @return void
*/
public function process() {
if ( empty( $this->data->languages ) ) {
return;
}
$this->data->total_size = 0;
ksort( $this->data->languages );
foreach ( $this->data->languages as & $mofiles ) {
foreach ( $mofiles as & $mofile ) {
if ( $mofile['found'] ) {
$this->data->total_size += $mofile['found'];
}
}
}
}
/**
* Store log data.
*
* @param mixed $mofile Should be a string path to the MO file, could be anything.
* @param string $domain Text domain.
* @return string
*/
public function log_file_load( $mofile, $domain ) {
if ( 'query-monitor' === $domain && self::hide_qm() ) {
return $mofile;
}
if ( is_string( $mofile ) && isset( $this->data->languages[ $domain ][ $mofile ] ) ) {
return $mofile;
}
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_func' => array(
'load_textdomain' => ( 'default' !== $domain ),
'load_muplugin_textdomain' => true,
'load_plugin_textdomain' => true,
'load_theme_textdomain' => true,
'load_child_theme_textdomain' => true,
'load_default_textdomain' => true,
),
) );
$found = ( is_string( $mofile ) ) && file_exists( $mofile ) ? filesize( $mofile ) : false;
if ( ! is_string( $mofile ) ) {
$mofile = gettype( $mofile );
}
$this->data->languages[ $domain ][ $mofile ] = array(
'caller' => $trace->get_caller(),
'domain' => $domain,
'file' => $mofile,
'found' => $found,
'handle' => null,
'type' => 'gettext',
);
return $mofile;
}
/**
* Filters the file path for loading script translations for the given script handle and textdomain.
*
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The textdomain.
*
* @return string|false Path to the translation file to load. False if there isn't one.
*/
public function log_script_file_load( $file, $handle, $domain ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
) );
$found = ( $file && file_exists( $file ) ) ? filesize( $file ) : false;
$key = $file ?: uniqid();
$this->data->languages[ $domain ][ $key ] = array(
'caller' => $trace->get_caller(),
'domain' => $domain,
'file' => $file,
'found' => $found,
'handle' => $handle,
'type' => 'jed',
);
return $file;
}
}
# Load early to catch early errors
QM_Collectors::add( new QM_Collector_Languages() );

View File

@ -0,0 +1,285 @@
<?php declare(strict_types = 1);
/**
* PSR-3 compatible logging collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Logger>
* @phpstan-type LogMessage WP_Error|Throwable|string|bool|null
*/
class QM_Collector_Logger extends QM_DataCollector {
public $id = 'logger';
public const EMERGENCY = 'emergency';
public const ALERT = 'alert';
public const CRITICAL = 'critical';
public const ERROR = 'error';
public const WARNING = 'warning';
public const NOTICE = 'notice';
public const INFO = 'info';
public const DEBUG = 'debug';
public function get_storage(): QM_Data {
return new QM_Data_Logger();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
$this->data->counts = array_fill_keys( $this->get_levels(), 0 );
foreach ( $this->get_levels() as $level ) {
add_action( "qm/{$level}", array( $this, $level ), 10, 2 );
}
add_action( 'qm/log', array( $this, 'log' ), 10, 3 );
}
/**
* @return void
*/
public function tear_down() {
foreach ( $this->get_levels() as $level ) {
remove_action( "qm/{$level}", array( $this, $level ), 10 );
}
remove_action( 'qm/log', array( $this, 'log' ), 10 );
parent::tear_down();
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function emergency( $message, array $context = array() ) {
$this->store( self::EMERGENCY, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function alert( $message, array $context = array() ) {
$this->store( self::ALERT, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function critical( $message, array $context = array() ) {
$this->store( self::CRITICAL, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function error( $message, array $context = array() ) {
$this->store( self::ERROR, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function warning( $message, array $context = array() ) {
$this->store( self::WARNING, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function notice( $message, array $context = array() ) {
$this->store( self::NOTICE, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function info( $message, array $context = array() ) {
$this->store( self::INFO, $message, $context );
}
/**
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param LogMessage $message
* @return void
*/
public function debug( $message, array $context = array() ) {
$this->store( self::DEBUG, $message, $context );
}
/**
* @param string $level
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param self::* $level
* @phpstan-param LogMessage $message
* @return void
*/
public function log( $level, $message, array $context = array() ) {
if ( ! in_array( $level, $this->get_levels(), true ) ) {
throw new InvalidArgumentException( 'Unsupported log level' );
}
$this->store( $level, $message, $context );
}
/**
* @param string $level
* @param mixed $message
* @param array<string, mixed> $context
* @phpstan-param self::* $level
* @phpstan-param LogMessage $message
* @return void
*/
protected function store( $level, $message, array $context = array() ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
) );
if ( $message instanceof WP_Error ) {
$message = sprintf(
'WP_Error: %s (%s)',
$message->get_error_message(),
$message->get_error_code()
);
}
if ( $message instanceof Throwable ) {
$message = sprintf(
'%1$s: %2$s',
get_class( $message ),
$message->getMessage()
);
}
if ( ! is_string( $message ) ) {
if ( null === $message ) {
$message = 'null';
} elseif ( false === $message ) {
$message = 'false';
} elseif ( true === $message ) {
$message = 'true';
}
$message = print_r( $message, true );
} elseif ( '' === trim( $message ) ) {
$message = '(Empty string)';
}
$this->data->counts[ $level ]++;
$this->data->logs[] = array(
'message' => self::interpolate( $message, $context ),
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'level' => $level,
);
}
/**
* @param string $message
* @param array<string, mixed> $context
* @return string
*/
protected static function interpolate( $message, array $context = array() ) {
// build a replacement array with braces around the context keys
$replace = array();
foreach ( $context as $key => $val ) {
// check that the value can be casted to string
if ( is_bool( $val ) ) {
$replace[ "{{$key}}" ] = ( $val ? 'true' : 'false' );
} elseif ( is_scalar( $val ) ) {
$replace[ "{{$key}}" ] = $val;
}
}
// interpolate replacement values into the message and return
return strtr( $message, $replace );
}
/**
* @return void
*/
public function process() {
if ( empty( $this->data->logs ) ) {
return;
}
$components = array();
foreach ( $this->data->logs as $row ) {
$component = $row['component'];
$components[ $component->name ] = $component->name;
}
$this->data->components = $components;
}
/**
* @return array<int, string>
* @phpstan-return list<self::*>
*/
public function get_levels() {
return array(
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
self::NOTICE,
self::INFO,
self::DEBUG,
);
}
/**
* @return array<int, string>
* @phpstan-return list<self::*>
*/
public function get_warning_levels() {
return array(
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
);
}
}
# Load early in case a plugin wants to log a message early in the bootstrap process
QM_Collectors::add( new QM_Collector_Logger() );

View File

@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
/**
* Multisite collector, used for monitoring use of `switch_to_blog()` and `restore_current_blog()`.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Multisite>
*/
class QM_Collector_Multisite extends QM_DataCollector {
public $id = 'multisite';
public function __construct() {
parent::__construct();
$this->data->switches = array();
add_action( 'switch_blog', array( $this, 'action_switch_blog' ), 10, 3 );
}
public function get_storage(): QM_Data {
return new QM_Data_Multisite();
}
/**
* Fires when the blog is switched.
*
* @param int $new_blog_id New blog ID.
* @param int $prev_blog_id Previous blog ID.
* @param string $context Additional context. Accepts 'switch' when called from switch_to_blog()
* or 'restore' when called from restore_current_blog().
* @return void
*/
public function action_switch_blog( $new_blog_id, $prev_blog_id, $context ) {
if ( intval( $new_blog_id ) === intval( $prev_blog_id ) ) {
return;
}
$this->data->switches[] = array(
'new' => $new_blog_id,
'prev' => $prev_blog_id,
'to' => ( 'switch' === $context ),
'trace' => new QM_Backtrace( array(
'ignore_hook' => array(
'switch_blog' => true,
),
'ignore_func' => array(
'switch_to_blog' => true,
'restore_current_blog' => true,
),
) ),
);
}
}
if ( is_multisite() ) {
# Load early to detect as many happenings during the bootstrap process as possible
QM_Collectors::add( new QM_Collector_Multisite() );
}

View File

@ -0,0 +1,114 @@
<?php declare(strict_types = 1);
/**
* General overview collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Overview>
*/
class QM_Collector_Overview extends QM_DataCollector {
public $id = 'overview';
public function get_storage(): QM_Data {
return new QM_Data_Overview();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_action( 'shutdown', array( $this, 'process_timing' ), 0 );
}
/**
* @return void
*/
public function tear_down() {
remove_action( 'shutdown', array( $this, 'process_timing' ), 0 );
parent::tear_down();
}
/**
* Processes the timing and memory related stats as early as possible, so the
* data isn't skewed by collectors that are processed before this one.
*
* @return void
*/
public function process_timing() {
$this->data->time_taken = self::timer_stop_float();
if ( function_exists( 'memory_get_peak_usage' ) ) {
$this->data->memory = memory_get_peak_usage();
} elseif ( function_exists( 'memory_get_usage' ) ) {
$this->data->memory = memory_get_usage();
} else {
$this->data->memory = 0;
}
}
/**
* @return void
*/
public function process() {
if ( ! isset( $this->data->time_taken ) ) {
$this->process_timing();
}
$this->data->time_limit = (int) ini_get( 'max_execution_time' );
$this->data->time_start = $_SERVER['REQUEST_TIME_FLOAT'];
if ( ! empty( $this->data->time_limit ) ) {
$this->data->time_usage = ( 100 / $this->data->time_limit ) * $this->data->time_taken;
} else {
$this->data->time_usage = 0;
}
if ( is_user_logged_in() ) {
$this->data->current_user = self::format_user( wp_get_current_user() );
} else {
$this->data->current_user = null;
}
if ( function_exists( 'current_user_switched' ) && current_user_switched() ) {
$this->data->switched_user = self::format_user( current_user_switched() );
} else {
$this->data->switched_user = null;
}
$this->data->memory_limit = QM_Util::convert_hr_to_bytes( ini_get( 'memory_limit' ) ?: '0' );
if ( $this->data->memory_limit > 0 ) {
$this->data->memory_usage = ( 100 / $this->data->memory_limit ) * $this->data->memory;
} else {
$this->data->memory_usage = 0;
}
$this->data->display_time_usage_warning = ( $this->data->time_usage >= 75 );
$this->data->display_memory_usage_warning = ( $this->data->memory_usage >= 75 );
$this->data->is_admin = is_admin();
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_overview( array $collectors, QueryMonitor $qm ) {
$collectors['overview'] = new QM_Collector_Overview();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_overview', 1, 2 );

View File

@ -0,0 +1,562 @@
<?php declare(strict_types = 1);
/**
* PHP error collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'QM_ERROR_FATALS' ) ) {
define( 'QM_ERROR_FATALS', E_ERROR | E_PARSE | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR );
}
/**
* @extends QM_DataCollector<QM_Data_PHP_Errors>
* @phpstan-type errorLabels array{
* warning: string,
* notice: string,
* strict: string,
* deprecated: string,
* }
* @phpstan-import-type errorObject from QM_Data_PHP_Errors
*/
class QM_Collector_PHP_Errors extends QM_DataCollector {
/**
* @var string
*/
public $id = 'php_errors';
/**
* @var array<string, array<string, string>>
* @phpstan-var array{
* errors: errorLabels,
* suppressed: errorLabels,
* silenced: errorLabels,
* }
*/
public $types;
/**
* @var int|null
*/
private $error_reporting = null;
/**
* @var string|false|null
*/
private $display_errors = null;
/**
* @var callable|null
*/
private $previous_error_handler = null;
/**
* @var callable|null
*/
private $previous_exception_handler = null;
/**
* @var string|null
*/
private static $unexpected_error = null;
public function get_storage(): QM_Data {
return new QM_Data_PHP_Errors();
}
/**
* @return void
*/
public function set_up() {
if ( defined( 'QM_DISABLE_ERROR_HANDLER' ) && QM_DISABLE_ERROR_HANDLER ) {
return;
}
parent::set_up();
// Capture the last error that occurred before QM loaded:
$prior_error = error_get_last();
// Non-fatal error handler:
$this->previous_error_handler = set_error_handler( array( $this, 'error_handler' ), ( E_ALL ^ QM_ERROR_FATALS ) );
// Fatal error and uncaught exception handler:
$this->previous_exception_handler = set_exception_handler( array( $this, 'exception_handler' ) );
$this->error_reporting = error_reporting();
$this->display_errors = ini_get( 'display_errors' );
ini_set( 'display_errors', '0' );
if ( $prior_error ) {
$this->error_handler(
$prior_error['type'],
$prior_error['message'],
$prior_error['file'],
$prior_error['line'],
null,
false
);
}
}
/**
* @return void
*/
public function tear_down() {
if ( defined( 'QM_DISABLE_ERROR_HANDLER' ) && QM_DISABLE_ERROR_HANDLER ) {
return;
}
if ( null !== $this->previous_error_handler ) {
restore_error_handler();
}
if ( null !== $this->previous_exception_handler ) {
restore_exception_handler();
}
if ( null !== $this->error_reporting ) {
error_reporting( $this->error_reporting );
}
if ( false !== $this->display_errors ) {
ini_set( 'display_errors', $this->display_errors );
}
parent::tear_down();
}
/**
* Uncaught error handler.
*
* @param Throwable $e The error or exception.
* @return void
*/
public function exception_handler( $e ) {
$error = 'Uncaught Error';
if ( $e instanceof Exception ) {
$error = 'Uncaught Exception';
}
$this->output_fatal( 'Fatal error', array(
'message' => sprintf(
'%s: %s',
$error,
$e->getMessage()
),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTrace(),
) );
// The error must be re-thrown or passed to the previously registered exception handler so that the error
// is logged appropriately instead of discarded silently.
if ( $this->previous_exception_handler ) {
call_user_func( $this->previous_exception_handler, $e );
} else {
throw $e;
}
exit( 1 );
}
/**
* @param int $errno The error number.
* @param string $message The error message.
* @param string $file The file location.
* @param int $line The line number.
* @param mixed[] $context The context being passed.
* @param bool $do_trace Whether a stack trace should be included in the logged error data.
* @return bool
*/
public function error_handler( $errno, $message, $file = null, $line = null, $context = null, $do_trace = true ) {
$type = null;
/**
* Fires before logging the PHP error in Query Monitor.
*
* @since 2.7.0
*
* @param int $errno The error number.
* @param string $message The error message.
* @param string|null $file The file location.
* @param int|null $line The line number.
* @param mixed[]|null $context The context being passed.
*/
do_action( 'qm/collect/new_php_error', $errno, $message, $file, $line, $context );
switch ( $errno ) {
case E_WARNING:
case E_USER_WARNING:
$type = 'warning';
break;
case E_NOTICE:
case E_USER_NOTICE:
$type = 'notice';
break;
case E_STRICT:
$type = 'strict';
break;
case E_DEPRECATED:
case E_USER_DEPRECATED:
$type = 'deprecated';
break;
}
if ( null === $type ) {
return false;
}
if ( ! class_exists( 'QM_Backtrace' ) ) {
return false;
}
$error_group = 'errors';
if ( 0 === error_reporting() && 0 !== $this->error_reporting ) {
// This is most likely an @-suppressed error
$error_group = 'suppressed';
}
if ( ! isset( self::$unexpected_error ) ) {
// These strings are from core. They're passed through `__()` as variables so they get translated at runtime
// but do not get seen by GlotPress when it populates its database of translatable strings for QM.
$unexpected_error = 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.';
$wordpress_forums = 'https://wordpress.org/support/forums/';
self::$unexpected_error = sprintf(
call_user_func( '__', $unexpected_error ),
call_user_func( '__', $wordpress_forums )
);
}
// Intentionally skip reporting these core warnings. They're a distraction when developing offline.
// The failed HTTP request will still appear in QM's output so it's not a big problem hiding these warnings.
if ( false !== strpos( $message, self::$unexpected_error ) ) {
return false;
}
$trace = new QM_Backtrace();
$trace->push_frame( array(
'file' => $file,
'line' => $line,
) );
$caller = $trace->get_caller();
if ( $caller ) {
$key = md5( $message . $file . $line . $caller['id'] );
} else {
$key = md5( $message . $file . $line );
}
if ( isset( $this->data->{$error_group}[ $type ][ $key ] ) ) {
$this->data->{$error_group}[ $type ][ $key ]['calls']++;
} else {
$this->data->{$error_group}[ $type ][ $key ] = array(
'errno' => $errno,
'type' => $type,
'message' => wp_strip_all_tags( $message ),
'file' => $file,
'filename' => ( $file ? QM_Util::standard_dir( $file, '' ) : '' ),
'line' => $line,
'filtered_trace' => ( $do_trace ? $trace->get_filtered_trace() : null ),
'component' => $trace->get_component(),
'calls' => 1,
);
}
/**
* Filters the PHP error handler return value. This can be used to control whether or not the default error
* handler is called after Query Monitor's.
*
* @since 2.7.0
*
* @param bool $return_value Error handler return value. Default false.
*/
return apply_filters( 'qm/collect/php_errors_return_value', false );
}
/**
* @param string $error
* @param mixed[] $e
* @phpstan-param array{
* message: string,
* file: string,
* line: int,
* type?: int,
* trace?: mixed|null,
* } $e
* @return void
*/
protected function output_fatal( $error, array $e ) {
$dispatcher = QM_Dispatchers::get( 'html' );
if ( empty( $dispatcher ) ) {
return;
}
if ( empty( $this->display_errors ) && ! $dispatcher::user_can_view() ) {
return;
}
// This hides the subsequent message from the fatal error handler in core. It cannot be
// disabled by a plugin so we'll just hide its output.
echo '<style type="text/css"> .wp-die-message { display: none; } </style>';
printf(
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
'<link rel="stylesheet" href="%1$s?ver=%2$s" media="all" />',
esc_url( QueryMonitor::init()->plugin_url( 'assets/query-monitor.css' ) ),
esc_attr( QM_VERSION )
);
// This unused wrapper with an attribute serves to help the #qm-fatal div break out of an
// attribute if a fatal has occurred within one.
echo '<div data-qm="qm">';
printf(
'<div id="qm-fatal" data-qm-message="%1$s" data-qm-file="%2$s" data-qm-line="%3$d">',
esc_attr( $e['message'] ),
esc_attr( QM_Util::standard_dir( $e['file'], '' ) ),
intval( $e['line'] )
);
echo '<div class="qm-fatal-wrap">';
if ( QM_Output_Html::has_clickable_links() ) {
$file = QM_Output_Html::output_filename( $e['file'], $e['file'], $e['line'], true );
} else {
$file = esc_html( $e['file'] );
}
$warning = QueryMonitor::icon( 'warning' );
printf(
'<p>%1$s <b>%2$s</b>: %3$s<br>in <b>%4$s</b> on line <b>%5$d</b></p>',
$warning,
esc_html( $error ),
nl2br( esc_html( $e['message'] ), false ),
$file,
intval( $e['line'] )
); // WPCS: XSS ok.
if ( ! empty( $e['trace'] ) ) {
echo '<p>Call stack:</p>';
echo '<ol>';
foreach ( $e['trace'] as $frame ) {
$callback = QM_Util::populate_callback( $frame );
if ( ! isset( $callback['name'] ) ) {
continue;
}
printf(
'<li>%s</li>',
QM_Output_Html::output_filename( $callback['name'], $frame['file'], $frame['line'] )
); // WPCS: XSS ok.
}
echo '</ol>';
}
echo '</div>';
echo '<h2>Query Monitor</h2>';
echo '</div>';
echo '</div>';
}
/**
* Runs post-processing on the collected errors and updates the
* errors collected in the data->errors property.
*
* Any unreportable errors are placed in the data->filtered_errors
* property.
*
* @return void
*/
public function process() {
$this->types = array(
'errors' => array(
'warning' => _x( 'Warning', 'PHP error level', 'query-monitor' ),
'notice' => _x( 'Notice', 'PHP error level', 'query-monitor' ),
'strict' => _x( 'Strict', 'PHP error level', 'query-monitor' ),
'deprecated' => _x( 'Deprecated', 'PHP error level', 'query-monitor' ),
),
'suppressed' => array(
'warning' => _x( 'Warning (Suppressed)', 'Suppressed PHP error level', 'query-monitor' ),
'notice' => _x( 'Notice (Suppressed)', 'Suppressed PHP error level', 'query-monitor' ),
'strict' => _x( 'Strict (Suppressed)', 'Suppressed PHP error level', 'query-monitor' ),
'deprecated' => _x( 'Deprecated (Suppressed)', 'Suppressed PHP error level', 'query-monitor' ),
),
'silenced' => array(
'warning' => _x( 'Warning (Silenced)', 'Silenced PHP error level', 'query-monitor' ),
'notice' => _x( 'Notice (Silenced)', 'Silenced PHP error level', 'query-monitor' ),
'strict' => _x( 'Strict (Silenced)', 'Silenced PHP error level', 'query-monitor' ),
'deprecated' => _x( 'Deprecated (Silenced)', 'Silenced PHP error level', 'query-monitor' ),
),
);
$components = array();
if ( ! empty( $this->data->errors ) ) {
/**
* Filters the levels used for reported PHP errors on a per-component basis.
*
* Error levels can be specified in order to silence certain error levels from
* plugins or the current theme. Most commonly, you may wish to use this filter
* in order to silence annoying notices from third party plugins that you do not
* have control over.
*
* Silenced errors will still appear in Query Monitor's output, but will not
* cause highlighting to appear in the top level admin toolbar.
*
* For example, to show all errors in the 'foo' plugin except PHP notices use:
*
* add_filter( 'qm/collect/php_error_levels', function( array $levels ) {
* $levels['plugin']['foo'] = ( E_ALL & ~E_NOTICE );
* return $levels;
* } );
*
* Errors from themes, WordPress core, and other components can also be filtered:
*
* add_filter( 'qm/collect/php_error_levels', function( array $levels ) {
* $levels['theme']['stylesheet'] = ( E_WARNING & E_USER_WARNING );
* $levels['theme']['template'] = ( E_WARNING & E_USER_WARNING );
* $levels['core']['core'] = ( 0 );
* return $levels;
* } );
*
* Any component which doesn't have an error level specified via this filter is
* assumed to have the default level of `E_ALL`, which shows all errors.
*
* Valid PHP error level bitmasks are supported for each component, including `0`
* to silence all errors from a component. See the PHP documentation on error
* reporting for more info: http://php.net/manual/en/function.error-reporting.php
*
* @since 2.7.0
*
* @param array<string,array<string,int>> $levels The error levels used for each component.
*/
$levels = apply_filters( 'qm/collect/php_error_levels', array() );
array_map( array( $this, 'filter_reportable_errors' ), $levels, array_keys( $levels ) );
foreach ( $this->types as $error_group => $error_types ) {
foreach ( $error_types as $type => $title ) {
if ( isset( $this->data->{$error_group}[ $type ] ) ) {
/**
* @var array<string, mixed> $error
* @phpstan-var errorObject $error
*/
foreach ( $this->data->{$error_group}[ $type ] as $error ) {
$components[ $error['component']->name ] = $error['component']->name;
}
}
}
}
}
$this->data->components = $components;
}
/**
* Filters the reportable PHP errors using the table specified. Users can customize the levels
* using the `qm/collect/php_error_levels` filter.
*
* @param array<string, int> $components The error levels keyed by component name.
* @param string $component_type The component type, for example 'plugin' or 'theme'.
* @return void
*/
public function filter_reportable_errors( array $components, $component_type ) {
$all_errors = $this->data->errors;
foreach ( $components as $component_context => $allowed_level ) {
foreach ( $all_errors as $error_level => $errors ) {
foreach ( $errors as $error_id => $error ) {
if ( $this->is_reportable_error( $error['errno'], $allowed_level ) ) {
continue;
}
if ( ! $this->is_affected_component( $error['component'], $component_type, $component_context ) ) {
continue;
}
unset( $this->data->errors[ $error_level ][ $error_id ] );
$this->data->silenced[ $error_level ][ $error_id ] = $error;
}
}
}
$this->data->errors = array_filter( $this->data->errors );
}
/**
* Checks if the component is of the given type and has the given context. This is
* used to scope an error to a plugin or theme.
*
* @param QM_Component $component The component.
* @param string $component_type The component type for comparison.
* @param string $component_context The component context for comparison.
* @return bool
*/
public function is_affected_component( $component, $component_type, $component_context ) {
return ( $component->type === $component_type && $component->context === $component_context );
}
/**
* Checks if the error number specified is viewable based on the
* flags specified.
*
* Eg:- If a plugin had the config flags,
*
* E_ALL & ~E_NOTICE
*
* then,
*
* is_reportable_error( E_NOTICE, E_ALL & ~E_NOTICE ) is false
* is_reportable_error( E_WARNING, E_ALL & ~E_NOTICE ) is true
*
* If the `$flag` is null, all errors are assumed to be
* reportable by default.
*
* @param int $error_no The errno from PHP
* @param int|null $flags The config flags specified by users
* @return bool Whether the error is reportable.
*/
public function is_reportable_error( $error_no, $flags ) {
$result = true;
if ( null !== $flags ) {
$result = (bool) ( $error_no & $flags );
}
return $result;
}
/**
* For testing purposes only. Sets the errors property manually.
* Needed to test the filter since the data property is protected.
*
* @param array<string, mixed> $errors The list of errors
* @return void
*/
public function set_php_errors( $errors ) {
$this->data->errors = $errors;
}
}
# Load early to catch early errors
QM_Collectors::add( new QM_Collector_PHP_Errors() );

View File

@ -0,0 +1,97 @@
<?php declare(strict_types = 1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Raw_Request>
*/
class QM_Collector_Raw_Request extends QM_DataCollector {
public $id = 'raw_request';
public function get_storage(): QM_Data {
return new QM_Data_Raw_Request();
}
/**
* Extracts headers from a PHP-style $_SERVER array.
*
* From WP_REST_Server::get_headers()
*
* @param array<string, string> $server Associative array similar to `$_SERVER`.
* @return array<string, string> Headers extracted from the input.
*/
protected function get_headers( array $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_.
$additional = array(
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
'CONTENT_TYPE' => true,
);
foreach ( $server as $key => $value ) {
if ( strpos( $key, 'HTTP_' ) === 0 ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
/**
* Process request and response data.
*
* @return void
*/
public function process() {
$request = array(
'ip' => $_SERVER['REMOTE_ADDR'],
'method' => strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ),
'scheme' => is_ssl() ? 'https' : 'http',
'host' => wp_unslash( $_SERVER['HTTP_HOST'] ),
'path' => wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ),
'query' => wp_unslash( $_SERVER['QUERY_STRING'] ?? '' ),
'headers' => $this->get_headers( wp_unslash( $_SERVER ) ),
);
ksort( $request['headers'] );
$request['url'] = sprintf( '%s://%s%s', $request['scheme'], $request['host'], $request['path'] );
$this->data->request = $request;
$headers = array();
$raw_headers = headers_list();
foreach ( $raw_headers as $row ) {
list( $key, $value ) = explode( ':', $row, 2 );
$headers[ trim( $key ) ] = trim( $value );
}
ksort( $headers );
$response = array(
'status' => http_response_code(),
'headers' => $headers,
);
$this->data->response = $response;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_raw_request( array $collectors, QueryMonitor $qm ) {
$collectors['raw_request'] = new QM_Collector_Raw_Request();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_raw_request', 10, 2 );

View File

@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
/**
* HTTP redirect collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Redirect>
*/
class QM_Collector_Redirects extends QM_DataCollector {
public $id = 'redirects';
public function get_storage(): QM_Data {
return new QM_Data_Redirect();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 9999, 2 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 9999 );
parent::tear_down();
}
/**
* @param string $location
* @param int $status
* @return string
*/
public function filter_wp_redirect( $location, $status ) {
if ( ! $location ) {
return $location;
}
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_func' => array(
'wp_redirect' => true,
),
) );
$this->data->trace = $trace;
$this->data->location = $location;
$this->data->status = $status;
return $location;
}
}
# Load early in case a plugin is doing a redirect when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Redirects() );

View File

@ -0,0 +1,332 @@
<?php declare(strict_types = 1);
/**
* Request collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Request>
*/
class QM_Collector_Request extends QM_DataCollector {
public $id = 'request';
public function get_storage(): QM_Data {
return new QM_Data_Request();
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array(
# Rewrites
'generate_rewrite_rules',
# Everything else
'parse_query',
'parse_request',
'parse_tax_query',
'pre_get_posts',
'send_headers',
'the_post',
'wp',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
global $wp_rewrite;
$filters = array(
# Rewrite rules
'author_rewrite_rules',
'category_rewrite_rules',
'comments_rewrite_rules',
'date_rewrite_rules',
'page_rewrite_rules',
'post_format_rewrite_rules',
'post_rewrite_rules',
'root_rewrite_rules',
'search_rewrite_rules',
'tag_rewrite_rules',
# Home URL
'home_url',
# Post permalinks
'_get_page_link',
'attachment_link',
'page_link',
'post_link',
'post_type_link',
'pre_post_link',
'preview_post_link',
'the_permalink',
# Post type archive permalinks
'post_type_archive_link',
# Term permalinks
'category_link',
'pre_term_link',
'tag_link',
'term_link',
# User permalinks
'author_link',
# Comment permalinks
'get_comment_link',
# More rewrite stuff
'iis7_url_rewrite_rules',
'mod_rewrite_rules',
'rewrite_rules',
'rewrite_rules_array',
# Everything else
'do_parse_request',
'pre_handle_404',
'query_string',
'query_vars',
'redirect_canonical',
'request',
'wp_headers',
);
foreach ( $wp_rewrite->extra_permastructs as $permastructname => $struct ) {
$filters[] = sprintf(
'%s_rewrite_rules',
$permastructname
);
}
return $filters;
}
/**
* @return array<int, string>
*/
public function get_concerned_options() {
return array(
'home',
'permalink_structure',
'rewrite_rules',
'siteurl',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_constants() {
return array(
'WP_HOME',
'WP_SITEURL',
);
}
/**
* @return void
*/
public function process() {
global $wp, $wp_query, $current_blog, $current_site, $wp_rewrite;
$qo = get_queried_object();
$user = wp_get_current_user();
if ( $user->exists() ) {
$user_title = sprintf(
/* translators: %d: User ID */
__( 'Current User: #%d', 'query-monitor' ),
$user->ID
);
} else {
/* translators: No user */
$user_title = _x( 'None', 'user', 'query-monitor' );
}
$this->data->user = array(
'title' => $user_title,
'data' => ( $user->exists() ? $user : false ),
);
if ( is_multisite() ) {
$this->data->multisite['current_site'] = array(
'title' => sprintf(
/* translators: %d: Multisite site ID */
__( 'Current Site: #%d', 'query-monitor' ),
$current_blog->blog_id
),
'data' => $current_blog,
);
}
if ( QM_Util::is_multi_network() ) {
$this->data->multisite['current_network'] = array(
'title' => sprintf(
/* translators: %d: Multisite network ID */
__( 'Current Network: #%d', 'query-monitor' ),
$current_site->id
),
'data' => $current_site,
);
}
if ( is_admin() ) {
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
$path = parse_url( home_url(), PHP_URL_PATH );
$home_path = trim( $path ?: '', '/' );
$request = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore
$this->data->request['request'] = str_replace( "/{$home_path}/", '', $request );
} else {
$this->data->request['request'] = '';
}
foreach ( array( 'query_string' ) as $item ) {
$this->data->request[ $item ] = $wp->$item;
}
} else {
foreach ( array( 'request', 'matched_rule', 'matched_query', 'query_string' ) as $item ) {
$this->data->request[ $item ] = $wp->$item;
}
}
/** This filter is documented in wp-includes/class-wp.php */
$plugin_qvars = array_flip( apply_filters( 'query_vars', array() ) );
/** @var array<string, mixed> */
$qvars = $wp_query->query_vars;
$query_vars = array();
foreach ( $qvars as $k => $v ) {
if ( isset( $plugin_qvars[ $k ] ) ) {
if ( '' !== $v ) {
$query_vars[ $k ] = $v;
}
} else {
if ( ! empty( $v ) ) {
$query_vars[ $k ] = $v;
}
}
}
ksort( $query_vars );
# First add plugin vars to $this->data->qvars:
foreach ( $query_vars as $k => $v ) {
if ( isset( $plugin_qvars[ $k ] ) ) {
$this->data->qvars[ $k ] = $v;
$this->data->plugin_qvars[ $k ] = $v;
}
}
# Now add all other vars to $this->data->qvars:
foreach ( $query_vars as $k => $v ) {
if ( ! isset( $plugin_qvars[ $k ] ) ) {
$this->data->qvars[ $k ] = $v;
}
}
switch ( true ) {
case ! is_object( $qo ):
// Nada
break;
case is_a( $qo, 'WP_Post' ):
// Single post
$this->data->queried_object['title'] = sprintf(
/* translators: 1: Post type name, 2: Post ID */
__( 'Single %1$s: #%2$d', 'query-monitor' ),
get_post_type_object( $qo->post_type )->labels->singular_name,
$qo->ID
);
break;
case is_a( $qo, 'WP_User' ):
// Author archive
$this->data->queried_object['title'] = sprintf(
/* translators: %s: Author name */
__( 'Author archive: %s', 'query-monitor' ),
$qo->user_nicename
);
break;
case is_a( $qo, 'WP_Term' ):
case property_exists( $qo, 'slug' ):
// Term archive
$this->data->queried_object['title'] = sprintf(
/* translators: %s: Taxonomy term name */
__( 'Term archive: %s', 'query-monitor' ),
$qo->slug
);
break;
case is_a( $qo, 'WP_Post_Type' ):
case property_exists( $qo, 'has_archive' ):
// Post type archive
$this->data->queried_object['title'] = sprintf(
/* translators: %s: Post type name */
__( 'Post type archive: %s', 'query-monitor' ),
$qo->name
);
break;
default:
// Unknown, but we have a queried object
$this->data->queried_object['title'] = __( 'Unknown queried object', 'query-monitor' );
break;
}
if ( $qo ) {
$this->data->queried_object['data'] = $qo;
}
if ( isset( $_SERVER['REQUEST_METHOD'] ) ) {
$this->data->request_method = strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); // phpcs:ignore
} else {
$this->data->request_method = '';
}
if ( is_admin() || QM_Util::is_async() || empty( $wp_rewrite->rules ) ) {
return;
}
$matching = array();
/** @var array<string, string> */
$rewrite_rules = $wp_rewrite->rules;
foreach ( $rewrite_rules as $match => $query ) {
if ( preg_match( "#^{$match}#", $this->data->request['request'] ) ) {
$matching[ $match ] = $query;
}
}
$this->data->matching_rewrites = $matching;
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_request( array $collectors, QueryMonitor $qm ) {
$collectors['request'] = new QM_Collector_Request();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_request', 10, 2 );

View File

@ -0,0 +1,594 @@
<?php declare(strict_types = 1);
/**
* Template and theme collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Theme>
*/
class QM_Collector_Theme extends QM_DataCollector {
/**
* @var string
*/
public $id = 'response';
/**
* @var bool
*/
protected $got_theme_compat = false;
/**
* @var array<int, mixed>
*/
protected $requested_template_parts = array();
/**
* @var array<int, mixed>
*/
protected $requested_template_part_posts = array();
/**
* @var array<int, mixed>
*/
protected $requested_template_part_files = array();
/**
* @var array<int, mixed>
*/
protected $requested_template_part_nopes = array();
/**
* @var ?WP_Block_Template
*/
protected $block_template = null;
public function get_storage(): QM_Data {
return new QM_Data_Theme();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_filter( 'body_class', array( $this, 'filter_body_class' ), 9999 );
add_filter( 'timber/output', array( $this, 'filter_timber_output' ), 9999, 3 );
add_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
add_action( 'get_template_part', array( $this, 'action_get_template_part' ), 10, 3 );
add_action( 'get_header', array( $this, 'action_get_position' ) );
add_action( 'get_sidebar', array( $this, 'action_get_position' ) );
add_action( 'get_footer', array( $this, 'action_get_position' ) );
add_action( 'render_block_core_template_part_post', array( $this, 'action_render_block_core_template_part_post' ), 10, 3 );
add_action( 'render_block_core_template_part_file', array( $this, 'action_render_block_core_template_part_file' ), 10, 3 );
add_action( 'render_block_core_template_part_none', array( $this, 'action_render_block_core_template_part_none' ), 10, 3 );
add_action( 'gutenberg_render_block_core_template_part_post', array( $this, 'action_render_block_core_template_part_post' ), 10, 3 );
add_action( 'gutenberg_render_block_core_template_part_file', array( $this, 'action_render_block_core_template_part_file' ), 10, 3 );
add_action( 'gutenberg_render_block_core_template_part_none', array( $this, 'action_render_block_core_template_part_none' ), 10, 3 );
}
/**
* @return void
*/
public function tear_down() {
remove_filter( 'body_class', array( $this, 'filter_body_class' ), 9999 );
remove_filter( 'timber/output', array( $this, 'filter_timber_output' ), 9999 );
remove_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
remove_action( 'get_template_part', array( $this, 'action_get_template_part' ), 10 );
remove_action( 'get_header', array( $this, 'action_get_position' ) );
remove_action( 'get_sidebar', array( $this, 'action_get_position' ) );
remove_action( 'get_footer', array( $this, 'action_get_position' ) );
remove_action( 'render_block_core_template_part_post', array( $this, 'action_render_block_core_template_part_post' ), 10 );
remove_action( 'render_block_core_template_part_file', array( $this, 'action_render_block_core_template_part_file' ), 10 );
remove_action( 'render_block_core_template_part_none', array( $this, 'action_render_block_core_template_part_none' ), 10 );
remove_action( 'gutenberg_render_block_core_template_part_post', array( $this, 'action_render_block_core_template_part_post' ), 10 );
remove_action( 'gutenberg_render_block_core_template_part_file', array( $this, 'action_render_block_core_template_part_file' ), 10 );
remove_action( 'gutenberg_render_block_core_template_part_none', array( $this, 'action_render_block_core_template_part_none' ), 10 );
parent::tear_down();
}
/**
* Fires before the header/sidebar/footer template file is loaded.
*
* @param string|null $name Name of the specific file to use. Null for the default.
* @return void
*/
public function action_get_position( $name ) {
$filter = current_filter();
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
$filter => true,
),
) );
$position = str_replace( 'get_', '', $filter );
$templates = array();
if ( '' !== (string) $name ) {
$templates[] = "{$position}-{$name}.php";
}
$templates[] = "{$position}.php";
$data = array(
'slug' => $position,
'name' => $name,
'templates' => $templates,
'caller' => $trace->get_caller(),
);
$this->requested_template_parts[] = $data;
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array(
'template_redirect',
);
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
$filters = array(
'stylesheet',
'stylesheet_directory',
'template',
'template_directory',
'template_include',
);
foreach ( self::get_query_filter_names() as $filter ) {
$filters[] = $filter;
$filters[] = "{$filter}_hierarchy";
}
return $filters;
}
/**
* @return array<int, string>
*/
public function get_concerned_options() {
return array(
'stylesheet',
'template',
);
}
/**
* @return array<int|string, string>
*/
public static function get_query_template_names() {
$names = array();
$names['embed'] = 'is_embed';
$names['404'] = 'is_404';
$names['search'] = 'is_search';
$names['front_page'] = 'is_front_page';
$names['home'] = 'is_home';
$names['privacy_policy'] = 'is_privacy_policy';
$names['post_type_archive'] = 'is_post_type_archive';
$names['taxonomy'] = 'is_tax';
$names['attachment'] = 'is_attachment';
$names['single'] = 'is_single';
$names['page'] = 'is_page';
$names['singular'] = 'is_singular';
$names['category'] = 'is_category';
$names['tag'] = 'is_tag';
$names['author'] = 'is_author';
$names['date'] = 'is_date';
$names['archive'] = 'is_archive';
$names['index'] = '__return_true';
return $names;
}
/**
* @return array<int|string, string>
*/
public static function get_query_filter_names() {
$names = array();
$names['embed'] = 'embed_template';
$names['404'] = '404_template';
$names['search'] = 'search_template';
$names['front_page'] = 'frontpage_template';
$names['home'] = 'home_template';
$names['privacy_policy'] = 'privacypolicy_template';
$names['taxonomy'] = 'taxonomy_template';
$names['attachment'] = 'attachment_template';
$names['single'] = 'single_template';
$names['page'] = 'page_template';
$names['singular'] = 'singular_template';
$names['category'] = 'category_template';
$names['tag'] = 'tag_template';
$names['author'] = 'author_template';
$names['date'] = 'date_template';
$names['archive'] = 'archive_template';
$names['index'] = 'index_template';
return $names;
}
/**
* @return void
*/
public function action_template_redirect() {
add_filter( 'template_include', array( $this, 'filter_template_include' ), PHP_INT_MAX );
foreach ( self::get_query_template_names() as $template => $conditional ) {
// If a matching theme-compat file is found, further conditional checks won't occur in template-loader.php
if ( $this->got_theme_compat ) {
break;
}
$get_template = "get_{$template}_template";
if ( function_exists( $conditional ) && function_exists( $get_template ) && call_user_func( $conditional ) ) {
$filter = str_replace( '_', '', "{$template}" );
add_filter( "{$filter}_template_hierarchy", array( $this, 'filter_template_hierarchy' ), PHP_INT_MAX );
add_filter( "{$filter}_template", array( $this, 'filter_template' ), PHP_INT_MAX, 3 );
call_user_func( $get_template );
remove_filter( "{$filter}_template_hierarchy", array( $this, 'filter_template_hierarchy' ), PHP_INT_MAX );
remove_filter( "{$filter}_template", array( $this, 'filter_template' ), PHP_INT_MAX );
}
}
}
/**
* Fires before a template part is loaded.
*
* @param string $slug The slug name for the generic template.
* @param string $name The name of the specialized template or an empty
* string if there is none.
* @param array<int, string> $templates Array of template files to search for, in order.
* @return void
*/
public function action_get_template_part( $slug, $name, $templates ) {
$part = compact( 'slug', 'name', 'templates' );
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
) );
$part['caller'] = $trace->get_caller();
$this->requested_template_parts[] = $part;
}
/**
* Fires when a post is loaded for a template part block.
*
* @param string $template_part_id
* @param mixed[] $attributes
* @param WP_Post $post
* @return void
*/
public function action_render_block_core_template_part_post( $template_part_id, $attributes, WP_Post $post ) {
$part = array(
'id' => $template_part_id,
'attributes' => $attributes,
'post' => $post->ID,
);
$this->requested_template_part_posts[] = $part;
}
/**
* Fires when a file is loaded for a template part block.
*
* @param string $template_part_id
* @param mixed[] $attributes
* @param string $template_part_file_path
* @return void
*/
public function action_render_block_core_template_part_file( $template_part_id, $attributes, $template_part_file_path ) {
$part = array(
'id' => $template_part_id,
'attributes' => $attributes,
'path' => $template_part_file_path,
);
$this->requested_template_part_files[] = $part;
}
/**
* Fires when neither a post nor file is found for a template part block.
*
* @param string $template_part_id
* @param mixed[] $attributes
* @param string $template_part_file_path
* @return void
*/
public function action_render_block_core_template_part_none( $template_part_id, $attributes, $template_part_file_path ) {
$part = array(
'id' => $template_part_id,
'attributes' => $attributes,
'path' => $template_part_file_path,
);
$this->requested_template_part_nopes[] = $part;
}
/**
* @param array<int, string> $templates
* @return array<int, string>
*/
public function filter_template_hierarchy( array $templates ) {
if ( ! isset( $this->data->template_hierarchy ) ) {
$this->data->template_hierarchy = array();
}
foreach ( $templates as $template_name ) {
if ( file_exists( ABSPATH . WPINC . '/theme-compat/' . $template_name ) ) {
$this->got_theme_compat = true;
break;
}
}
if ( self::wp_is_block_theme() ) {
$block_theme_folders = self::wp_get_block_theme_folders();
foreach ( $templates as $template ) {
if ( str_ends_with( $template, '.php' ) ) {
// Standard PHP template, inject the HTML version:
$this->data->template_hierarchy[] = $block_theme_folders['wp_template'] . '/' . str_replace( '.php', '.html', $template );
$this->data->template_hierarchy[] = $template;
} else {
// Block theme custom template (eg. from `customTemplates` in theme.json), doesn't have a suffix:
$this->data->template_hierarchy[] = $block_theme_folders['wp_template'] . '/' . $template . '.html';
}
}
} else {
$this->data->template_hierarchy = array_merge( $this->data->template_hierarchy, $templates );
}
return $templates;
}
/**
* @param string $template Path to the template. See locate_template().
* @param string $type Sanitized filename without extension.
* @param array<int, string> $templates A list of template candidates, in descending order of priority.
* @return string Full path to template file.
*/
public function filter_template( $template, $type, $templates ) {
if ( $this->data->block_template instanceof \WP_Block_Template ) {
return $template;
}
$block_template = self::wp_resolve_block_template( $type, $templates, $template );
if ( $block_template ) {
$this->data->block_template = $block_template;
}
return $template;
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function filter_body_class( array $class ) {
$this->data->body_class = $class;
return $class;
}
/**
* @param string $template_path
* @return string
*/
public function filter_template_include( $template_path ) {
$this->data->template_path = $template_path;
return $template_path;
}
/**
* @param mixed[] $output
* @param mixed $data
* @param string $file
* @return mixed[]
*/
public function filter_timber_output( $output, $data = null, $file = null ) {
if ( $file ) {
$this->data->timber_files[] = $file;
}
return $output;
}
/**
* @return void
*/
public function process() {
$stylesheet_directory = QM_Util::standard_dir( get_stylesheet_directory() );
$template_directory = QM_Util::standard_dir( get_template_directory() );
$theme_directory = QM_Util::standard_dir( get_theme_root() );
if ( isset( $this->data->template_hierarchy ) ) {
$this->data->template_hierarchy = array_unique( $this->data->template_hierarchy );
}
if ( ! empty( $this->requested_template_parts ) ) {
$this->data->template_parts = array();
$this->data->theme_template_parts = array();
$this->data->count_template_parts = array();
foreach ( $this->requested_template_parts as $part ) {
$file = locate_template( $part['templates'] );
if ( ! $file ) {
$this->data->unsuccessful_template_parts[] = $part;
continue;
}
$file = QM_Util::standard_dir( $file );
if ( isset( $this->data->count_template_parts[ $file ] ) ) {
$this->data->count_template_parts[ $file ]++;
continue;
}
$this->data->count_template_parts[ $file ] = 1;
$filename = str_replace( array(
$stylesheet_directory,
$template_directory,
), '', $file );
$display = trim( $filename, '/' );
$theme_display = trim( str_replace( $theme_directory, '', $file ), '/' );
$this->data->template_parts[ $file ] = $display;
$this->data->theme_template_parts[ $file ] = $theme_display;
}
}
if (
! empty( $this->requested_template_part_posts ) ||
! empty( $this->requested_template_part_files ) ||
! empty( $this->requested_template_part_nopes )
) {
$this->data->template_parts = array();
$this->data->theme_template_parts = array();
$this->data->count_template_parts = array();
$posts = ! empty( $this->requested_template_part_posts ) ? $this->requested_template_part_posts : array();
$files = ! empty( $this->requested_template_part_files ) ? $this->requested_template_part_files : array();
$nopes = ! empty( $this->requested_template_part_nopes ) ? $this->requested_template_part_nopes : array();
$all = array_merge( $posts, $files, $nopes );
foreach ( $all as $part ) {
$file = $part['path'] ?? $part['post'];
if ( isset( $this->data->count_template_parts[ $file ] ) ) {
$this->data->count_template_parts[ $file ]++;
continue;
}
$this->data->count_template_parts[ $file ] = 1;
if ( isset( $part['post'] ) ) {
$display = $part['id'];
$theme_display = $display;
} else {
$file = QM_Util::standard_dir( $file );
$filename = str_replace( array(
$stylesheet_directory,
$template_directory,
), '', $file );
$display = trim( $filename, '/' );
$theme_display = trim( str_replace( $theme_directory, '', $file ), '/' );
}
$this->data->template_parts[ $file ] = $display;
$this->data->theme_template_parts[ $file ] = $theme_display;
}
}
if ( ! empty( $this->data->template_path ) ) {
$template_path = QM_Util::standard_dir( $this->data->template_path );
$template_file = str_replace( array( $stylesheet_directory, $template_directory, ABSPATH ), '', $template_path );
$template_file = ltrim( $template_file, '/' );
$theme_template_file = str_replace( array( $theme_directory, ABSPATH ), '', $template_path );
$theme_template_file = ltrim( $theme_template_file, '/' );
$this->data->template_path = $template_path;
$this->data->template_file = $template_file;
$this->data->theme_template_file = $theme_template_file;
}
$this->data->stylesheet = get_stylesheet();
$this->data->template = get_template();
$this->data->is_child_theme = ( $this->data->stylesheet !== $this->data->template );
$this->data->theme_dirs = array(
$this->data->stylesheet => $stylesheet_directory,
$this->data->template => $template_directory,
);
$this->data->theme_folders = self::wp_get_block_theme_folders();
$stylesheet_theme_json = $stylesheet_directory . '/theme.json';
$template_theme_json = $template_directory . '/theme.json';
if ( is_readable( $stylesheet_theme_json ) ) {
$this->data->stylesheet_theme_json = $stylesheet_theme_json;
}
if ( is_readable( $template_theme_json ) ) {
$this->data->template_theme_json = $template_theme_json;
}
if ( isset( $this->data->body_class ) ) {
asort( $this->data->body_class );
}
}
/**
* @return bool
*/
protected static function wp_is_block_theme() {
return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
}
/**
* @return array<string, string>
*/
protected static function wp_get_block_theme_folders() {
if ( ! function_exists( 'get_block_theme_folders' ) ) {
return array(
'wp_template' => 'templates',
'wp_template_part' => 'parts',
);
}
return get_block_theme_folders();
}
/**
* @param string $template_type The current template type.
* @param array<int, string> $template_hierarchy The current template hierarchy, ordered by priority.
* @param string $fallback_template A PHP fallback template to use if no matching block template is found.
* @return WP_Block_Template|null template A template object, or null if none could be found.
*/
protected static function wp_resolve_block_template( $template_type, $template_hierarchy, $fallback_template ) {
if ( ! function_exists( 'resolve_block_template' ) ) {
return null;
}
if ( ! current_theme_supports( 'block-templates' ) ) {
return null;
}
return resolve_block_template( $template_type, $template_hierarchy, $fallback_template );
}
}
/**
* @param array<string, QM_Collector> $collectors
* @param QueryMonitor $qm
* @return array<string, QM_Collector>
*/
function register_qm_collector_theme( array $collectors, QueryMonitor $qm ) {
$collectors['response'] = new QM_Collector_Theme();
return $collectors;
}
if ( ! is_admin() ) {
add_filter( 'qm/collectors', 'register_qm_collector_theme', 10, 2 );
}

View File

@ -0,0 +1,168 @@
<?php declare(strict_types = 1);
/**
* Timing and profiling collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Timing>
*/
class QM_Collector_Timing extends QM_DataCollector {
/**
* @var string
*/
public $id = 'timing';
/**
* @var array<string, QM_Timer>
*/
private $track_timer = array();
/**
* @var array<string, QM_Timer>
*/
private $start = array();
/**
* @var array<string, QM_Timer>
*/
private $stop = array();
public function get_storage(): QM_Data {
return new QM_Data_Timing();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_action( 'qm/start', array( $this, 'action_function_time_start' ), 10, 1 );
add_action( 'qm/stop', array( $this, 'action_function_time_stop' ), 10, 1 );
add_action( 'qm/lap', array( $this, 'action_function_time_lap' ), 10, 2 );
}
/**
* @return void
*/
public function tear_down() {
remove_action( 'qm/start', array( $this, 'action_function_time_start' ), 10 );
remove_action( 'qm/stop', array( $this, 'action_function_time_stop' ), 10 );
remove_action( 'qm/lap', array( $this, 'action_function_time_lap' ), 10 );
parent::tear_down();
}
/**
* @param string $function
* @return void
*/
public function action_function_time_start( $function ) {
$this->track_timer[ $function ] = new QM_Timer();
$this->start[ $function ] = $this->track_timer[ $function ]->start();
}
/**
* @param string $function
* @return void
*/
public function action_function_time_stop( $function ) {
if ( ! isset( $this->track_timer[ $function ] ) ) {
$trace = new QM_Backtrace();
$this->data->warning[] = array(
'function' => $function,
'message' => __( 'Timer not started', 'query-monitor' ),
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
);
return;
}
$this->stop[ $function ] = $this->track_timer[ $function ]->stop();
$this->calculate_time( $function );
}
/**
* @param string $function
* @param string $name
* @return void
*/
public function action_function_time_lap( $function, $name = null ) {
if ( ! isset( $this->track_timer[ $function ] ) ) {
$trace = new QM_Backtrace();
$this->data->warning[] = array(
'function' => $function,
'message' => __( 'Timer not started', 'query-monitor' ),
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
);
return;
}
$this->track_timer[ $function ]->lap( array(), $name );
}
/**
* @param string $function
* @return void
*/
public function calculate_time( $function ) {
$trace = $this->track_timer[ $function ]->get_trace();
$function_time = $this->track_timer[ $function ]->get_time();
$function_memory = $this->track_timer[ $function ]->get_memory();
$function_laps = $this->track_timer[ $function ]->get_laps();
$start_time = $this->track_timer[ $function ]->get_start_time();
$end_time = $this->track_timer[ $function ]->get_end_time();
$this->data->timing[] = array(
'function' => $function,
'function_time' => $function_time,
'function_memory' => $function_memory,
'laps' => $function_laps,
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'start_time' => ( $start_time - $_SERVER['REQUEST_TIME_FLOAT'] ),
'end_time' => ( $end_time - $_SERVER['REQUEST_TIME_FLOAT'] ),
);
}
/**
* @return void
*/
public function process() {
foreach ( $this->start as $function => $value ) {
if ( ! isset( $this->stop[ $function ] ) ) {
$trace = $this->track_timer[ $function ]->get_trace();
$this->data->warning[] = array(
'function' => $function,
'message' => __( 'Timer not stopped', 'query-monitor' ),
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
);
}
}
if ( ! empty( $this->data->timing ) ) {
usort( $this->data->timing, array( $this, 'sort_by_start_time' ) );
}
}
/**
* @param mixed[] $a
* @param mixed[] $b
* @return int
* @phpstan-return -1|0|1
*/
public function sort_by_start_time( array $a, array $b ) {
return $a['start_time'] <=> $b['start_time'];
}
}
# Load early in case a plugin is setting the function to be checked when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Timing() );

View File

@ -0,0 +1,111 @@
<?php declare(strict_types = 1);
/**
* Transient storage collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Transients>
*/
class QM_Collector_Transients extends QM_DataCollector {
public $id = 'transients';
public function get_storage(): QM_Data {
return new QM_Data_Transients();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_action( 'setted_site_transient', array( $this, 'action_setted_site_transient' ), 10, 3 );
add_action( 'setted_transient', array( $this, 'action_setted_blog_transient' ), 10, 3 );
}
/**
* @return void
*/
public function tear_down() {
remove_action( 'setted_site_transient', array( $this, 'action_setted_site_transient' ), 10 );
remove_action( 'setted_transient', array( $this, 'action_setted_blog_transient' ), 10 );
parent::tear_down();
}
/**
* @param string $transient
* @param mixed $value
* @param int $expiration
* @return void
*/
public function action_setted_site_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'site', $value, $expiration );
}
/**
* @param string $transient
* @param mixed $value
* @param int $expiration
* @return void
*/
public function action_setted_blog_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'blog', $value, $expiration );
}
/**
* @param string $transient
* @param string $type
* @param mixed $value
* @param int $expiration
* @phpstan-param 'site'|'blog' $value
* @return void
*/
public function setted_transient( $transient, $type, $value, $expiration ) {
$trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
'ignore_func' => array(
'set_transient' => true,
'set_site_transient' => true,
),
) );
$name = str_replace( array(
'_site_transient_',
'_transient_',
), '', $transient );
$size = strlen( (string) maybe_serialize( $value ) );
$this->data->trans[] = array(
'name' => $name,
'filtered_trace' => $trace->get_filtered_trace(),
'component' => $trace->get_component(),
'type' => $type,
'value' => $value,
'expiration' => $expiration,
'exp_diff' => ( $expiration ? human_time_diff( 0, $expiration ) : '' ),
'size' => $size,
'size_formatted' => (string) size_format( $size ),
);
}
/**
* @return void
*/
public function process() {
$this->data->has_type = is_multisite();
}
}
# Load early in case a plugin is setting transients when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Transients() );