Initial commit

This commit is contained in:
Felix Förtsch
2020-10-20 14:39:50 +02:00
commit 648ded8896
1225 changed files with 216511 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
<?php
/**
* Admin screen collector.
*
* @package query-monitor
*/
class QM_Collector_Admin extends QM_Collector {
public $id = 'response';
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;
}
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;
}
public function process() {
global $pagenow, $wp_list_table;
$current_screen = get_current_screen();
if ( isset( $_GET['page'] ) && null !== $current_screen ) { // phpcs:ignore
$this->data['base'] = $current_screen->base;
} else {
$this->data['base'] = $pagenow;
}
$this->data['pagenow'] = $pagenow;
$this->data['typenow'] = isset( $GLOBALS['typenow'] ) ? $GLOBALS['typenow'] : '';
$this->data['taxnow'] = isset( $GLOBALS['taxnow'] ) ? $GLOBALS['taxnow'] : '';
$this->data['hook_suffix'] = isset( $GLOBALS['hook_suffix'] ) ? $GLOBALS['hook_suffix'] : '';
$this->data['current_screen'] = ( $current_screen ) ? get_object_vars( $current_screen ) : null;
$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'] ] ) ) {
$list_table = array();
# And now, WordPress' legendary inconsistency comes into play:
if ( ! empty( $this->data['current_screen']['taxonomy'] ) ) {
$list_table['column'] = $this->data['current_screen']['taxonomy'];
} elseif ( ! empty( $this->data['current_screen']['post_type'] ) ) {
$list_table['column'] = $this->data['current_screen']['post_type'] . '_posts';
} else {
$list_table['column'] = $this->data['current_screen']['base'];
}
if ( ! empty( $this->data['current_screen']['post_type'] ) && empty( $this->data['current_screen']['taxonomy'] ) ) {
$list_table['columns'] = $this->data['current_screen']['post_type'] . '_posts';
} else {
$list_table['columns'] = $this->data['current_screen']['id'];
}
if ( 'edit-comments' === $list_table['column'] ) {
$list_table['column'] = 'comments';
} elseif ( 'upload' === $list_table['column'] ) {
$list_table['column'] = 'media';
} elseif ( 'link-manager' === $list_table['column'] ) {
$list_table['column'] = 'link';
}
$list_table['sortables'] = $this->data['current_screen']['id'];
$this->data['list_table'] = array(
'columns_filter' => "manage_{$list_table['columns']}_columns",
'sortables_filter' => "manage_{$list_table['sortables']}_sortable_columns",
'column_action' => "manage_{$list_table['column']}_custom_column",
);
if ( ! empty( $wp_list_table ) ) {
$this->data['list_table']['class_name'] = get_class( $wp_list_table );
}
}
}
}
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,259 @@
<?php
/**
* Enqueued scripts and styles collector.
*
* @package query-monitor
*/
abstract class QM_Collector_Assets extends QM_Collector {
public function __construct() {
parent::__construct();
add_action( 'admin_print_footer_scripts', array( $this, 'action_print_footer_scripts' ) );
add_action( 'wp_print_footer_scripts', array( $this, 'action_print_footer_scripts' ) );
add_action( 'admin_head', array( $this, 'action_head' ), 9999 );
add_action( 'wp_head', array( $this, 'action_head' ), 9999 );
add_action( 'login_head', array( $this, 'action_head' ), 9999 );
add_action( 'embed_head', array( $this, 'action_head' ), 9999 );
}
abstract public function get_dependency_type();
public function action_head() {
$type = $this->get_dependency_type();
$this->data['header'] = $GLOBALS[ "wp_{$type}" ]->done;
}
public function action_print_footer_scripts() {
if ( empty( $this->data['header'] ) ) {
return;
}
$type = $this->get_dependency_type();
$this->data['footer'] = array_diff( $GLOBALS[ "wp_{$type}" ]->done, $this->data['header'] );
}
public function process() {
if ( empty( $this->data['header'] ) && empty( $this->data['footer'] ) ) {
return;
}
$this->data['is_ssl'] = is_ssl();
$this->data['host'] = wp_unslash( $_SERVER['HTTP_HOST'] );
$this->data['default_version'] = get_bloginfo( 'version' );
$home_url = home_url();
$positions = array(
'missing',
'broken',
'header',
'footer',
);
$this->data['counts'] = array(
'missing' => 0,
'broken' => 0,
'header' => 0,
'footer' => 0,
'total' => 0,
);
$type = $this->get_dependency_type();
foreach ( array( 'header', 'footer' ) as $position ) {
if ( empty( $this->data[ $position ] ) ) {
$this->data[ $position ] = array();
}
}
$raw = $GLOBALS[ "wp_{$type}" ];
$broken = array_values( array_diff( $raw->queue, $raw->done ) );
$missing = array_values( array_diff( $raw->queue, array_keys( $raw->registered ) ) );
// A broken asset is one which has been deregistered without also being dequeued
if ( ! empty( $broken ) ) {
foreach ( $broken as $key => $handle ) {
$item = $raw->query( $handle );
if ( $item ) {
$broken = array_merge( $broken, self::get_broken_dependencies( $item, $raw ) );
} else {
unset( $broken[ $key ] );
$missing[] = $handle;
}
}
if ( ! empty( $broken ) ) {
$this->data['broken'] = array_unique( $broken );
}
}
// A missing asset is one which has been enqueued with dependencies that don't exist
if ( ! empty( $missing ) ) {
$this->data['missing'] = array_unique( $missing );
foreach ( $this->data['missing'] as $handle ) {
$raw->add( $handle, false );
$key = array_search( $handle, $raw->done, true );
if ( false !== $key ) {
unset( $raw->done[ $key ] );
}
}
}
$all_dependencies = array();
$all_dependents = array();
$missing_dependencies = array();
foreach ( $positions as $position ) {
if ( empty( $this->data[ $position ] ) ) {
continue;
}
foreach ( $this->data[ $position ] as $handle ) {
$dependency = $raw->query( $handle );
if ( ! $dependency ) {
continue;
}
$all_dependencies = array_merge( $all_dependencies, $dependency->deps );
$dependents = $this->get_dependents( $dependency, $raw );
$all_dependents = array_merge( $all_dependents, $dependents );
list( $host, $source, $local ) = $this->get_dependency_data( $dependency );
if ( empty( $dependency->ver ) ) {
$ver = '';
} else {
$ver = $dependency->ver;
}
$warning = ! in_array( $handle, $raw->done, true );
if ( is_wp_error( $source ) ) {
$display = $source->get_error_message();
} else {
$display = ltrim( str_replace( $home_url, '', remove_query_arg( 'ver', $source ) ), '/' );
}
$dependencies = $dependency->deps;
foreach ( $dependencies as & $dep ) {
if ( ! $raw->query( $dep ) ) {
// A missing dependency is a dependecy on an asset that doesn't exist
$missing_dependencies[ $dep ] = true;
}
}
$this->data['assets'][ $position ][ $handle ] = array(
'host' => $host,
'source' => $source,
'local' => $local,
'ver' => $ver,
'warning' => $warning,
'display' => $display,
'dependents' => $dependents,
'dependencies' => $dependencies,
);
$this->data['counts'][ $position ]++;
$this->data['counts']['total']++;
}
}
unset( $this->data[ $position ] );
$all_dependencies = array_unique( $all_dependencies );
sort( $all_dependencies );
$this->data['dependencies'] = $all_dependencies;
$all_dependents = array_unique( $all_dependents );
sort( $all_dependents );
$this->data['dependents'] = $all_dependents;
$this->data['missing_dependencies'] = $missing_dependencies;
}
protected static function get_broken_dependencies( _WP_Dependency $item, WP_Dependencies $dependencies ) {
$broken = array();
foreach ( $item->deps as $handle ) {
$dep = $dependencies->query( $handle );
if ( $dep ) {
$broken = array_merge( $broken, self::get_broken_dependencies( $dep, $dependencies ) );
} else {
$broken[] = $item->handle;
}
}
return $broken;
}
public function get_dependents( _WP_Dependency $dependency, WP_Dependencies $dependencies ) {
$dependents = array();
$handles = array_unique( array_merge( $dependencies->queue, $dependencies->done ) );
foreach ( $handles as $handle ) {
$item = $dependencies->query( $handle );
if ( $item ) {
if ( in_array( $dependency->handle, $item->deps, true ) ) {
$dependents[] = $handle;
}
}
}
sort( $dependents );
return $dependents;
}
public function get_dependency_data( _WP_Dependency $dependency ) {
$data = $this->get_data();
$loader = rtrim( $this->get_dependency_type(), 's' );
$src = $dependency->src;
if ( null === $dependency->ver ) {
$ver = '';
} else {
$ver = $dependency->ver ? $dependency->ver : $this->data['default_version'];
}
if ( ! empty( $src ) && ! empty( $ver ) ) {
$src = add_query_arg( 'ver', $ver, $src );
}
/** This filter is documented in wp-includes/class.wp-scripts.php */
$source = apply_filters( "{$loader}_loader_src", $src, $dependency->handle );
$host = (string) parse_url( $source, PHP_URL_HOST );
$scheme = (string) parse_url( $source, PHP_URL_SCHEME );
$http_host = $data['host'];
if ( empty( $host ) && ! empty( $http_host ) ) {
$host = $http_host;
}
if ( $scheme && $data['is_ssl'] && ( 'https' !== $scheme ) && ( 'localhost' !== $host ) ) {
$source = new WP_Error( 'qm_insecure_content', __( 'Insecure content', 'query-monitor' ), array(
'src' => $source,
) );
}
if ( is_wp_error( $source ) ) {
$error_data = $source->get_error_data();
if ( $error_data && isset( $error_data['src'] ) ) {
$host = (string) parse_url( $error_data['src'], PHP_URL_HOST );
}
} elseif ( empty( $source ) ) {
$source = '';
$host = '';
}
$local = ( $http_host === $host );
return array( $host, $source, $local );
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Enqueued scripts collector.
*
* @package query-monitor
*/
class QM_Collector_Assets_Scripts extends QM_Collector_Assets {
public $id = 'assets_scripts';
public function get_dependency_type() {
return 'scripts';
}
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',
);
}
}
public function get_concerned_filters() {
return array(
'print_scripts_array',
'script_loader_src',
'script_loader_tag',
);
}
}
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,30 @@
<?php
/**
* Enqueued styles collector.
*
* @package query-monitor
*/
class QM_Collector_Assets_Styles extends QM_Collector_Assets {
public $id = 'assets_styles';
public function get_dependency_type() {
return 'styles';
}
public function get_concerned_filters() {
return array(
'print_styles_array',
'style_loader_src',
'style_loader_tag',
);
}
}
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,160 @@
<?php
/**
* Block editor (née Gutenberg) collector.
*
* @package query-monitor
*/
class QM_Collector_Block_Editor extends QM_Collector {
public $id = 'block_editor';
protected $block_timing = array();
protected $block_timer = null;
public function __construct() {
parent::__construct();
add_filter( 'pre_render_block', array( $this, 'filter_pre_render_block' ), 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 );
}
public function get_concerned_filters() {
return array(
'allowed_block_types',
'pre_render_block',
'render_block_data',
'render_block',
);
}
public function filter_pre_render_block( $pre_render, array $block ) {
if ( null !== $pre_render ) {
$this->block_timing[] = false;
}
return $pre_render;
}
public function filter_render_block_data( array $block ) {
$this->block_timer = new QM_Timer();
$this->block_timer->start();
return $block;
}
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_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'] ) ) );
}
}
protected function process_block( array $block ) {
// Remove empty blocks caused by two consecutive line breaks in content
if ( ! $block['blockName'] && ! trim( $block['innerHTML'] ) ) {
array_shift( $this->block_timing );
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 ( $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;
}
protected static function wp_block_editor_enabled() {
return ( function_exists( 'parse_blocks' ) || function_exists( 'gutenberg_parse_blocks' ) );
}
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;
}
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;
}
protected static function wp_get_dynamic_block_names() {
if ( function_exists( 'get_dynamic_block_names' ) ) {
return get_dynamic_block_names();
}
return array();
}
}
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,104 @@
<?php
/**
* Object cache collector.
*
* @package query-monitor
*/
class QM_Collector_Cache extends QM_Collector {
public $id = 'cache';
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) $wp_object_cache->cache_hits;
}
if ( array_key_exists( 'cache_misses', $object_vars ) ) {
$this->data['stats']['cache_misses'] = (int) $wp_object_cache->cache_misses;
}
if ( method_exists( $wp_object_cache, 'getStats' ) ) {
$stats = $wp_object_cache->getStats();
} elseif ( array_key_exists( 'stats', $object_vars ) && is_array( $wp_object_cache->stats ) ) {
$stats = $wp_object_cache->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;
}
$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(
'APCu' => 'APCu',
'Memcache' => 'Memcache',
'Memcached' => 'Memcached',
'Redis' => 'Redis',
) );
$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;
}
}
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,233 @@
<?php
/**
* User capability check collector.
*
* @package query-monitor
*/
class QM_Collector_Caps extends QM_Collector {
public $id = 'caps';
public function __construct() {
parent::__construct();
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 );
}
public static function enabled() {
return ( defined( 'QM_ENABLE_CAPS_PANEL' ) && QM_ENABLE_CAPS_PANEL );
}
public function get_concerned_actions() {
return array(
'wp_roles_init',
);
}
public function get_concerned_filters() {
return array(
'map_meta_cap',
'role_has_cap',
'user_has_cap',
);
}
public function get_concerned_options() {
$blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
return array(
"{$blog_prefix}user_roles",
);
}
public function get_concerned_constants() {
return array(
'ALLOW_UNFILTERED_UPLOADS',
'DISALLOW_FILE_EDIT',
'DISALLOW_UNFILTERED_HTML',
);
}
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 );
}
/**
* Logs user capability checks.
*
* This does not get called for Super Admins. See filter_map_meta_cap() below.
*
* @param bool[] $user_caps Concerned user's capabilities.
* @param string[] $caps Required primitive capabilities for the requested capability.
* @param array $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.
* }
* @return bool[] Concerned user's capabilities.
*/
public function filter_user_has_cap( array $user_caps, array $caps, array $args ) {
$trace = new QM_Backtrace();
$result = true;
foreach ( $caps as $cap ) {
if ( empty( $user_caps[ $cap ] ) ) {
$result = false;
break;
}
}
$this->data['caps'][] = array(
'args' => $args,
'trace' => $trace,
'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 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 array $args {
* Arguments that accompany the requested capability check.
*
* @type mixed ...$0 Optional second and further parameters.
* }
* @return 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();
$result = ( ! in_array( 'do_not_allow', $required_caps, true ) );
array_unshift( $args, $user_id );
array_unshift( $args, $cap );
$this->data['caps'][] = array(
'args' => $args,
'trace' => $trace,
'result' => $result,
);
return $required_caps;
}
public function process() {
if ( empty( $this->data['caps'] ) ) {
return;
}
$all_parts = array();
$all_users = array();
$components = array();
$this->data['caps'] = array_values( array_filter( $this->data['caps'], array( $this, 'filter_remove_noise' ) ) );
if ( self::hide_qm() ) {
$this->data['caps'] = array_values( array_filter( $this->data['caps'], array( $this, 'filter_remove_qm' ) ) );
}
foreach ( $this->data['caps'] as $i => $cap ) {
$name = $cap['args'][0];
if ( ! is_string( $name ) ) {
$name = '';
}
$trace = $cap['trace']->get_trace();
$filtered_trace = $cap['trace']->get_display_trace();
$last = end( $filtered_trace );
if ( isset( $last['function'] ) && 'map_meta_cap' === $last['function'] ) {
array_shift( $filtered_trace ); // remove the map_meta_cap() call
}
array_shift( $filtered_trace ); // remove the WP_User->has_cap() call
array_shift( $filtered_trace ); // remove the *_user_can() call
if ( ! count( $filtered_trace ) ) {
$responsible_name = QM_Util::standard_dir( $trace[1]['file'], '' ) . ':' . $trace[1]['line'];
$responsible_item = $trace[1];
$responsible_item['display'] = $responsible_name;
$responsible_item['calling_file'] = $trace[1]['file'];
$responsible_item['calling_line'] = $trace[1]['line'];
array_unshift( $filtered_trace, $responsible_item );
}
$component = $cap['trace']->get_component();
$this->data['caps'][ $i ]['filtered_trace'] = $filtered_trace;
$this->data['caps'][ $i ]['component'] = $component;
$parts = array_values( array_filter( preg_split( '#[_/-]#', $name ) ) );
$this->data['caps'][ $i ]['parts'] = $parts;
$this->data['caps'][ $i ]['name'] = $name;
$this->data['caps'][ $i ]['user'] = $cap['args'][1];
$this->data['caps'][ $i ]['args'] = array_slice( $cap['args'], 2 );
$all_parts = array_merge( $all_parts, $parts );
$all_users[] = $cap['args'][1];
$components[ $component->name ] = $component->name;
unset( $this->data['caps'][ $i ]['trace'] );
}
$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;
}
public function filter_remove_noise( array $cap ) {
$trace = $cap['trace']->get_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;
}
}
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,107 @@
<?php
/**
* Template conditionals collector.
*
* @package query-monitor
*/
class QM_Collector_Conditionals extends QM_Collector {
public $id = 'conditionals';
public function process() {
/**
* Allows users to filter the names of conditional functions that are exposed by QM.
*
* @since 2.7.0
*
* @param 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 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' );
}
}
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,34 @@
<?php
/**
* Database query calling function collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Callers extends QM_Collector {
public $id = 'db_callers';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
if ( isset( $dbq->data['times'] ) ) {
$this->data['times'] = $dbq->data['times'];
QM_Util::rsort( $this->data['times'], 'ltime' );
}
if ( isset( $dbq->data['types'] ) ) {
$this->data['types'] = $dbq->data['types'];
}
}
}
}
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,34 @@
<?php
/**
* Database query calling component collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Components extends QM_Collector {
public $id = 'db_components';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
if ( isset( $dbq->data['component_times'] ) ) {
$this->data['times'] = $dbq->data['component_times'];
QM_Util::rsort( $this->data['times'], 'ltime' );
}
if ( isset( $dbq->data['types'] ) ) {
$this->data['types'] = $dbq->data['types'];
}
}
}
}
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,103 @@
<?php
/**
* Duplicate database query collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Dupes extends QM_Collector {
public $id = 'db_dupes';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( ! $dbq ) {
return;
}
if ( ! isset( $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_queries' ) );
// Ignore dupes from `WP_Query->set_found_posts()`
unset( $this->data['dupes']['SELECT FOUND_ROWS()'] );
$stacks = array();
$tops = array();
$callers = array();
$components = 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['dbs']['$wpdb']->rows[ $query_id ]['trace'] ) ) {
$trace = $dbq->data['dbs']['$wpdb']->rows[ $query_id ]['trace'];
$stack = wp_list_pluck( $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 {
$stack = array_reverse( explode( ', ', $dbq->data['dbs']['$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;
}
// 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( wp_list_pluck( $stacks[ $sql ], 0 ) );
}
if ( ! empty( $sources ) ) {
$this->data['dupe_sources'] = $sources;
$this->data['dupe_callers'] = $callers;
$this->data['dupe_components'] = $components;
}
}
public function _filter_dupe_queries( $queries ) {
return ( count( $queries ) > 1 );
}
}
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,241 @@
<?php
/**
* Database query collector.
*
* @package query-monitor
*/
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;
}
class QM_Collector_DB_Queries extends QM_Collector {
public $id = 'db_queries';
public $db_objects = array();
public function get_errors() {
if ( ! empty( $this->data['errors'] ) ) {
return $this->data['errors'];
}
return false;
}
public function get_expensive() {
if ( ! empty( $this->data['expensive'] ) ) {
return $this->data['expensive'];
}
return false;
}
public static function is_expensive( array $row ) {
return $row['ltime'] > QM_DB_EXPENSIVE;
}
public function process() {
$this->data['total_qs'] = 0;
$this->data['total_time'] = 0;
$this->data['errors'] = array();
/**
* Filters the `wpdb` instances that are exposed to QM.
*
* This allows Query Monitor to display multiple instances of `wpdb` on one page load.
*
* @since 2.7.0
*
* @param wpdb[] $db_objects Array of `wpdb` instances, keyed by their name.
*/
$this->db_objects = apply_filters( 'qm/collect/db_objects', array(
'$wpdb' => $GLOBALS['wpdb'],
) );
foreach ( $this->db_objects as $name => $db ) {
if ( is_a( $db, 'wpdb' ) ) {
$this->process_db_object( $name, $db );
} else {
unset( $this->db_objects[ $name ] );
}
}
}
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;
}
}
public function process_db_object( $id, wpdb $db ) {
global $EZSQL_ERROR, $wp_the_query;
// With SAVEQUERIES defined as false, `wpdb::queries` is empty but `wpdb::num_queries` is not.
if ( empty( $db->queries ) ) {
$this->data['total_qs'] += $db->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( $db, 'remove_placeholder_escape' ) ) {
$request = $db->remove_placeholder_escape( $request );
}
foreach ( (array) $db->queries as $query ) {
# @TODO: decide what I want to do with this:
if ( false !== strpos( $query[2], 'wp_admin_bar' ) and !isset( $_REQUEST['qm_display_admin_bar'] ) ) { // phpcs:ignore
continue;
}
$sql = $query[0];
$ltime = $query[1];
$stack = $query[2];
$has_start = isset( $query[3] );
$has_trace = isset( $query['trace'] );
$has_result = isset( $query['result'] );
if ( $has_result ) {
$result = $query['result'];
} else {
$result = null;
}
$total_time += $ltime;
if ( $has_trace ) {
$trace = $query['trace'];
$component = $query['trace']->get_component();
$caller = $query['trace']->get_caller();
$caller_name = $caller['display'];
$caller = $caller['display'];
} else {
$trace = null;
$component = null;
$callers = explode( ',', $stack );
$caller = trim( end( $callers ) );
if ( false !== strpos( $caller, '(' ) ) {
$caller_name = substr( $caller, 0, strpos( $caller, '(' ) ) . '()';
} else {
$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'] = $stack;
}
if ( is_wp_error( $result ) ) {
$this->data['errors'][] = $row;
}
if ( self::is_expensive( $row ) ) {
$this->data['expensive'][] = $row;
}
$rows[ $i ] = $row;
$i++;
}
if ( '$wpdb' === $id && ! $has_result && ! empty( $EZSQL_ERROR ) && is_array( $EZSQL_ERROR ) ) {
// Fallback for displaying database errors when wp-content/db.php isn't in place
foreach ( $EZSQL_ERROR as $error ) {
$row = array(
'caller' => null,
'caller_name' => null,
'stack' => '',
'sql' => $error['query'],
'ltime' => 0,
'result' => new WP_Error( 'qmdb', $error['error_str'] ),
'type' => '',
'component' => false,
'trace' => null,
'is_main_query' => false,
);
$this->data['errors'][] = $row;
}
}
$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['dbs'][ $id ] = (object) compact( 'rows', 'types', 'has_result', 'has_trace', 'total_time', 'total_qs', 'has_main_query' );
}
}
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,109 @@
<?php
/**
* Mock 'Debug Bar' data collector.
*
* @package query-monitor
*/
final class QM_Collector_Debug_Bar extends QM_Collector {
public $id = 'debug_bar';
private $panel = null;
public function set_panel( Debug_Bar_Panel $panel ) {
$this->panel = $panel;
}
public function get_panel() {
return $this->panel;
}
public function process() {
$this->get_panel()->prerender();
}
public function is_visible() {
return $this->get_panel()->is_visible();
}
public function render() {
return $this->get_panel()->render();
}
}
function register_qm_collectors_debug_bar() {
global $debug_bar;
if ( class_exists( 'Debug_Bar' ) || qm_debug_bar_being_activated() ) {
return;
}
$collectors = QM_Collectors::init();
$qm = QueryMonitor::init();
require_once $qm->plugin_path( 'classes/debug_bar.php' );
$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 );
}
}
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,309 @@
<?php
/**
* Environment data collector.
*
* @package query-monitor
*/
class QM_Collector_Environment extends QM_Collector {
public $id = 'environment';
protected $php_vars = array(
'max_execution_time',
'memory_limit',
'upload_max_filesize',
'post_max_size',
'display_errors',
'log_errors',
);
public function __construct() {
global $wpdb;
parent::__construct();
# If QM_DB is in place then we'll use the values which were
# caught early before any plugins had a chance to alter them
foreach ( $this->php_vars as $setting ) {
if ( isset( $wpdb->qm_php_vars ) && isset( $wpdb->qm_php_vars[ $setting ] ) ) {
$val = $wpdb->qm_php_vars[ $setting ];
} else {
$val = ini_get( $setting );
}
$this->data['php']['variables'][ $setting ]['before'] = $val;
}
}
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;
}
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
);
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
foreach ( $dbq->db_objects as $id => $db ) {
if ( method_exists( $db, 'db_version' ) ) {
$server = $db->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'] );
}
} else {
$server = null;
}
$variables = $db->get_results( "
SHOW VARIABLES
WHERE Variable_name IN ( '" . implode( "', '", array_keys( $mysql_vars ) ) . "' )
" );
if ( is_resource( $db->dbh ) ) {
# Old mysql extension
$extension = 'mysql';
} elseif ( is_object( $db->dbh ) ) {
# mysqli or PDO
$extension = get_class( $db->dbh );
} else {
# Who knows?
$extension = null;
}
if ( isset( $db->use_mysqli ) && $db->use_mysqli ) {
$client = mysqli_get_client_version();
$info = mysqli_get_server_info( $db->dbh );
} else {
// Please do not report this code as a PHP 7 incompatibility. Observe the surrounding logic.
// phpcs:ignore
if ( preg_match( '|[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}|', mysql_get_client_info(), $matches ) ) {
$client = $matches[0];
} else {
$client = null;
}
// Please do not report this code as a PHP 7 incompatibility. Observe the surrounding logic.
// phpcs:ignore
$info = mysql_get_server_info( $db->dbh );
}
if ( $client ) {
$client_version = implode( '.', QM_Util::get_client_version( $client ) );
$client_version = sprintf( '%s (%s)', $client, $client_version );
} else {
$client_version = null;
}
$info = array(
'server-version' => $server,
'extension' => $extension,
'client-version' => $client_version,
'user' => $db->dbuser,
'host' => $db->dbhost,
'database' => $db->dbname,
);
$this->data['db'][ $id ] = array(
'info' => $info,
'vars' => $mysql_vars,
'variables' => $variables,
);
}
}
$this->data['php']['version'] = phpversion();
$this->data['php']['sapi'] = php_sapi_name();
$this->data['php']['user'] = self::get_current_user();
$this->data['php']['old'] = version_compare( $this->data['php']['version'], 7.2, '<' );
foreach ( $this->php_vars as $setting ) {
$this->data['php']['variables'][ $setting ]['after'] = ini_get( $setting );
}
if ( defined( 'SORT_FLAG_CASE' ) ) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants
$sort_flags = SORT_STRING | SORT_FLAG_CASE;
} else {
$sort_flags = SORT_STRING;
}
if ( is_callable( 'get_loaded_extensions' ) ) {
$extensions = get_loaded_extensions();
sort( $extensions, $sort_flags );
$this->data['php']['extensions'] = array_combine( $extensions, array_map( array( $this, 'get_extension_version' ), $extensions ) );
} else {
$this->data['php']['extensions'] = array();
}
$this->data['php']['error_reporting'] = error_reporting();
$this->data['php']['error_levels'] = self::get_error_levels( $this->data['php']['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' ),
);
if ( function_exists( 'wp_get_environment_type' ) ) {
$this->data['wp']['environment_type'] = wp_get_environment_type();
}
$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( '' );
}
if ( isset( $server[1] ) ) {
$server_version = $server[1];
} else {
$server_version = null;
}
if ( isset( $_SERVER['SERVER_ADDR'] ) ) {
$address = wp_unslash( $_SERVER['SERVER_ADDR'] );
} else {
$address = null;
}
$this->data['server'] = array(
'name' => $server[0],
'version' => $server_version,
'address' => $address,
'host' => null,
'OS' => null,
);
if ( function_exists( 'php_uname' ) ) {
$this->data['server']['host'] = php_uname( 'n' );
$this->data['server']['OS'] = php_uname( 's' ) . ' ' . php_uname( 'r' );
}
}
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;
}
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 ( ! empty( $u ) && isset( $u['gid']) ) {
$g = posix_getgrgid( $u['gid'] );
if ( ! empty( $g ) && isset( $u['name'], $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;
}
}
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,57 @@
<?php
/**
* Hooks and actions collector.
*
* @package query-monitor
*/
class QM_Collector_Hooks extends QM_Collector {
public $id = 'hooks';
protected static $hide_core;
public function process() {
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_filter( 'all' ) ) {
$hooks[] = QM_Hook::process( 'all', $wp_filter, self::$hide_qm, self::$hide_core );
}
if ( defined( 'QM_SHOW_ALL_HOOKS' ) && QM_SHOW_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 ) {
$hook = QM_Hook::process( $name, $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,285 @@
<?php
/**
* HTTP API request collector.
*
* @package query-monitor
*/
class QM_Collector_HTTP extends QM_Collector {
public $id = 'http';
private $transport = null;
private $info = null;
public function __construct() {
parent::__construct();
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.before_request', array( $this, 'action_curl_before_request' ), 9999 );
add_action( 'requests-curl.after_request', array( $this, 'action_curl_after_request' ), 9999, 2 );
add_action( 'requests-fsockopen.before_request', array( $this, 'action_fsockopen_before_request' ), 9999 );
add_action( 'requests-fsockopen.after_request', array( $this, 'action_fsockopen_after_request' ), 9999, 2 );
}
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;
}
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',
);
}
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 $args HTTP request arguments.
* @param string $url The request URL.
* @return array HTTP request arguments.
*/
public function filter_http_request_args( array $args, $url ) {
$trace = new QM_Backtrace();
if ( isset( $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->data['http'][ $args['_qm_key'] ]['start'];
} else {
$start = microtime( true );
}
$key = microtime( true ) . $url;
$this->data['http'][ $key ] = array(
'url' => $url,
'args' => $args,
'start' => $start,
'trace' => $trace,
);
$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 bool|array|WP_Error $response The preemptive HTTP response. Default false.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return bool|array|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 $args HTTP request arguments.
* @param string $url The request URL.
*/
public function action_http_api_debug( $response, $action, $class, $args, $url ) {
switch ( $action ) {
case 'response':
if ( ! empty( $class ) ) {
$this->data['http'][ $args['_qm_key'] ]['transport'] = str_replace( 'wp_http_', '', strtolower( $class ) );
} else {
$this->data['http'][ $args['_qm_key'] ]['transport'] = null;
}
$this->log_http_response( $response, $args, $url );
break;
case 'transports_list':
# Nothing
break;
}
}
public function action_curl_before_request() {
$this->transport = 'curl';
}
public function action_curl_after_request( $headers, array $info = null ) {
$this->info = $info;
}
public function action_fsockopen_before_request() {
$this->transport = 'fsockopen';
}
public function action_fsockopen_after_request( $headers, array $info = null ) {
$this->info = $info;
}
/**
* Log an HTTP response.
*
* @param array|WP_Error $response The HTTP response.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
*/
public function log_http_response( $response, array $args, $url ) {
$this->data['http'][ $args['_qm_key'] ]['end'] = microtime( true );
$this->data['http'][ $args['_qm_key'] ]['response'] = $response;
$this->data['http'][ $args['_qm_key'] ]['args'] = $args;
if ( isset( $args['_qm_original_key'] ) ) {
$this->data['http'][ $args['_qm_original_key'] ]['end'] = $this->data['http'][ $args['_qm_original_key'] ]['start'];
$this->data['http'][ $args['_qm_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->data['http'][ $args['_qm_key'] ]['info'] = $this->info;
$this->data['http'][ $args['_qm_key'] ]['transport'] = $this->transport;
$this->info = null;
$this->transport = null;
}
public function process() {
$this->data['ltime'] = 0;
if ( ! isset( $this->data['http'] ) ) {
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',
) );
foreach ( $this->data['http'] as $key => & $http ) {
if ( ! isset( $http['response'] ) ) {
// Timed out
$http['response'] = new WP_Error( 'http_request_timed_out', __( 'Request timed out', 'query-monitor' ) );
$http['end'] = floatval( $http['start'] + $http['args']['timeout'] );
}
if ( is_wp_error( $http['response'] ) ) {
if ( ! in_array( $http['response']->get_error_code(), $silent, true ) ) {
$this->data['errors']['alert'][] = $key;
}
$http['type'] = -1;
} elseif ( ! $http['args']['blocking'] ) {
$http['type'] = -2;
} else {
$http['type'] = intval( wp_remote_retrieve_response_code( $http['response'] ) );
if ( $http['type'] >= 400 ) {
$this->data['errors']['warning'][] = $key;
}
}
$http['ltime'] = ( $http['end'] - $http['start'] );
if ( isset( $http['info'] ) ) {
if ( isset( $http['info']['total_time'] ) ) {
$http['ltime'] = $http['info']['total_time'];
}
if ( ! empty( $http['info']['url'] ) ) {
if ( rtrim( $http['url'], '/' ) !== rtrim( $http['info']['url'], '/' ) ) {
$http['redirected_to'] = $http['info']['url'];
}
}
}
$this->data['ltime'] += $http['ltime'];
$http['component'] = $http['trace']->get_component();
$this->log_type( $http['type'] );
$this->log_component( $http['component'], $http['ltime'], $http['type'] );
}
}
}
# 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,168 @@
<?php
/**
* Language and locale collector.
*
* @package query-monitor
*/
class QM_Collector_Languages extends QM_Collector {
public $id = 'languages';
public function __construct() {
parent::__construct();
add_filter( 'override_load_textdomain', array( $this, 'log_file_load' ), 9999, 3 );
add_filter( 'load_script_translation_file', array( $this, 'log_script_file_load' ), 9999, 3 );
}
public function get_concerned_actions() {
return array(
'load_textdomain',
'unload_textdomain',
);
}
public function get_concerned_filters() {
return array(
'determine_locale',
'gettext',
'gettext_with_context',
'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',
);
}
public function get_concerned_options() {
return array(
'WPLANG',
);
}
public function get_concerned_constants() {
return array(
'WPLANG',
);
}
public function process() {
$this->data['locale'] = get_locale();
$this->data['user_locale'] = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale();
ksort( $this->data['languages'] );
foreach ( $this->data['languages'] as & $mofiles ) {
foreach ( $mofiles as & $mofile ) {
$mofile['found_formatted'] = $mofile['found'] ? size_format( $mofile['found'] ) : '';
}
}
}
/**
* Store log data.
*
* @param bool $override Whether to override the text domain. Default false.
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the MO file.
* @return bool
*/
public function log_file_load( $override, $domain, $mofile ) {
if ( 'query-monitor' === $domain && self::hide_qm() ) {
return $override;
}
$trace = new QM_Backtrace();
$filtered = $trace->get_filtered_trace();
$caller = array();
foreach ( $filtered as $i => $item ) {
if ( in_array( $item['function'], array(
'load_muplugin_textdomain',
'load_plugin_textdomain',
'load_theme_textdomain',
'load_child_theme_textdomain',
'load_default_textdomain',
), true ) ) {
$caller = $item;
$display = $i + 1;
if ( isset( $filtered[ $display ] ) ) {
$caller['display'] = $filtered[ $display ]['display'];
}
break;
}
}
if ( empty( $caller ) ) {
if ( isset( $filtered[1] ) ) {
$caller = $filtered[1];
} else {
$caller = $filtered[0];
}
}
if ( ! isset( $caller['file'] ) && isset( $filtered[0]['file'] ) && isset( $filtered[0]['line'] ) ) {
$caller['file'] = $filtered[0]['file'];
$caller['line'] = $filtered[0]['line'];
}
$found = file_exists( $mofile ) ? filesize( $mofile ) : false;
$this->data['languages'][ $domain ][] = array(
'caller' => $caller,
'domain' => $domain,
'file' => $mofile,
'found' => $found,
'handle' => null,
'type' => 'gettext',
);
return $override;
}
/**
* 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();
$filtered = $trace->get_filtered_trace();
$caller = $filtered[0];
$found = ( $file && file_exists( $file ) ) ? filesize( $file ) : false;
$this->data['languages'][ $domain ][] = array(
'caller' => $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,162 @@
<?php
/**
* PSR-3 compatible logging collector.
*
* @package query-monitor
*/
class QM_Collector_Logger extends QM_Collector {
public $id = 'logger';
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
public function __construct() {
parent::__construct();
foreach ( $this->get_levels() as $level ) {
add_action( "qm/{$level}", array( $this, $level ), 10, 2 );
}
add_action( 'qm/log', array( $this, 'log' ), 10, 3 );
}
public function emergency( $message, array $context = array() ) {
$this->store( self::EMERGENCY, $message, $context );
}
public function alert( $message, array $context = array() ) {
$this->store( self::ALERT, $message, $context );
}
public function critical( $message, array $context = array() ) {
$this->store( self::CRITICAL, $message, $context );
}
public function error( $message, array $context = array() ) {
$this->store( self::ERROR, $message, $context );
}
public function warning( $message, array $context = array() ) {
$this->store( self::WARNING, $message, $context );
}
public function notice( $message, array $context = array() ) {
$this->store( self::NOTICE, $message, $context );
}
public function info( $message, array $context = array() ) {
$this->store( self::INFO, $message, $context );
}
public function debug( $message, array $context = array() ) {
$this->store( self::DEBUG, $message, $context );
}
public function log( $level, $message, array $context = array() ) {
if ( ! in_array( $level, $this->get_levels(), true ) ) {
throw new InvalidArgumentException( __( 'Unsupported log level', 'query-monitor' ) );
}
$this->store( $level, $message, $context );
}
protected function store( $level, $message, array $context = array() ) {
$type = 'string';
$trace = new QM_Backtrace( array(
'ignore_frames' => 2,
) );
if ( is_wp_error( $message ) ) {
$type = 'wp_error';
$message = sprintf(
'WP_Error: %s (%s)',
$message->get_error_message(),
$message->get_error_code()
);
}
if ( ( $message instanceof Exception ) || ( $message instanceof Throwable ) ) {
$type = 'throwable';
$message = get_class( $message ) . ': ' . $message->getMessage();
}
if ( ! QM_Util::is_stringy( $message ) ) {
$type = 'dump';
$message = print_r( $message, true );
}
$this->data['logs'][] = array(
'message' => self::interpolate( $message, $context ),
'context' => $context,
'trace' => $trace,
'level' => $level,
'type' => $type,
);
}
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 ) || QM_Util::is_stringy( $val ) ) {
$replace[ "{{$key}}" ] = $val;
}
}
// interpolate replacement values into the message and return
return strtr( $message, $replace );
}
public function process() {
if ( empty( $this->data['logs'] ) ) {
return;
}
$components = array();
foreach ( $this->data['logs'] as $row ) {
$component = $row['trace']->get_component();
$components[ $component->name ] = $component->name;
}
$this->data['components'] = $components;
}
public function get_levels() {
return array(
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
self::NOTICE,
self::INFO,
self::DEBUG,
);
}
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,65 @@
<?php
/**
* General overview collector.
*
* @package query-monitor
*/
class QM_Collector_Overview extends QM_Collector {
public $id = 'overview';
public function process() {
$this->data['time_taken'] = self::timer_stop_float();
$this->data['time_limit'] = ini_get( 'max_execution_time' );
$this->data['time_start'] = $GLOBALS['timestart'];
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 ( 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;
}
if ( is_user_logged_in() ) {
$this->data['current_user'] = self::format_user( wp_get_current_user() );
} else {
$this->data['current_user'] = false;
}
if ( function_exists( 'current_user_switched' ) && current_user_switched() ) {
$this->data['switched_user'] = self::format_user( current_user_switched() );
} else {
$this->data['switched_user'] = false;
}
$this->data['memory_limit'] = QM_Util::convert_hr_to_bytes( ini_get( 'memory_limit' ) );
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();
}
}
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,504 @@
<?php
/**
* PHP error collector.
*
* @package query-monitor
*/
define( 'QM_ERROR_FATALS', E_ERROR | E_PARSE | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR );
class QM_Collector_PHP_Errors extends QM_Collector {
public $id = 'php_errors';
public $types = array();
private $error_reporting = null;
private $display_errors = null;
private $exception_handler = null;
private static $unexpected_error;
public function __construct() {
if ( defined( 'QM_DISABLE_ERROR_HANDLER' ) && QM_DISABLE_ERROR_HANDLER ) {
return;
}
parent::__construct();
// Capture the last error that occurred before QM loaded:
$prior_error = error_get_last();
// Non-fatal error handler for all PHP versions:
set_error_handler( array( $this, 'error_handler' ), ( E_ALL ^ QM_ERROR_FATALS ) );
if ( ! interface_exists( 'Throwable' ) ) {
// Fatal error handler for PHP < 7:
register_shutdown_function( array( $this, 'shutdown_handler' ) );
}
// Fatal error handler for PHP >= 7, and uncaught exception handler for all PHP versions:
$this->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
);
}
}
/**
* Uncaught exception handler.
*
* In PHP >= 7 this will receive a Throwable object.
* In PHP < 7 it will receive an Exception object.
*
* @param Throwable|Exception $e The error or exception.
*/
public function exception_handler( $e ) {
if ( is_a( $e, 'Exception' ) ) {
$error = 'Uncaught Exception';
} else {
$error = 'Uncaught Error';
}
$this->output_fatal( 'Fatal error', array(
'message' => sprintf(
'%s: %s',
$error,
$e->getMessage()
),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTrace(),
) );
// The exception 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->exception_handler ) {
call_user_func( $this->exception_handler, $e );
} else {
throw $e;
}
exit( 1 );
}
public function error_handler( $errno, $message, $file = null, $line = null, $context = null, $do_trace = true ) {
/**
* 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 $file The file location.
* @param string $line The line number.
* @param string $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;
default:
return false;
break;
}
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( array(
'ignore_current_filter' => false,
) );
$caller = $trace->get_caller();
$key = md5( $message . $file . $line . $caller['id'] );
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' => QM_Util::standard_dir( $file, '' ),
'line' => $line,
'trace' => ( $do_trace ? $trace : null ),
'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 );
}
/**
* Displays fatal error output for sites running PHP < 7.
*/
public function shutdown_handler() {
$e = error_get_last();
if ( empty( $e ) || ! ( $e['type'] & QM_ERROR_FATALS ) ) {
return;
}
if ( $e['type'] & E_RECOVERABLE_ERROR ) {
$error = 'Catchable fatal error';
} else {
$error = 'Fatal error';
}
$this->output_fatal( $error, $e );
}
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;
}
if ( ! function_exists( '__' ) ) {
wp_load_translations_early();
}
require_once dirname( __DIR__ ) . '/output/Html.php';
// 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="%s" media="all" />',
esc_url( includes_url( 'css/dashicons.css' ) )
);
printf(
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
'<link rel="stylesheet" href="%s" media="all" />',
esc_url( QueryMonitor::init()->plugin_url( 'assets/query-monitor.css' ) )
);
// This unused wrapper with ann attribute serves to help the #qm-fatal div break out of an
// attribute if a fatal has occured 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'], '' ) ),
esc_attr( $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'] );
}
printf(
'<p><span class="dashicons dashicons-warning" aria-hidden="true"></span> <b>%1$s</b>: %2$s<br>in <b>%3$s</b> on line <b>%4$d</b></p>',
esc_html( $error ),
nl2br( esc_html( $e['message'] ), false ),
$file,
intval( $e['line'] )
); // WPCS: XSS ok.
if ( ! empty( $e['trace'] ) ) {
echo '<p>' . esc_html__( 'Call stack:', 'query-monitor' ) . '</p>';
echo '<ol>';
foreach ( $e['trace'] as $frame ) {
$callback = QM_Util::populate_callback( $frame );
printf(
'<li>%s</li>',
QM_Output_Html::output_filename( $callback['name'], $frame['file'], $frame['line'] )
); // WPCS: XSS ok.
}
echo '</ol>';
}
echo '</div>';
echo '<h2>' . esc_html__( 'Query Monitor', 'query-monitor' ) . '</h2>';
echo '</div>';
echo '</div>';
}
public function post_process() {
ini_set( 'display_errors', $this->display_errors );
restore_error_handler();
restore_exception_handler();
}
/**
* 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.
*/
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 ) && ! 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 int[] $levels The error levels used for each component.
*/
$levels = apply_filters( 'qm/collect/php_error_levels', array() );
/**
* Controls whether silenced PHP errors are hidden entirely by Query Monitor.
*
* To hide silenced errors, use:
*
* add_filter( 'qm/collect/hide_silenced_php_errors', '__return_true' );
*
* @since 2.7.0
*
* @param bool $hide Whether to hide silenced PHP errors. Default false.
*/
$this->hide_silenced_php_errors = apply_filters( 'qm/collect/hide_silenced_php_errors', false );
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 ] ) ) {
foreach ( $this->data[ $error_group ][ $type ] as $error ) {
if ( $error['trace'] ) {
$component = $error['trace']->get_component();
$components[ $component->name ] = $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 int[] $components The error levels keyed by component name.
* @param string $component_type The component type, for example 'plugin' or 'theme'.
*/
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 ( ! $error['trace'] ) {
continue;
}
if ( ! $this->is_affected_component( $error['trace']->get_component(), $component_type, $component_context ) ) {
continue;
}
unset( $this->data['errors'][ $error_level ][ $error_id ] );
if ( $this->hide_silenced_php_errors ) {
continue;
}
$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 object $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 ) {
if ( empty( $component ) ) {
return false;
}
return ( $component->type === $component_type && $component->context === $component_context );
}
/**
* Checks if the error number specified is viewable based on the
* flags specified.
*
* @param int $error_no The errno from PHP
* @param int $flags The config flags specified by users
* @return int Truthy int value if reportable else 0.
*
* 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.
*/
public function is_reportable_error( $error_no, $flags ) {
if ( ! is_null( $flags ) ) {
$result = $error_no & $flags;
} else {
$result = 1;
}
return (bool) $result;
}
/**
* For testing purposes only. Sets the errors property manually.
* Needed to test the filter since the data property is protected.
*
* @param array $errors The list of errors
*/
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,88 @@
<?php
class QM_Collector_Raw_Request extends QM_Collector {
public $id = 'raw_request';
/**
* Extracts headers from a PHP-style $_SERVER array.
*
* From WP_REST_Server::get_headers()
*
* @param array $server Associative array similar to `$_SERVER`.
* @return array 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.
*/
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' => isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/',
'query' => isset( $_SERVER['QUERY_STRING'] ) ? 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' => self::http_response_code(),
'headers' => $headers,
);
$this->data['response'] = $response;
}
public static function http_response_code() {
if ( is_callable( 'http_response_code' ) ) {
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.http_response_codeFound
return http_response_code();
}
return null;
}
}
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,36 @@
<?php
/**
* HTTP redirect collector.
*
* @package query-monitor
*/
class QM_Collector_Redirects extends QM_Collector {
public $id = 'redirects';
public function __construct() {
parent::__construct();
add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 9999, 2 );
}
public function filter_wp_redirect( $location, $status ) {
if ( ! $location ) {
return $location;
}
$trace = new QM_Backtrace();
$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,295 @@
<?php
/**
* Request collector.
*
* @package query-monitor
*/
class QM_Collector_Request extends QM_Collector {
public $id = 'request';
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',
);
}
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;
}
public function get_concerned_options() {
return array(
'home',
'permalink_structure',
'rewrite_rules',
'siteurl',
);
}
public function get_concerned_constants() {
return array(
'WP_HOME',
'WP_SITEURL',
);
}
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'] ) ) {
$home_path = trim( parse_url( home_url(), PHP_URL_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() ) );
$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, 'term_id' ):
// 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();
foreach ( $wp_rewrite->rules as $match => $query ) {
if ( preg_match( "#^{$match}#", $this->data['request']['request'] ) ) {
$matching[ $match ] = $query;
}
}
$this->data['matching_rewrites'] = $matching;
}
}
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,280 @@
<?php
/**
* Template and theme collector.
*
* @package query-monitor
*/
class QM_Collector_Theme extends QM_Collector {
public $id = 'response';
protected $got_theme_compat = false;
protected $query_templates = array();
public function __construct() {
parent::__construct();
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 );
}
public function get_concerned_actions() {
return array(
'template_redirect',
);
}
public function get_concerned_filters() {
$filters = array(
'stylesheet',
'stylesheet_directory',
'template',
'template_directory',
'template_include',
);
foreach ( self::get_query_template_names() as $template => $conditional ) {
// @TODO this isn't correct for post type archives
$filter = str_replace( '_', '', $template );
$filters[] = "{$filter}_template_hierarchy";
$filters[] = "{$filter}_template";
}
return $filters;
}
public function get_concerned_options() {
return array(
'stylesheet',
'template',
);
}
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';
if ( function_exists( 'is_privacy_policy' ) ) {
$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;
}
// https://core.trac.wordpress.org/ticket/14310
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 );
$loaded_template = call_user_func( $get_template );
$default_template = locate_template( $this->query_templates );
if ( $default_template !== $loaded_template ) {
$this->data['template_altered'] = true;
}
remove_filter( "{$filter}_template_hierarchy", array( $this, 'filter_template_hierarchy' ), 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.
* @param string[] $templates Array of template files to search for, in order.
*/
public function action_get_template_part( $slug, $name, $templates ) {
$data = compact( 'slug', 'name', 'templates' );
$data['trace'] = new QM_Backtrace( array(
'ignore_frames' => 4,
) );
$this->data['requested_template_parts'][] = $data;
}
public function filter_template_hierarchy( array $templates ) {
$this->query_templates = $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;
}
}
$this->data['template_hierarchy'] = array_merge( $this->data['template_hierarchy'], $templates );
return $templates;
}
public function filter_body_class( array $class ) {
$this->data['body_class'] = $class;
return $class;
}
public function filter_template_include( $template_path ) {
$this->data['template_path'] = $template_path;
return $template_path;
}
public function filter_timber_output( $output, $data = null, $file = null ) {
if ( $file ) {
$this->data['timber_files'][] = $file;
}
return $output;
}
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'] );
}
$this->data['has_template_part_action'] = function_exists( 'wp_body_open' );
if ( $this->data['has_template_part_action'] ) {
// Since WP 5.2, the `get_template_part` action populates this data nicely:
if ( ! empty( $this->data['requested_template_parts'] ) ) {
$this->data['template_parts'] = array();
$this->data['theme_template_parts'] = array();
$this->data['count_template_parts'] = array();
foreach ( $this->data['requested_template_parts'] as $part ) {
$file = locate_template( $part['templates'] );
$part['caller'] = $part['trace']->get_caller();
unset( $part['trace'] );
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 );
$slug = trim( str_replace( '.php', '', $filename ), '/' );
$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;
}
}
} else {
// Prior to WP 5.2, we need to look into `get_included_files()` and do our best to figure out
// if each one is a template part:
foreach ( get_included_files() as $file ) {
$file = QM_Util::standard_dir( $file );
$filename = str_replace( array(
$stylesheet_directory,
$template_directory,
), '', $file );
if ( $filename !== $file ) {
$slug = trim( str_replace( '.php', '', $filename ), '/' );
$display = trim( $filename, '/' );
$theme_display = trim( str_replace( $theme_directory, '', $file ), '/' );
$count = did_action( "get_template_part_{$slug}" );
if ( $count ) {
$this->data['template_parts'][ $file ] = $display;
$this->data['theme_template_parts'][ $file ] = $theme_display;
$this->data['count_template_parts'][ $file ] = $count;
} else {
$slug = trim( preg_replace( '|\-[^\-]+$|', '', $slug ), '/' );
$count = did_action( "get_template_part_{$slug}" );
if ( $count ) {
$this->data['template_parts'][ $file ] = $display;
$this->data['theme_template_parts'][ $file ] = $theme_display;
$this->data['count_template_parts'][ $file ] = $count;
}
}
}
}
}
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'] );
if ( isset( $this->data['body_class'] ) ) {
asort( $this->data['body_class'] );
}
}
}
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,101 @@
<?php
/**
* Timing and profiling collector.
*
* @package query-monitor
*/
class QM_Collector_Timing extends QM_Collector {
public $id = 'timing';
private $track_timer = array();
private $start = array();
private $stop = array();
public function __construct() {
parent::__construct();
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 );
}
public function action_function_time_start( $function ) {
$this->track_timer[ $function ] = new QM_Timer();
$this->start[ $function ] = $this->track_timer[ $function ]->start();
}
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' ),
'trace' => $trace,
);
return;
}
$this->stop[ $function ] = $this->track_timer[ $function ]->stop();
$this->calculate_time( $function );
}
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' ),
'trace' => $trace,
);
return;
}
$this->track_timer[ $function ]->lap( array(), $name );
}
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,
'trace' => $trace,
'start_time' => ( $start_time - $GLOBALS['timestart'] ),
'end_time' => ( $end_time - $GLOBALS['timestart'] ),
);
}
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' ),
'trace' => $trace,
);
}
}
if ( ! empty( $this->data['timing'] ) ) {
usort( $this->data['timing'], array( $this, 'sort_by_start_time' ) );
}
}
public function sort_by_start_time( array $a, array $b ) {
if ( $a['start_time'] === $b['start_time'] ) {
return 0;
} else {
return ( $a['start_time'] > $b['start_time'] ) ? 1 : -1;
}
}
}
# 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,81 @@
<?php
/**
* Transient storage collector.
*
* @package query-monitor
*/
class QM_Collector_Transients extends QM_Collector {
public $id = 'transients';
public function __construct() {
parent::__construct();
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 );
}
public function tear_down() {
parent::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 );
}
public function action_setted_site_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'site', $value, $expiration );
}
public function action_setted_blog_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'blog', $value, $expiration );
}
public function setted_transient( $transient, $type, $value, $expiration ) {
$trace = new QM_Backtrace( array(
'ignore_frames' => 1, # Ignore the action_setted_(site|blog)_transient method
) );
$name = str_replace( array(
'_site_transient_',
'_transient_',
), '', $transient );
$size = strlen( maybe_serialize( $value ) );
$this->data['trans'][] = array(
'name' => $name,
'trace' => $trace,
'type' => $type,
'value' => $value,
'expiration' => $expiration,
'exp_diff' => ( $expiration ? human_time_diff( 0, $expiration ) : '' ),
'size' => $size,
'size_formatted' => size_format( $size ),
);
}
public function process() {
$this->data['has_type'] = is_multisite();
if ( empty( $this->data['trans'] ) ) {
return;
}
foreach ( $this->data['trans'] as $i => $transient ) {
$filtered_trace = $transient['trace']->get_display_trace();
array_shift( $filtered_trace ); // remove do_action('setted_(site_)?transient')
array_shift( $filtered_trace ); // remove set_(site_)?transient()
$component = $transient['trace']->get_component();
$this->data['trans'][ $i ]['filtered_trace'] = $filtered_trace;
$this->data['trans'][ $i ]['component'] = $component;
unset( $this->data['trans'][ $i ]['trace'] );
}
}
}
# 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() );