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

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
/**
* Abstract output class for HTTP headers.
*
* @package query-monitor
*/
abstract class QM_Output_Headers extends QM_Output {
/**
* @return void
*/
public function output() {
$id = $this->collector->id;
foreach ( $this->get_output() as $key => $value ) {
if ( ! is_scalar( $value ) ) {
$value = json_encode( $value );
}
# Remove illegal characters (Header may not contain NUL bytes)
if ( is_string( $value ) ) {
$value = str_replace( chr( 0 ), '', $value );
}
header( sprintf( 'X-QM-%s-%s: %s', $id, $key, $value ) );
}
}
}

View File

@ -0,0 +1,626 @@
<?php declare(strict_types = 1);
/**
* Abstract output class for HTML pages.
*
* @package query-monitor
*/
abstract class QM_Output_Html extends QM_Output {
/**
* @var string|false|null
*/
protected static $file_link_format = null;
/**
* @var string|null
*/
protected $current_id = null;
/**
* @var string|null
*/
protected $current_name = null;
/**
* @return string
*/
public function name() {
return $this->collector->id;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( $this->name() ),
) );
return $menu;
}
/**
* @return string
*/
public function get_output() {
ob_start();
// compat until I convert all the existing outputters to use `get_output()`
$this->output();
$out = (string) ob_get_clean();
return $out;
}
/**
* @param string $id
* @param string $name
* @return void
*/
protected function before_tabular_output( $id = null, $name = null ) {
if ( null === $id ) {
$id = $this->collector->id();
}
if ( null === $name ) {
$name = $this->name();
}
$this->current_id = $id;
$this->current_name = $name;
printf(
'<div class="qm" id="%1$s" role="tabpanel" aria-labelledby="%1$s-caption" tabindex="-1">',
esc_attr( $id )
);
echo '<table class="qm-sortable">';
printf(
'<caption class="qm-screen-reader-text"><h2 id="%1$s-caption">%2$s</h2></caption>',
esc_attr( $id ),
esc_html( $name )
);
}
/**
* @return void
*/
protected function after_tabular_output() {
echo '</table>';
echo '</div>';
$this->output_concerns();
}
/**
* @param string $id
* @param string $name
* @return void
*/
protected function before_non_tabular_output( $id = null, $name = null ) {
if ( null === $id ) {
$id = $this->collector->id();
}
if ( null === $name ) {
$name = $this->name();
}
$this->current_id = $id;
$this->current_name = $name;
printf(
'<div class="qm qm-non-tabular" id="%1$s" role="tabpanel" aria-labelledby="%1$s-caption" tabindex="-1">',
esc_attr( $id )
);
echo '<div class="qm-boxed">';
printf(
'<h2 class="qm-screen-reader-text" id="%1$s-caption">%2$s</h2>',
esc_attr( $id ),
esc_html( $name )
);
}
/**
* @return void
*/
protected function after_non_tabular_output() {
echo '</div>';
echo '</div>';
$this->output_concerns();
}
/**
* @return void
*/
protected function output_concerns() {
$concerns = array(
'concerned_actions' => array(
__( 'Related Hooks with Actions Attached', 'query-monitor' ),
__( 'Action', 'query-monitor' ),
),
'concerned_filters' => array(
__( 'Related Hooks with Filters Attached', 'query-monitor' ),
__( 'Filter', 'query-monitor' ),
),
);
if ( empty( $this->collector->concerned_actions ) && empty( $this->collector->concerned_filters ) ) {
return;
}
printf(
'<div class="qm qm-concerns" id="%1$s" role="tabpanel" aria-labelledby="%1$s-caption" tabindex="-1">',
esc_attr( $this->current_id . '-concerned_hooks' )
);
echo '<table>';
printf(
'<caption><h2 id="%1$s-caption">%2$s</h2></caption>',
esc_attr( $this->current_id . '-concerned_hooks' ),
sprintf(
/* translators: %s: Panel name */
esc_html__( '%s: Related Hooks with Filters or Actions Attached', 'query-monitor' ),
esc_html( $this->name() )
)
);
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Hook', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Type', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Priority', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Callback', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $concerns as $key => $labels ) {
if ( empty( $this->collector->$key ) ) {
continue;
}
QM_Output_Html_Hooks::output_hook_table( $this->collector->$key, true );
}
echo '</tbody>';
echo '</table>';
echo '</div>';
}
/**
* @param string $id
* @param string $name
* @return void
*/
protected function before_debug_bar_output( $id = null, $name = null ) {
if ( null === $id ) {
$id = $this->collector->id();
}
if ( null === $name ) {
$name = $this->name();
}
printf(
'<div class="qm qm-debug-bar" id="%1$s" role="tabpanel" aria-labelledby="%1$s-caption" tabindex="-1">',
esc_attr( $id )
);
printf(
'<h2 class="qm-screen-reader-text" id="%1$s-caption">%2$s</h2>',
esc_attr( $id ),
esc_html( $name )
);
}
/**
* @return void
*/
protected function after_debug_bar_output() {
echo '</div>';
}
/**
* @param string $notice
* @return string
*/
protected function build_notice( $notice ) {
$return = '<section>';
$return .= '<div class="qm-notice">';
$return .= '<p>';
$return .= $notice;
$return .= '</p>';
$return .= '</div>';
$return .= '</section>';
return $return;
}
/**
* @param array<string, mixed> $vars
* @return void
*/
public static function output_inner( array $vars ) {
echo '<table>';
foreach ( $vars as $key => $value ) {
echo '<tr>';
echo '<td>' . esc_html( $key ) . '</td>';
if ( is_array( $value ) ) {
echo '<td>';
self::output_inner( $value );
echo '</td>';
} elseif ( is_object( $value ) ) {
echo '<td>';
self::output_inner( get_object_vars( $value ) );
echo '</td>';
} elseif ( is_bool( $value ) ) {
if ( $value ) {
echo '<td class="qm-true">true</td>';
} else {
echo '<td class="qm-false">false</td>';
}
} else {
echo '<td>';
echo nl2br( esc_html( $value ) );
echo '</td>';
}
echo '</td>';
echo '</tr>';
}
echo '</table>';
}
/**
* Returns the table filter controls. Safe for output.
*
* @param string $name The name for the `data-` attributes that get filtered by this control.
* @param (string|int)[] $values Option values for this control.
* @param string $label Label text for the filter control.
* @param array $args {
* @type string $highlight The name for the `data-` attributes that get highlighted by this control.
* @type string[] $prepend Associative array of options to prepend to the list of values.
* @type string[] $append Associative array of options to append to the list of values.
* }
* @phpstan-param array{
* highlight?: string,
* prepend?: array<string, string>,
* append?: array<string, string>,
* } $args
* @return string Markup for the table filter controls.
*/
protected function build_filter( $name, $values, $label, $args = array() ) {
if ( empty( $values ) || ! is_array( $values ) ) {
return esc_html( $label ); // Return label text, without being marked up as a label element.
}
if ( ! is_array( $args ) ) {
$args = array(
'highlight' => $args,
);
}
$args = array_merge( array(
'highlight' => '',
'prepend' => array(),
'append' => array(),
'all' => _x( 'All', '"All" option for filters', 'query-monitor' ),
), $args );
$core_val = __( 'WordPress Core', 'query-monitor' );
$core_key = array_search( $core_val, $values, true );
if ( 'component' === $name && count( $values ) > 1 && false !== $core_key ) {
$args['append'][ $core_val ] = $core_val;
$args['append']['non-core'] = __( 'Non-WordPress Core', 'query-monitor' );
unset( $values[ $core_key ] );
}
$filter_id = 'qm-filter-' . $this->collector->id . '-' . $name;
$out = '<div class="qm-filter-container">';
$out .= '<label for="' . esc_attr( $filter_id ) . '">' . esc_html( $label ) . '</label>';
$out .= '<select id="' . esc_attr( $filter_id ) . '" class="qm-filter" data-filter="' . esc_attr( $name ) . '" data-highlight="' . esc_attr( $args['highlight'] ) . '">';
$out .= '<option value="">' . esc_html( $args['all'] ) . '</option>';
if ( ! empty( $args['prepend'] ) ) {
foreach ( $args['prepend'] as $value => $label ) {
$out .= '<option value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
}
}
foreach ( $values as $key => $value ) {
if ( is_int( $key ) && $key >= 0 ) {
$out .= '<option value="' . esc_attr( $value ) . '">' . esc_html( $value ) . '</option>';
} else {
$out .= '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
}
}
if ( ! empty( $args['append'] ) ) {
foreach ( $args['append'] as $value => $label ) {
$out .= '<option value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
}
}
$out .= '</select>';
$out .= '</div>';
return $out;
}
/**
* Returns the column sorter controls. Safe for output.
*
* @param string $heading Heading text for the column. Optional.
* @return string Markup for the column sorter controls.
*/
protected function build_sorter( $heading = '' ) {
$out = '';
$out .= '<span class="qm-th">';
$out .= '<span class="qm-sort-heading">';
if ( '#' === $heading ) {
$out .= '<span class="qm-screen-reader-text">' . esc_html__( 'Sequence', 'query-monitor' ) . '</span>';
} elseif ( $heading ) {
$out .= esc_html( $heading );
}
$out .= '</span>';
$out .= '<button class="qm-sort-controls" aria-label="' . esc_attr__( 'Sort data by this column', 'query-monitor' ) . '">';
$out .= QueryMonitor::icon( 'arrow-down' );
$out .= '</button>';
$out .= '</span>';
return $out;
}
/**
* Returns a toggle control. Safe for output.
*
* @return string Markup for the column sorter controls.
*/
protected static function build_toggler() {
$out = '<button class="qm-toggle" data-on="+" data-off="-" aria-expanded="false" aria-label="' . esc_attr__( 'Toggle more information', 'query-monitor' ) . '"><span aria-hidden="true">+</span></button>';
return $out;
}
/**
* Returns a filter trigger.
*
* @param string $target
* @param string $filter
* @param string $value
* @param string $label
* @return string
*/
protected static function build_filter_trigger( $target, $filter, $value, $label ) {
return sprintf(
'<button class="qm-filter-trigger" data-qm-target="%1$s" data-qm-filter="%2$s" data-qm-value="%3$s">%4$s%5$s</button>',
esc_attr( $target ),
esc_attr( $filter ),
esc_attr( $value ),
$label,
QueryMonitor::icon( 'filter' )
);
}
/**
* Returns a link.
*
* @param string $href
* @param string $label
* @return string
*/
protected static function build_link( $href, $label ) {
return sprintf(
'<a href="%1$s" class="qm-link">%2$s%3$s</a>',
esc_attr( $href ),
$label,
QueryMonitor::icon( 'external' )
);
}
/**
* @param array<string, mixed> $args
* @return array<string, mixed>
*/
protected function menu( array $args ) {
return array_merge( array(
'id' => esc_attr( "query-monitor-{$this->collector->id}" ),
'href' => esc_attr( '#' . $this->collector->id() ),
), $args );
}
/**
* Returns the given SQL string in a nicely presented format. Safe for output.
*
* @param string $sql An SQL query string.
* @return string The SQL formatted with markup.
*/
public static function format_sql( $sql ) {
$sql = str_replace( array( "\r\n", "\r", "\n", "\t" ), ' ', $sql );
$sql = esc_html( $sql );
$sql = trim( $sql );
$regex = 'ADD|AFTER|ALTER|AND|BEGIN|COMMIT|CREATE|DELETE|DESCRIBE|DO|DROP|ELSE|END|EXCEPT|EXPLAIN|FROM|GROUP|HAVING|INNER|INSERT|INTERSECT|LEFT|LIMIT|ON|OR|ORDER|OUTER|RENAME|REPLACE|RIGHT|ROLLBACK|SELECT|SET|SHOW|START|THEN|TRUNCATE|UNION|UPDATE|USE|USING|VALUES|WHEN|WHERE|XOR';
$sql = preg_replace( '# (' . $regex . ') #', '<br> $1 ', $sql );
$keywords = '\b(?:ACTION|ADD|AFTER|AGAINST|ALTER|AND|ASC|AS|AUTO_INCREMENT|BEGIN|BETWEEN|BIGINT|BINARY|BIT|BLOB|BOOLEAN|BOOL|BREAK|BY|CASE|COLLATE|COLUMNS?|COMMIT|CONTINUE|CREATE|DATA(?:BASES?)?|DATE(?:TIME)?|DECIMAL|DECLARE|DEC|DEFAULT|DELAYED|DELETE|DESCRIBE|DESC|DISTINCT|DOUBLE|DO|DROP|DUPLICATE|ELSE|END|ENUM|EXCEPT|EXISTS|EXPLAIN|FIELDS|FLOAT|FORCE|FOREIGN|FOR|FROM|FULL|FUNCTION|GROUP|HAVING|IF|IGNORE|INDEX|INNER|INSERT|INTEGER|INTERSECT|INTERVAL|INTO|INT|IN|IS|JOIN|KEYS?|LEFT|LIKE|LIMIT|LONG(?:BLOB|TEXT)|MEDIUM(?:BLOB|INT|TEXT)|MATCH|MERGE|MIDDLEINT|NOT|NO|NULLIF|ON|ORDER|OR|OUTER|PRIMARY|PROC(?:EDURE)?|REGEXP|RENAME|REPLACE|RIGHT|RLIKE|ROLLBACK|SCHEMA|SELECT|SET|SHOW|SMALLINT|START|TABLES?|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TRUNCATE|UNION|UNIQUE|UNSIGNED|UPDATE|USE|USING|VALUES?|VAR(?:BINARY|CHAR)|WHEN|WHERE|WHILE|XOR)\b';
$sql = preg_replace( '#' . $keywords . '#', '<b>$0</b>', $sql );
return '<code>' . $sql . '</code>';
}
/**
* Returns the given URL in a nicely presented format. Safe for output.
*
* @param string $url A URL.
* @return string The URL formatted with markup.
*/
public static function format_url( $url ) {
return str_replace( array( '?', '&amp;' ), array( '<br>?', '<br>&amp;' ), esc_html( $url ) );
}
/**
* Returns a file path, name, and line number, or a clickable link to the file. Safe for output.
*
* @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/
*
* @param string $text The display text, such as a function name or file name.
* @param string $file The full file path and name.
* @param int $line Optional. A line number, if appropriate.
* @param bool $is_filename Optional. Is the text a plain file name? Default false.
* @return string The fully formatted file link or file name, safe for output.
*/
public static function output_filename( $text, $file, $line = 0, $is_filename = false ) {
if ( empty( $file ) ) {
if ( $is_filename ) {
return esc_html( $text );
} else {
return '<code>' . esc_html( $text ) . '</code>';
}
}
$link_line = $line ?: 1;
if ( ! self::has_clickable_links() ) {
$fallback = QM_Util::standard_dir( $file, '' );
if ( $line ) {
$fallback .= ':' . $line;
}
if ( $is_filename ) {
$return = esc_html( $text );
} else {
$return = '<code>' . esc_html( $text ) . '</code>';
}
if ( $fallback !== $text ) {
$return .= '<br><span class="qm-info qm-supplemental">' . esc_html( $fallback ) . '</span>';
}
return $return;
}
$map = self::get_file_path_map();
if ( ! empty( $map ) ) {
foreach ( $map as $from => $to ) {
$file = str_replace( $from, $to, $file );
}
}
/** @var string */
$link_format = self::get_file_link_format();
$link = sprintf( $link_format, rawurlencode( $file ), intval( $link_line ) );
if ( $is_filename ) {
$format = '<a href="%1$s" class="qm-edit-link">%2$s%3$s</a>';
} else {
$format = '<a href="%1$s" class="qm-edit-link"><code>%2$s</code>%3$s</a>';
}
return sprintf(
$format,
esc_attr( $link ),
esc_html( $text ),
QueryMonitor::icon( 'edit' )
);
}
/**
* Provides a protocol URL for edit links in QM stack traces for various editors.
*
* @param string $editor The chosen code editor.
* @param string|false $default_format A format to use if no editor is found.
* @return string|false A protocol URL format or boolean false.
*/
public static function get_editor_file_link_format( $editor, $default_format ) {
switch ( $editor ) {
case 'phpstorm':
return 'phpstorm://open?file=%f&line=%l';
case 'vscode':
return 'vscode://file/%f:%l';
case 'atom':
return 'atom://open/?url=file://%f&line=%l';
case 'sublime':
return 'subl://open/?url=file://%f&line=%l';
case 'textmate':
return 'txmt://open/?url=file://%f&line=%l';
case 'netbeans':
return 'nbopen://%f:%l';
case 'nova':
return 'nova://open?path=%f&line=%l';
default:
return $default_format;
}
}
/**
* @return string|false
*/
public static function get_file_link_format() {
if ( ! isset( self::$file_link_format ) ) {
$format = ini_get( 'xdebug.file_link_format' );
if ( defined( 'QM_EDITOR_COOKIE' ) && isset( $_COOKIE[ QM_EDITOR_COOKIE ] ) ) {
$format = self::get_editor_file_link_format(
$_COOKIE[ QM_EDITOR_COOKIE ],
$format
);
}
/**
* Filters the clickable file link format.
*
* @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/
* @since 3.0.0
*
* @param string|false $format The format of the clickable file link, or false if there is none.
*/
$format = apply_filters( 'qm/output/file_link_format', $format );
if ( empty( $format ) ) {
self::$file_link_format = false;
} else {
self::$file_link_format = str_replace( array( '%f', '%l' ), array( '%1$s', '%2$d' ), $format );
}
}
return self::$file_link_format;
}
/**
* @return array<string, string>
*/
public static function get_file_path_map() {
/**
* Filters the file path mapping for clickable file links.
*
* @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/
* @since 3.0.0
*
* @param array<string, string> $file_map Array of file path mappings. Keys are the source paths and values are the replacement paths.
*/
return apply_filters( 'qm/output/file_path_map', array() );
}
/**
* @return bool
*/
public static function has_clickable_links() {
return ( false !== self::get_file_link_format() );
}
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types = 1);
/**
* Abstract output class for raw output encoded as JSON.
*
* @package query-monitor
*/
abstract class QM_Output_Raw extends QM_Output {
}

View File

@ -0,0 +1,73 @@
<?php declare(strict_types = 1);
/**
* General overview output for HTTP headers.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Headers_Overview extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_Overview Collector.
*/
protected $collector;
/**
* @return array<string, mixed>
*/
public function get_output() {
/** @var QM_Data_Overview $data */
$data = $this->collector->get_data();
$headers = array();
$headers['time_taken'] = number_format_i18n( $data->time_taken, 4 );
$headers['time_usage'] = sprintf(
/* translators: 1: Percentage of time limit used, 2: Time limit in seconds */
__( '%1$s%% of %2$ss limit', 'query-monitor' ),
number_format_i18n( $data->time_usage, 1 ),
number_format_i18n( $data->time_limit )
);
if ( ! empty( $data->memory ) ) {
$headers['memory'] = sprintf(
/* translators: %s: Memory used in megabytes */
__( '%s MB', 'query-monitor' ),
number_format_i18n( ( $data->memory / 1024 / 1024 ), 1 )
);
if ( $data->memory_limit > 0 ) {
$headers['memory_usage'] = sprintf(
/* translators: 1: Percentage of memory limit used, 2: Memory limit in megabytes */
__( '%1$s%% of %2$s MB server limit', 'query-monitor' ),
number_format_i18n( $data->memory_usage, 1 ),
number_format_i18n( $data->memory_limit / 1024 / 1024 )
);
}
}
return $headers;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_headers_overview( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'overview' );
if ( $collector ) {
$output['overview'] = new QM_Output_Headers_Overview( $collector );
}
return $output;
}
add_filter( 'qm/outputter/headers', 'register_qm_output_headers_overview', 10, 2 );

