initial
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Boot Query Monitor from the SQLite Database Integration plugin.
|
||||
*
|
||||
* When the Query Monitor plugin exists in its standard location, let's check
|
||||
* if it is active, so we can boot it eagerly. This is a workaround to avoid
|
||||
* SQLite and Query Monitor competing for the "wp-content/db.php" file.
|
||||
*
|
||||
* This file is a modified version of the original Query Monitor "db.php" file.
|
||||
*
|
||||
* See: https://github.com/johnbillion/query-monitor/blob/develop/wp-content/db.php
|
||||
*/
|
||||
|
||||
/*
|
||||
* In Playground, the SQLite plugin is preloaded without using the "db.php" file.
|
||||
* To prevent Query Monitor from injecting its own "db.php" file, we need to set
|
||||
* the "QM_DB_SYMLINK" constant to "false".
|
||||
*/
|
||||
if ( ! defined( 'QM_DB_SYMLINK' ) ) {
|
||||
define( 'QM_DB_SYMLINK', false );
|
||||
}
|
||||
|
||||
// Check if we should load Query Monitor (as per the original "db.php" file).
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! defined( 'DB_USER' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'QM_DISABLED' ) && QM_DISABLED ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'cli' === php_sapi_name() && ! defined( 'QM_TESTS' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_admin() ) {
|
||||
if ( isset( $_GET['action'] ) && 'upgrade-plugin' === $_GET['action'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_POST['action'] ) && 'update-plugin' === $_POST['action'] ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Register SQLite enhancements for Query Monitor when plugins are loaded.
|
||||
*
|
||||
* This will also ensure that the plugin Query Monitor is fully initialized even
|
||||
* when we can't load it eagerly, e.g. on a multisite install.
|
||||
*/
|
||||
function register_sqlite_enhancements_for_query_monitor() {
|
||||
if ( ! class_exists( 'QM_Backtrace' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'QM_VERSION' ) && version_compare( QM_VERSION, '4.0.0', '>=' ) ) {
|
||||
require_once __DIR__ . '/qm4.php';
|
||||
} else {
|
||||
require_once __DIR__ . '/qm3.php';
|
||||
}
|
||||
|
||||
if ( ! defined( 'SQLITE_QUERY_MONITOR_LOADED' ) ) {
|
||||
define( 'SQLITE_QUERY_MONITOR_LOADED', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( function_exists( 'add_action' ) ) {
|
||||
add_action( 'plugins_loaded', 'register_sqlite_enhancements_for_query_monitor' );
|
||||
}
|
||||
|
||||
/*
|
||||
* On a multisite install, we can't easily determine the current site eagerly.
|
||||
* Therefore, let's bail out and let Query Monitor activate later as a plugin.
|
||||
*/
|
||||
if ( is_multisite() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Now, let's try to load Query Monitor eagerly, to start logging queries early.
|
||||
* When the plugin is installed, we will check the database if it is also active.
|
||||
*/
|
||||
global $wpdb;
|
||||
if ( ! isset( $wpdb ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Query Monitor is installed.
|
||||
if ( defined( 'WP_PLUGIN_DIR' ) ) {
|
||||
$plugins_dir = WP_PLUGIN_DIR;
|
||||
} else {
|
||||
$plugins_dir = WP_CONTENT_DIR . '/plugins';
|
||||
}
|
||||
|
||||
$qm_dir = "{$plugins_dir}/query-monitor";
|
||||
$qm_php = "{$qm_dir}/classes/PHP.php";
|
||||
|
||||
if ( ! is_readable( $qm_php ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Query Monitor is active.
|
||||
if ( null === $wpdb->options ) {
|
||||
global $table_prefix;
|
||||
$wpdb->set_prefix( $table_prefix ?? '' );
|
||||
}
|
||||
|
||||
$query_monitor_active = false;
|
||||
try {
|
||||
// Make sure no errors are displayed when the query fails.
|
||||
$show_errors = $wpdb->hide_errors();
|
||||
$value = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
|
||||
'active_plugins'
|
||||
)
|
||||
);
|
||||
$wpdb->show_errors( $show_errors );
|
||||
|
||||
if ( null !== $value ) {
|
||||
$query_monitor_active = in_array(
|
||||
'query-monitor/query-monitor.php',
|
||||
unserialize( $value->option_value ),
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch ( Throwable $e ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $query_monitor_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load Query Monitor eagerly (as per the original "db.php" file).
|
||||
require_once $qm_php;
|
||||
|
||||
if ( ! QM_PHP::version_met() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! file_exists( "{$qm_dir}/vendor/autoload.php" ) ) {
|
||||
add_action( 'all_admin_notices', 'QM_PHP::vendor_nope' );
|
||||
return;
|
||||
}
|
||||
|
||||
require_once "{$qm_dir}/vendor/autoload.php";
|
||||
|
||||
if ( ! class_exists( 'QM_Backtrace' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! defined( 'SAVEQUERIES' ) ) {
|
||||
define( 'SAVEQUERIES', true );
|
||||
}
|
||||
|
||||
// Mark the Query Monitor integration as loaded.
|
||||
define( 'SQLITE_QUERY_MONITOR_LOADED', true );
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
if ( ! class_exists( 'QM_Output_Html_DB_Queries' ) || ! class_exists( 'QM_Collectors' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override Query Monitor's "QM_Output_Html_DB_Queries" to inject SQLite info.
|
||||
*/
|
||||
class SQLite_QM_Output_Html_DB_Queries extends QM_Output_Html_DB_Queries {
|
||||
/**
|
||||
* Override the parent method to inject SQLite info.
|
||||
*
|
||||
* Currently, Query Monitor doesn't provide a way to customize the rendered
|
||||
* query HTML. To overcome this limitation, we capture the HTML generated by
|
||||
* the parent method and modify it to inject the SQLite query data.
|
||||
*
|
||||
* @param array<string, mixed> $row The row data.
|
||||
* @param array<int, string> $cols The column names.
|
||||
* @return void
|
||||
*/
|
||||
protected function output_query_row( array $row, array $cols ) {
|
||||
// Capture the query row HTML.
|
||||
ob_start();
|
||||
parent::output_query_row( $row, $cols );
|
||||
$data = ob_get_length() > 0 ? ob_get_clean() : '';
|
||||
|
||||
// Get the corresponding SQLite queries.
|
||||
global $wpdb;
|
||||
static $query_index = 0;
|
||||
$sqlite_queries = $wpdb->queries[ $query_index ]['sqlite_queries'] ?? array();
|
||||
$sqlite_query_count = count( $sqlite_queries );
|
||||
$query_index += 1;
|
||||
|
||||
// Build the SQLite info HTML.
|
||||
$sqlite_info = sprintf(
|
||||
'<div class="qm-info" style="margin: 15px 0 8px;">Executed %d SQLite %s:</div>',
|
||||
$sqlite_query_count,
|
||||
1 === $sqlite_query_count ? 'Query' : 'Queries'
|
||||
);
|
||||
$sqlite_info .= '<ol>';
|
||||
foreach ( $sqlite_queries as $query ) {
|
||||
$sqlite_info .= '<li class="qm-sqlite-query" style="list-style: decimal !important; margin-left: 20px !important;">';
|
||||
$sqlite_info .= '<code>' . str_replace( '<br>', '', self::format_sql( $query['sql'] ) ) . '</code>';
|
||||
$sqlite_info .= '</li>';
|
||||
}
|
||||
$sqlite_info .= '</ol>';
|
||||
|
||||
// Inject toggle button and SQLite info into the query row HTML.
|
||||
$toggle_button = '<button class="qm-toggle sqlite-toggle" data-on="+" data-off="-" aria-expanded="false" aria-label="Toggle SQLite queries" title="Toggle SQLite queries"><span aria-hidden="true">+</span></button>';
|
||||
$toggle_content = sprintf( '<div class="qm-toggled" style="display: none;">%s</div>', $sqlite_info );
|
||||
|
||||
$data = str_replace( 'qm-row-sql', 'qm-row-sql qm-has-toggle', $data );
|
||||
$data = preg_replace(
|
||||
'/(<td class="qm-row-sql.*?">)(.*?)(<\/td>)/s',
|
||||
implode(
|
||||
array(
|
||||
'$1',
|
||||
str_replace( '$', '\\$', $toggle_button ),
|
||||
'$2',
|
||||
str_replace( '$', '\\$', $toggle_content ),
|
||||
'$3',
|
||||
)
|
||||
),
|
||||
$data
|
||||
);
|
||||
echo $data;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the default Query Monitor class and replace it with the custom one.
|
||||
remove_filter( 'qm/outputter/html', 'register_qm_output_html_db_queries', 20 );
|
||||
|
||||
/**
|
||||
* Register the custom HTML output class.
|
||||
*
|
||||
* @param array<string, QM_Output> $output
|
||||
* @param QM_Collectors $collectors
|
||||
* @return array<string, QM_Output>
|
||||
*/
|
||||
function register_sqlite_qm_output_html_db_queries( array $output, $collectors ) {
|
||||
$collector = QM_Collectors::get( 'db_queries' );
|
||||
if ( $collector ) {
|
||||
$output['db_queries'] = new SQLite_QM_Output_Html_DB_Queries( $collector );
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
add_filter( 'qm/outputter/html', 'register_sqlite_qm_output_html_db_queries', 20, 2 );
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName, Generic.Files.OneObjectStructurePerFile.MultipleFound
|
||||
|
||||
if ( ! class_exists( 'QM_Collector' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data transfer object for SQLite query data.
|
||||
*/
|
||||
class SQLite_QM_Data extends QM_Data {
|
||||
/**
|
||||
* SQLite queries indexed by normalized MySQL SQL text.
|
||||
*
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
public $queries = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collector for SQLite query data.
|
||||
*
|
||||
* Extracts SQLite queries from $wpdb->queries and stores them
|
||||
* indexed by SQL text for the QM 4.0+ JS integration.
|
||||
*/
|
||||
class SQLite_QM_Collector extends QM_Collector {
|
||||
/** @var string */
|
||||
public $id = 'sqlite';
|
||||
|
||||
public function get_storage(): QM_Data {
|
||||
return new SQLite_QM_Data();
|
||||
}
|
||||
|
||||
public function process(): void {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $wpdb->queries ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Index by SQL rather than row position — robust to filtering, sorting, etc.
|
||||
$mapped = array();
|
||||
foreach ( $wpdb->queries as $query ) {
|
||||
// Query Monitor skips queries with 'wp_admin_bar' in the stack.
|
||||
if ( false !== strpos( $query[2] ?? '', 'wp_admin_bar' ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! empty( $query['sqlite_queries'] ) ) {
|
||||
$sql = trim( preg_replace( '/\s+/', ' ', $query[0] ) );
|
||||
$mapped[ $sql ] = array_column( $query['sqlite_queries'], 'sql' );
|
||||
}
|
||||
}
|
||||
$this->data->queries = $mapped;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML outputter for SQLite query data.
|
||||
*
|
||||
* With $client_side_rendered = true, QM auto-serializes the collector data
|
||||
* into "window.QueryMonitorData.data.sqlite". This outputter's only job is to
|
||||
* emit the inline JS module that reads that data and injects SQLite query
|
||||
* details into QM 4.0's shadow DOM DB queries panel.
|
||||
*/
|
||||
class SQLite_QM_Output_Html extends QM_Output_Html {
|
||||
/** @var bool */
|
||||
public static $client_side_rendered = true;
|
||||
|
||||
public function name(): string {
|
||||
return 'SQLite';
|
||||
}
|
||||
|
||||
public function output(): void {
|
||||
if ( empty( $this->get_collector()->get_data()->queries ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$js_path = __DIR__ . '/query-monitor-sqlite.js';
|
||||
if ( is_readable( $js_path ) ) {
|
||||
echo '<script type="module">';
|
||||
include $js_path;
|
||||
echo '</script>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the SQLite collector.
|
||||
*/
|
||||
function register_sqlite_qm_collector( array $collectors ): array {
|
||||
$collectors['sqlite'] = new SQLite_QM_Collector();
|
||||
return $collectors;
|
||||
}
|
||||
|
||||
add_filter( 'qm/collectors', 'register_sqlite_qm_collector', 20 );
|
||||
|
||||
/**
|
||||
* Register the SQLite HTML outputter.
|
||||
*/
|
||||
function register_sqlite_qm_output_html( array $output, QM_Collectors $collectors ): array {
|
||||
$collector = QM_Collectors::get( 'sqlite' );
|
||||
if ( $collector ) {
|
||||
$output['sqlite'] = new SQLite_QM_Output_Html( $collector );
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
add_filter( 'qm/outputter/html', 'register_sqlite_qm_output_html', 30, 2 );
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
const STYLE = `
|
||||
details.qm-sqlite {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
details.qm-sqlite summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
details.qm-sqlite ol {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 24px;
|
||||
list-style: decimal;
|
||||
}
|
||||
`;
|
||||
|
||||
const container = document.getElementById( 'query-monitor-container' );
|
||||
const sqliteData = window.QueryMonitorData?.data?.sqlite?.data?.queries;
|
||||
|
||||
if ( container && sqliteData ) {
|
||||
// QM attaches the shadow root in its own DOMContentLoaded listener.
|
||||
// Our module is loaded after QM's, so our listener fires after QM's.
|
||||
document.addEventListener( 'DOMContentLoaded', () => {
|
||||
const shadowRoot = container.shadowRoot;
|
||||
if ( ! shadowRoot ) {
|
||||
return;
|
||||
}
|
||||
inject( shadowRoot, sqliteData );
|
||||
|
||||
// Re-inject after Preact re-renders (panel switches, filters, etc.).
|
||||
// Debounced to avoid excessive work during rapid DOM updates.
|
||||
let timer;
|
||||
new MutationObserver( () => {
|
||||
clearTimeout( timer );
|
||||
timer = setTimeout( () => inject( shadowRoot, sqliteData ), 100 );
|
||||
} ).observe( shadowRoot, { childList: true, subtree: true } );
|
||||
} );
|
||||
}
|
||||
|
||||
function inject( shadowRoot, data ) {
|
||||
const panel = shadowRoot.getElementById( 'qm-db_queries' );
|
||||
if ( ! panel ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! shadowRoot.querySelector( 'style.qm-sqlite-style' ) ) {
|
||||
const style = document.createElement( 'style' );
|
||||
style.className = 'qm-sqlite-style';
|
||||
style.textContent = STYLE;
|
||||
shadowRoot.appendChild( style );
|
||||
}
|
||||
|
||||
// Match by SQL rather than row position — robust to filtering, sorting, etc.
|
||||
for ( const code of panel.querySelectorAll( 'td.qm-cell-sql > code' ) ) {
|
||||
const cell = code.parentElement;
|
||||
const key = code.innerText.replace( /\s+/g, ' ' ).trim();
|
||||
const queries = data[ key ];
|
||||
const existing = cell.querySelector( 'details.qm-sqlite' );
|
||||
|
||||
// Preact may recycle DOM nodes on filter/sort, leaving stale details
|
||||
// from a previous query. Remove them when the SQL key no longer matches.
|
||||
if ( existing ) {
|
||||
if ( queries?.length && existing.dataset.sqliteKey === key ) {
|
||||
continue;
|
||||
}
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
if ( queries?.length ) {
|
||||
cell.append( buildDetails( key, queries ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildDetails( key, queries ) {
|
||||
const details = document.createElement( 'details' );
|
||||
details.className = 'qm-sqlite';
|
||||
details.dataset.sqliteKey = key;
|
||||
// Prevent QM's row click handlers from firing when toggling.
|
||||
details.addEventListener( 'click', ( e ) => e.stopPropagation() );
|
||||
|
||||
const summary = document.createElement( 'summary' );
|
||||
summary.textContent = `Executed ${ queries.length } SQLite ${ queries.length === 1 ? 'Query' : 'Queries' }`;
|
||||
|
||||
const ol = document.createElement( 'ol' );
|
||||
for ( const sql of queries ) {
|
||||
const li = document.createElement( 'li' );
|
||||
li.className = 'qm-sqlite-query';
|
||||
const code = document.createElement( 'code' );
|
||||
code.textContent = sql;
|
||||
li.append( code );
|
||||
ol.append( li );
|
||||
}
|
||||
|
||||
details.append( summary, ol );
|
||||
return details;
|
||||
}
|
||||
Reference in New Issue
Block a user