View File

@ -0,0 +1,86 @@
<?php declare(strict_types = 1);
/**
* PHP error output for HTTP headers.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Headers_PHP_Errors extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_PHP_Errors Collector.
*/
protected $collector;
/**
* @return array<string, mixed>
*/
public function get_output() {
/** @var QM_Data_PHP_Errors $data */
$data = $this->collector->get_data();
$headers = array();
if ( empty( $data->errors ) ) {
return array();
}
$count = 0;
foreach ( $data->errors as $type => $errors ) {
foreach ( $errors as $error_key => $error ) {
$count++;
$stack = array();
if ( ! empty( $error['filtered_trace'] ) ) {
$stack = array_column( $error['filtered_trace'], 'display' );
}
$output_error = array(
'key' => $error_key,
'type' => $error['type'],
'message' => $error['message'],
'file' => QM_Util::standard_dir( $error['file'], '' ),
'line' => $error['line'],
'stack' => $stack,
'component' => $error['component']->name,
);
$key = sprintf( 'error-%d', $count );
$headers[ $key ] = json_encode( $output_error );
}
}
return array_merge(
array(
'error-count' => $count,
),
$headers
);
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_headers_php_errors( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'php_errors' );
if ( $collector ) {
$output['php_errors'] = new QM_Output_Headers_PHP_Errors( $collector );
}
return $output;
}
add_filter( 'qm/outputter/headers', 'register_qm_output_headers_php_errors', 110, 2 );

View File

@ -0,0 +1,53 @@
<?php declare(strict_types = 1);
/**
* HTTP redirects output for HTTP headers.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Headers_Redirects extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_Redirects Collector.
*/
protected $collector;
/**
* @return array<string, mixed>
*/
public function get_output() {
/** @var QM_Data_Redirect $data */
$data = $this->collector->get_data();
$headers = array();
if ( ! isset( $data->trace ) ) {
return array();
}
$headers['Redirect-Trace'] = implode( ', ', $data->trace->get_stack() );
return $headers;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_headers_redirects( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'redirects' );
if ( $collector ) {
$output['redirects'] = new QM_Output_Headers_Redirects( $collector );
}
return $output;
}
add_filter( 'qm/outputter/headers', 'register_qm_output_headers_redirects', 140, 2 );

View File

@ -0,0 +1,139 @@
<?php declare(strict_types = 1);
/**
* Admin screen output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Admin extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Admin Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 60 );
}
/**
* @return string
*/
public function name() {
return __( 'Admin Screen', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Admin $data */
$data = $this->collector->get_data();
if ( empty( $data->current_screen ) ) {
return;
}
$this->before_non_tabular_output();
echo '<section>';
echo '<h3>get_current_screen()</h3>';
echo '<table>';
echo '<thead class="qm-screen-reader-text">';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Property', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Value', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( get_object_vars( $data->current_screen ) as $key => $value ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $key ) . '</th>';
echo '<td>' . esc_html( $value ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Globals', 'query-monitor' ) . '</h3>';
echo '<table>';
echo '<thead class="qm-screen-reader-text">';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Global Variable', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Value', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
$admin_globals = array(
'pagenow',
'typenow',
'taxnow',
'hook_suffix',
);
foreach ( $admin_globals as $key ) {
echo '<tr>';
echo '<th scope="row">$' . esc_html( $key ) . '</th>';
echo '<td>' . esc_html( $data->{$key} ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
if ( ! empty( $data->list_table ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'List Table', 'query-monitor' ) . '</h3>';
if ( ! empty( $data->list_table['class_name'] ) ) {
echo '<h4>' . esc_html__( 'Class:', 'query-monitor' ) . '</h4>';
echo '<p><code>' . esc_html( $data->list_table['class_name'] ) . '</code></p>';
}
echo '<h4>' . esc_html__( 'Column Filters:', 'query-monitor' ) . '</h4>';
echo '<p><code>' . esc_html( $data->list_table['columns_filter'] ) . '</code></p>';
echo '<p><code>' . esc_html( $data->list_table['sortables_filter'] ) . '</code></p>';
echo '<h4>' . esc_html__( 'Column Action:', 'query-monitor' ) . '</h4>';
echo '<p><code>' . esc_html( $data->list_table['column_action'] ) . '</code></p>';
echo '</section>';
}
$this->after_non_tabular_output();
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_admin( array $output, QM_Collectors $collectors ) {
if ( ! is_admin() ) {
return $output;
}
$collector = QM_Collectors::get( 'response' );
if ( $collector ) {
$output['response'] = new QM_Output_Html_Admin( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_admin', 70, 2 );

View File

@ -0,0 +1,288 @@
<?php declare(strict_types = 1);
/**
* Scripts and styles output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class QM_Output_Html_Assets extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Assets Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 70 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return array<string, string>
*/
abstract public function get_type_labels();
/**
* @return void
*/
public function output() {
/** @var QM_Data_Assets */
$data = $this->collector->get_data();
$type_label = $this->get_type_labels();
if ( empty( $data->assets ) ) {
$this->before_non_tabular_output();
$notice = esc_html( $type_label['none'] );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
return;
}
$position_labels = array(
// @TODO translator comments or context:
'missing' => __( 'Missing', 'query-monitor' ),
'broken' => __( 'Missing Dependencies', 'query-monitor' ),
'header' => __( 'Header', 'query-monitor' ),
'footer' => __( 'Footer', 'query-monitor' ),
);
$type = $this->collector->get_dependency_type();
$hosts = array(
__( 'Other', 'query-monitor' ),
);
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Position', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Handle', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
$args = array(
'prepend' => array(
'local' => $data->host,
),
);
echo $this->build_filter( $type . '-host', $hosts, __( 'Host', 'query-monitor' ), $args ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col">' . esc_html__( 'Source', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( $type . '-dependencies', $data->dependencies, __( 'Dependencies', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( $type . '-dependents', $data->dependents, __( 'Dependents', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col">' . esc_html__( 'Version', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $position_labels as $position => $label ) {
if ( ! empty( $data->assets[ $position ] ) ) {
foreach ( $data->assets[ $position ] as $handle => $asset ) {
$this->dependency_row( $handle, $asset, $label );
}
}
}
echo '</tbody>';
echo '<tfoot>';
echo '<tr>';
printf(
'<td colspan="7">%1$s</td>',
sprintf(
esc_html( $type_label['total'] ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $data->counts['total'] ) ) . '</span>'
)
);
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
}
/**
* @param string $handle
* @param array<string, mixed> $asset
* @param string $label
* @return void
*/
protected function dependency_row( $handle, array $asset, $label ) {
/** @var QM_Data_Assets */
$data = $this->collector->get_data();
$highlight_deps = array_map( array( $this, '_prefix_type' ), $asset['dependencies'] );
$highlight_dependents = array_map( array( $this, '_prefix_type' ), $asset['dependents'] );
$dependencies_list = implode( ' ', $asset['dependencies'] );
$dependents_list = implode( ' ', $asset['dependents'] );
$dependency_output = array();
foreach ( $asset['dependencies'] as $dep ) {
if ( isset( $data->missing_dependencies[ $dep ] ) ) {
$warning = QueryMonitor::icon( 'warning' );
$dependency_output[] = sprintf(
'<span style="white-space:nowrap">%1$s%2$s</span>',
$warning,
sprintf(
/* translators: %s: Name of missing script or style dependency */
__( '%s (missing)', 'query-monitor' ),
esc_html( $dep )
)
);
} else {
$dependency_output[] = $dep;
}
}
$qm_host = ( $asset['local'] ) ? 'local' : __( 'Other', 'query-monitor' );
$class = '';
if ( $asset['warning'] ) {
$class = 'qm-warn';
}
$type = $this->collector->get_dependency_type();
echo '<tr data-qm-subject="' . esc_attr( $type . '-' . $handle ) . '" data-qm-' . esc_attr( $type ) . '-host="' . esc_attr( $qm_host ) . '" data-qm-' . esc_attr( $type ) . '-dependents="' . esc_attr( $dependents_list ) . '" data-qm-' . esc_attr( $type ) . '-dependencies="' . esc_attr( $dependencies_list ) . '" class="' . esc_attr( $class ) . '">';
echo '<td class="qm-nowrap">';
$warning = QueryMonitor::icon( 'warning' );
if ( $asset['warning'] ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $warning;
}
echo esc_html( $label );
echo '</td>';
$host = $asset['host'];
$parts = explode( '.', $host );
foreach ( $parts as $k => $part ) {
if ( strlen( $part ) > 16 ) {
$parts[ $k ] = substr( $parts[ $k ], 0, 6 ) . '&hellip;' . substr( $parts[ $k ], -6 );
}
}
$host = implode( '.', $parts );
if ( ! empty( $asset['port'] ) && ! empty( $asset['host'] ) ) {
$host = "{$host}:{$asset['port']}";
}
echo '<td class="qm-nowrap qm-ltr">' . esc_html( $handle ) . '</td>';
echo '<td class="qm-nowrap qm-ltr">' . esc_html( $host ) . '</td>';
echo '<td class="qm-ltr">';
if ( $asset['source'] instanceof WP_Error ) {
$error_data = $asset['source']->get_error_data();
if ( $error_data && isset( $error_data['src'] ) ) {
printf(
'<span class="qm-warn">%1$s%2$s:</span><br>',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$warning,
esc_html( $asset['source']->get_error_message() )
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link( $error_data['src'], esc_html( $error_data['src'] ) );
} else {
printf(
'<span class="qm-warn">%1$s%2$s</span>',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$warning,
esc_html( $asset['source']->get_error_message() )
);
}
} elseif ( ! empty( $asset['source'] ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link( $asset['source'], esc_html( $asset['display'] ) );
}
echo '</td>';
echo '<td class="qm-ltr qm-highlighter" data-qm-highlight="' . esc_attr( implode( ' ', $highlight_deps ) ) . '">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo implode( ', ', $dependency_output );
echo '</td>';
echo '<td class="qm-ltr qm-highlighter" data-qm-highlight="' . esc_attr( implode( ' ', $highlight_dependents ) ) . '">' . implode( ', ', array_map( 'esc_html', $asset['dependents'] ) ) . '</td>';
echo '<td class="qm-ltr">' . esc_html( $asset['ver'] ) . '</td>';
echo '</tr>';
}
/**
* @param string $val
* @return string
*/
public function _prefix_type( $val ) {
return $this->collector->get_dependency_type() . '-' . $val;
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
/** @var QM_Data_Assets */
$data = $this->collector->get_data();
if ( ! empty( $data->broken ) || ! empty( $data->missing ) ) {
$class[] = 'qm-error';
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Assets */
$data = $this->collector->get_data();
if ( empty( $data->assets ) ) {
return $menu;
}
$type_label = $this->get_type_labels();
$label = sprintf(
$type_label['count'],
number_format_i18n( $data->counts['total'] )
);
$args = array(
'title' => esc_html( $label ),
'id' => esc_attr( "query-monitor-{$this->collector->id}" ),
'href' => esc_attr( '#' . $this->collector->id() ),
);
if ( ! empty( $data->broken ) || ! empty( $data->missing ) ) {
$args['meta']['classname'] = 'qm-error';
}
$id = $this->collector->id();
$menu[ $id ] = $this->menu( $args );
return $menu;
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
/**
* Enqueued scripts output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Assets_Scripts extends QM_Output_Html_Assets {
/**
* Collector instance.
*
* @var QM_Collector_Assets_Scripts Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Scripts', 'query-monitor' );
}
/**
* @return array<string, string>
*/
public function get_type_labels() {
return array(
/* translators: %s: Total number of enqueued scripts */
'total' => _x( 'Total: %s', 'Enqueued scripts', 'query-monitor' ),
'plural' => __( 'Scripts', 'query-monitor' ),
/* translators: %s: Total number of enqueued scripts */
'count' => _x( 'Scripts (%s)', 'Enqueued scripts', 'query-monitor' ),
'none' => __( 'No JavaScript files were enqueued.', 'query-monitor' ),
);
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_assets_scripts( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'assets_scripts' );
if ( $collector ) {
$output['assets_scripts'] = new QM_Output_Html_Assets_Scripts( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_assets_scripts', 80, 2 );

View File

@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
/**
* Enqueued styles output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Assets_Styles extends QM_Output_Html_Assets {
/**
* Collector instance.
*
* @var QM_Collector_Assets_Styles Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Styles', 'query-monitor' );
}
/**
* @return array<string, string>
*/
public function get_type_labels() {
return array(
/* translators: %s: Total number of enqueued styles */
'total' => _x( 'Total: %s', 'Enqueued styles', 'query-monitor' ),
'plural' => __( 'Styles', 'query-monitor' ),
/* translators: %s: Total number of enqueued styles */
'count' => _x( 'Styles (%s)', 'Enqueued styles', 'query-monitor' ),
'none' => __( 'No CSS files were enqueued.', 'query-monitor' ),
);
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_assets_styles( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'assets_styles' );
if ( $collector ) {
$output['assets_styles'] = new QM_Output_Html_Assets_Styles( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_assets_styles', 80, 2 );

View File

@ -0,0 +1,360 @@
<?php declare(strict_types = 1);
/**
* Block editor data output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Block_Editor extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Block_Editor Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 55 );
}
/**
* @return string
*/
public function name() {
return __( 'Blocks', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Block_Editor */
$data = $this->collector->get_data();
if ( empty( $data->block_editor_enabled ) || empty( $data->post_blocks ) ) {
return;
}
if ( ! $data->post_has_blocks ) {
$this->before_non_tabular_output();
$notice = __( 'This post contains no blocks.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
return;
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">#</th>';
echo '<th scope="col">' . esc_html__( 'Block Name', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Attributes', 'query-monitor' ) . '</th>';
if ( $data->has_block_context ) {
echo '<th scope="col">' . esc_html__( 'Context', 'query-monitor' ) . '</th>';
}
echo '<th scope="col">' . esc_html__( 'Render Callback', 'query-monitor' ) . '</th>';
if ( $data->has_block_timing ) {
echo '<th scope="col" class="qm-num">' . esc_html__( 'Render Time', 'query-monitor' ) . '</th>';
}
echo '<th scope="col">' . esc_html__( 'Inner HTML', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->post_blocks as $i => $block ) {
self::render_block( ++$i, $block, $data );
}
echo '</tbody>';
echo '<tfoot>';
echo '<tr>';
$colspan = 5;
if ( $data->has_block_context ) {
$colspan++;
}
if ( $data->has_block_timing ) {
$colspan++;
}
printf(
'<td colspan="%1$d">%2$s</td>',
intval( $colspan ),
sprintf(
/* translators: %s: Total number of content blocks used */
esc_html( _nx( 'Total: %s', 'Total: %s', $data->total_blocks, 'Content blocks used', 'query-monitor' ) ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $data->total_blocks ) ) . '</span>'
)
);
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
}
/**
* @param int|string $i
* @param array<string, mixed> $block
* @param QM_Data_Block_Editor $data
* @return void
*/
protected static function render_block( $i, array $block, QM_Data_Block_Editor $data ) {
$block_error = false;
$row_class = '';
$referenced_post = null;
$referenced_type = null;
$referenced_template_part = null;
$referenced_pto = null;
$error_message = null;
if ( 'core/block' === $block['blockName'] && ! empty( $block['attrs']['ref'] ) ) {
$referenced_post = get_post( $block['attrs']['ref'] );
if ( ! $referenced_post ) {
$block_error = true;
$error_message = esc_html__( 'Referenced block does not exist.', 'query-monitor' );
} else {
$referenced_type = $referenced_post->post_type;
$referenced_pto = get_post_type_object( $referenced_type );
if ( 'wp_block' !== $referenced_type ) {
$block_error = true;
$error_message = sprintf(
/* translators: %1$s: Erroneous post type name, %2$s: WordPress block post type name */
esc_html__( 'Referenced post is of type %1$s instead of %2$s.', 'query-monitor' ),
'<code>' . esc_html( $referenced_type ) . '</code>',
'<code>wp_block</code>'
);
}
}
}
$media_blocks = array(
'core/audio' => 'id',
'core/cover' => 'id',
'core/cover-image' => 'id',
'core/file' => 'id',
'core/image' => 'id',
'core/media-text' => 'mediaId', // (╯°□°)╯︵ ┻━┻
'core/video' => 'id',
);
if ( isset( $media_blocks[ $block['blockName'] ] ) && is_array( $block['attrs'] ) && ! empty( $block['attrs'][ $media_blocks[ $block['blockName'] ] ] ) ) {
$referenced_post = get_post( $block['attrs'][ $media_blocks[ $block['blockName'] ] ] );
if ( ! $referenced_post ) {
$block_error = true;
$error_message = esc_html__( 'Referenced media does not exist.', 'query-monitor' );
} else {
$referenced_type = $referenced_post->post_type;
$referenced_pto = get_post_type_object( $referenced_type );
if ( 'attachment' !== $referenced_type ) {
$block_error = true;
$error_message = sprintf(
/* translators: %1$s: Erroneous post type name, %2$s: WordPress attachment post type name */
esc_html__( 'Referenced media is of type %1$s instead of %2$s.', 'query-monitor' ),
'<code>' . esc_html( $referenced_type ) . '</code>',
'<code>attachment</code>'
);
}
}
}
$template_part_blocks = array(
'core/template-part' => true,
);
if ( isset( $template_part_blocks[ $block['blockName'] ] ) && is_array( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! empty( $block['attrs']['theme'] ) ) {
$referenced_template_part = sprintf(
'%s//%s',
$block['attrs']['theme'],
$block['attrs']['slug']
);
$referenced_pto = get_post_type_object( 'wp_template_part' );
}
if ( $block_error ) {
$row_class = 'qm-warn';
}
echo '<tr class="' . esc_attr( $row_class ) . '">';
echo '<th scope="row" class="qm-row-num qm-num"><span class="qm-sticky">' . esc_html( $i ) . '</span></th>';
echo '<td class="qm-row-block-name"><span class="qm-sticky">';
if ( $block['blockName'] ) {
echo esc_html( $block['blockName'] );
} else {
echo '<em>' . esc_html__( 'None (Classic block)', 'query-monitor' ) . '</em>';
}
if ( $error_message ) {
echo '<br>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $error_message;
}
if ( ! empty( $referenced_post ) && ! empty( $referenced_pto ) ) {
echo '<br>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link( get_edit_post_link( $referenced_post ), esc_html( $referenced_pto->labels->edit_item ) );
}
if ( ! empty( $referenced_template_part ) ) {
echo '<br>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link( QM_Util::get_site_editor_url( $referenced_template_part ), esc_html( $referenced_pto->labels->edit_item ) );
}
echo '</span></td>';
echo '<td class="qm-row-block-attrs">';
if ( $block['attrs'] && is_array( $block['attrs'] ) ) {
echo '<pre class="qm-pre-wrap"><code>' . esc_html( QM_Util::json_format( $block['attrs'] ) ) . '</code></pre>';
}
echo '</td>';
if ( $data->has_block_context ) {
echo '<td class="qm-row-block-context">';
if ( isset( $block['context'] ) ) {
echo '<pre class="qm-pre-wrap"><code>' . esc_html( QM_Util::json_format( $block['context'] ) ) . '</code></pre>';
}
echo '</td>';
}
if ( isset( $block['callback']['error'] ) ) {
$class = ' qm-warn';
} else {
$class = '';
}
if ( $block['dynamic'] ) {
if ( isset( $block['callback']['file'] ) ) {
if ( self::has_clickable_links() ) {
echo '<td class="qm-nowrap qm-ltr' . esc_attr( $class ) . '">';
echo self::output_filename( $block['callback']['name'], $block['callback']['file'], $block['callback']['line'] ); // WPCS: XSS ok.
echo '</td>';
} else {
echo '<td class="qm-nowrap qm-ltr qm-has-toggle' . esc_attr( $class ) . '">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ol>';
echo '<li>';
echo self::output_filename( $block['callback']['name'], $block['callback']['file'], $block['callback']['line'] ); // WPCS: XSS ok.
echo '</li>';
echo '</ol></td>';
}
} else {
echo '<td class="qm-ltr qm-nowrap' . esc_attr( $class ) . '">';
echo '<code>' . esc_html( $block['callback']['name'] ) . '</code>';
if ( isset( $block['callback']['error'] ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br>' . QueryMonitor::icon( 'warning' );
echo esc_html( sprintf(
/* translators: %s: Error message text */
__( 'Error: %s', 'query-monitor' ),
$block['callback']['error']->get_error_message()
) );
}
echo '</td>';
}
if ( $data->has_block_timing ) {
echo '<td class="qm-num">';
if ( isset( $block['timing'] ) ) {
echo esc_html( number_format_i18n( $block['timing'], 4 ) );
}
echo '</td>';
}
} else {
echo '<td></td>';
if ( $data->has_block_timing ) {
echo '<td></td>';
}
}
$inner_html = $block['innerHTML'];
if ( $block['size'] > 300 ) {
echo '<td class="qm-ltr qm-has-toggle qm-row-block-html">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<div class="qm-inverse-toggled"><pre class="qm-pre-wrap"><code>';
echo esc_html( substr( $inner_html, 0, 200 ) ) . '&nbsp;&hellip;';
echo '</code></pre></div>';
echo '<div class="qm-toggled"><pre class="qm-pre-wrap"><code>';
echo esc_html( $inner_html );
echo '</code></pre></div>';
echo '</td>';
} else {
echo '<td class="qm-row-block-html"><pre class="qm-pre-wrap"><code>';
echo esc_html( $inner_html );
echo '</code></pre></td>';
}
echo '</tr>';
if ( ! empty( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $j => $inner_block ) {
$x = ++$j;
self::render_block( "{$i}.{$x}", $inner_block, $data );
}
}
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Block_Editor */
$data = $this->collector->get_data();
if ( empty( $data->block_editor_enabled ) || empty( $data->post_blocks ) ) {
return $menu;
}
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html__( 'Blocks', 'query-monitor' ),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_block_editor( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'block_editor' );
if ( $collector ) {
$output['block_editor'] = new QM_Output_Html_Block_Editor( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_block_editor', 60, 2 );

View File

@ -0,0 +1,242 @@
<?php declare(strict_types = 1);
/**
* User capability checks output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Caps extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Caps Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 105 );
}
/**
* @return string
*/
public function name() {
return __( 'Capability Checks', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
$collector = $this->collector;
if ( ! $collector::enabled() ) {
$this->before_non_tabular_output();
echo '<section>';
echo '<div class="qm-notice">';
echo '<p>';
printf(
/* translators: %s: Configuration file name. */
esc_html__( 'For performance reasons, this panel is not enabled by default. To enable it, add the following code to your %s file:', 'query-monitor' ),
'<code>wp-config.php</code>'
);
echo '</p>';
echo "<p><code>define( 'QM_ENABLE_CAPS_PANEL', true );</code></p>";
echo '</div>';
echo '</section>';
$this->after_non_tabular_output();
return;
}
/** @var QM_Data_Caps */
$data = $this->collector->get_data();
if ( ! empty( $data->caps ) ) {
$this->before_tabular_output();
$results = array(
'true',
'false',
);
$parts = $data->parts;
$components = $data->components;
usort( $parts, 'strcasecmp' );
usort( $components, 'strcasecmp' );
echo '<thead>';
echo '<tr>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'name', $parts, __( 'Capability Check', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</th>';
$users = $data->users;
sort( $users );
echo '<th scope="col" class="qm-filterable-column qm-num">';
echo $this->build_filter( 'user', $users, __( 'User', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'result', $results, __( 'Result', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $components, __( 'Component', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->caps as $row ) {
$component = $row['component'];
$row_attr = array();
$row_attr['data-qm-name'] = implode( ' ', $row['parts'] );
$row_attr['data-qm-user'] = $row['user'];
$row_attr['data-qm-component'] = $component->name;
$row_attr['data-qm-result'] = ( $row['result'] ) ? 'true' : 'false';
if ( 'core' !== $component->context ) {
$row_attr['data-qm-component'] .= ' non-core';
}
if ( '' === $row['name'] ) {
$row_attr['class'] = 'qm-warn';
}
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
printf( // WPCS: XSS ok.
'<tr %s>',
$attr
);
$name = esc_html( $row['name'] );
if ( ! empty( $row['args'] ) ) {
foreach ( $row['args'] as $arg ) {
$name .= ',&nbsp;' . esc_html( QM_Util::display_variable( $arg ) );
}
}
printf( // WPCS: XSS ok.
'<td class="qm-ltr qm-nowrap"><code>%s</code></td>',
$name
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $row['user'] )
);
$result = ( $row['result'] ) ? '<span class="qm-true">true&nbsp;&#x2713;</span>' : 'false';
printf( // WPCS: XSS ok.
'<td class="qm-nowrap">%s</td>',
$result
);
$stack = array();
foreach ( $row['filtered_trace'] as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
$caller = array_shift( $stack );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
$count = count( $data->caps );
echo '<tr>';
echo '<td colspan="5">';
printf(
/* translators: %s: Number of user capability checks */
esc_html( _nx( 'Total: %s', 'Total: %s', $count, 'User capability checks', 'query-monitor' ) ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $count ) ) . '</span>'
);
echo '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
} else {
$this->before_non_tabular_output();
$notice = __( 'No capability checks were recorded.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => $this->name(),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_caps( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'caps' );
if ( $collector ) {
$output['caps'] = new QM_Output_Html_Caps( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_caps', 105, 2 );

View File

@ -0,0 +1,131 @@
<?php declare(strict_types = 1);
/**
* Template conditionals output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Conditionals extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Conditionals Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 1000 );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 1000 );
}
/**
* @return string
*/
public function name() {
return __( 'Conditionals', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Conditionals $data */
$data = $this->collector->get_data();
$this->before_non_tabular_output();
echo '<section>';
echo '<h3>' . esc_html__( 'True Conditionals', 'query-monitor' ) . '</h3>';
echo '<ul>';
foreach ( $data->conds['true'] as $cond ) {
echo '<li class="qm-ltr qm-true"><code>' . esc_html( $cond ) . '() </code></li>';
}
echo '</ul>';
echo '</section>';
echo '</div>';
echo '<div class="qm-boxed">';
echo '<section>';
echo '<h3>' . esc_html__( 'False Conditionals', 'query-monitor' ) . '</h3>';
echo '<ul>';
foreach ( $data->conds['false'] as $cond ) {
echo '<li class="qm-ltr qm-false"><code>' . esc_html( $cond ) . '() </code></li>';
}
echo '</ul>';
echo '</section>';
$this->after_non_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Conditionals $data */
$data = $this->collector->get_data();
foreach ( $data->conds['true'] as $cond ) {
$id = $this->collector->id() . '-' . $cond;
$menu[ $id ] = $this->menu( array(
'title' => esc_html( $cond . '()' ),
'id' => 'query-monitor-conditionals-' . esc_attr( $cond ),
'meta' => array(
'classname' => 'qm-true qm-ltr',
),
) );
}
return $menu;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
/** @var QM_Data_Conditionals $data */
$data = $this->collector->get_data();
foreach ( $data->conds['true'] as $cond ) {
$id = $this->collector->id() . '-' . $cond;
unset( $menu[ $id ] );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html__( 'Conditionals', 'query-monitor' ),
'id' => 'query-monitor-conditionals',
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_conditionals( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'conditionals' );
if ( $collector ) {
$output['conditionals'] = new QM_Output_Html_Conditionals( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_conditionals', 50, 2 );

View File

@ -0,0 +1,154 @@
<?php declare(strict_types = 1);
/**
* Database query calling function output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_DB_Callers extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_DB_Callers Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 30 );
}
/**
* @return string
*/
public function name() {
return __( 'Queries by Caller', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_DB_Callers $data */
$data = $this->collector->get_data();
if ( empty( $data->types ) ) {
return;
}
$total_time = 0;
if ( ! empty( $data->times ) ) {
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
foreach ( $data->types as $type_name => $type_count ) {
echo '<th scope="col" class="qm-num qm-ltr qm-sortable-column" role="columnheader">';
echo $this->build_sorter( $type_name ); // WPCS: XSS ok;
echo '</th>';
}
echo '<th scope="col" class="qm-num qm-sorted-desc qm-sortable-column" role="columnheader" aria-sort="descending">';
echo $this->build_sorter( __( 'Time', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->times as $row ) {
$total_time += $row['ltime'];
$stime = number_format_i18n( $row['ltime'], 4 );
echo '<tr>';
echo '<td class="qm-ltr">';
echo self::build_filter_trigger( 'db_queries', 'caller', $row['caller'], '<code>' . esc_html( $row['caller'] ) . '</code>' ); // WPCS: XSS ok;
echo '</td>';
foreach ( $data->types as $type_name => $type_count ) {
if ( isset( $row['types'][ $type_name ] ) ) {
echo "<td class='qm-num'>" . esc_html( number_format_i18n( $row['types'][ $type_name ] ) ) . '</td>';
} else {
echo "<td class='qm-num'></td>";
}
}
echo '<td class="qm-num" data-qm-sort-weight="' . esc_attr( (string) $row['ltime'] ) . '">' . esc_html( $stime ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
$total_stime = number_format_i18n( $total_time, 4 );
echo '<tr>';
echo '<td></td>';
foreach ( $data->types as $type_name => $type_count ) {
echo '<td class="qm-num">' . esc_html( number_format_i18n( $type_count ) ) . '</td>';
}
echo '<td class="qm-num">' . esc_html( $total_stime ) . '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
} else {
$this->before_non_tabular_output();
echo '<div class="qm-none">';
echo '<p>' . esc_html__( 'None', 'query-monitor' ) . '</p>';
echo '</div>';
$this->after_non_tabular_output();
}
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
/** @var QM_Collector_DB_Queries|null $dbq */
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
if ( ! empty( $dbq_data->times ) ) {
$menu['qm-db_queries']['children'][] = $this->menu( array(
'title' => esc_html__( 'Queries by Caller', 'query-monitor' ),
) );
}
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_db_callers( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'db_callers' );
if ( $collector ) {
$output['db_callers'] = new QM_Output_Html_DB_Callers( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_db_callers', 30, 2 );

View File

@ -0,0 +1,150 @@
<?php declare(strict_types = 1);
/**
* Database query calling component output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_DB_Components extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_DB_Components Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 40 );
}
/**
* @return string
*/
public function name() {
return __( 'Queries by Component', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_DB_Components $data */
$data = $this->collector->get_data();
if ( empty( $data->types ) || empty( $data->times ) ) {
return;
}
$total_time = 0;
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
foreach ( $data->types as $type_name => $type_count ) {
echo '<th scope="col" class="qm-num qm-sortable-column" role="columnheader">';
echo $this->build_sorter( $type_name ); // WPCS: XSS ok;
echo '</th>';
}
echo '<th scope="col" class="qm-num qm-sorted-desc qm-sortable-column" role="columnheader" aria-sort="descending">';
echo $this->build_sorter( __( 'Time', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->times as $row ) {
$total_time += $row['ltime'];
echo '<tr>';
echo '<td class="qm-row-component">';
echo self::build_filter_trigger( 'db_queries', 'component', $row['component'], esc_html( $row['component'] ) ); // WPCS: XSS ok;
foreach ( $data->types as $type_name => $type_count ) {
if ( isset( $row['types'][ $type_name ] ) ) {
echo '<td class="qm-num">' . esc_html( number_format_i18n( $row['types'][ $type_name ] ) ) . '</td>';
} else {
echo '<td class="qm-num">&nbsp;</td>';
}
}
echo '<td class="qm-num" data-qm-sort-weight="' . esc_attr( (string) $row['ltime'] ) . '">' . esc_html( number_format_i18n( $row['ltime'], 4 ) ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
$total_stime = number_format_i18n( $total_time, 4 );
echo '<tr>';
echo '<td>&nbsp;</td>';
foreach ( $data->types as $type_name => $type_count ) {
echo '<td class="qm-num">' . esc_html( number_format_i18n( $type_count ) ) . '</td>';
}
echo '<td class="qm-num">' . esc_html( $total_stime ) . '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
/** @var QM_Data_DB_Components $data */
$data = $this->collector->get_data();
if ( empty( $data->types ) || empty( $data->times ) ) {
return $menu;
}
/** @var QM_Collector_DB_Queries|null $dbq */
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
if ( ! empty( $dbq_data->component_times ) ) {
$menu['qm-db_queries']['children'][] = $this->menu( array(
'title' => esc_html__( 'Queries by Component', 'query-monitor' ),
) );
}
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_db_components( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'db_components' );
if ( $collector ) {
$output['db_components'] = new QM_Output_Html_DB_Components( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_db_components', 40, 2 );

View File

@ -0,0 +1,193 @@
<?php declare(strict_types = 1);
/**
* Duplicate database query output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_DB_Dupes extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_DB_Dupes Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 45 );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 25 );
}
/**
* @return string
*/
public function name() {
return __( 'Duplicate Queries', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_DB_Dupes $data */
$data = $this->collector->get_data();
if ( empty( $data->dupes ) ) {
return;
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Query', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Count', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Time', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Callers', 'query-monitor' ) . '</th>';
if ( ! empty( $data->dupe_components ) ) {
echo '<th scope="col">' . esc_html__( 'Components', 'query-monitor' ) . '</th>';
}
echo '<th scope="col">' . esc_html__( 'Potential Troublemakers', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
/* translators: %s: Number of calls to a PHP function */
$call_text = _n_noop( '%s call', '%s calls', 'query-monitor' );
foreach ( $data->dupes as $sql => $queries ) {
// This should probably happen in the collector's processor
$type = QM_Util::get_query_type( $sql );
$sql_out = self::format_sql( $sql );
$time = $data->dupe_times[ $sql ];
if ( 'SELECT' !== $type ) {
$sql_out = "<span class='qm-nonselectsql'>{$sql_out}</span>";
}
echo '<tr>';
echo '<td class="qm-row-sql qm-ltr qm-wrap">';
echo $sql_out; // WPCS: XSS ok;
echo '</td>';
echo '<td class="qm-num">';
echo esc_html( number_format_i18n( count( $queries ), 0 ) );
echo '</td>';
echo '<td class="qm-num">';
echo esc_html( number_format_i18n( $time, 4 ) );
echo '</td>';
echo '<td class="qm-row-caller qm-nowrap qm-ltr">';
foreach ( $data->dupe_callers[ $sql ] as $caller => $calls ) {
echo self::build_filter_trigger( 'db_queries', 'caller', $caller, '<code>' . esc_html( $caller ) . '</code>' ); // WPCS: XSS ok;
printf(
'<br><span class="qm-info qm-supplemental">%s</span><br>',
esc_html( sprintf(
translate_nooped_plural( $call_text, $calls, 'query-monitor' ),
number_format_i18n( $calls )
) )
);
}
echo '</td>';
if ( isset( $data->dupe_components[ $sql ] ) ) {
echo '<td class="qm-row-component qm-nowrap">';
foreach ( $data->dupe_components[ $sql ] as $component => $calls ) {
printf(
'%s<br><span class="qm-info qm-supplemental">%s</span><br>',
esc_html( $component ),
esc_html( sprintf(
translate_nooped_plural( $call_text, $calls, 'query-monitor' ),
number_format_i18n( $calls )
) )
);
}
echo '</td>';
}
echo '<td class="qm-row-caller qm-nowrap qm-ltr">';
foreach ( $data->dupe_sources[ $sql ] as $source => $calls ) {
printf(
'<code>%s</code><br><span class="qm-info qm-supplemental">%s</span><br>',
esc_html( $source ),
esc_html( sprintf(
translate_nooped_plural( $call_text, $calls, 'query-monitor' ),
number_format_i18n( $calls )
) )
);
}
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Collector_DB_Dupes|null $dbq */
$dbq = QM_Collectors::get( 'db_dupes' );
if ( $dbq ) {
/** @var QM_Data_DB_Queries $dbq_data */
$dbq_data = $dbq->get_data();
if ( ! empty( $dbq_data->dupes ) ) {
$count = 0;
foreach ( $dbq_data->dupes as $dupe ) {
$count += count( $dupe );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
/* translators: %s: Number of duplicate database queries */
__( 'Duplicate Queries (%s)', 'query-monitor' ),
number_format_i18n( $count )
) ),
) );
}
}
return $menu;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
$id = $this->collector->id();
if ( isset( $menu[ $id ] ) ) {
$menu['qm-db_queries']['children'][] = $menu[ $id ];
unset( $menu[ $id ] );
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_db_dupes( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'db_dupes' );
if ( $collector ) {
$output['db_dupes'] = new QM_Output_Html_DB_Dupes( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_db_dupes', 45, 2 );

View File

@ -0,0 +1,628 @@
<?php declare(strict_types = 1);
/**
* Database query output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_DB_Queries extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_DB_Queries Collector.
*/
protected $collector;
/**
* @var int
*/
public $query_row = 0;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 20 );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 20 );
add_filter( 'qm/output/title', array( $this, 'admin_title' ), 20 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return string
*/
public function name() {
return __( 'Database Queries', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_DB_Queries $data */
$data = $this->collector->get_data();
if ( empty( $data->wpdb ) ) {
$this->output_empty_queries();
return;
}
if ( ! empty( $data->errors ) ) {
$this->output_error_queries( $data->errors );
}
if ( ! empty( $data->expensive ) ) {
$this->output_expensive_queries( $data->expensive );
}
$this->output_queries( $data->wpdb, $data );
}
/**
* @return void
*/
protected function output_empty_queries() {
$this->before_non_tabular_output();
if ( ! SAVEQUERIES ) {
$notice = sprintf(
/* translators: 1: Name of PHP constant, 2: Value of PHP constant */
esc_html__( 'No database queries were logged because the %1$s constant is set to %2$s.', 'query-monitor' ),
'<code>SAVEQUERIES</code>',
'<code>false</code>'
);
} else {
$notice = __( 'No database queries were logged.', 'query-monitor' );
}
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
/**
* @param array<int, mixed> $errors
* @return void
*/
protected function output_error_queries( array $errors ) {
$this->before_tabular_output( 'qm-query-errors', __( 'Database Errors', 'query-monitor' ) );
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Query', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Error Message', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Error Code', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $errors as $row ) {
$this->output_query_row( $row, array( 'sql', 'caller', 'component', 'errno', 'result' ) );
}
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param array<int, mixed> $expensive
* @return void
*/
protected function output_expensive_queries( array $expensive ) {
$dp = strlen( substr( strrchr( (string) QM_DB_EXPENSIVE, '.' ) ?: '.0', 1 ) );
$panel_name = sprintf(
/* translators: %s: Database query time in seconds */
esc_html__( 'Slow Database Queries (above %ss)', 'query-monitor' ),
'<span class="qm-warn">' . esc_html( number_format_i18n( QM_DB_EXPENSIVE, $dp ) ) . '</span>'
);
$this->before_tabular_output( 'qm-query-expensive', $panel_name );
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Query', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
if ( isset( $expensive[0]['component'] ) ) {
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
}
if ( isset( $expensive[0]['result'] ) ) {
echo '<th scope="col" class="qm-num">' . esc_html__( 'Rows', 'query-monitor' ) . '</th>';
}
echo '<th scope="col" class="qm-num">' . esc_html__( 'Time', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $expensive as $row ) {
$this->output_query_row( $row, array( 'sql', 'caller', 'component', 'result', 'time' ) );
}
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param stdClass $db
* @param QM_Data_DB_Queries $data
* @return void
*/
protected function output_queries( stdClass $db, QM_Data_DB_Queries $data ) {
$this->query_row = 0;
$span = 4;
if ( $db->has_result ) {
$span++;
}
if ( $db->has_trace ) {
$span++;
}
if ( ! empty( $db->rows ) ) {
$this->before_tabular_output();
echo '<thead>';
/**
* Filter whether to show the QM extended query information prompt.
*
* By default QM shows a prompt to install the QM db.php drop-in,
* this filter allows a dev to choose not to show the prompt.
*
* @since 2.9.0
*
* @param bool $show_prompt Whether to show the prompt.
*/
if ( apply_filters( 'qm/show_extended_query_prompt', true ) && ! $db->has_trace ) {
echo '<tr>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<th colspan="' . intval( $span ) . '" class="qm-warn">' . QueryMonitor::icon( 'warning' );
if ( file_exists( WP_CONTENT_DIR . '/db.php' ) ) {
/* translators: %s: File name */
$message = __( 'Extended query information such as the component and affected rows is not available. A conflicting %s file is present.', 'query-monitor' );
} elseif ( defined( 'QM_DB_SYMLINK' ) && ! QM_DB_SYMLINK ) {
/* translators: 1: File name, 2: Configuration constant name */
$message = __( 'Extended query information such as the component and affected rows is not available. Query Monitor was prevented from symlinking its %1$s file into place by the %2$s constant.', 'query-monitor' );
} else {
/* translators: %s: File name */
$message = __( 'Extended query information such as the component and affected rows is not available. Query Monitor was unable to symlink its %s file into place.', 'query-monitor' );
}
printf(
esc_html( $message ),
'<code>db.php</code>',
'<code>QM_DB_SYMLINK</code>'
);
printf(
' <a href="%s" target="_blank" class="qm-external-link">See this wiki page for more information.</a>',
'https://github.com/johnbillion/query-monitor/wiki/db.php-Symlink'
);
echo '</th>';
echo '</tr>';
}
$types = array_keys( $db->types );
$prepend = array();
$callers = array_column( $data->times, 'caller' );
sort( $types );
usort( $callers, 'strcasecmp' );
if ( count( $types ) > 1 ) {
$prepend['non-select'] = __( 'Non-SELECT', 'query-monitor' );
}
$args = array(
'prepend' => $prepend,
);
echo '<tr>';
echo '<th scope="col" class="qm-sorted-asc qm-sortable-column" role="columnheader" aria-sort="ascending">';
echo $this->build_sorter( '#' ); // WPCS: XSS ok;
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'type', $types, __( 'Query', 'query-monitor' ), $args ); // WPCS: XSS ok;
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
$prepend = array();
if ( $db->has_main_query ) {
$prepend['qm-main-query'] = __( 'Main Query', 'query-monitor' );
}
$args = array(
'prepend' => $prepend,
);
echo $this->build_filter( 'caller', $callers, __( 'Caller', 'query-monitor' ), $args ); // WPCS: XSS ok.
echo '</th>';
if ( $db->has_trace ) {
$components = array_column( $data->component_times, 'component' );
usort( $components, 'strcasecmp' );
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $components, __( 'Component', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
}
if ( $db->has_result ) {
if ( empty( $data->errors ) ) {
$class = 'qm-num';
} else {
$class = '';
}
echo '<th scope="col" class="' . esc_attr( $class ) . ' qm-sortable-column" role="columnheader">';
echo $this->build_sorter( __( 'Rows', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
}
echo '<th scope="col" class="qm-num qm-sortable-column" role="columnheader">';
echo $this->build_sorter( __( 'Time', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $db->rows as $row ) {
$this->output_query_row( $row, array( 'row', 'sql', 'caller', 'component', 'result', 'time' ) );
}
echo '</tbody>';
echo '<tfoot>';
$total_stime = number_format_i18n( $db->total_time, 4 );
echo '<tr>';
echo '<td colspan="' . intval( $span - 1 ) . '">';
printf(
/* translators: %s: Number of database queries */
esc_html( _nx( 'Total: %s', 'Total: %s', $db->total_qs, 'Query count', 'query-monitor' ) ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $db->total_qs ) ) . '</span>'
);
echo '</td>';
echo '<td class="qm-num qm-items-time">' . esc_html( $total_stime ) . '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
} else {
$this->before_non_tabular_output();
$notice = __( 'No queries! Nice work.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
}
/**
* @param array<string, mixed> $row
* @param array<int, string> $cols
* @return void
*/
protected function output_query_row( array $row, array $cols ) {
$cols = array_flip( $cols );
if ( ! isset( $row['component'] ) ) {
unset( $cols['component'] );
}
if ( ! isset( $row['result'] ) ) {
unset( $cols['result'], $cols['errno'] );
}
$stime = number_format_i18n( $row['ltime'], 4 );
$sql = $row['sql'];
if ( 'Unknown' === $row['type'] ) {
$sql = "<code>{$sql}</code>";
} else {
$sql = self::format_sql( $row['sql'] );
}
if ( 'SELECT' !== $row['type'] ) {
$sql = "<span class='qm-nonselectsql'>{$sql}</span>";
}
if ( isset( $row['trace'] ) ) {
$caller = $row['trace']->get_caller();
$caller_name = self::output_filename( $row['caller'], $caller['calling_file'], $caller['calling_line'] );
$stack = array();
$filtered_trace = $row['trace']->get_filtered_trace();
array_shift( $filtered_trace );
foreach ( $filtered_trace as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
} else {
if ( ! empty( $row['caller'] ) ) {
$caller_name = '<code>' . esc_html( $row['caller'] ) . '</code>';
} else {
$caller_name = '<code>' . esc_html__( 'Unknown', 'query-monitor' ) . '</code>';
}
$stack = $row['stack'];
array_shift( $stack );
$stack = array_map( function( $frame ) {
return '<code>' . esc_html( $frame ) . '</code>';
}, $stack );
}
$row_attr = array();
if ( $row['result'] instanceof WP_Error ) {
$row_attr['class'] = 'qm-warn';
}
if ( isset( $cols['sql'] ) ) {
$row_attr['data-qm-type'] = $row['type'];
if ( 'SELECT' !== $row['type'] ) {
$row_attr['data-qm-type'] .= ' non-select';
}
}
if ( isset( $cols['component'] ) && $row['component'] ) {
$row_attr['data-qm-component'] = $row['component']->name;
if ( 'core' !== $row['component']->context ) {
$row_attr['data-qm-component'] .= ' non-core';
}
}
if ( isset( $cols['caller'] ) && ! empty( $row['caller_name'] ) ) {
$row_attr['data-qm-caller'] = $row['caller_name'];
if ( $row['is_main_query'] ) {
$row_attr['data-qm-caller'] .= ' qm-main-query';
}
}
if ( isset( $cols['time'] ) ) {
$row_attr['data-qm-time'] = $row['ltime'];
}
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
echo "<tr{$attr}>"; // WPCS: XSS ok.
if ( isset( $cols['row'] ) ) {
echo '<th scope="row" class="qm-row-num qm-num">' . intval( ++$this->query_row ) . '</th>';
}
if ( isset( $cols['sql'] ) ) {
printf( // WPCS: XSS ok.
'<td class="qm-row-sql qm-ltr qm-wrap">%s</td>',
$sql
);
}
if ( isset( $cols['caller'] ) ) {
echo '<td class="qm-row-caller qm-ltr qm-has-toggle qm-nowrap">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller_name}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol>';
if ( $row['is_main_query'] ) {
printf(
'<p>%s</p>',
esc_html__( 'Main Query', 'query-monitor' )
);
}
echo '</td>';
}
if ( isset( $cols['stack'] ) ) {
echo '<td class="qm-row-caller qm-row-stack qm-nowrap qm-ltr"><ol>';
if ( ! empty( $stack ) ) {
echo '<li>' . implode( '</li><li>', $stack ) . '</li>'; // WPCS: XSS ok.
}
echo "<li>{$caller_name}</li>"; // WPCS: XSS ok.
echo '</ol></td>';
}
if ( isset( $cols['component'] ) ) {
if ( $row['component'] ) {
echo "<td class='qm-row-component qm-nowrap'>" . esc_html( $row['component']->name ) . "</td>\n";
} else {
echo "<td class='qm-row-component qm-nowrap'>" . esc_html__( 'Unknown', 'query-monitor' ) . "</td>\n";
}
}
if ( isset( $cols['result'] ) ) {
if ( $row['result'] instanceof WP_Error ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<td class='qm-row-result qm-row-error'>" . QueryMonitor::icon( 'warning' ) . esc_html( $row['result']->get_error_message() ) . "</td>\n";
} else {
echo "<td class='qm-row-result qm-num'>" . esc_html( $row['result'] ) . "</td>\n";
}
}
if ( isset( $cols['errno'] ) && ( $row['result'] instanceof WP_Error ) ) {
echo "<td class='qm-row-result qm-row-error'>" . esc_html( $row['result']->get_error_code() ) . "</td>\n";
}
if ( isset( $cols['time'] ) ) {
$expensive = $this->collector->is_expensive( $row );
$td_class = ( $expensive ) ? ' qm-warn' : '';
echo '<td class="qm-num qm-row-time' . esc_attr( $td_class ) . '" data-qm-sort-weight="' . esc_attr( $row['ltime'] ) . '">';
if ( $expensive ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
}
echo esc_html( $stime );
echo "</td>\n";
}
echo '</tr>';
}
/**
* @param array<int, string> $title
* @return array<int, string>
*/
public function admin_title( array $title ) {
/** @var QM_Data_DB_Queries $data */
$data = $this->collector->get_data();
if ( isset( $data->wpdb ) ) {
$title[] = sprintf(
/* translators: %s: A time in seconds with a decimal fraction. No space between value and unit symbol. */
esc_html_x( '%ss', 'Time in seconds', 'query-monitor' ),
number_format_i18n( $data->wpdb->total_time, 2 )
);
/* translators: %s: Number of database queries. Note the space between value and unit symbol. */
$text = _n( '%s Q', '%s Q', $data->wpdb->total_qs, 'query-monitor' );
// Avoid a potentially blank translation for the plural form.
// @see https://meta.trac.wordpress.org/ticket/5377
if ( '' === $text ) {
$text = '%s Q';
}
$title[] = preg_replace( '#\s?([^0-9,\.]+)#', '<small>$1</small>', sprintf(
esc_html( $text ),
number_format_i18n( $data->wpdb->total_qs )
) );
} elseif ( isset( $data->total_qs ) ) {
/* translators: %s: Number of database queries. Note the space between value and unit symbol. */
$text = _n( '%s Q', '%s Q', $data->total_qs, 'query-monitor' );
// Avoid a potentially blank translation for the plural form.
// @see https://meta.trac.wordpress.org/ticket/5377
if ( '' === $text ) {
$text = '%s Q';
}
$title[] = preg_replace( '#\s?([^0-9,\.]+)#', '<small>$1</small>', sprintf(
esc_html( $text ),
number_format_i18n( $data->total_qs )
) );
}
return $title;
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
if ( $this->collector->get_errors() ) {
$class[] = 'qm-error';
}
if ( $this->collector->get_expensive() ) {
$class[] = 'qm-expensive';
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_DB_Queries $data */
$data = $this->collector->get_data();
$errors = $this->collector->get_errors();
$expensive = $this->collector->get_expensive();
$id = $this->collector->id();
$menu[ $id ] = $this->menu( array(
'title' => esc_html__( 'Database Queries', 'query-monitor' ),
// 'href' => esc_attr( sprintf( '#%s', $this->collector->id() ) ),
) );
if ( $errors ) {
$id = $this->collector->id() . '-errors';
$count = count( $errors );
$menu[ $id ] = $this->menu( array(
'id' => 'query-monitor-errors',
'href' => '#qm-query-errors',
'title' => esc_html( sprintf(
/* translators: %s: Number of database errors */
__( 'Database Errors (%s)', 'query-monitor' ),
number_format_i18n( $count )
) ),
) );
}
if ( $expensive ) {
$id = $this->collector->id() . '-expensive';
$count = count( $expensive );
$menu[ $id ] = $this->menu( array(
'id' => 'query-monitor-expensive',
'href' => '#qm-query-expensive',
'title' => esc_html( sprintf(
/* translators: %s: Number of slow database queries */
__( 'Slow Queries (%s)', 'query-monitor' ),
number_format_i18n( $count )
) ),
) );
}
return $menu;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
foreach ( array( 'errors', 'expensive' ) as $sub ) {
$id = $this->collector->id() . '-' . $sub;
if ( isset( $menu[ $id ] ) ) {
$menu['qm-db_queries']['children'][] = $menu[ $id ];
unset( $menu[ $id ] );
}
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_db_queries( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'db_queries' );
if ( $collector ) {
$output['db_queries'] = new QM_Output_Html_DB_Queries( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_db_queries', 20, 2 );

View File

@ -0,0 +1,114 @@
<?php declare(strict_types = 1);
/**
* 'Debug Bar' output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Debug_Bar extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Debug_Bar Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 200 );
}
/**
* @return string
*/
public function name() {
/** @var string */
return $this->collector->get_panel()->title();
}
/**
* @return void
*/
public function output() {
$class = get_class( $this->collector->get_panel() );
if ( ! $class ) {
return;
}
$target = sanitize_html_class( $class );
$this->before_debug_bar_output();
echo '<div id="debug-menu-target-' . esc_attr( $target ) . '" class="debug-menu-target qm-debug-bar-output">';
ob_start();
$this->collector->render();
$panel = (string) ob_get_clean();
$panel = str_replace( array(
'<h4',
'<h3',
'<h2',
'<h1',
'</h4>',
'</h3>',
'</h2>',
'</h1>',
), array(
'<h5',
'<h4',
'<h3',
'<h2',
'</h5>',
'</h4>',
'</h3>',
'</h2>',
), $panel );
echo $panel; // phpcs:ignore
echo '</div>';
$this->after_debug_bar_output();
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_debug_bar( array $output, QM_Collectors $collectors ) {
global $debug_bar;
if ( empty( $debug_bar ) ) {
return $output;
}
foreach ( $debug_bar->panels as $panel ) {
$class = get_class( $panel );
if ( ! $class ) {
continue;
}
$panel_id = strtolower( sanitize_html_class( $class ) );
/** @var QM_Collector_Debug_Bar|null */
$collector = QM_Collectors::get( "debug_bar_{$panel_id}" );
if ( $collector && $collector->is_visible() ) {
$output[ "debug_bar_{$panel_id}" ] = new QM_Output_Html_Debug_Bar( $collector );
}
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_debug_bar', 200, 2 );

View File

@ -0,0 +1,194 @@
<?php declare(strict_types = 1);
/**
* Doing it Wrong output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Doing_It_Wrong extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Doing_It_Wrong Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 15 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return string
*/
public function name() {
return __( 'Doing it Wrong', 'query-monitor' );
}
/**
* @return array<string, string>
*/
public function get_type_labels() {
return array(
/* translators: %s: Total number of Doing it Wrong occurrences */
'total' => _x( 'Total: %s', 'Doing it Wrong', 'query-monitor' ),
'plural' => __( 'Doing it Wrong occurrences', 'query-monitor' ),
/* translators: %s: Total number of Doing it Wrong occurrences */
'count' => _x( 'Doing it Wrong (%s)', 'Doing it Wrong', 'query-monitor' ),
);
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Doing_It_Wrong $data */
$data = $this->collector->get_data();
if ( empty( $data->actions ) ) {
$this->before_non_tabular_output();
$notice = __( 'No occurrences.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
return;
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Message', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->actions as $row ) {
$stack = array();
foreach ( $row['filtered_trace'] as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
$caller = array_shift( $stack );
echo '<tr>';
printf( '<td>%s</td>', esc_html( wp_strip_all_tags( $row['message'] ) ) );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
echo '<td class="qm-nowrap">' . esc_html( $row['component']->name ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
printf(
'<tr><td colspan="3">%s</td></tr>',
sprintf(
/* translators: %s: Total number of Doing it Wrong occurrences */
esc_html_x( 'Total: %s', 'Total Doing it Wrong occurrences', 'query-monitor' ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( count( $data->actions ) ) ) . '</span>'
)
);
echo '</tfoot>';
$this->after_tabular_output();
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
/** @var QM_Data_Doing_It_Wrong */
$data = $this->collector->get_data();
if ( ! empty( $data->actions ) ) {
$class[] = 'qm-notice';
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Doing_It_Wrong */
$data = $this->collector->get_data();
if ( empty( $data->actions ) ) {
return $menu;
}
$type_label = $this->get_type_labels();
$label = sprintf(
$type_label['count'],
number_format_i18n( count( $data->actions ) )
);
$args = array(
'title' => esc_html( $label ),
'id' => esc_attr( "query-monitor-{$this->collector->id}" ),
'href' => esc_attr( '#' . $this->collector->id() ),
);
if ( ! empty( $data->actions ) ) {
$args['meta']['classname'] = 'qm-notice';
}
$id = $this->collector->id();
$menu[ $id ] = $this->menu( $args );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_doing_it_wrong( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'doing_it_wrong' );
if ( $collector ) {
$output['doing_it_wrong'] = new QM_Output_Html_Doing_It_Wrong( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_doing_it_wrong', 110, 2 );

View File

@ -0,0 +1,315 @@
<?php declare(strict_types = 1);
/**
* Environment data output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Environment extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Environment Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 110 );
}
/**
* @return string
*/
public function name() {
return __( 'Environment', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Environment $data */
$data = $this->collector->get_data();
$this->before_non_tabular_output();
echo '<section>';
echo '<h3>PHP</h3>';
echo '<table>';
echo '<tbody>';
$append = '';
$class = '';
$php_warning = $data->php['old'];
if ( $php_warning ) {
$append .= sprintf(
'&nbsp;<span class="qm-info">(<a href="%s" target="_blank" class="qm-external-link">%s</a>)</span>',
'https://wordpress.org/support/update-php/',
esc_html__( 'Help', 'query-monitor' )
);
$class = 'qm-warn';
}
echo '<tr class="' . esc_attr( $class ) . '">';
echo '<th scope="row">' . esc_html__( 'Version', 'query-monitor' ) . '</th>';
echo '<td>';
if ( $php_warning ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
}
echo esc_html( $data->php['version'] ?: esc_html__( 'Unknown', 'query-monitor' ) );
echo $append; // WPCS: XSS ok.
echo '</td>';
echo '</tr>';
echo '<tr>';
echo '<th scope="row">SAPI</th>';
echo '<td>' . esc_html( $data->php['sapi'] ?: esc_html__( 'Unknown', 'query-monitor' ) ) . '</td>';
echo '</tr>';
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'User', 'query-monitor' ) . '</th>';
if ( ! empty( $data->php['user'] ) ) {
echo '<td>' . esc_html( $data->php['user'] ) . '</td>';
} else {
echo '<td><em>' . esc_html__( 'Unknown', 'query-monitor' ) . '</em></td>';
}
echo '</tr>';
foreach ( $data->php['variables'] as $key => $val ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $key ) . '</th>';
echo '<td>';
echo esc_html( $val );
echo '</td>';
echo '</tr>';
}
$out = array();
foreach ( $data->php['error_levels'] as $level => $reported ) {
if ( $reported ) {
$out[] = esc_html( $level ) . '&nbsp;&#x2713;';
} else {
$out[] = '<span class="qm-false">' . esc_html( $level ) . '</span>';
}
}
$error_levels = implode( '</li><li>', $out );
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Error Reporting', 'query-monitor' ) . '</th>';
echo '<td class="qm-has-toggle qm-ltr">';
echo esc_html( (string) $data->php['error_reporting'] );
echo self::build_toggler(); // WPCS: XSS ok;
echo '<div class="qm-toggled">';
echo "<ul class='qm-supplemental'><li>{$error_levels}</li></ul>"; // WPCS: XSS ok.
echo '</div>';
echo '</td>';
echo '</tr>';
if ( ! empty( $data->php['extensions'] ) ) {
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Extensions', 'query-monitor' ) . '</th>';
echo '<td class="qm-has-inner qm-has-toggle qm-ltr">';
printf( // WPCS: XSS ok.
'<div class="qm-inner-toggle">%1$s %2$s</div>',
esc_html( number_format_i18n( count( $data->php['extensions'] ) ) ),
self::build_toggler()
);
echo '<div class="qm-toggled">';
self::output_inner( $data->php['extensions'] );
echo '</div>';
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
if ( isset( $data->db ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Database', 'query-monitor' ) . '</h3>';
echo '<table>';
echo '<tbody>';
$info = array(
'server-version' => __( 'Server Version', 'query-monitor' ),
'extension' => __( 'Extension', 'query-monitor' ),
'client-version' => __( 'Client Version', 'query-monitor' ),
'user' => __( 'User', 'query-monitor' ),
'host' => __( 'Host', 'query-monitor' ),
'database' => __( 'Database', 'query-monitor' ),
);
foreach ( $info as $field => $label ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $label ) . '</th>';
if ( ! isset( $data->db['info'][ $field ] ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<td><span class="qm-warn">' . QueryMonitor::icon( 'warning' ) . esc_html__( 'Unknown', 'query-monitor' ) . '</span></td>';
} else {
echo '<td>' . esc_html( $data->db['info'][ $field ] ) . '</td>';
}
echo '</tr>';
}
foreach ( $data->db['variables'] as $setting ) {
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$key = (string) $setting->Variable_name;
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$val = (string) $setting->Value;
$append = '';
if ( is_numeric( $val ) && ( $val >= ( 1024 * 1024 ) ) ) {
$append .= sprintf(
'&nbsp;<span class="qm-info">(~%s)</span>',
esc_html( (string) size_format( $val ) )
);
}
echo '<tr>';
echo '<th scope="row">' . esc_html( $key ) . '</th>';
echo '<td>';
echo esc_html( $val );
echo $append; // WPCS: XSS ok.
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
}
echo '<section>';
echo '<h3>WordPress</h3>';
echo '<table>';
echo '<tbody>';
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Version', 'query-monitor' ) . '</th>';
echo '<td>' . esc_html( $data->wp['version'] ) . '</td>';
echo '</tr>';
if ( isset( $data->wp['environment_type'] ) ) {
echo '<tr>';
echo '<th scope="row">';
esc_html_e( 'Environment Type', 'query-monitor' );
printf(
'&nbsp;<span class="qm-info">(<a href="%s" target="_blank" class="qm-external-link">%s</a>)</span>',
'https://make.wordpress.org/core/2020/07/24/new-wp_get_environment_type-function-in-wordpress-5-5/',
esc_html__( 'Help', 'query-monitor' )
);
echo '</th>';
echo '<td>' . esc_html( $data->wp['environment_type'] ) . '</td>';
echo '</tr>';
}
if ( isset( $data->wp['development_mode'] ) ) {
echo '<tr>';
echo '<th scope="row">';
esc_html_e( 'Development Mode', 'query-monitor' );
printf(
'&nbsp;<span class="qm-info">(<a href="%s" target="_blank" class="qm-external-link">%s</a>)</span>',
'https://core.trac.wordpress.org/changeset/56042',
esc_html__( 'Help', 'query-monitor' )
);
echo '</th>';
echo '<td>' . esc_html( $data->wp['development_mode'] ) . '</td>';
echo '</tr>';
}
foreach ( $data->wp['constants'] as $key => $val ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $key ) . '</th>';
echo '<td>' . esc_html( $val ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Server', 'query-monitor' ) . '</h3>';
$server = array(
'name' => __( 'Software', 'query-monitor' ),
'version' => __( 'Version', 'query-monitor' ),
'address' => __( 'IP Address', 'query-monitor' ),
'host' => __( 'Host', 'query-monitor' ),
/* translators: OS stands for Operating System */
'OS' => __( 'OS', 'query-monitor' ),
'arch' => __( 'Architecture', 'query-monitor' ),
);
echo '<table>';
echo '<tbody>';
foreach ( $server as $field => $label ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $label ) . '</th>';
if ( ! empty( $data->server[ $field ] ) ) {
echo '<td>' . esc_html( $data->server[ $field ] ) . '</td>';
} else {
echo '<td><em>' . esc_html__( 'Unknown', 'query-monitor' ) . '</em></td>';
}
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</section>';
$this->after_non_tabular_output();
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_environment( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'environment' );
if ( $collector ) {
$output['environment'] = new QM_Output_Html_Environment( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_environment', 120, 2 );

View File

@ -0,0 +1,146 @@
<?php declare(strict_types = 1);
/**
* Request and response headers output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Headers extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Raw_Request Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 20 );
}
/**
* Collector name.
*
* This is unused.
*
* @return string
*/
public function name() {
return __( 'Request Data', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
$this->output_request();
$this->output_response();
}
/**
* @return void
*/
public function output_request() {
/** @var QM_Data_Raw_Request $data */
$data = $this->collector->get_data();
$this->before_tabular_output();
$this->output_header_table( $data->request['headers'], __( 'Request Header Name', 'query-monitor' ) );
$this->after_tabular_output();
}
/**
* @return void
*/
public function output_response() {
/** @var QM_Data_Raw_Request $data */
$data = $this->collector->get_data();
$id = sprintf( 'qm-%s-response', $this->collector->id );
$this->before_tabular_output( $id );
$this->output_header_table( $data->response['headers'], __( 'Response Header Name', 'query-monitor' ) );
$this->after_tabular_output();
}
/**
* @param array<string, string> $headers
* @param string $title
* @return void
*/
protected function output_header_table( array $headers, $title ) {
echo '<thead>';
echo '<tr>';
echo '<th>';
echo esc_html( $title );
echo '</th><th>';
esc_html_e( 'Value', 'query-monitor' );
echo '</th></tr>';
echo '<tbody>';
foreach ( $headers as $name => $value ) {
echo '<tr>';
$formatted = str_replace( ' ', '-', ucwords( strtolower( str_replace( array( '-', '_' ), ' ', $name ) ) ) );
printf( '<th scope="row"><code>%s</code></th>', esc_html( $formatted ) );
printf( '<td><pre class="qm-pre-wrap"><code>%s</code></pre></td>', esc_html( $value ) );
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
echo '<tr>';
echo '<td colspan="2">';
esc_html_e( 'Note that header names are not case-sensitive.', 'query-monitor' );
echo '</td>';
echo '</tr>';
echo '</tfoot>';
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
if ( ! isset( $menu['qm-request'] ) ) {
return $menu;
}
$ids = array(
$this->collector->id() => __( 'Request Headers', 'query-monitor' ),
$this->collector->id() . '-response' => __( 'Response Headers', 'query-monitor' ),
);
foreach ( $ids as $id => $title ) {
$menu['qm-request']['children'][] = array(
'id' => $id,
'href' => '#' . $id,
'title' => esc_html( $title ),
);
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_headers( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'raw_request' );
if ( $collector ) {
$output['raw_request'] = new QM_Output_Html_Headers( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_headers', 100, 2 );

View File

@ -0,0 +1,255 @@
<?php declare(strict_types = 1);
/**
* Hooks and actions output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Hooks extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Hooks Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 80 );
}
/**
* @return string
*/
public function name() {
/** @var QM_Data_Hooks */
$data = $this->collector->get_data();
$name = __( 'Hooks & Actions', 'query-monitor' );
if ( $data->all_hooks ) {
$name = __( 'Hooks, Actions, & Filters', 'query-monitor' );
}
return $name;
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Hooks */
$data = $this->collector->get_data();
if ( empty( $data->hooks ) ) {
return;
}
$this->before_tabular_output();
$callback_label = __( 'Action', 'query-monitor' );
$th_type = '';
if ( $data->all_hooks ) {
$callback_label = __( 'Callback', 'query-monitor' );
$th_type = '<th scope="col" class="qm-filterable-column">' . $this->build_filter( 'type', array(
'action' => __( 'Action', 'query-monitor' ),
'filter' => __( 'Filter', 'query-monitor' ),
), __( 'Type', 'query-monitor' ) ) . '</th>';
}
echo '<thead>';
echo '<tr>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'name', $data->parts, __( 'Hook', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo $th_type; // WPCS: XSS ok.
echo '<th scope="col">' . esc_html__( 'Priority', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html( $callback_label ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $data->components, __( 'Component', 'query-monitor' ), array(
'highlight' => 'subject',
) ); // WPCS: XSS ok.
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
self::output_hook_table( $data->hooks, $data->all_hooks );
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param array<int, mixed[]> $hooks
* @param bool $all_hooks
* @return void
*/
public static function output_hook_table( array $hooks, bool $all_hooks ) {
$core = __( 'WordPress Core', 'query-monitor' );
foreach ( $hooks as $hook ) {
$row_attr = array();
$row_attr['data-qm-name'] = implode( ' ', $hook['parts'] );
$row_attr['data-qm-component'] = implode( ' ', $hook['components'] );
$row_attr['data-qm-type'] = $hook['type'];
if ( ! empty( $row_attr['data-qm-component'] ) && $core !== $row_attr['data-qm-component'] ) {
$row_attr['data-qm-component'] .= ' non-core';
}
$attr = '';
if ( ! empty( $hook['actions'] ) ) {
$rowspan = count( $hook['actions'] );
} else {
$rowspan = 1;
}
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
if ( ! empty( $hook['actions'] ) ) {
$first = true;
foreach ( $hook['actions'] as $action ) {
$component = '';
$subject = '';
if ( isset( $action['callback']['component'] ) ) {
$component = $action['callback']['component']->name;
$subject = $component;
}
if ( $core !== $component ) {
$subject .= ' non-core';
}
printf( // WPCS: XSS ok.
'<tr data-qm-subject="%s" %s>',
esc_attr( $subject ),
$attr
);
if ( $first ) {
echo '<th scope="row" rowspan="' . intval( $rowspan ) . '" class="qm-nowrap qm-ltr"><span class="qm-sticky">';
echo '<code>' . esc_html( $hook['name'] ) . '</code>';
if ( 'all' === $hook['name'] ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br><span class="qm-warn">' . QueryMonitor::icon( 'warning' );
printf(
/* translators: %s: Action name */
esc_html__( 'Warning: The %s action is extremely resource intensive. Try to avoid using it.', 'query-monitor' ),
'<code>all</code>'
);
echo '</span>';
}
echo '</span></th>';
if ( $all_hooks ) {
$type = ( 'action' === $hook['type'] ) ? __( 'Action', 'query-monitor' ) : __( 'Filter', 'query-monitor' );
echo '<td rowspan="' . intval( $rowspan ) . '" class="qm-nowrap qm-ltr"><span class="qm-sticky">' . esc_html( $type ) . '</td>';
}
}
if ( isset( $action['callback']['error'] ) ) {
$class = ' qm-warn';
} else {
$class = '';
}
echo '<td class="qm-num' . esc_attr( $class ) . '">';
echo esc_html( $action['priority'] );
if ( PHP_INT_MAX === $action['priority'] ) {
echo ' <span class="qm-info">(PHP_INT_MAX)</span>';
} elseif ( PHP_INT_MIN === $action['priority'] ) {
echo ' <span class="qm-info">(PHP_INT_MIN)</span>';
} elseif ( -PHP_INT_MAX === $action['priority'] ) {
echo ' <span class="qm-info">(-PHP_INT_MAX)</span>';
}
echo '</td>';
if ( isset( $action['callback']['file'] ) ) {
if ( self::has_clickable_links() ) {
echo '<td class="qm-nowrap qm-ltr' . esc_attr( $class ) . '">';
echo self::output_filename( $action['callback']['name'], $action['callback']['file'], $action['callback']['line'] ); // WPCS: XSS ok.
echo '</td>';
} else {
echo '<td class="qm-nowrap qm-ltr qm-has-toggle' . esc_attr( $class ) . '">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ol>';
echo '<li>';
echo self::output_filename( $action['callback']['name'], $action['callback']['file'], $action['callback']['line'] ); // WPCS: XSS ok.
echo '</li>';
echo '</ol></td>';
}
} else {
echo '<td class="qm-ltr qm-nowrap' . esc_attr( $class ) . '">';
echo '<code>' . esc_html( $action['callback']['name'] ) . '</code>';
if ( isset( $action['callback']['error'] ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br>' . QueryMonitor::icon( 'warning' );
echo esc_html( sprintf(
/* translators: %s: Error message text */
__( 'Error: %s', 'query-monitor' ),
$action['callback']['error']->get_error_message()
) );
}
echo '</td>';
}
echo '<td class="qm-nowrap' . esc_attr( $class ) . '">';
echo esc_html( $component );
echo '</td>';
echo '</tr>';
$first = false;
}
} else {
echo "<tr{$attr}>"; // WPCS: XSS ok.
echo '<th scope="row" class="qm-ltr">';
echo '<code>' . esc_html( $hook['name'] ) . '</code>';
echo '</th>';
echo '<td></td>';
echo '<td></td>';
echo '<td></td>';
if ( $all_hooks ) {
echo '<td></td>';
}
echo '</tr>';
}
}
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_hooks( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'hooks' );
if ( $collector ) {
$output['hooks'] = new QM_Output_Html_Hooks( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_hooks', 80, 2 );

View File

@ -0,0 +1,391 @@
<?php declare(strict_types = 1);
/**
* HTTP API request output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_HTTP extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_HTTP Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 90 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return string
*/
public function name() {
return __( 'HTTP API Calls', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_HTTP $data */
$data = $this->collector->get_data();
if ( ! empty( $data->http ) ) {
$statuses = array_keys( $data->types );
$components = array_column( $data->component_times, 'component' );
usort( $statuses, 'strcasecmp' );
usort( $components, 'strcasecmp' );
$status_output = array();
$hosts = array_unique( array_column( $data->http, 'host' ) );
sort( $hosts );
foreach ( $statuses as $status ) {
if ( 'error' === $status ) {
$status_output['error'] = __( 'Error', 'query-monitor' );
} elseif ( 'non-blocking' === $status ) {
/* translators: A non-blocking HTTP API request */
$status_output['non-blocking'] = __( 'Non-blocking', 'query-monitor' );
} else {
$status_output[] = $status;
}
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Method', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'host', $hosts, __( 'URL', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'type', $status_output, __( 'Status', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $components, __( 'Component', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Size', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Timeout', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Time', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
$i = 0;
foreach ( $data->http as $row ) {
$ltime = $row['ltime'];
$i++;
$is_error = false;
$row_attr = array();
$css = '';
if ( $row['response'] instanceof WP_Error ) {
$response = $row['response']->get_error_message();
$is_error = true;
} elseif ( ! $row['args']['blocking'] ) {
/* translators: A non-blocking HTTP API request */
$response = __( 'Non-blocking', 'query-monitor' );
} else {
$code = wp_remote_retrieve_response_code( $row['response'] );
$msg = wp_remote_retrieve_response_message( $row['response'] );
if ( intval( $code ) >= 400 ) {
$is_error = true;
}
$response = $code . ' ' . $msg;
}
if ( $is_error ) {
$css = 'qm-warn';
}
$url = self::format_url( $row['url'] );
$info = '';
$url = preg_replace( '|^http:|', '<span class="qm-warn">http</span>:', $url );
if ( 'https' === parse_url( $row['url'], PHP_URL_SCHEME ) ) {
if ( empty( $row['args']['sslverify'] ) && ! $row['local'] ) {
$info .= '<span class="qm-warn">' . QueryMonitor::icon( 'warning' ) . esc_html( sprintf(
/* translators: An HTTP API request has disabled certificate verification. 1: Relevant argument name */
__( 'Certificate verification disabled (%s)', 'query-monitor' ),
'sslverify=false'
) ) . '</span><br>';
$url = preg_replace( '|^https:|', '<span class="qm-warn">https</span>:', $url );
} elseif ( ! $is_error && $row['args']['blocking'] ) {
$url = preg_replace( '|^https:|', '<span class="qm-true">https</span>:', $url );
}
}
$component = $row['component'];
$stack = array();
$filtered_trace = $row['filtered_trace'];
foreach ( $filtered_trace as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
$row_attr['data-qm-component'] = $component->name;
$row_attr['data-qm-type'] = $row['type'];
$row_attr['data-qm-time'] = $row['ltime'];
$row_attr['data-qm-host'] = $row['host'];
if ( 'core' !== $component->context ) {
$row_attr['data-qm-component'] .= ' non-core';
}
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( (string) $v ) . '"';
}
printf( // WPCS: XSS ok.
'<tr %s class="%s">',
$attr,
esc_attr( $css )
);
printf(
'<td>%s</td>',
esc_html( $row['args']['method'] )
);
if ( ! empty( $row['redirected_to'] ) ) {
$url .= sprintf(
'<br><span class="qm-warn">%1$s%2$s</span><br>%3$s',
QueryMonitor::icon( 'warning' ),
/* translators: An HTTP API request redirected to another URL */
__( 'Redirected to:', 'query-monitor' ),
self::format_url( $row['redirected_to'] )
);
}
printf( // WPCS: XSS ok.
'<td class="qm-url qm-ltr qm-wrap">%s%s</td>',
$info,
$url
);
$show_toggle = ! empty( $row['info'] );
echo '<td class="qm-has-toggle qm-col-status">';
if ( $is_error ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
}
echo esc_html( $response );
if ( $show_toggle ) {
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ul class="qm-toggled">';
}
if ( ! empty( $row['info'] ) ) {
$time_fields = array(
'namelookup_time' => __( 'DNS Resolution Time', 'query-monitor' ),
'connect_time' => __( 'Connection Time', 'query-monitor' ),
'starttransfer_time' => __( 'Transfer Start Time (TTFB)', 'query-monitor' ),
);
foreach ( $time_fields as $key => $value ) {
if ( ! isset( $row['info'][ $key ] ) ) {
continue;
}
printf(
'<li><span class="qm-info qm-supplemental">%1$s: %2$s</span></li>',
esc_html( $value ),
esc_html( number_format_i18n( $row['info'][ $key ], 4 ) )
);
}
$other_fields = array(
'content_type' => __( 'Response Content Type', 'query-monitor' ),
'primary_ip' => __( 'IP Address', 'query-monitor' ),
);
foreach ( $other_fields as $key => $value ) {
if ( ! isset( $row['info'][ $key ] ) ) {
continue;
}
printf(
'<li><span class="qm-info qm-supplemental">%1$s: %2$s</span></li>',
esc_html( $value ),
esc_html( $row['info'][ $key ] )
);
}
}
if ( $show_toggle ) {
echo '</ul>';
}
echo '</td>';
$caller = array_shift( $stack );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
$size = '';
if ( isset( $row['info']['size_download'] ) ) {
$size = sprintf(
/* translators: %s: Memory used in kilobytes */
__( '%s kB', 'query-monitor' ),
number_format_i18n( $row['info']['size_download'] / 1024, 1 )
);
}
printf(
'<td class="qm-nowrap qm-num">%s</td>',
esc_html( $size )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $row['args']['timeout'] )
);
if ( empty( $ltime ) ) {
$stime = '';
} else {
$stime = number_format_i18n( $ltime, 4 );
}
printf(
'<td class="qm-num">%s</td>',
esc_html( $stime )
);
echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
$total_stime = number_format_i18n( $data->ltime, 4 );
$count = count( $data->http );
echo '<tr>';
printf(
'<td colspan="7">%s</td>',
sprintf(
/* translators: %s: Number of HTTP API requests */
esc_html( _nx( 'Total: %s', 'Total: %s', $count, 'HTTP API calls', 'query-monitor' ) ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $count ) ) . '</span>'
)
);
echo '<td class="qm-num qm-items-time">' . esc_html( $total_stime ) . '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
} else {
$this->before_non_tabular_output();
$notice = __( 'No HTTP API calls.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
/** @var QM_Data_HTTP $data */
$data = $this->collector->get_data();
if ( isset( $data->errors['alert'] ) ) {
$class[] = 'qm-alert';
}
if ( isset( $data->errors['warning'] ) ) {
$class[] = 'qm-warning';
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_HTTP $data */
$data = $this->collector->get_data();
$count = ! empty( $data->http ) ? count( $data->http ) : 0;
$title = ( empty( $count ) )
? __( 'HTTP API Calls', 'query-monitor' )
/* translators: %s: Number of calls to the HTTP API */
: __( 'HTTP API Calls (%s)', 'query-monitor' );
$args = array(
'title' => esc_html( sprintf(
$title,
number_format_i18n( $count )
) ),
);
if ( isset( $data->errors['alert'] ) ) {
$args['meta']['classname'] = 'qm-alert';
}
if ( isset( $data->errors['warning'] ) ) {
$args['meta']['classname'] = 'qm-warning';
}
$menu[ $this->collector->id() ] = $this->menu( $args );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_http( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'http' );
if ( $collector ) {
$output['http'] = new QM_Output_Html_HTTP( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_http', 90, 2 );

View File

@ -0,0 +1,212 @@
<?php declare(strict_types = 1);
/**
* Language and locale output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Languages extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Languages Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 80 );
}
/**
* @return string
*/
public function name() {
return __( 'Languages', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Languages $data */
$data = $this->collector->get_data();
$this->before_non_tabular_output();
echo '<section>';
echo '<h3><code>get_locale()</code></h3>';
echo '<p>' . esc_html( $data->locale ) . '</p>';
echo '</section>';
echo '<section>';
echo '<h3><code>get_user_locale()</code></h3>';
echo '<p>' . esc_html( $data->user_locale ) . '</p>';
echo '</section>';
echo '<section>';
echo '<h3><code>determine_locale()</code></h3>';
echo '<p>' . esc_html( $data->determined_locale ) . '</p>';
echo '</section>';
if ( isset( $data->mlp_language ) ) {
echo '<section>';
echo '<h3>';
printf(
/* translators: %s: Name of a multilingual plugin */
esc_html__( '%s Language', 'query-monitor' ),
'MultilingualPress'
);
echo '</h3>';
echo '<p>' . esc_html( $data->mlp_language ) . '</p>';
echo '</section>';
}
if ( isset( $data->pll_language ) ) {
echo '<section>';
echo '<h3>';
printf(
/* translators: %s: Name of a multilingual plugin */
esc_html__( '%s Language', 'query-monitor' ),
'Polylang'
);
echo '</h3>';
echo '<p>' . esc_html( $data->pll_language ) . '</p>';
echo '</section>';
}
echo '<section>';
echo '<h3><code>get_language_attributes()</code></h3>';
echo '<p><code>' . esc_html( $data->language_attributes ) . '</code></p>';
echo '</section>';
if ( ! empty( $data->languages ) ) {
echo '<table class="qm-full-width">';
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Text Domain', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Type', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Translation File', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Size', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->languages as $textdomain => $mofiles ) {
foreach ( $mofiles as $mofile ) {
echo '<tr>';
if ( $mofile['handle'] ) {
echo '<td class="qm-ltr">' . esc_html( $mofile['domain'] ) . ' (' . esc_html( $mofile['handle'] ) . ')</td>';
} else {
echo '<td class="qm-ltr">' . esc_html( $mofile['domain'] ) . '</td>';
}
echo '<td>' . esc_html( $mofile['type'] ) . '</td>';
if ( self::has_clickable_links() ) {
echo '<td class="qm-nowrap qm-ltr">';
echo self::output_filename( $mofile['caller']['display'], $mofile['caller']['file'], $mofile['caller']['line'] ); // WPCS: XSS ok.
echo '</td>';
} else {
echo '<td class="qm-nowrap qm-ltr qm-has-toggle">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ol>';
echo '<li>';
// undefined:
echo self::output_filename( $mofile['caller']['display'], $mofile['caller']['file'], $mofile['caller']['line'] ); // WPCS: XSS ok.
echo '</li>';
echo '</ol></td>';
}
echo '<td class="qm-ltr">';
if ( $mofile['file'] ) {
if ( $mofile['found'] && 'jed' === $mofile['type'] && self::has_clickable_links() ) {
echo self::output_filename( QM_Util::standard_dir( $mofile['file'], '' ), $mofile['file'], 1, true ); // WPCS: XSS ok.
} else {
echo esc_html( QM_Util::standard_dir( $mofile['file'], '' ) );
}
} else {
echo '<em>' . esc_html__( 'None', 'query-monitor' ) . '</em>';
}
echo '</td>';
if ( $mofile['found'] ) {
echo '<td class="qm-nowrap qm-num">';
echo esc_html( sprintf(
/* translators: %s: Memory used in kilobytes */
__( '%s kB', 'query-monitor' ),
number_format_i18n( $mofile['found'] / 1024, 1 )
) );
echo '</td>';
} else {
echo '<td class="qm-nowrap">';
echo esc_html__( 'Not Found', 'query-monitor' );
echo '</td>';
}
echo '</tr>';
}
}
echo '</tbody>';
echo '<tfoot>';
echo '<tr>';
echo '<td colspan="4">&nbsp;</td>';
echo '<td class="qm-num">';
echo esc_html( sprintf(
/* translators: %s: Memory used in kilobytes */
__( '%s kB', 'query-monitor' ),
number_format_i18n( $data->total_size / 1024, 1 )
) );
echo '</td>';
echo '</tr>';
echo '</tfoot>';
echo '</table>';
}
$this->after_non_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
$args = array(
'title' => esc_html( $this->name() ),
);
$menu[ $this->collector->id() ] = $this->menu( $args );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_languages( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'languages' );
if ( $collector ) {
$output['languages'] = new QM_Output_Html_Languages( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_languages', 81, 2 );

View File

@ -0,0 +1,249 @@
<?php declare(strict_types = 1);
/**
* PSR-3 compatible logging output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Logger extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Logger Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 47 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return string
*/
public function name() {
return __( 'Logger', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Logger $data */
$data = $this->collector->get_data();
if ( empty( $data->logs ) ) {
$this->before_non_tabular_output();
$notice = sprintf(
/* translators: %s: Link to help article */
__( 'No data logged. <a href="%s">Read about logging variables in Query Monitor</a>.', 'query-monitor' ),
'https://querymonitor.com/docs/logging-variables/'
);
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
return;
}
$levels = array();
foreach ( $this->collector->get_levels() as $level ) {
if ( $data->counts[ $level ] ) {
$levels[ $level ] = sprintf(
'%s (%d)',
ucfirst( $level ),
$data->counts[ $level ]
);
} else {
$levels[ $level ] = ucfirst( $level );
}
}
$this->before_tabular_output();
$level_args = array(
'all' => sprintf(
/* translators: %s: Total number of items in a list */
__( 'All (%d)', 'query-monitor' ),
count( $data->logs )
),
);
echo '<thead>';
echo '<tr>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'type', $levels, __( 'Level', 'query-monitor' ), $level_args ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-col-message">' . esc_html__( 'Message', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $data->components, __( 'Component', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->logs as $row ) {
$component = $row['component'];
$row_attr = array();
$row_attr['data-qm-component'] = $component->name;
$row_attr['data-qm-type'] = $row['level'];
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
$is_warning = in_array( $row['level'], $this->collector->get_warning_levels(), true );
if ( $is_warning ) {
$class = 'qm-warn';
} else {
$class = '';
}
echo '<tr' . $attr . ' class="' . esc_attr( $class ) . '">'; // WPCS: XSS ok.
echo '<td class="qm-nowrap">';
if ( $is_warning ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'blank' );
}
echo esc_html( ucfirst( $row['level'] ) );
echo '</td>';
printf(
'<td><pre>%s</pre></td>',
esc_html( $row['message'] )
);
$stack = array();
$filtered_trace = $row['filtered_trace'];
foreach ( $filtered_trace as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
$caller = array_shift( $stack );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
echo '</tr>';
}
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
/** @var QM_Data_Logger $data */
$data = $this->collector->get_data();
if ( empty( $data->logs ) ) {
return $class;
}
foreach ( $data->logs as $log ) {
if ( in_array( $log['level'], $this->collector->get_warning_levels(), true ) ) {
$class[] = 'qm-warning';
break;
}
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Logger $data */
$data = $this->collector->get_data();
$key = 'log';
$count = 0;
if ( ! empty( $data->logs ) ) {
foreach ( $data->logs as $log ) {
if ( in_array( $log['level'], $this->collector->get_warning_levels(), true ) ) {
$key = 'warning';
break;
}
}
$count = count( $data->logs );
/* translators: %s: Number of logs that are available */
$label = __( 'Logs (%s)', 'query-monitor' );
} else {
$label = __( 'Logs', 'query-monitor' );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'id' => "query-monitor-logger-{$key}",
'title' => esc_html( sprintf(
$label,
number_format_i18n( $count )
) ),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_logger( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'logger' );
if ( $collector ) {
$output['logger'] = new QM_Output_Html_Logger( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_logger', 12, 2 );

View File

@ -0,0 +1,167 @@
<?php declare(strict_types = 1);
/**
* Multisite output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Multisite extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Multisite Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 55 );
}
public function name() {
return __( 'Multisite', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['switches'] ) ) {
$this->before_non_tabular_output();
$notice = __( 'No data logged.', 'query-monitor' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->build_notice( $notice );
$this->after_non_tabular_output();
return;
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col" class="qm-num">#</th>';
echo '<th scope="col">' . esc_html__( 'Function', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Site Switch', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->build_filter( 'component', array(), __( 'Component', 'query-monitor' ) );
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
$i = 0;
foreach ( $data['switches'] as $row ) {
$component = $row['trace']->get_component();
$row_attr = array();
$row_attr['data-qm-component'] = $component->name;
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<tr' . $attr . '>';
echo '<td class="qm-num">';
if ( $row['to'] ) {
echo intval( ++$i );
}
echo '</td>';
echo '<td class="qm-nowrap"><code>';
if ( $row['to'] ) {
printf(
'switch_to_blog(%d)',
intval($row['new'] )
);
} else {
echo 'restore_current_blog()';
}
echo '</code></td>';
echo '<td class="qm-nowrap">';
if ( $row['to'] ) {
echo esc_html( sprintf(
'%1$s &rarr; %2$s',
$row['prev'],
$row['new']
) );
} else {
echo esc_html( sprintf(
'%1$s &larr; %2$s',
$row['new'],
$row['prev']
) );
}
echo '</td>';
$stack = array();
$filtered_trace = $row['trace']->get_display_trace();
foreach ( $filtered_trace as $item ) {
$stack[] = self::output_filename( $item['display'], $item['calling_file'], $item['calling_line'] );
}
$caller = array_shift( $stack );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
echo '</tr>';
}
echo '</tbody>';
$this->after_tabular_output();
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_multisite( array $output, QM_Collectors $collectors ) {
$collector = is_multisite() ? QM_Collectors::get( 'multisite' ) : null;
if ( $collector ) {
$output['multisite'] = new QM_Output_Html_Multisite( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_multisite', 65, 2 );

View File

@ -0,0 +1,413 @@
<?php declare(strict_types = 1);
/**
* General overview output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Overview extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Overview Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/title', array( $this, 'admin_title' ), 10 );
}
/**
* @return string
*/
public function name() {
return __( 'Overview', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Overview $data */
$data = $this->collector->get_data();
$db_query_num = null;
/** @var QM_Collector_DB_Queries|null $db_queries */
$db_queries = QM_Collectors::get( 'db_queries' );
if ( $db_queries ) {
/** @var QM_Data_DB_Queries $db_queries_data */
$db_queries_data = $db_queries->get_data();
if ( ! empty( $db_queries_data->types ) ) {
$db_query_num = $db_queries_data->types;
}
}
/** @var QM_Collector_Raw_Request|null $raw_request */
$raw_request = QM_Collectors::get( 'raw_request' );
/** @var QM_Collector_Cache|null $cache */
$cache = QM_Collectors::get( 'cache' );
/** @var QM_Collector_HTTP|null $http */
$http = QM_Collectors::get( 'http' );
$qm_broken = __( 'A JavaScript problem on the page is preventing Query Monitor from working correctly. jQuery may have been blocked from loading.', 'query-monitor' );
$ajax_errors = __( 'PHP errors were triggered during an Ajax request. See your browser developer console for details.', 'query-monitor' );
$this->before_non_tabular_output();
echo '<section id="qm-broken">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p class="qm-warn">' . QueryMonitor::icon( 'warning' ) . esc_html( $qm_broken ) . '</p>';
echo '</section>';
echo '<section id="qm-ajax-errors">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p class="qm-warn">' . QueryMonitor::icon( 'warning' ) . esc_html( $ajax_errors ) . '</p>';
echo '</section>';
if ( $raw_request ) {
echo '<section id="qm-overview-raw-request">';
/** @var QM_Data_Raw_Request $raw_data */
$raw_data = $raw_request->get_data();
if ( ! empty( $raw_data->response['status'] ) ) {
$status = $raw_data->response['status'];
} else {
$status = __( 'Unknown HTTP Response Code', 'query-monitor' );
}
printf(
'<h3>%1$s %2$s → %3$s</h3>',
esc_html( $raw_data->request['method'] ),
esc_html( $raw_data->request['url'] ),
esc_html( $status )
);
echo '</section>';
}
echo '</div>';
echo '<div class="qm-grid">';
echo '<section>';
echo '<h3>' . esc_html__( 'Page Generation Time', 'query-monitor' ) . '</h3>';
echo '<p>';
echo esc_html(
sprintf(
/* translators: %s: A time in seconds with a decimal fraction. No space between value and unit. */
_x( '%ss', 'Time in seconds', 'query-monitor' ),
number_format_i18n( $data->time_taken, 4 )
)
);
if ( $data->time_limit > 0 ) {
if ( $data->display_time_usage_warning ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br><span class="qm-warn">' . QueryMonitor::icon( 'warning' );
} else {
echo '<br><span class="qm-info">';
}
echo esc_html( sprintf(
/* translators: 1: Percentage of time limit used, 2: Time limit in seconds */
__( '%1$s%% of %2$ss limit', 'query-monitor' ),
number_format_i18n( $data->time_usage, 1 ),
number_format_i18n( $data->time_limit )
) );
echo '</span>';
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br><span class="qm-warn">' . QueryMonitor::icon( 'warning' );
printf(
/* translators: 1: Name of the PHP directive, 2: Value of the PHP directive */
esc_html__( 'No execution time limit. The %1$s PHP configuration directive is set to %2$s.', 'query-monitor' ),
'<code>max_execution_time</code>',
'0'
);
echo '</span>';
}
echo '</p>';
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Peak Memory Usage', 'query-monitor' ) . '</h3>';
echo '<p>';
if ( empty( $data->memory ) ) {
esc_html_e( 'Unknown', 'query-monitor' );
} else {
echo esc_html( sprintf(
/* translators: 1: Memory used in bytes, 2: Memory used in megabytes */
__( '%1$s bytes (%2$s MB)', 'query-monitor' ),
number_format_i18n( $data->memory ),
number_format_i18n( ( $data->memory / 1024 / 1024 ), 1 )
) );
if ( $data->memory_limit > 0 ) {
if ( $data->display_memory_usage_warning ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br><span class="qm-warn">' . QueryMonitor::icon( 'warning' );
} else {
echo '<br><span class="qm-info">';
}
echo esc_html( sprintf(
/* translators: 1: Percentage of memory limit used, 2: Memory limit in megabytes */
__( '%1$s%% of %2$s MB server limit', 'query-monitor' ),
number_format_i18n( $data->memory_usage, 1 ),
number_format_i18n( $data->memory_limit / 1024 / 1024 )
) );
echo '</span>';
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<br><span class="qm-warn">' . QueryMonitor::icon( 'warning' );
printf(
/* translators: 1: Name of the PHP directive, 2: Value of the PHP directive */
esc_html__( 'No memory limit. The %1$s PHP configuration directive is set to %2$s.', 'query-monitor' ),
'<code>memory_limit</code>',
'0'
);
echo '</span>';
}
}
echo '</p>';
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Database Queries', 'query-monitor' ) . '</h3>';
if ( isset( $db_query_num, $db_queries_data ) ) {
echo '<p>';
echo esc_html(
sprintf(
/* translators: %s: A time in seconds with a decimal fraction. No space between value and unit. */
_x( '%ss', 'Time in seconds', 'query-monitor' ),
number_format_i18n( $db_queries_data->total_time, 4 )
)
);
echo '</p>';
echo '<p>';
if ( ! isset( $db_query_num['SELECT'] ) || count( $db_query_num ) > 1 ) {
foreach ( $db_query_num as $type_name => $type_count ) {
$label = sprintf(
'%1$s: %2$s',
esc_html( $type_name ),
esc_html( number_format_i18n( $type_count ) )
);
echo self::build_filter_trigger( 'db_queries', 'type', (string) $type_name, esc_html( $label ) ); // WPCS: XSS ok;
echo '<br>';
}
}
$label = sprintf(
'%1$s: %2$s',
esc_html( _x( 'Total', 'database queries', 'query-monitor' ) ),
esc_html( number_format_i18n( $db_queries_data->total_qs ) )
);
echo self::build_filter_trigger( 'db_queries', 'type', '', esc_html( $label ) ); // WPCS: XSS ok;
echo '</p>';
} else {
printf(
'<p><em>%s</em></p>',
esc_html__( 'None', 'query-monitor' )
);
}
echo '</section>';
if ( $http ) {
echo '<section>';
echo '<h3>' . esc_html__( 'HTTP API Calls', 'query-monitor' ) . '</h3>';
$http_data = $http->get_data();
if ( ! empty( $http_data->http ) ) {
echo '<p>';
echo esc_html(
sprintf(
/* translators: %s: A time in seconds with a decimal fraction. No space between value and unit. */
_x( '%ss', 'Time in seconds', 'query-monitor' ),
number_format_i18n( $http_data->ltime, 4 )
)
);
echo '</p>';
$label = sprintf(
'%1$s: %2$s',
esc_html( _x( 'Total', 'HTTP API calls', 'query-monitor' ) ),
esc_html( number_format_i18n( count( $http_data->http ) ) )
);
echo self::build_filter_trigger( 'http', 'type', '', esc_html( $label ) ); // WPCS: XSS ok;
} else {
printf(
'<p><em>%s</em></p>',
esc_html__( 'None', 'query-monitor' )
);
}
echo '</section>';
}
echo '<section>';
echo '<h3>' . esc_html__( 'Object Cache', 'query-monitor' ) . '</h3>';
if ( $cache ) {
/** @var QM_Data_Cache $cache_data */
$cache_data = $cache->get_data();
if ( ! empty( $cache_data->stats ) && ! empty( $cache_data->cache_hit_percentage ) ) {
$cache_hit_percentage = $cache_data->cache_hit_percentage;
echo '<p>';
echo esc_html( sprintf(
/* translators: 1: Cache hit rate percentage, 2: number of cache hits, 3: number of cache misses */
__( '%1$s%% hit rate (%2$s hits, %3$s misses)', 'query-monitor' ),
number_format_i18n( $cache_hit_percentage, 1 ),
number_format_i18n( $cache_data->stats['cache_hits'], 0 ),
number_format_i18n( $cache_data->stats['cache_misses'], 0 )
) );
echo '</p>';
}
if ( $cache_data->has_object_cache ) {
echo '<p><span class="qm-info">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link(
network_admin_url( 'plugins.php?plugin_status=dropins' ),
esc_html__( 'Persistent object cache plugin in use', 'query-monitor' )
);
echo '</span></p>';
} else {
echo '<p><span class="qm-warn">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
echo esc_html__( 'Persistent object cache plugin not in use', 'query-monitor' );
echo '</span></p>';
$potentials = array_filter( $cache_data->object_cache_extensions );
if ( ! empty( $potentials ) ) {
foreach ( $potentials as $name => $value ) {
$url = sprintf(
'https://wordpress.org/plugins/search/%s/',
strtolower( $name )
);
echo '<p>';
echo wp_kses(
sprintf(
/* translators: 1: PHP extension name, 2: URL to plugin directory */
__( 'The %1$s object cache extension for PHP is installed but is not in use by WordPress. You should <a href="%2$s" target="_blank" class="qm-external-link">install a %1$s plugin</a>.', 'query-monitor' ),
esc_html( $name ),
esc_url( $url )
),
array(
'a' => array(
'href' => array(),
'target' => array(),
'class' => array(),
),
)
);
echo '</p>';
}
} else {
echo '<p>';
echo esc_html__( 'Speak to your web host about enabling an object cache extension such as Redis or Memcached.', 'query-monitor' );
echo '</p>';
}
}
} else {
echo '<p>';
echo esc_html__( 'Object cache statistics are not available', 'query-monitor' );
echo '</p>';
}
echo '</section>';
if ( $cache ) {
/** @var QM_Data_Cache $cache_data */
$cache_data = $cache->get_data();
echo '<section>';
echo '<h3>' . esc_html__( 'Opcode Cache', 'query-monitor' ) . '</h3>';
if ( $cache_data->has_opcode_cache ) {
foreach ( array_filter( $cache_data->opcode_cache_extensions ) as $opcache_name => $opcache_state ) {
echo '<p>';
echo esc_html( sprintf(
/* translators: %s: Name of cache driver */
__( 'Opcode cache in use: %s', 'query-monitor' ),
$opcache_name
) );
echo '</p>';
}
} else {
echo '<p><span class="qm-warn">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
echo esc_html__( 'Opcode cache not in use', 'query-monitor' );
echo '</span></p>';
echo '<p>';
echo esc_html__( 'Speak to your web host about enabling an opcode cache such as OPcache.', 'query-monitor' );
echo '</p>';
}
echo '</section>';
}
$this->after_non_tabular_output();
}
/**
* @param array<int, string> $title
* @return array<int, string>
*/
public function admin_title( array $title ) {
/** @var QM_Data_Overview $data */
$data = $this->collector->get_data();
if ( empty( $data->memory ) ) {
$memory = '??';
} else {
$memory = number_format_i18n( ( $data->memory / 1024 / 1024 ), 1 );
}
$title[] = sprintf(
/* translators: %s: A time in seconds with a decimal fraction. No space between value and unit symbol. */
esc_html_x( '%ss', 'Time in seconds', 'query-monitor' ),
number_format_i18n( $data->time_taken, 2 )
);
$title[] = preg_replace( '#\s?([^0-9,\.]+)#', '<small>$1</small>', sprintf(
/* translators: %s: Memory usage in megabytes with a decimal fraction. Note the space between value and unit symbol. */
esc_html__( '%s MB', 'query-monitor' ),
$memory
) );
return $title;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_overview( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'overview' );
if ( $collector ) {
$output['overview'] = new QM_Output_Html_Overview( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_overview', 10, 2 );

View File

@ -0,0 +1,349 @@
<?php declare(strict_types = 1);
/**
* PHP error output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_PHP_Errors extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_PHP_Errors Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 10 );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 10 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
/**
* @return string
*/
public function name() {
return __( 'PHP Errors', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_PHP_Errors $data */
$data = $this->collector->get_data();
if ( empty( $data->errors ) && empty( $data->silenced ) && empty( $data->suppressed ) ) {
return;
}
$levels = array(
'Warning',
'Notice',
'Strict',
'Deprecated',
);
$components = $data->components;
$count = 0;
usort( $components, 'strcasecmp' );
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'type', $levels, __( 'Level', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-col-message">' . esc_html__( 'Message', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Location', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Count', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( 'component', $components, __( 'Component', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $this->collector->types as $error_group => $error_types ) {
foreach ( $error_types as $type => $title ) {
if ( ! isset( $data->{$error_group}[ $type ] ) ) {
continue;
}
foreach ( $data->{$error_group}[ $type ] as $error_key => $error ) {
$count += $error['calls'];
$row_attr = array();
$row_attr['data-qm-type'] = ucfirst( $type );
$row_attr['data-qm-key'] = $error_key;
$row_attr['data-qm-count'] = $error['calls'];
if ( $error['component'] ) {
$component = $error['component'];
$row_attr['data-qm-component'] = $component->name;
if ( 'core' !== $component->context ) {
$row_attr['data-qm-component'] .= ' non-core';
}
}
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $v ) . '"';
}
$is_warning = ( 'errors' === $error_group && 'warning' === $type );
if ( $is_warning ) {
$class = 'qm-warn';
} else {
$class = '';
}
echo '<tr ' . $attr . 'class="' . esc_attr( $class ) . '">'; // WPCS: XSS ok.
echo '<td class="qm-nowrap">';
if ( $is_warning ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'warning' );
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo QueryMonitor::icon( 'blank' );
}
echo esc_html( $title );
echo '</td>';
echo '<td class="qm-ltr">' . esc_html( $error['message'] ) . '</td>';
$stack = array();
if ( $error['filtered_trace'] ) {
$filtered_trace = $error['filtered_trace'];
// debug_backtrace() (used within QM_Backtrace) doesn't like being used within an error handler so
// we need to handle its somewhat unreliable stack trace items.
// https://bugs.php.net/bug.php?id=39070
// https://bugs.php.net/bug.php?id=64987
foreach ( $filtered_trace as $i => $item ) {
if ( isset( $item['file'], $item['line'] ) ) {
$stack[] = self::output_filename( $item['display'], $item['file'], $item['line'] );
} elseif ( 0 === $i ) {
$stack[] = self::output_filename( $item['display'], $error['file'], $error['line'] );
} else {
$stack[] = $item['display'] . '<br><span class="qm-info qm-supplemental"><em>' . __( 'Unknown location', 'query-monitor' ) . '</em></span>';
}
}
}
echo '<td class="qm-row-caller qm-row-stack qm-nowrap qm-ltr qm-has-toggle">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo '<li>';
echo self::output_filename( $error['filename'] . ':' . $error['line'], $error['file'], $error['line'], true ); // WPCS: XSS ok.
echo '</li>';
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
echo '<td class="qm-num">' . esc_html( number_format_i18n( $error['calls'] ) ) . '</td>';
if ( ! empty( $component ) ) {
echo '<td class="qm-nowrap">' . esc_html( $component->name ) . '</td>';
} else {
echo '<td><em>' . esc_html__( 'Unknown', 'query-monitor' ) . '</em></td>';
}
echo '</tr>';
}
}
}
echo '</tbody>';
echo '<tfoot>';
echo '<tr>';
echo '<td colspan="5">';
printf(
/* translators: %s: Number of PHP errors */
esc_html( _nx( 'Total: %s', 'Total: %s', $count, 'PHP error count', 'query-monitor' ) ),
'<span class="qm-items-number">' . esc_html( number_format_i18n( $count ) ) . '</span>'
);
echo '</td>';
echo '</tr>';
echo '</tfoot>';
$this->after_tabular_output();
}
/**
* @param array<int, string> $class
* @return array<int, string>
*/
public function admin_class( array $class ) {
/** @var QM_Data_PHP_Errors $data */
$data = $this->collector->get_data();
if ( ! empty( $data->errors ) ) {
foreach ( $data->errors as $type => $errors ) {
$class[] = 'qm-' . $type;
}
}
return $class;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_PHP_Errors $data */
$data = $this->collector->get_data();
$menu_label = array();
$types = array(
/* translators: %s: Number of deprecated PHP errors */
'deprecated' => _nx_noop( '%s Deprecated', '%s Deprecated', 'PHP error level', 'query-monitor' ),
/* translators: %s: Number of strict PHP errors */
'strict' => _nx_noop( '%s Strict', '%s Stricts', 'PHP error level', 'query-monitor' ),
/* translators: %s: Number of PHP notices */
'notice' => _nx_noop( '%s Notice', '%s Notices', 'PHP error level', 'query-monitor' ),
/* translators: %s: Number of PHP warnings */
'warning' => _nx_noop( '%s Warning', '%s Warnings', 'PHP error level', 'query-monitor' ),
);
$key = 'quiet';
$generic = false;
foreach ( $types as $type => $label ) {
$count = 0;
$has_errors = false;
if ( isset( $data->suppressed[ $type ] ) ) {
$has_errors = true;
$generic = true;
}
if ( isset( $data->silenced[ $type ] ) ) {
$has_errors = true;
$generic = true;
}
if ( isset( $data->errors[ $type ] ) ) {
$has_errors = true;
$key = $type;
$count += (int) array_sum( array_column( $data->errors[ $type ], 'calls' ) );
}
if ( ! $has_errors ) {
continue;
}
if ( $count ) {
$label = sprintf(
translate_nooped_plural(
$label,
$count,
'query-monitor'
),
number_format_i18n( $count )
);
$menu_label[] = $label;
}
}
if ( empty( $menu_label ) && ! $generic ) {
return $menu;
}
/* translators: %s: List of PHP error types */
$title = __( 'PHP Errors (%s)', 'query-monitor' );
/* translators: used between list items, there is a space after the comma */
$sep = __( ', ', 'query-monitor' );
if ( count( $menu_label ) ) {
$title = sprintf(
$title,
implode( $sep, array_reverse( $menu_label ) )
);
} else {
$title = __( 'PHP Errors', 'query-monitor' );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'id' => "query-monitor-{$key}s",
'title' => $title,
) );
return $menu;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
if ( ! isset( $menu[ $this->collector->id() ] ) ) {
return $menu;
}
/** @var QM_Data_PHP_Errors $data */
$data = $this->collector->get_data();
$count = 0;
$types = array(
'suppressed',
'silenced',
'errors',
);
foreach ( $types as $type ) {
if ( ! empty( $data->{$type} ) ) {
foreach ( $data->{$type} as $errors ) {
$count += array_sum( array_column( $errors, 'calls' ) );
}
}
}
$menu[ $this->collector->id() ]['title'] = esc_html( sprintf(
/* translators: %s: Number of errors */
__( 'PHP Errors (%s)', 'query-monitor' ),
number_format_i18n( $count )
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_php_errors( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'php_errors' );
if ( $collector ) {
$output['php_errors'] = new QM_Output_Html_PHP_Errors( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_php_errors', 110, 2 );

View File

@ -0,0 +1,252 @@
<?php declare(strict_types = 1);
/**
* Request data output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Request extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Request Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 50 );
}
/**
* @return string
*/
public function name() {
return __( 'Request', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Request $data */
$data = $this->collector->get_data();
/** @var QM_Collector_DB_Queries|null $db_queries */
$db_queries = QM_Collectors::get( 'db_queries' );
/** @var QM_Collector_Raw_Request|null $raw_request */
$raw_request = QM_Collectors::get( 'raw_request' );
$this->before_non_tabular_output();
foreach ( array(
'request' => __( 'Request', 'query-monitor' ),
'matched_rule' => __( 'Matched Rule', 'query-monitor' ),
'matched_query' => __( 'Matched Query', 'query-monitor' ),
'query_string' => __( 'Query String', 'query-monitor' ),
) as $item => $name ) {
if ( is_admin() && ! isset( $data->request[ $item ] ) ) {
continue;
}
if ( ! empty( $data->request[ $item ] ) ) {
if ( in_array( $item, array( 'request', 'matched_query', 'query_string' ), true ) ) {
$value = self::format_url( $data->request[ $item ] );
} else {
$value = esc_html( $data->request[ $item ] );
}
} else {
$value = '<em>' . esc_html__( 'none', 'query-monitor' ) . '</em>';
}
echo '<section>';
echo '<h3>' . esc_html( $name ) . '</h3>';
echo '<p class="qm-ltr"><code>' . $value . '</code></p>'; // WPCS: XSS ok.
echo '</section>';
}
echo '</div>';
echo '<div class="qm-boxed">';
if ( ! empty( $data->matching_rewrites ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'All Matching Rewrite Rules', 'query-monitor' ) . '</h3>';
echo '<table>';
foreach ( $data->matching_rewrites as $rule => $query ) {
$query = str_replace( 'index.php?', '', $query );
echo '<tr>';
echo '<td class="qm-ltr"><code>' . esc_html( $rule ) . '</code></td>';
echo '<td class="qm-ltr"><code>';
echo self::format_url( $query ); // WPCS: XSS ok.
echo '</code></td>';
echo '</tr>';
}
echo '</table>';
echo '</section>';
}
echo '<section>';
echo '<h3>';
esc_html_e( 'Query Vars', 'query-monitor' );
echo '</h3>';
if ( $db_queries ) {
$db_queries_data = $db_queries->get_data();
if ( ! empty( $db_queries_data->wpdb->has_main_query ) ) {
echo '<p>';
echo self::build_filter_trigger( 'db_queries', 'caller', 'qm-main-query', esc_html__( 'View Main Query', 'query-monitor' ) ); // WPCS: XSS ok;
echo '</p>';
}
}
if ( ! empty( $data->qvars ) ) {
echo '<table>';
foreach ( $data->qvars as $var => $value ) {
echo '<tr>';
if ( isset( $data->plugin_qvars[ $var ] ) ) {
echo '<th scope="row" class="qm-ltr"><span class="qm-current">' . esc_html( $var ) . '</span></td>';
} else {
echo '<th scope="row" class="qm-ltr">' . esc_html( $var ) . '</td>';
}
if ( is_array( $value ) || is_object( $value ) ) {
echo '<td class="qm-ltr"><pre>';
echo esc_html( print_r( $value, true ) );
echo '</pre></td>';
} else {
echo '<td class="qm-ltr qm-wrap">' . esc_html( $value ) . '</td>';
}
echo '</tr>';
}
echo '</table>';
} else {
echo '<p><em>' . esc_html__( 'none', 'query-monitor' ) . '</em></p>';
}
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Response', 'query-monitor' ) . '</h3>';
echo '<h4>' . esc_html__( 'Queried Object', 'query-monitor' ) . '</h4>';
if ( ! empty( $data->queried_object ) ) {
$class = get_class( $data->queried_object['data'] );
$class = $class ?: __( 'Unknown', 'query-monitor' );
printf(
'<p>%1$s (%2$s)</p>',
esc_html( $data->queried_object['title'] ),
esc_html( $class )
);
} else {
echo '<p><em>' . esc_html__( 'none', 'query-monitor' ) . '</em></p>';
}
echo '<h4>' . esc_html__( 'Current User', 'query-monitor' ) . '</h4>';
if ( ! empty( $data->user['data'] ) ) {
printf( // WPCS: XSS ok.
'<p>%s</p>',
esc_html( $data->user['title'] )
);
} else {
echo '<p><em>' . esc_html__( 'none', 'query-monitor' ) . '</em></p>';
}
if ( ! empty( $data->multisite ) ) {
echo '<h4>' . esc_html__( 'Multisite', 'query-monitor' ) . '</h4>';
foreach ( $data->multisite as $var => $value ) {
printf( // WPCS: XSS ok.
'<p>%s</p>',
esc_html( $value['title'] )
);
}
}
echo '</section>';
if ( ! empty( $raw_request ) ) {
/** @var QM_Data_Raw_Request $raw_data */
$raw_data = $raw_request->get_data();
echo '<section>';
echo '<h3>' . esc_html__( 'Request Data', 'query-monitor' ) . '</h3>';
echo '<table>';
foreach ( array(
'ip' => __( 'Remote IP', 'query-monitor' ),
'method' => __( 'HTTP method', 'query-monitor' ),
'url' => __( 'Requested URL', 'query-monitor' ),
) as $item => $name ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $name ) . '</td>';
echo '<td class="qm-ltr qm-wrap">' . esc_html( $raw_data->request[ $item ] ) . '</td>';
echo '</tr>';
}
echo '</table>';
echo '</section>';
}
$this->after_non_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Request $data */
$data = $this->collector->get_data();
$count = isset( $data->plugin_qvars ) ? count( $data->plugin_qvars ) : 0;
$title = ( empty( $count ) )
? __( 'Request', 'query-monitor' )
/* translators: %s: Number of additional query variables */
: __( 'Request (+%s)', 'query-monitor' );
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
$title,
number_format_i18n( $count )
) ),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_request( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'request' );
if ( $collector ) {
$output['request'] = new QM_Output_Html_Request( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_request', 60, 2 );

View File

@ -0,0 +1,303 @@
<?php declare(strict_types = 1);
/**
* Template and theme output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Theme extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Theme Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 60 );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 60 );
}
/**
* @return string
*/
public function name() {
return __( 'Theme', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Theme $data */
$data = $this->collector->get_data();
if ( empty( $data->stylesheet ) ) {
return;
}
$this->before_non_tabular_output();
echo '<section>';
echo '<h3>' . esc_html__( 'Theme', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html( $data->stylesheet ) . '</p>';
if ( self::has_clickable_links() ) {
echo '<p>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::output_filename( 'style.css', sprintf( '%s/style.css', $data->theme_dirs[ $data->stylesheet ] ), 0, true );
echo '</p>';
if ( $data->stylesheet_theme_json ) {
echo '<p>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::output_filename( 'theme.json', $data->stylesheet_theme_json, 0, true );
echo '</p>';
}
}
if ( $data->is_child_theme ) {
echo '<h3>' . esc_html__( 'Parent Theme', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html( $data->template ) . '</p>';
if ( self::has_clickable_links() ) {
echo '<p>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::output_filename( 'style.css', sprintf( '%s/style.css', $data->theme_dirs[ $data->template ] ), 0, true );
echo '</p>';
if ( $data->template_theme_json ) {
echo '<p>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::output_filename( 'theme.json', $data->template_theme_json, 0, true );
echo '</p>';
}
}
}
echo '</section>';
echo '<section>';
if ( ! empty( $data->block_template ) ) {
echo '<h3>' . esc_html__( 'Block Template', 'query-monitor' ) . '</h3>';
if ( $data->block_template->wp_id ) {
echo '<p>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::build_link(
QM_Util::get_site_editor_url( $data->block_template->id, 'wp_template' ),
esc_html( $data->block_template->id )
);
echo '</p>';
} else {
if ( self::has_clickable_links() ) {
$file = sprintf(
'%s/%s/%s.html',
$data->theme_dirs[ $data->block_template->theme ],
$data->theme_folders[ $data->block_template->type ],
$data->block_template->slug
);
} else {
$file = '';
}
echo '<p class="qm-ltr">' . self::output_filename( sprintf(
'%s/%s.html',
$data->theme_folders[ $data->block_template->type ],
$data->block_template->slug
), $file, 0, true ) . '</p>'; // WPCS: XSS ok.
}
} else {
echo '<h3>' . esc_html__( 'Template File', 'query-monitor' ) . '</h3>';
if ( ! empty( $data->template_path ) ) {
if ( $data->is_child_theme ) {
$display = $data->theme_template_file;
} else {
$display = $data->template_file;
}
if ( self::has_clickable_links() ) {
$file = $data->template_path;
} else {
$file = '';
}
echo '<p class="qm-ltr">' . self::output_filename( $display, $file, 0, true ) . '</p>'; // WPCS: XSS ok.
} else {
echo '<p><em>' . esc_html__( 'Unknown', 'query-monitor' ) . '</em></p>';
}
}
if ( ! empty( $data->template_hierarchy ) ) {
echo '<h3>' . esc_html__( 'Template Hierarchy', 'query-monitor' ) . '</h3>';
echo '<ol class="qm-ltr"><li>' . implode( '</li><li>', array_map( 'esc_html', $data->template_hierarchy ) ) . '</li></ol>';
}
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Template Parts', 'query-monitor' ) . '</h3>';
if ( ! empty( $data->template_parts ) ) {
if ( $data->is_child_theme ) {
$parts = $data->theme_template_parts;
} else {
$parts = $data->template_parts;
}
echo '<ul class="qm-ltr">';
foreach ( $parts as $filename => $display ) {
echo '<li>';
if ( is_int( $filename ) ) {
echo self::build_link( QM_Util::get_site_editor_url( $display ), esc_html( $display ) ); // WPCS: XSS ok.
} elseif ( self::has_clickable_links() ) {
echo self::output_filename( $display, $filename, 0, true ); // WPCS: XSS ok.
} else {
echo esc_html( $display );
}
if ( $data->count_template_parts[ $filename ] > 1 ) {
$count = sprintf(
/* translators: %s: The number of times that a template part file was included in the page */
_nx( 'Included %s time', 'Included %s times', $data->count_template_parts[ $filename ], 'template parts', 'query-monitor' ),
esc_html( number_format_i18n( $data->count_template_parts[ $filename ] ) )
);
echo '<br><span class="qm-info qm-supplemental">' . esc_html( $count ) . '</span>';
}
echo '</li>';
}
echo '</ul>';
} else {
echo '<p><em>' . esc_html__( 'None', 'query-monitor' ) . '</em></p>';
}
if ( ! empty( $data->unsuccessful_template_parts ) ) {
echo '<h4>' . esc_html__( 'Not Loaded', 'query-monitor' ) . '</h4>';
echo '<ul>';
foreach ( $data->unsuccessful_template_parts as $requested ) {
if ( $requested['name'] ) {
echo '<li>';
$text = $requested['slug'] . '-' . $requested['name'] . '.php';
echo self::output_filename( $text, $requested['caller']['file'], $requested['caller']['line'], true ); // WPCS: XSS ok.
echo '</li>';
}
echo '<li>';
$text = $requested['slug'] . '.php';
echo self::output_filename( $text, $requested['caller']['file'], $requested['caller']['line'], true ); // WPCS: XSS ok.
echo '</li>';
}
echo '</ul>';
}
echo '</section>';
if ( ! empty( $data->timber_files ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Twig Template Files', 'query-monitor' ) . '</h3>';
echo '<ul class="qm-ltr">';
foreach ( $data->timber_files as $filename ) {
echo '<li>' . esc_html( $filename ) . '</li>';
}
echo '</ul>';
echo '</section>';
}
if ( ! empty( $data->body_class ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Body Classes', 'query-monitor' ) . '</h3>';
echo '<ul class="qm-ltr">';
foreach ( $data->body_class as $class ) {
echo '<li>' . esc_html( $class ) . '</li>';
}
echo '</ul>';
echo '</section>';
}
$this->after_non_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Theme $data */
$data = $this->collector->get_data();
if ( ! empty( $data->block_template ) ) {
if ( $data->block_template->wp_id ) {
$name = $data->block_template->id;
} else {
$name = sprintf(
'%s/%s.html',
$data->theme_folders[ $data->block_template->type ],
$data->block_template->slug
);
}
} elseif ( isset( $data->template_file ) ) {
$name = ( $data->is_child_theme ) ? $data->theme_template_file : $data->template_file;
} else {
$name = __( 'Unknown', 'query-monitor' );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
/* translators: %s: Template file name */
__( 'Template: %s', 'query-monitor' ),
$name
) ),
) );
return $menu;
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function panel_menu( array $menu ) {
if ( isset( $menu[ $this->collector->id() ] ) ) {
$menu[ $this->collector->id() ]['title'] = __( 'Template', 'query-monitor' );
}
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_theme( array $output, QM_Collectors $collectors ) {
if ( is_admin() ) {
return $output;
}
$collector = QM_Collectors::get( 'response' );
if ( $collector ) {
$output['response'] = new QM_Output_Html_Theme( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_theme', 70, 2 );

View File

@ -0,0 +1,244 @@
<?php declare(strict_types = 1);
/**
* Timing and profiling output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Timing extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Timing Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 46 );
}
/**
* @return string
*/
public function name() {
return __( 'Timing', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Timing $data */
$data = $this->collector->get_data();
if ( empty( $data->timing ) && empty( $data->warning ) ) {
$this->before_non_tabular_output();
$notice = sprintf(
/* translators: %s: Link to help article */
__( 'No data logged. <a href="%s">Read about timing and profiling in Query Monitor</a>.', 'query-monitor' ),
'https://querymonitor.com/blog/2018/07/profiling-and-logging/'
);
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
return;
}
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Tracked Function', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Started', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Stopped', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Time', 'query-monitor' ) . '</th>';
echo '<th scope="col" class="qm-num">' . esc_html__( 'Memory', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
if ( ! empty( $data->timing ) ) {
foreach ( $data->timing as $row ) {
$component = $row['component'];
$trace = $row['filtered_trace'];
$file = self::output_filename( $row['function'], $trace[0]['file'], $trace[0]['line'] );
echo '<tr>';
if ( self::has_clickable_links() ) {
echo '<td class="qm-ltr">';
echo $file; // WPCS: XSS ok.
echo '</td>';
} else {
echo '<td class="qm-ltr qm-has-toggle">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ol>';
echo '<li>';
echo $file; // WPCS: XSS ok.
echo '</li>';
echo '</ol></td>';
}
printf(
'<td class="qm-num">%s</td>',
esc_html( number_format_i18n( $row['start_time'], 4 ) )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( number_format_i18n( $row['end_time'], 4 ) )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( number_format_i18n( $row['function_time'], 4 ) )
);
$mem = sprintf(
/* translators: %s: Approximate memory used in kilobytes */
__( '~%s kB', 'query-monitor' ),
number_format_i18n( $row['function_memory'] / 1024 )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $mem )
);
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
echo '</tr>';
if ( ! empty( $row['laps'] ) ) {
foreach ( $row['laps'] as $lap_id => $lap ) {
echo '<tr>';
echo '<td class="qm-ltr"><code>&mdash;&nbsp;';
echo esc_html( $row['function'] . ': ' . $lap_id );
echo '</code></td>';
echo '<td class="qm-num"></td>';
echo '<td class="qm-num"></td>';
printf(
'<td class="qm-num">%s</td>',
esc_html( number_format_i18n( $lap['time_used'], 4 ) )
);
$mem = sprintf(
/* translators: %s: Approximate memory used in kilobytes */
__( '~%s kB', 'query-monitor' ),
number_format_i18n( $lap['memory_used'] / 1024 )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $mem )
);
echo '<td class="qm-nowrap"></td>';
echo '</tr>';
}
}
}
}
if ( ! empty( $data->warning ) ) {
foreach ( $data->warning as $row ) {
$component = $row['component'];
$trace = $row['filtered_trace'];
$file = self::output_filename( $row['function'], $trace[0]['file'], $trace[0]['line'] );
echo '<tr class="qm-warn">';
if ( self::has_clickable_links() ) {
echo '<td class="qm-ltr">';
echo $file; // WPCS: XSS ok.
echo '</td>';
} else {
echo '<td class="qm-ltr qm-has-toggle">';
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ol>';
echo '<li>';
echo $file; // WPCS: XSS ok.
echo '</li>';
echo '</ol></td>';
}
printf(
'<td colspan="4">%1$s%2$s</td>',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
QueryMonitor::icon( 'warning' ),
esc_html( $row['message'] )
);
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
}
}
echo '</tbody>';
$this->after_tabular_output();
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Timing $data */
$data = $this->collector->get_data();
$count = 0;
if ( ! empty( $data->timing ) || ! empty( $data->warning ) ) {
if ( ! empty( $data->timing ) ) {
$count += count( $data->timing );
}
if ( ! empty( $data->warning ) ) {
$count += count( $data->warning );
}
/* translators: %s: Number of function timing results that are available */
$label = __( 'Timings (%s)', 'query-monitor' );
} else {
$label = __( 'Timings', 'query-monitor' );
}
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
$label,
number_format_i18n( $count )
) ),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_timing( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'timing' );
if ( $collector ) {
$output['timing'] = new QM_Output_Html_Timing( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_timing', 15, 2 );

View File

@ -0,0 +1,175 @@
<?php declare(strict_types = 1);
/**
* Transient storage output for HTML pages.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Output_Html_Transients extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_Transients Collector.
*/
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/menus', array( $this, 'admin_menu' ), 100 );
}
/**
* @return string
*/
public function name() {
return __( 'Transients', 'query-monitor' );
}
/**
* @return void
*/
public function output() {
/** @var QM_Data_Transients $data */
$data = $this->collector->get_data();
if ( ! empty( $data->trans ) ) {
$this->before_tabular_output();
echo '<thead>';
echo '<tr>';
echo '<th scope="col">' . esc_html__( 'Updated Transient', 'query-monitor' ) . '</th>';
if ( $data->has_type ) {
echo '<th scope="col">' . esc_html_x( 'Type', 'transient type', 'query-monitor' ) . '</th>';
}
echo '<th scope="col">' . esc_html__( 'Expiration', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html_x( 'Size', 'size of transient value', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Caller', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Component', 'query-monitor' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $data->trans as $row ) {
$component = $row['component'];
echo '<tr>';
printf(
'<td class="qm-ltr"><code>%s</code></td>',
esc_html( $row['name'] )
);
if ( $data->has_type ) {
printf(
'<td class="qm-ltr qm-nowrap">%s</td>',
esc_html( $row['type'] )
);
}
if ( 0 === $row['expiration'] ) {
printf(
'<td class="qm-nowrap"><em>%s</em></td>',
esc_html__( 'none', 'query-monitor' )
);
} else {
printf(
'<td class="qm-nowrap">%s <span class="qm-info">(~%s)</span></td>',
esc_html( (string) $row['expiration'] ),
esc_html( $row['exp_diff'] )
);
}
printf(
'<td class="qm-nowrap">~%s</td>',
esc_html( $row['size_formatted'] )
);
$stack = array();
foreach ( $row['filtered_trace'] as $frame ) {
$stack[] = self::output_filename( $frame['display'], $frame['calling_file'], $frame['calling_line'] );
}
$caller = array_shift( $stack );
echo '<td class="qm-has-toggle qm-nowrap qm-ltr">';
if ( ! empty( $stack ) ) {
echo self::build_toggler(); // WPCS: XSS ok;
}
echo '<ol>';
echo "<li>{$caller}</li>"; // WPCS: XSS ok.
if ( ! empty( $stack ) ) {
echo '<div class="qm-toggled"><li>' . implode( '</li><li>', $stack ) . '</li></div>'; // WPCS: XSS ok.
}
echo '</ol></td>';
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
echo '</tr>';
}
$this->after_tabular_output();
} else {
$this->before_non_tabular_output();
$notice = __( 'No transients set.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
}
/**
* @param array<string, mixed[]> $menu
* @return array<string, mixed[]>
*/
public function admin_menu( array $menu ) {
/** @var QM_Data_Transients $data */
$data = $this->collector->get_data();
$count = count( $data->trans );
$title = ( empty( $count ) )
? __( 'Transient Updates', 'query-monitor' )
/* translators: %s: Number of transient values that were updated */
: __( 'Transient Updates (%s)', 'query-monitor' );
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
$title,
number_format_i18n( $count )
) ),
) );
return $menu;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_html_transients( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'transients' );
if ( $collector ) {
$output['transients'] = new QM_Output_Html_Transients( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_html_transients', 100, 2 );

View File

@ -0,0 +1,60 @@
<?php declare(strict_types = 1);
/**
* Raw cache output.
*
* @package query-monitor
*/
class QM_Output_Raw_Cache extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_Cache Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Object Cache', 'query-monitor' );
}
/**
* @return array<string, mixed>
*/
public function get_output() {
$output = array(
'hit_percentage' => null,
'hits' => null,
'misses' => null,
);
/** @var QM_Data_Cache $data */
$data = $this->collector->get_data();
if ( ! empty( $data->stats ) && ! empty( $data->cache_hit_percentage ) ) {
$output['hit_percentage'] = round( $data->cache_hit_percentage, 1 );
$output['hits'] = (int) $data->stats['cache_hits'];
$output['misses'] = (int) $data->stats['cache_misses'];
}
return $output;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_cache( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'cache' );
if ( $collector ) {
$output['cache'] = new QM_Output_Raw_Cache( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_cache', 30, 2 );

View File

@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
/**
* Raw conditionals output.
*
* @package query-monitor
*/
class QM_Output_Raw_Conditionals extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_Conditionals Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Conditionals', 'query-monitor' );
}
/**
* @return mixed
*/
public function get_output() {
$data = $this->collector->get_data();
return $data->conds['true'];
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_conditionals( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'conditionals' );
if ( $collector ) {
$output['conditionals'] = new QM_Output_Raw_Conditionals( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_conditionals', 20, 2 );

View File

@ -0,0 +1,139 @@
<?php declare(strict_types = 1);
/**
* Raw database query output.
*
* @package query-monitor
*/
class QM_Output_Raw_DB_Queries extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_DB_Queries Collector.
*/
protected $collector;
/**
* @var int
*/
public $query_row = 0;
/**
* @return string
*/
public function name() {
return __( 'Database Queries', 'query-monitor' );
}
/**
* @return array<string, mixed>
*/
public function get_output() {
$output = array();
/** @var QM_Data_DB_Queries $data */
$data = $this->collector->get_data();
if ( empty( $data->wpdb ) ) {
return $output;
}
$output['wpdb'] = $this->output_queries( $data->wpdb );
if ( ! empty( $data->errors ) ) {
$output['errors'] = array(
'total' => count( $data->errors ),
'errors' => $data->errors,
);
}
if ( ! empty( $data->dupes ) ) {
$dupes = $data->dupes;
// Filter out SQL queries that do not have dupes
$dupes = array_filter( $dupes, array( $this->collector, 'filter_dupe_items' ) );
// Ignore dupes from `WP_Query->set_found_posts()`
unset( $dupes['SELECT FOUND_ROWS()'] );
$output['dupes'] = array(
'total' => count( $dupes ),
'queries' => $dupes,
);
}
return $output;
}
/**
* @param stdClass $db
* @return array
* @phpstan-return array{
* total: int,
* time: float,
* queries: mixed[],
* }|array{}
*/
protected function output_queries( stdClass $db ) {
$this->query_row = 0;
$output = array();
if ( empty( $db->rows ) ) {
return $output;
}
foreach ( $db->rows as $row ) {
$output[] = $this->output_query_row( $row );
}
return array(
'total' => $db->total_qs,
'time' => round( $db->total_time, 4 ),
'queries' => $output,
);
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
protected function output_query_row( array $row ) {
$output = array();
$output['i'] = ++$this->query_row;
$output['sql'] = $row['sql'];
$output['time'] = round( $row['ltime'], 4 );
if ( isset( $row['trace'] ) ) {
$stack = array();
$filtered_trace = $row['trace']->get_filtered_trace();
foreach ( $filtered_trace as $item ) {
$stack[] = $item['display'];
}
} else {
$stack = $row['stack'];
}
$output['stack'] = $stack;
$output['result'] = $row['result'];
return $output;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_db_queries( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'db_queries' );
if ( $collector ) {
$output['db_queries'] = new QM_Output_Raw_DB_Queries( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_db_queries', 20, 2 );

View File

@ -0,0 +1,75 @@
<?php declare(strict_types = 1);
/**
* Raw HTTP API request output.
*
* @package query-monitor
*/
class QM_Output_Raw_HTTP extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_HTTP Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'HTTP API Calls', 'query-monitor' );
}
/**
* @return array<string, mixed>
*/
public function get_output() {
$output = array();
/** @var QM_Data_HTTP $data */
$data = $this->collector->get_data();
if ( empty( $data->http ) ) {
return $output;
}
$requests = array();
foreach ( $data->http as $http ) {
$stack = array();
foreach ( $http['filtered_trace'] as $item ) {
$stack[] = $item['display'];
}
$requests[] = array(
'url' => $http['url'],
'method' => $http['args']['method'],
'response' => ( $http['response'] instanceof WP_Error ) ? $http['response']->get_error_message() : $http['response']['response'],
'time' => round( $http['ltime'], 4 ),
'stack' => $stack,
);
}
$output['total'] = count( $requests );
$output['time'] = round( $data->ltime, 4 );
$output['requests'] = $requests;
return $output;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_http( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'http' );
if ( $collector ) {
$output['http'] = new QM_Output_Raw_HTTP( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_http', 30, 2 );

View File

@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
/**
* Raw logger output.
*
* @package query-monitor
*/
class QM_Output_Raw_Logger extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_Logger Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Logs', 'query-monitor' );
}
/**
* @return array<string, array<int, array<string, mixed>>>
* @phpstan-return array<QM_Collector_Logger::*, list<array{
* message: string,
* stack: list<string>,
* }>>
*/
public function get_output() {
$output = array();
/** @var QM_Data_Logger $data */
$data = $this->collector->get_data();
if ( empty( $data->logs ) ) {
return $output;
}
foreach ( $data->logs as $log ) {
$output[ $log['level'] ][] = array(
'message' => $log['message'],
'stack' => array_column( $log['filtered_trace'], 'display' ),
);
}
return $output;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_logger( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'logger' );
if ( $collector ) {
$output['logger'] = new QM_Output_Raw_Logger( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_logger', 30, 2 );

View File

@ -0,0 +1,73 @@
<?php declare(strict_types = 1);
/**
* Raw transients output.
*
* @package query-monitor
*/
class QM_Output_Raw_Transients extends QM_Output_Raw {
/**
* Collector instance.
*
* @var QM_Collector_Transients Collector.
*/
protected $collector;
/**
* @return string
*/
public function name() {
return __( 'Transients', 'query-monitor' );
}
/**
* @return array<string, mixed>
*/
public function get_output() {
$output = array();
$data = $this->collector->get_data();
if ( empty( $data->trans ) ) {
return $output;
}
$transients = array();
foreach ( $data->trans as $transient ) {
$stack = array();
foreach ( $transient['filtered_trace'] as $frame ) {
$stack[] = $frame['display'];
}
$transients[] = array(
'name' => $transient['name'],
'type' => $transient['type'],
'size' => $transient['size_formatted'],
'expiration' => $transient['expiration'],
'stack' => $stack,
);
}
$output['total'] = count( $transients );
$output['transients'] = $transients;
return $output;
}
}
/**
* @param array<string, QM_Output> $output
* @param QM_Collectors $collectors
* @return array<string, QM_Output>
*/
function register_qm_output_raw_transients( array $output, QM_Collectors $collectors ) {
$collector = QM_Collectors::get( 'transients' );
if ( $collector ) {
$output['transients'] = new QM_Output_Raw_Transients( $collector );
}
return $output;
}
add_filter( 'qm/outputter/raw', 'register_qm_output_raw_transients', 30, 2 );