Initial commit

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

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{{description}}
Copyright (C) {{year}} {{fullname}}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,736 @@
/**
* Front-end functionality for Query Monitor.
*
* @package query-monitor
*/
var QM_i18n = {
// http://core.trac.wordpress.org/ticket/20491
number_format : function( number, decimals ) {
if ( isNaN( number ) ) {
return;
}
if ( ! decimals ) {
decimals = 0;
}
number = parseFloat( number );
var num_float = number.toFixed( decimals ),
num_int = Math.floor( number ),
num_str = num_int.toString(),
fraction = num_float.substring( num_float.indexOf( '.' ) + 1, num_float.length ),
o = '';
if ( num_str.length > 3 ) {
for ( i = num_str.length; i > 3; i -= 3 ) {
o = qm_number_format.thousands_sep + num_str.slice( i - 3, i ) + o;
}
o = num_str.slice( 0, i ) + o;
} else {
o = num_str;
}
if ( decimals ) {
o = o + qm_number_format.decimal_point + fraction;
}
return o;
}
};
if ( window.jQuery ) {
jQuery( function($) {
var toolbarHeight = $('#wpadminbar').outerHeight();
var minheight = 100;
var maxheight = ( $(window).height() - toolbarHeight );
var minwidth = 300;
var maxwidth = $(window).width();
var container = $('#query-monitor-main');
var body = $('body');
var body_margin = body.css('margin-bottom');
var container_height_key = 'qm-container-height';
var container_pinned_key = 'qm-' + ( $('body').hasClass('wp-admin') ? 'admin' : 'front' ) + '-container-pinned';
var container_position_key = 'qm-container-position';
var container_width_key = 'qm-container-width';
if ( container.hasClass('qm-peek') ) {
minheight = 27;
}
container.removeClass('qm-no-js').addClass('qm-js');
if ( $('#qm-fatal').length ) {
console.error(qm_l10n.fatal_error + ': ' + $('#qm-fatal').attr('data-qm-message') );
if ( $('#wp-admin-bar-query-monitor').length ) {
$('#wp-admin-bar-query-monitor')
.addClass('qm-error')
.find('a').eq(0)
.text(qm_l10n.fatal_error);
var fatal_container = document.createDocumentFragment();
var fatal_message_menu = $('#wp-admin-bar-query-monitor-placeholder')
.clone()
.attr('id','wp-admin-bar-qm-fatal-message');
fatal_message_menu
.find('a').eq(0)
.text($('#qm-fatal').attr('data-qm-message'))
.attr('href','#qm-fatal');
fatal_container.appendChild( fatal_message_menu.get(0) );
var fatal_file_menu = $('#wp-admin-bar-query-monitor-placeholder')
.clone()
.attr('id','wp-admin-bar-qm-fatal-file');
fatal_file_menu
.find('a').eq(0)
.text($('#qm-fatal').attr('data-qm-file') + ':' + $('#qm-fatal').attr('data-qm-line'))
.attr('href','#qm-fatal');
fatal_container.appendChild( fatal_file_menu.get(0) );
$('#wp-admin-bar-query-monitor ul').append(fatal_container);
}
}
var link_click = function(e){
var href = $( this ).attr('href') || $( this ).data('qm-href');
if ( '#qm-fatal' === href ) {
return;
}
show_panel( href );
$(href).focus();
$('#wp-admin-bar-query-monitor').removeClass('hover');
e.preventDefault();
};
var stripes = function( table ) {
table.each(function() {
$(this).find('tbody tr').removeClass('qm-odd').not('[class*="qm-hide-"]').filter(':even').addClass('qm-odd');
} );
};
var show_panel = function( panel ) {
container.addClass('qm-show').removeClass('qm-hide');
$( '.qm' ).removeClass('qm-panel-show');
$('#qm-panels').scrollTop(0);
$( panel ).addClass('qm-panel-show');
if ( container.height() < minheight ) {
container.height( minheight );
}
if ( container.hasClass('qm-show-right') ) {
body.css( 'margin-bottom', '' );
} else {
body.css( 'margin-bottom', 'calc( ' + body_margin + ' + ' + container.height() + 'px )' );
}
$('#qm-panel-menu').find('button').removeAttr('aria-selected');
$('#qm-panel-menu').find('li').removeClass('qm-current-menu');
var selected_menu = $('#qm-panel-menu').find('[data-qm-href="' + panel + '"]').attr('aria-selected',true);
if ( selected_menu.length ) {
var selected_menu_top = selected_menu.position().top - 27;
var menu_height = $('#qm-panel-menu').height();
var menu_scroll = $('#qm-panel-menu').scrollTop();
selected_menu.closest('#qm-panel-menu > ul > li').addClass('qm-current-menu');
var selected_menu_off_bottom = ( selected_menu_top > ( menu_height ) );
var selected_menu_off_top = ( selected_menu_top < 0 );
if ( selected_menu_off_bottom || selected_menu_off_top ) {
$('#qm-panel-menu').scrollTop( selected_menu_top + menu_scroll - ( menu_height / 2 ) + ( selected_menu.outerHeight() / 2 ) );
}
}
$('.qm-title-heading select').val(panel);
localStorage.setItem( container_pinned_key, panel );
var filters = $( panel ).find('.qm-filter');
if ( filters.length ) {
filters.change();
} else {
stripes( $(panel).find('table') );
}
};
if ( $('#wp-admin-bar-query-monitor').length ) {
var admin_bar_menu_container = document.createDocumentFragment();
if ( window.qm && window.qm.menu ) {
$('#wp-admin-bar-query-monitor')
.addClass(qm.menu.top.classname)
.attr('dir','ltr')
.find('a').eq(0)
.html(qm.menu.top.title);
$.each( qm.menu.sub, function( i, el ) {
var new_menu = $('#wp-admin-bar-query-monitor-placeholder')
.clone()
.attr('id','wp-admin-bar-' + el.id);
new_menu
.find('a').eq(0)
.html(el.title)
.attr('href',el.href);
if ( ( typeof el.meta != 'undefined' ) && ( typeof el.meta.classname != 'undefined' ) ) {
new_menu.addClass(el.meta.classname);
}
admin_bar_menu_container.appendChild( new_menu.get(0) );
} );
$('#wp-admin-bar-query-monitor ul').append(admin_bar_menu_container);
}
$('#wp-admin-bar-query-monitor').find('a').on('click',link_click);
$('#wp-admin-bar-query-monitor,#wp-admin-bar-query-monitor-default').show();
} else {
container.addClass('qm-peek').removeClass('qm-hide');
$('#qm-overview').addClass('qm-panel-show');
}
$('#qm-panel-menu').find('button').on('click',link_click);
container.find('.qm-filter').on('change',function(e){
var filter = $(this).attr('data-filter'),
table = $(this).closest('table'),
tr = table.find('tbody tr[data-qm-' + filter + ']'),
// Escape the following chars with a backslash before passing into jQ selectors: [ ] ( ) ' " \
val = $(this).val().replace(/[[\]()'"\\]/g, "\\$&"),
total = tr.removeClass('qm-hide-' + filter).length,
hilite = $(this).attr('data-highlight'),
time = 0;
key = $(this).attr('id');
if ( val ) {
localStorage.setItem( key, $(this).val() );
} else {
localStorage.removeItem( key );
}
if ( hilite ) {
table.find('tr').removeClass('qm-highlight');
}
if ( $(this).val() !== '' ) {
if ( hilite ) {
tr.filter('[data-qm-' + hilite + '*="' + val + '"]').addClass('qm-highlight');
}
tr.not('[data-qm-' + filter + '*="' + val + '"]').addClass('qm-hide-' + filter);
$(this).closest('th').addClass('qm-filtered');
} else {
$(this).closest('th').removeClass('qm-filtered');
}
var matches = tr.filter(':visible');
matches.each(function(i){
var row_time = $(this).attr('data-qm-time');
if ( row_time ) {
time += parseFloat( row_time );
}
});
if ( time ) {
time = QM_i18n.number_format( time, 4 );
}
if ( table.find('.qm-filtered').length ) {
var count = matches.length + ' / ' + tr.length;
} else {
var count = matches.length;
}
table.find('.qm-items-number').text(count);
table.find('.qm-items-time').text(time);
stripes(table);
});
container.find('.qm-filter').each(function () {
var key = $(this).attr('id');
var value = localStorage.getItem( key );
if ( value !== null ) {
// Escape the following chars with a backslash before passing into jQ selectors: [ ] ( ) ' " \
var val = value.replace(/[[\]()'"\\]/g, "\\$&");
if ( ! $(this).find('option[value="' + val + '"]').length ) {
$('<option>').attr('value',value).text(value).appendTo(this);
}
$(this).val(value).change();
}
});
container.find('.qm-filter-trigger').on('click',function(e){
var filter = $(this).data('qm-filter'),
value = $(this).data('qm-value'),
target = $(this).data('qm-target');
$('#qm-' + target).find('.qm-filter').not('[data-filter="' + filter + '"]').val('').removeClass('qm-highlight').change();
$('#qm-' + target).find('[data-filter="' + filter + '"]').val(value).addClass('qm-highlight').change();
show_panel( '#qm-' + target );
$('#qm-' + target).focus();
e.preventDefault();
});
container.find('.qm-toggle').on('click',function(e){
var el = $(this);
var currentState = el.attr('aria-expanded');
var newState = 'true';
if (currentState === 'true') {
newState = 'false';
}
el.attr('aria-expanded', newState);
var toggle = $(this).closest('td').find('.qm-toggled');
if ( currentState === 'true' ) {
if ( toggle.length ) {
toggle.slideToggle(200,function(){
el.closest('td').removeClass('qm-toggled-on');
el.text(el.attr('data-on'));
});
} else {
el.closest('td').removeClass('qm-toggled-on');
el.text(el.attr('data-on'));
}
} else {
el.closest('td').addClass('qm-toggled-on');
el.text(el.attr('data-off'));
toggle.slideToggle(200);
}
e.preventDefault();
});
container.find('.qm-highlighter').on('mouseenter',function(e){
var subject = $(this).data('qm-highlight');
var table = $(this).closest('table');
if ( ! subject ) {
return;
}
$(this).addClass('qm-highlight');
$.each( subject.split(' '), function( i, el ){
table.find('tr[data-qm-subject="' + el + '"]').addClass('qm-highlight');
});
}).on('mouseleave',function(e){
$(this).removeClass('qm-highlight');
$(this).closest('table').find('tr').removeClass('qm-highlight');
});
$('.qm').find('tbody a,tbody button').on('focus',function(e){
$(this).closest('tr').addClass('qm-hovered');
}).on('blur',function(e){
$(this).closest('tr').removeClass('qm-hovered');
});
container.find('.qm table').on('sorted.qm',function(){
stripes( $(this) );
});
$( document ).ajaxSuccess( function( event, response, options ) {
var errors = response.getResponseHeader( 'X-QM-php_errors-error-count' );
if ( ! errors ) {
return event;
}
errors = parseInt( errors, 10 );
if ( window.console ) {
console.group( qm_l10n.ajax_error );
}
for ( var key = 1; key <= errors; key++ ) {
error = $.parseJSON( response.getResponseHeader( 'X-QM-php_errors-error-' + key ) );
if ( window.console ) {
switch ( error.type ) {
case 'warning':
console.error( error );
break;
default:
console.warn( error );
break;
}
}
if ( $('#qm-php_errors').find('[data-qm-key="' + error.key + '"]').length ) {
continue;
}
if ( $('#wp-admin-bar-query-monitor').length ) {
if ( ! qm.ajax_errors[error.type] ) {
$('#wp-admin-bar-query-monitor')
.addClass('qm-' + error.type)
.find('a').first().append('<span class="ab-label qm-ajax-' + error.type + '"> &nbsp; Ajax: ' + error.type + '</span>');
}
}
qm.ajax_errors[error.type] = true;
}
if ( window.console ) {
console.groupEnd();
}
$( '#qm-ajax-errors' ).show();
return event;
} );
$('.qm-auth').on('click',function(e){
var state = $('#qm-settings').data('qm-state');
var action = ( 'off' === state ? 'on' : 'off' );
$.ajax(qm_l10n.ajaxurl,{
type : 'POST',
context : this,
data : {
action : 'qm_auth_' + action,
nonce : qm_l10n.auth_nonce[action]
},
success : function(response){
$(this).text( $(this).data('qm-text-' + action) );
$('#qm-settings').attr('data-qm-state',action).data('qm-state',action);
},
dataType : 'json',
xhrFields: {
withCredentials: true
}
});
e.preventDefault();
});
var editorSuccessIndicator = $('#qm-editor-save-status');
editorSuccessIndicator.hide();
$('.qm-editor-button').on('click',function(e){
var state = $('#qm-settings').data('qm-state');
var editor = $('#qm-editor-select').val();
$.ajax(qm_l10n.ajaxurl,{
type : 'POST',
context : this,
data : {
action : 'qm_editor_set',
nonce : qm_l10n.auth_nonce['editor-set'],
editor : editor
},
success : function(response){
if (response.success) {
editorSuccessIndicator.show();
}
},
dataType : 'json',
xhrFields: {
withCredentials: true
}
});
e.preventDefault();
});
$.qm.tableSort({target: $('.qm-sortable')});
var startY, startX, resizerHeight;
$(document).on('mousedown touchstart', '.qm-resizer', function(event) {
resizerHeight = $(this).outerHeight() - 1;
startY = container.outerHeight() + ( event.clientY || event.originalEvent.targetTouches[0].pageY );
startX = container.outerWidth() + ( event.clientX || event.originalEvent.targetTouches[0].pageX );
$(document).on('mousemove touchmove', qm_do_resizer_drag);
$(document).on('mouseup touchend', qm_stop_resizer_drag);
});
function qm_do_resizer_drag(event) {
if ( ! container.hasClass('qm-show-right') ) {
var h = ( startY - ( event.clientY || event.originalEvent.targetTouches[0].pageY ) );
if ( h >= resizerHeight && h <= maxheight ) {
container.height( h );
body.css( 'margin-bottom', 'calc( ' + body_margin + ' + ' + h + 'px )' );
}
} else {
var w = ( startX - event.clientX );
if ( w >= minwidth && w <= maxwidth ) {
container.width( w );
}
body.css( 'margin-bottom', '' );
}
}
function qm_stop_resizer_drag(event) {
$(document).off('mousemove touchmove', qm_do_resizer_drag);
$(document).off('mouseup touchend', qm_stop_resizer_drag);
if ( ! container.hasClass('qm-show-right') ) {
localStorage.removeItem( container_position_key );
localStorage.setItem( container_height_key, container.height() );
} else {
localStorage.setItem( container_position_key, 'right' );
localStorage.setItem( container_width_key, container.width() );
}
}
var p = localStorage.getItem( container_position_key );
var h = localStorage.getItem( container_height_key );
var w = localStorage.getItem( container_width_key );
if ( ! container.hasClass('qm-peek') ) {
if ( p === 'right' ) {
if ( w !== null ) {
if ( w < minwidth ) {
w = minwidth;
}
if ( w > maxwidth ) {
w = maxwidth;
}
container.width( w );
}
container.addClass('qm-show-right');
} else if ( p !== 'right' && h !== null ) {
if ( h < minheight ) {
h = minheight;
}
if ( h > maxheight ) {
h = maxheight;
}
container.height( h );
}
}
$(window).on('resize', function(){
var h = container.height();
var w = container.width();
maxheight = ( $(window).height() - toolbarHeight );
maxwidth = $(window).width();
if ( h < minheight ) {
container.height( minheight );
}
if ( h > maxheight ) {
container.height( maxheight );
}
localStorage.setItem( container_height_key, container.height() );
if ( w > $(window).width() ) {
container.width( minwidth );
localStorage.setItem( container_width_key, container.width() );
}
if ( $(window).width() < 960 ) {
container.removeClass('qm-show-right');
localStorage.removeItem( container_position_key );
}
});
$('.qm-button-container-close').click(function(){
container.removeClass('qm-show').height('').width('');
body.css( 'margin-bottom', '' );
localStorage.removeItem( container_pinned_key );
});
$('.qm-button-container-settings,a[href="#qm-settings"]').click(function(){
show_panel( '#qm-settings' );
$('#qm-settings').focus();
});
$('.qm-button-container-position').click(function(){
container.toggleClass('qm-show-right');
if ( container.hasClass('qm-show-right') ) {
var w = localStorage.getItem( container_width_key );
if ( w !== null && w < $(window).width() ) {
container.width( w );
}
body.css( 'margin-bottom', '' );
localStorage.setItem( container_position_key, 'right' );
} else {
body.css( 'margin-bottom', 'calc( ' + body_margin + ' + ' + container.height() + 'px )' );
localStorage.removeItem( container_position_key );
}
});
var pinned = localStorage.getItem( container_pinned_key );
if ( pinned && $( pinned ).length ) {
show_panel( pinned );
}
$('.qm-title-heading select').change(function(){
show_panel( $(this).val() );
$($(this).val()).focus();
});
} );
/**
* Table sorting library.
*
* This is a modified version of jQuery table-sort v0.1.1
* https://github.com/gajus/table-sort
*
* Licensed under the BSD.
* https://github.com/gajus/table-sort/blob/master/LICENSE
*
* Author: Gajus Kuizinas <g.kuizinas@anuary.com>
*/
(function ($) {
$.qm = $.qm || {};
$.qm.tableSort = function (settings) {
// @param object columns NodeList table colums.
// @param integer row_width defines the number of columns per row.
var table_to_array = function (columns, row_width) {
columns = Array.prototype.slice.call(columns, 0);
var rows = [];
var row_index = 0;
for (var i = 0, j = columns.length; i < j; i += row_width) {
var row = [];
for (var k = 0; k < row_width; k++) {
var e = columns[i + k];
var data = e.dataset.qmSortWeight;
if (data === undefined) {
data = e.textContent || e.innerText;
}
var number = parseFloat(data);
data = isNaN(number) ? data : number;
row.push(data);
}
rows.push({index: row_index++, data: row});
}
return rows;
};
if ( ! settings.target || ! ( settings.target instanceof $) ) {
throw 'Target is not defined or it is not instance of jQuery.';
}
settings.target.each(function () {
var table = $(this);
table.find('.qm-sortable-column').on('click', function (e) {
var desc = ! $(this).hasClass('qm-sorted-desc');
var index = $(this).index();
table.find('thead th').removeClass('qm-sorted-asc qm-sorted-desc').attr('aria-sort','none');
if ( desc ) {
$(this).addClass('qm-sorted-desc').attr('aria-sort','descending');
} else {
$(this).addClass('qm-sorted-asc').attr('aria-sort','ascending');
}
table.find('tbody').each(function () {
var tbody = $(this);
var rows = this.rows;
var columns = this.querySelectorAll('th,td');
if (this.data_matrix === undefined) {
this.data_matrix = table_to_array(columns, $(rows[0]).find('th,td').length);
}
var data = this.data_matrix;
data.sort(function (a, b) {
if (a.data[index] == b.data[index]) {
return 0;
}
return (desc ? a.data[index] > b.data[index] : a.data[index] < b.data[index]) ? -1 : 1;
});
// Detach the tbody to prevent unnecessary overhead related
// to the browser environment.
tbody = tbody.detach();
// Convert NodeList into an array.
rows = Array.prototype.slice.call(rows, 0);
var last_row = rows[data[data.length - 1].index];
for (var i = 0, j = data.length - 1; i < j; i++) {
tbody[0].insertBefore(rows[data[i].index], last_row);
// Restore the index.
data[i].index = i;
}
// Restore the index.
data[data.length - 1].index = data.length - 1;
table.append(tbody);
});
table.trigger('sorted.qm');
e.preventDefault();
});
});
};
})(jQuery);
}
window.addEventListener('load', function() {
if ( ( 'undefined' === typeof jQuery ) || ! window.jQuery ) {
/* Fallback for running without jQuery (`QM_NO_JQUERY`) */
document.getElementById( 'query-monitor-main' ).className += ' qm-broken';
console.error( document.getElementById( 'qm-broken' ).textContent );
if ( 'undefined' === typeof jQuery ) {
console.error( 'QM error from JS: undefined jQuery' );
}
if ( ! window.jQuery ) {
console.error( 'QM error from JS: no jQuery' );
}
var menu_item = document.getElementById( 'wp-admin-bar-query-monitor' );
if ( menu_item ) {
menu_item.addEventListener( 'click', function() {
document.getElementById( 'query-monitor-main' ).className += ' qm-show';
} );
}
}
} );

View File

@@ -0,0 +1,130 @@
<?php
/**
* Plugin activation handler.
*
* @package query-monitor
*/
class QM_Activation extends QM_Plugin {
protected function __construct( $file ) {
# PHP version handling
if ( ! self::php_version_met() ) {
add_action( 'all_admin_notices', array( $this, 'php_notice' ) );
return;
}
# Filters
add_filter( 'pre_update_option_active_plugins', array( $this, 'filter_active_plugins' ) );
add_filter( 'pre_update_site_option_active_sitewide_plugins', array( $this, 'filter_active_sitewide_plugins' ) );
# Activation and deactivation
register_activation_hook( $file, array( $this, 'activate' ) );
register_deactivation_hook( $file, array( $this, 'deactivate' ) );
# Parent setup:
parent::__construct( $file );
}
public function activate( $sitewide = false ) {
$db = WP_CONTENT_DIR . '/db.php';
if ( ! file_exists( $db ) && function_exists( 'symlink' ) ) {
@symlink( $this->plugin_path( 'wp-content/db.php' ), $db ); // phpcs:ignore
}
if ( $sitewide ) {
update_site_option( 'active_sitewide_plugins', get_site_option( 'active_sitewide_plugins' ) );
} else {
update_option( 'active_plugins', get_option( 'active_plugins' ) );
}
}
public function deactivate() {
$admins = QM_Util::get_admins();
// Remove legacy capability handling:
if ( $admins ) {
$admins->remove_cap( 'view_query_monitor' );
}
# Only delete db.php if it belongs to Query Monitor
if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && class_exists( 'QM_DB' ) ) {
unlink( WP_CONTENT_DIR . '/db.php' ); // phpcs:ignore
}
}
public function filter_active_plugins( $plugins ) {
// this needs to run on the cli too
if ( empty( $plugins ) ) {
return $plugins;
}
$f = preg_quote( basename( $this->plugin_base() ), '/' );
return array_merge(
preg_grep( '/' . $f . '$/', $plugins ),
preg_grep( '/' . $f . '$/', $plugins, PREG_GREP_INVERT )
);
}
public function filter_active_sitewide_plugins( $plugins ) {
if ( empty( $plugins ) ) {
return $plugins;
}
$f = $this->plugin_base();
if ( isset( $plugins[ $f ] ) ) {
unset( $plugins[ $f ] );
return array_merge( array(
$f => time(),
), $plugins );
} else {
return $plugins;
}
}
public function php_notice() {
?>
<div id="qm_php_notice" class="error">
<p>
<span class="dashicons dashicons-warning" style="color:#dd3232" aria-hidden="true"></span>
<?php
echo esc_html( sprintf(
/* Translators: 1: Minimum required PHP version, 2: Current PHP version. */
__( 'The Query Monitor plugin requires PHP version %1$s or higher. This site is running version %2$s.', 'query-monitor' ),
self::$minimum_php_version,
PHP_VERSION
) );
?>
</p>
</div>
<?php
}
public static function init( $file = null ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QM_Activation( $file );
}
return $instance;
}
}

View File

@@ -0,0 +1,348 @@
<?php
/**
* Function call backtrace container.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Backtrace' ) ) {
class QM_Backtrace {
protected static $ignore_class = array(
'wpdb' => true,
'QueryMonitor' => true,
'W3_Db' => true,
'Debug_Bar_PHP' => true,
'WP_Hook' => true,
);
protected static $ignore_method = array();
protected static $ignore_func = array(
'include_once' => true,
'require_once' => true,
'include' => true,
'require' => true,
'call_user_func_array' => true,
'call_user_func' => true,
'trigger_error' => true,
'_doing_it_wrong' => true,
'_deprecated_argument' => true,
'_deprecated_file' => true,
'_deprecated_function' => true,
'dbDelta' => true,
);
protected static $show_args = array(
'do_action' => 1,
'apply_filters' => 1,
'do_action_ref_array' => 1,
'apply_filters_ref_array' => 1,
'get_template_part' => 2,
'get_extended_template_part' => 2,
'load_template' => 'dir',
'dynamic_sidebar' => 1,
'get_header' => 1,
'get_sidebar' => 1,
'get_footer' => 1,
'class_exists' => 2,
'current_user_can' => 3,
'user_can' => 4,
'current_user_can_for_blog' => 4,
'author_can' => 4,
);
protected static $filtered = false;
protected $trace = null;
protected $filtered_trace = null;
protected $calling_line = 0;
protected $calling_file = '';
public function __construct( array $args = array(), array $trace = null ) {
$this->trace = ( null === $trace ) ? debug_backtrace( false ) : $trace;
$args = array_merge( array(
'ignore_current_filter' => true,
'ignore_frames' => 0,
), $args );
$this->ignore( 1 ); # Self-awareness
/**
* If error_handler() is in the trace, QM fails later when it tries
* to get $lowest['file'] in get_filtered_trace()
*/
if ( 'error_handler' === $this->trace[0]['function'] ) {
$this->ignore( 1 );
}
if ( $args['ignore_frames'] ) {
$this->ignore( $args['ignore_frames'] );
}
if ( $args['ignore_current_filter'] ) {
$this->ignore_current_filter();
}
foreach ( $this->trace as $k => $frame ) {
if ( ! isset( $frame['args'] ) ) {
continue;
}
if ( isset( $frame['function'] ) && isset( self::$show_args[ $frame['function'] ] ) ) {
$show = self::$show_args[ $frame['function'] ];
if ( 'dir' === $show ) {
$show = 1;
}
$frame['args'] = array_slice( $frame['args'], 0, $show );
} else {
unset( $frame['args'] );
}
$this->trace[ $k ] = $frame;
}
}
public function get_stack() {
$trace = $this->get_filtered_trace();
$stack = wp_list_pluck( $trace, 'display' );
return $stack;
}
public function get_caller() {
$trace = $this->get_filtered_trace();
return reset( $trace );
}
public function get_component() {
$components = array();
foreach ( $this->trace as $frame ) {
$component = self::get_frame_component( $frame );
if ( $component ) {
if ( 'plugin' === $component->type ) {
// If the component is a plugin then it can't be anything else,
// so short-circuit and return early.
return $component;
}
$components[ $component->type ] = $component;
}
}
foreach ( QM_Util::get_file_dirs() as $type => $dir ) {
if ( isset( $components[ $type ] ) ) {
return $components[ $type ];
}
}
# This should not happen
}
public static function get_frame_component( array $frame ) {
try {
if ( isset( $frame['class'] ) ) {
if ( ! class_exists( $frame['class'], false ) ) {
return null;
}
if ( ! method_exists( $frame['class'], $frame['function'] ) ) {
return null;
}
$ref = new ReflectionMethod( $frame['class'], $frame['function'] );
$file = $ref->getFileName();
} elseif ( isset( $frame['function'] ) && function_exists( $frame['function'] ) ) {
$ref = new ReflectionFunction( $frame['function'] );
$file = $ref->getFileName();
} elseif ( isset( $frame['file'] ) ) {
$file = $frame['file'];
} else {
return null;
}
return QM_Util::get_file_component( $file );
} catch ( ReflectionException $e ) {
return null;
}
}
public function get_trace() {
return $this->trace;
}
public function get_display_trace() {
return $this->get_filtered_trace();
}
public function get_filtered_trace() {
if ( ! isset( $this->filtered_trace ) ) {
$trace = array_map( array( $this, 'filter_trace' ), $this->trace );
$trace = array_values( array_filter( $trace ) );
if ( empty( $trace ) && ! empty( $this->trace ) ) {
$lowest = $this->trace[0];
$file = QM_Util::standard_dir( $lowest['file'], '' );
$lowest['calling_file'] = $lowest['file'];
$lowest['calling_line'] = $lowest['line'];
$lowest['function'] = $file;
$lowest['display'] = $file;
$lowest['id'] = $file;
unset( $lowest['class'], $lowest['args'], $lowest['type'] );
$trace[0] = $lowest;
}
$this->filtered_trace = $trace;
}
return $this->filtered_trace;
}
public function ignore( $num ) {
for ( $i = 0; $i < $num; $i++ ) {
unset( $this->trace[ $i ] );
}
$this->trace = array_values( $this->trace );
return $this;
}
public function ignore_current_filter() {
if ( isset( $this->trace[2] ) && isset( $this->trace[2]['function'] ) ) {
if ( in_array( $this->trace[2]['function'], array( 'apply_filters', 'do_action' ), true ) ) {
$this->ignore( 3 ); # Ignore filter and action callbacks
}
}
}
public function filter_trace( array $trace ) {
if ( ! self::$filtered && function_exists( 'did_action' ) && did_action( 'plugins_loaded' ) ) {
/**
* Filters which classes to ignore when constructing user-facing call stacks.
*
* @since 2.7.0
*
* @param bool[] $ignore_class Array of class names to ignore. The array keys are class names to ignore,
* the array values are whether to ignore the class or not (usually true).
*/
self::$ignore_class = apply_filters( 'qm/trace/ignore_class', self::$ignore_class );
/**
* Filters which class methods to ignore when constructing user-facing call stacks.
*
* @since 2.7.0
*
* @param bool[] $ignore_method Array of method names to ignore. The array keys are method names to ignore,
* the array values are whether to ignore the method or not (usually true).
*/
self::$ignore_method = apply_filters( 'qm/trace/ignore_method', self::$ignore_method );
/**
* Filters which functions to ignore when constructing user-facing call stacks.
*
* @since 2.7.0
*
* @param bool[] $ignore_func Array of function names to ignore. The array keys are function names to ignore,
* the array values are whether to ignore the function or not (usually true).
*/
self::$ignore_func = apply_filters( 'qm/trace/ignore_func', self::$ignore_func );
/**
* Filters the number of argument values to show for the given function name when constructing user-facing
* call stacks.
*
* @since 2.7.0
*
* @param (int|string)[] $show_args The number of argument values to show for the given function name. The
* array keys are function names, the array values are either integers or
* "dir" to specifically treat the function argument as a directory path.
*/
self::$show_args = apply_filters( 'qm/trace/show_args', self::$show_args );
self::$filtered = true;
}
$return = $trace;
if ( isset( $trace['class'] ) ) {
if ( isset( self::$ignore_class[ $trace['class'] ] ) ) {
$return = null;
} elseif ( isset( self::$ignore_method[ $trace['class'] ][ $trace['function'] ] ) ) {
$return = null;
} elseif ( 0 === strpos( $trace['class'], 'QM' ) ) {
$return = null;
} else {
$return['id'] = $trace['class'] . $trace['type'] . $trace['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $trace['class'] . $trace['type'] . $trace['function'] ) . '()';
}
} else {
if ( isset( self::$ignore_func[ $trace['function'] ] ) ) {
$return = null;
} elseif ( isset( self::$show_args[ $trace['function'] ] ) ) {
$show = self::$show_args[ $trace['function'] ];
if ( 'dir' === $show ) {
if ( isset( $trace['args'][0] ) ) {
$arg = QM_Util::standard_dir( $trace['args'][0], '' );
$return['id'] = $trace['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $trace['function'] ) . "('{$arg}')";
}
} else {
$args = array();
for ( $i = 0; $i < $show; $i++ ) {
if ( isset( $trace['args'][ $i ] ) ) {
if ( is_string( $trace['args'][ $i ] ) ) {
$args[] = '\'' . $trace['args'][ $i ] . '\'';
} else {
$args[] = QM_Util::display_variable( $trace['args'][ $i ] );
}
}
}
$return['id'] = $trace['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $trace['function'] ) . '(' . implode( ',', $args ) . ')';
}
} else {
$return['id'] = $trace['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $trace['function'] ) . '()';
}
}
if ( $return ) {
$return['calling_file'] = $this->calling_file;
$return['calling_line'] = $this->calling_line;
}
if ( isset( $trace['line'] ) ) {
$this->calling_line = $trace['line'];
}
if ( isset( $trace['file'] ) ) {
$this->calling_file = $trace['file'];
}
return $return;
}
}
} else {
add_action( 'init', 'QueryMonitor::symlink_warning' );
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Plugin CLI command.
*
* @package query-monitor
*/
class QM_CLI extends QM_Plugin {
protected function __construct( $file ) {
# Register command
WP_CLI::add_command( 'qm enable', array( $this, 'enable' ) );
# Parent setup:
parent::__construct( $file );
}
/**
* Enable QM by creating the symlink for db.php
*/
public function enable() {
$drop_in = WP_CONTENT_DIR . '/db.php';
if ( file_exists( $drop_in ) ) {
if ( false !== strpos( file_get_contents( $drop_in ), 'class QM_DB' ) ) {
WP_CLI::success( "Query Monitor's wp-content/db.php is already in place" );
exit( 0 );
} else {
WP_CLI::error( 'Unknown wp-content/db.php already is already in place' );
}
}
if ( ! function_exists( 'symlink' ) ) {
WP_CLI::error( 'The symlink function is not available' );
}
if ( symlink( $this->plugin_path( 'wp-content/db.php' ), $drop_in ) ) {
WP_CLI::success( 'wp-content/db.php symlink created' );
exit( 0 );
} else {
WP_CLI::error( 'Failed to create wp-content/db.php symlink' );
}
}
public static function init( $file = null ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QM_CLI( $file );
}
return $instance;
}
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* Abstract data collector.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Collector' ) ) {
abstract class QM_Collector {
protected $timer;
protected $data = array(
'types' => array(),
'component_times' => array(),
);
protected static $hide_qm = null;
public $concerned_actions = array();
public $concerned_filters = array();
public $concerned_constants = array();
public $tracked_hooks = array();
public function __construct() {}
final public function id() {
return "qm-{$this->id}";
}
protected function log_type( $type ) {
if ( isset( $this->data['types'][ $type ] ) ) {
$this->data['types'][ $type ]++;
} else {
$this->data['types'][ $type ] = 1;
}
}
protected function maybe_log_dupe( $sql, $i ) {
$sql = str_replace( array( "\r\n", "\r", "\n" ), ' ', $sql );
$sql = str_replace( array( "\t", '`' ), '', $sql );
$sql = preg_replace( '/ +/', ' ', $sql );
$sql = trim( $sql );
$sql = rtrim( $sql, ';' );
$this->data['dupes'][ $sql ][] = $i;
}
protected function log_component( $component, $ltime, $type ) {
if ( ! isset( $this->data['component_times'][ $component->name ] ) ) {
$this->data['component_times'][ $component->name ] = array(
'component' => $component->name,
'ltime' => 0,
'types' => array(),
);
}
$this->data['component_times'][ $component->name ]['ltime'] += $ltime;
if ( isset( $this->data['component_times'][ $component->name ]['types'][ $type ] ) ) {
$this->data['component_times'][ $component->name ]['types'][ $type ]++;
} else {
$this->data['component_times'][ $component->name ]['types'][ $type ] = 1;
}
}
public static function timer_stop_float() {
global $timestart;
return microtime( true ) - $timestart;
}
public static function format_bool_constant( $constant ) {
// @TODO this should be in QM_Util
if ( ! defined( $constant ) ) {
/* translators: Undefined PHP constant */
return __( 'undefined', 'query-monitor' );
} elseif ( is_string( constant( $constant ) ) && ! is_numeric( constant( $constant ) ) ) {
return constant( $constant );
} elseif ( ! constant( $constant ) ) {
return 'false';
} else {
return 'true';
}
}
final public function get_data() {
return $this->data;
}
final public function set_id( $id ) {
$this->id = $id;
}
final public function process_concerns() {
global $wp_filter;
$tracked = array();
$id = $this->id;
/**
* Filters the concerned actions for the given panel.
*
* The dynamic portion of the hook name, `$id`, refers to the collector ID, which is typically the `$id`
* property of the collector class.
*
* @since 3.3.0
*
* @param string[] $actions Array of action names that this panel concerns itself with.
*/
$concerned_actions = apply_filters( "qm/collect/concerned_actions/{$id}", $this->get_concerned_actions() );
/**
* Filters the concerned filters for the given panel.
*
* The dynamic portion of the hook name, `$id`, refers to the collector ID, which is typically the `$id`
* property of the collector class.
*
* @since 3.3.0
*
* @param string[] $filters Array of filter names that this panel concerns itself with.
*/
$concerned_filters = apply_filters( "qm/collect/concerned_filters/{$id}", $this->get_concerned_filters() );
/**
* Filters the concerned options for the given panel.
*
* The dynamic portion of the hook name, `$id`, refers to the collector ID, which is typically the `$id`
* property of the collector class.
*
* @since 3.3.0
*
* @param string[] $options Array of option names that this panel concerns itself with.
*/
$concerned_options = apply_filters( "qm/collect/concerned_options/{$id}", $this->get_concerned_options() );
/**
* Filters the concerned constants for the given panel.
*
* The dynamic portion of the hook name, `$id`, refers to the collector ID, which is typically the `$id`
* property of the collector class.
*
* @since 3.3.0
*
* @param string[] $constants Array of constant names that this panel concerns itself with.
*/
$concerned_constants = apply_filters( "qm/collect/concerned_constants/{$id}", $this->get_concerned_constants() );
foreach ( $concerned_actions as $action ) {
if ( has_action( $action ) ) {
$this->concerned_actions[ $action ] = QM_Hook::process( $action, $wp_filter, true, true );
}
$tracked[] = $action;
}
foreach ( $concerned_filters as $filter ) {
if ( has_filter( $filter ) ) {
$this->concerned_filters[ $filter ] = QM_Hook::process( $filter, $wp_filter, true, true );
}
$tracked[] = $filter;
}
$option_filters = array(
// Should this include the pre_delete_ and pre_update_ filters too?
'pre_option_%s',
'default_option_%s',
'option_%s',
'pre_site_option_%s',
'default_site_option_%s',
'site_option_%s',
);
foreach ( $concerned_options as $option ) {
foreach ( $option_filters as $option_filter ) {
$filter = sprintf(
$option_filter,
$option
);
if ( has_filter( $filter ) ) {
$this->concerned_filters[ $filter ] = QM_Hook::process( $filter, $wp_filter, true, true );
}
$tracked[] = $filter;
}
}
$this->concerned_actions = array_filter( $this->concerned_actions, array( $this, 'filter_concerns' ) );
$this->concerned_filters = array_filter( $this->concerned_filters, array( $this, 'filter_concerns' ) );
foreach ( $concerned_constants as $constant ) {
if ( defined( $constant ) ) {
$this->concerned_constants[ $constant ] = constant( $constant );
}
}
sort( $tracked );
$this->tracked_hooks = $tracked;
}
public function filter_concerns( $concerns ) {
return ! empty( $concerns['actions'] );
}
public static function format_user( WP_User $user_object ) {
$user = get_object_vars( $user_object->data );
unset(
$user['user_pass'],
$user['user_activation_key']
);
$user['roles'] = $user_object->roles;
return $user;
}
public static function enabled() {
return true;
}
public static function hide_qm() {
if ( null === self::$hide_qm ) {
self::$hide_qm = QM_HIDE_SELF;
}
return self::$hide_qm;
}
public function filter_remove_qm( array $item ) {
$component = $item['trace']->get_component();
return ( 'query-monitor' !== $component->context );
}
public function process() {}
public function post_process() {}
public function tear_down() {}
public function get_timer() {
return $this->timer;
}
public function set_timer( QM_Timer $timer ) {
$this->timer = $timer;
}
public function get_concerned_actions() {
return array();
}
public function get_concerned_filters() {
return array();
}
public function get_concerned_options() {
return array();
}
public function get_concerned_constants() {
return array();
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* Container for data collectors.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Collectors' ) ) {
class QM_Collectors implements IteratorAggregate {
private $items = array();
private $processed = false;
public function getIterator() {
return new ArrayIterator( $this->items );
}
public static function add( QM_Collector $collector ) {
$collectors = self::init();
$collectors->items[ $collector->id ] = $collector;
}
/**
* Fetches a collector instance.
*
* @param string $id The collector ID.
* @return QM_Collector|null The collector object.
*/
public static function get( $id ) {
$collectors = self::init();
if ( isset( $collectors->items[ $id ] ) ) {
return $collectors->items[ $id ];
}
return null;
}
public static function init() {
static $instance;
if ( ! $instance ) {
$instance = new QM_Collectors();
}
return $instance;
}
public function process() {
if ( $this->processed ) {
return;
}
foreach ( $this as $collector ) {
$collector->tear_down();
$timer = new QM_Timer();
$timer->start();
$collector->process();
$collector->process_concerns();
$collector->set_timer( $timer->stop() );
}
foreach ( $this as $collector ) {
$collector->post_process();
}
$this->processed = true;
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* Abstract dispatcher.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Dispatcher' ) ) {
abstract class QM_Dispatcher {
/**
* Outputter instances.
*
* @var QM_Output[] Array of outputters.
*/
protected $outputters = array();
/**
* Query Monitor plugin instance.
*
* @var QM_Plugin Plugin instance.
*/
protected $qm;
public function __construct( QM_Plugin $qm ) {
$this->qm = $qm;
if ( ! defined( 'QM_COOKIE' ) ) {
define( 'QM_COOKIE', 'wp-query_monitor_' . COOKIEHASH );
}
if ( ! defined( 'QM_EDITOR_COOKIE' ) ) {
define( 'QM_EDITOR_COOKIE', 'wp-query_monitor_editor_' . COOKIEHASH );
}
add_action( 'init', array( $this, 'init' ) );
}
abstract public function is_active();
final public function should_dispatch() {
$e = error_get_last();
# Don't dispatch if a fatal has occurred:
if ( ! empty( $e ) && ( $e['type'] & QM_ERROR_FATALS ) ) {
return false;
}
/**
* Allows users to disable this dispatcher.
*
* The dynamic portion of the hook name, `$this->id`, refers to the dispatcher ID.
*
* @since 2.8.0
*
* @param bool $true Whether or not the dispatcher is enabled.
*/
if ( ! apply_filters( "qm/dispatch/{$this->id}", true ) ) {
return false;
}
return $this->is_active();
}
/**
* Processes and fetches the outputters for this dispatcher.
*
* @param string $outputter_id The outputter ID.
* @return QM_Output[] Array of outputters.
*/
public function get_outputters( $outputter_id ) {
$collectors = QM_Collectors::init();
$collectors->process();
/**
* Allows users to filter what outputs.
*
* The dynamic portion of the hook name, `$outputter_id`, refers to the outputter ID.
*
* @since 2.8.0
*
* @param QM_Output[] $outputters Array of outputters.
* @param QM_Collectors $collectors List of collectors.
*/
$this->outputters = apply_filters( "qm/outputter/{$outputter_id}", array(), $collectors );
return $this->outputters;
}
public function init() {
if ( ! self::user_can_view() ) {
return;
}
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', 1 );
}
add_action( 'send_headers', 'nocache_headers' );
}
protected function before_output() {
// nothing
}
protected function after_output() {
// nothing
}
public static function user_can_view() {
if ( ! did_action( 'plugins_loaded' ) ) {
return false;
}
if ( current_user_can( 'view_query_monitor' ) ) {
return true;
}
return self::user_verified();
}
public static function user_verified() {
if ( isset( $_COOKIE[QM_COOKIE] ) ) { // phpcs:ignore
return self::verify_cookie( wp_unslash( $_COOKIE[QM_COOKIE] ) ); // phpcs:ignore
}
return false;
}
public static function editor_cookie() {
if ( defined( 'QM_EDITOR_COOKIE' ) && isset( $_COOKIE[QM_EDITOR_COOKIE] ) ) { // phpcs:ignore
return $_COOKIE[QM_EDITOR_COOKIE]; // phpcs:ignore
}
return '';
}
public static function verify_cookie( $value ) {
$old_user_id = wp_validate_auth_cookie( $value, 'logged_in' );
if ( $old_user_id ) {
return user_can( $old_user_id, 'view_query_monitor' );
}
return false;
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Container for dispatchers.
*
* @package query-monitor
*/
class QM_Dispatchers implements IteratorAggregate {
private $items = array();
public function getIterator() {
return new ArrayIterator( $this->items );
}
public static function add( QM_Dispatcher $dispatcher ) {
$dispatchers = self::init();
$dispatchers->items[ $dispatcher->id ] = $dispatcher;
}
public static function get( $id ) {
$dispatchers = self::init();
if ( isset( $dispatchers->items[ $id ] ) ) {
return $dispatchers->items[ $id ];
}
return false;
}
public static function init() {
static $instance;
if ( ! $instance ) {
$instance = new QM_Dispatchers();
}
return $instance;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Hook processor.
*
* @package query-monitor
*/
class QM_Hook {
public static function process( $name, array $wp_filter, $hide_qm = false, $hide_core = false ) {
$actions = array();
$components = array();
if ( isset( $wp_filter[ $name ] ) ) {
# http://core.trac.wordpress.org/ticket/17817
$action = $wp_filter[ $name ];
foreach ( $action as $priority => $callbacks ) {
foreach ( $callbacks as $callback ) {
$callback = QM_Util::populate_callback( $callback );
if ( isset( $callback['component'] ) ) {
if (
( $hide_qm && 'query-monitor' === $callback['component']->context )
|| ( $hide_core && 'core' === $callback['component']->context )
) {
continue;
}
$components[ $callback['component']->name ] = $callback['component']->name;
}
// This isn't used and takes up a ton of memory:
unset( $callback['function'] );
$actions[] = array(
'priority' => $priority,
'callback' => $callback,
);
}
}
}
$parts = array_values( array_filter( preg_split( '#[_/-]#', $name ) ) );
return array(
'name' => $name,
'actions' => $actions,
'parts' => $parts,
'components' => $components,
);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Abstract output handler.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Output' ) ) {
abstract class QM_Output {
/**
* Collector instance.
*
* @var QM_Collector Collector.
*/
protected $collector;
/**
* Timer instance.
*
* @var QM_Timer Timer.
*/
protected $timer;
public function __construct( QM_Collector $collector ) {
$this->collector = $collector;
}
abstract public function get_output();
public function output() {
// nothing
}
public function get_collector() {
return $this->collector;
}
final public function get_timer() {
return $this->timer;
}
final public function set_timer( QM_Timer $timer ) {
$this->timer = $timer;
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* Abstract plugin wrapper.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Plugin' ) ) {
abstract class QM_Plugin {
private $plugin = array();
public static $minimum_php_version = '5.3.6';
/**
* Class constructor
*/
protected function __construct( $file ) {
$this->file = $file;
}
/**
* Returns the URL for for a file/dir within this plugin.
*
* @param string $file The path within this plugin, e.g. '/js/clever-fx.js'
* @return string URL
*/
final public function plugin_url( $file = '' ) {
return $this->_plugin( 'url', $file );
}
/**
* Returns the filesystem path for a file/dir within this plugin.
*
* @param string $file The path within this plugin, e.g. '/js/clever-fx.js'
* @return string Filesystem path
*/
final public function plugin_path( $file = '' ) {
return $this->_plugin( 'path', $file );
}
/**
* Returns a version number for the given plugin file.
*
* @param string $file The path within this plugin, e.g. '/js/clever-fx.js'
* @return string Version
*/
final public function plugin_ver( $file ) {
return filemtime( $this->plugin_path( $file ) );
}
/**
* Returns the current plugin's basename, eg. 'my_plugin/my_plugin.php'.
*
* @return string Basename
*/
final public function plugin_base() {
return $this->_plugin( 'base' );
}
/**
* Populates and returns the current plugin info.
*/
final private function _plugin( $item, $file = '' ) {
if ( ! array_key_exists( $item, $this->plugin ) ) {
switch ( $item ) {
case 'url':
$this->plugin[ $item ] = plugin_dir_url( $this->file );
break;
case 'path':
$this->plugin[ $item ] = plugin_dir_path( $this->file );
break;
case 'base':
$this->plugin[ $item ] = plugin_basename( $this->file );
break;
}
}
return $this->plugin[ $item ] . ltrim( $file, '/' );
}
public static function php_version_met() {
static $met = null;
if ( null === $met ) {
$met = version_compare( PHP_VERSION, self::$minimum_php_version, '>=' );
}
return $met;
}
public static function php_version_nope() {
printf(
'<div id="qm-php-nope" class="notice notice-error is-dismissible"><p>%s</p></div>',
wp_kses(
sprintf(
/* translators: 1: Required PHP version number, 2: Current PHP version number, 3: URL of PHP update help page */
__( 'The Query Monitor plugin requires PHP version %1$s or higher. This site is running PHP version %2$s. <a href="%3$s">Learn about updating PHP</a>.', 'query-monitor' ),
self::$minimum_php_version,
PHP_VERSION,
'https://wordpress.org/support/update-php/'
),
array(
'a' => array(
'href' => array(),
),
)
)
);
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* A convenience class for wrapping certain user-facing functionality.
*
* @package query-monitor
*/
class QM {
public static function emergency( $message, array $context = array() ) {
/**
* Fires when an `emergency` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/emergency', $message, $context );
}
public static function alert( $message, array $context = array() ) {
/**
* Fires when an `alert` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/alert', $message, $context );
}
public static function critical( $message, array $context = array() ) {
/**
* Fires when a `critical` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/critical', $message, $context );
}
public static function error( $message, array $context = array() ) {
/**
* Fires when an `error` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/error', $message, $context );
}
public static function warning( $message, array $context = array() ) {
/**
* Fires when a `warning` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/warning', $message, $context );
}
public static function notice( $message, array $context = array() ) {
/**
* Fires when a `notice` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/notice', $message, $context );
}
public static function info( $message, array $context = array() ) {
/**
* Fires when an `info` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/info', $message, $context );
}
public static function debug( $message, array $context = array() ) {
/**
* Fires when a `debug` level message is logged.
*
* @since 3.1.0
*
* @param string $message The message.
* @param array $context The context passed.
*/
do_action( 'qm/debug', $message, $context );
}
public static function log( $level, $message, array $context = array() ) {
$logger = QM_Collectors::get( 'logger' );
$logger->log( $level, $message, $context );
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* The main Query Monitor plugin class.
*
* @package query-monitor
*/
class QueryMonitor extends QM_Plugin {
protected function __construct( $file ) {
# Actions
add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) );
add_action( 'init', array( $this, 'action_init' ) );
add_action( 'members_register_caps', array( $this, 'action_register_members_caps' ) );
add_action( 'members_register_cap_groups', array( $this, 'action_register_members_groups' ) );
# Filters
add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 10, 4 );
add_filter( 'ure_built_in_wp_caps', array( $this, 'filter_ure_caps' ) );
add_filter( 'ure_capabilities_groups_tree', array( $this, 'filter_ure_groups' ) );
add_filter( 'network_admin_plugin_action_links_query-monitor/query-monitor.php', array( $this, 'filter_plugin_action_links' ) );
add_filter( 'plugin_action_links_query-monitor/query-monitor.php', array( $this, 'filter_plugin_action_links' ) );
# Parent setup:
parent::__construct( $file );
# Load and register built-in collectors:
$collectors = array();
foreach ( glob( $this->plugin_path( 'collectors/*.php' ) ) as $file ) {
$key = basename( $file, '.php' );
$collectors[ $key ] = $file;
}
/**
* Allow filtering of built-in collector files.
*
* @since 2.14.0
*
* @param string[] $collectors Array of file paths to be loaded.
*/
foreach ( apply_filters( 'qm/built-in-collectors', $collectors ) as $file ) {
include $file;
}
}
public function filter_plugin_action_links( array $actions ) {
return array_merge( array(
'settings' => '<a href="#qm-settings">' . esc_html__( 'Settings', 'query-monitor' ) . '</a>',
'add-ons' => '<a href="https://github.com/johnbillion/query-monitor/wiki/Query-Monitor-Add-on-Plugins">' . esc_html__( 'Add-ons', 'query-monitor' ) . '</a>',
), $actions );
}
/**
* Filter a user's capabilities so they can be altered at runtime.
*
* This is used to:
* - Grant the 'view_query_monitor' capability to the user if they have the ability to manage options.
*
* This does not get called for Super Admins.
*
* @param bool[] $user_caps Array of key/value pairs where keys represent a capability name and boolean values
* represent whether the user has that capability.
* @param string[] $required_caps Required primitive capabilities for the requested capability.
* @param array $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters.
* }
* @param WP_User $user Concerned user object.
* @return bool[] Concerned user's capabilities.
*/
public function filter_user_has_cap( array $user_caps, array $required_caps, array $args, WP_User $user ) {
if ( 'view_query_monitor' !== $args[0] ) {
return $user_caps;
}
if ( array_key_exists( 'view_query_monitor', $user_caps ) ) {
return $user_caps;
}
if ( ! is_multisite() && user_can( $args[1], 'manage_options' ) ) {
$user_caps['view_query_monitor'] = true;
}
return $user_caps;
}
public function action_plugins_loaded() {
// Hide QM itself from output by default:
if ( ! defined( 'QM_HIDE_SELF' ) ) {
define( 'QM_HIDE_SELF', true );
}
/**
* Filters the collectors that are being added.
*
* @since 2.11.2
*
* @param QM_Collector[] $collectors Array of collector instances.
* @param QueryMonitor $instance QueryMonitor instance.
*/
foreach ( apply_filters( 'qm/collectors', array(), $this ) as $collector ) {
QM_Collectors::add( $collector );
}
# Load dispatchers:
foreach ( glob( $this->plugin_path( 'dispatchers/*.php' ) ) as $file ) {
include $file;
}
/**
* Filters the dispatchers that are being added.
*
* @since 2.11.2
*
* @param QM_Dispatcher[] $dispatchers Array of dispatcher instances.
* @param QueryMonitor $instance QueryMonitor instance.
*/
foreach ( apply_filters( 'qm/dispatchers', array(), $this ) as $dispatcher ) {
QM_Dispatchers::add( $dispatcher );
}
}
public function action_init() {
load_plugin_textdomain( 'query-monitor', false, dirname( $this->plugin_base() ) . '/languages' );
}
public static function symlink_warning() {
$db = WP_CONTENT_DIR . '/db.php';
trigger_error( sprintf(
/* translators: %s: Symlink file location */
esc_html__( 'The symlink at %s is no longer pointing to the correct location. Please remove the symlink, then deactivate and reactivate Query Monitor.', 'query-monitor' ),
'<code>' . esc_html( $db ) . '</code>'
), E_USER_WARNING );
}
/**
* Registers the Query Monitor user capability group for the Members plugin.
*
* @link https://wordpress.org/plugins/members/
*/
public function action_register_members_groups() {
members_register_cap_group( 'query_monitor', array(
'label' => __( 'Query Monitor', 'query-monitor' ),
'caps' => array(
'view_query_monitor',
),
'icon' => 'dashicons-admin-tools',
'priority' => 30,
) );
}
/**
* Registers the View Query Monitor user capability for the Members plugin.
*
* @link https://wordpress.org/plugins/members/
*/
public function action_register_members_caps() {
members_register_cap( 'view_query_monitor', array(
'label' => _x( 'View Query Monitor', 'Human readable label for the user capability required to view Query Monitor.', 'query-monitor' ),
'group' => 'query_monitor',
) );
}
/**
* Registers the Query Monitor user capability group for the User Role Editor plugin.
*
* @link https://wordpress.org/plugins/user-role-editor/
*
* @param array[] $groups Array of existing groups.
* @return array[] Updated array of groups.
*/
public function filter_ure_groups( array $groups ) {
$groups['query_monitor'] = array(
'caption' => esc_html__( 'Query Monitor', 'query-monitor' ),
'parent' => 'custom',
'level' => 2,
);
return $groups;
}
/**
* Registers the View Query Monitor user capability for the User Role Editor plugin.
*
* @link https://wordpress.org/plugins/user-role-editor/
*
* @param array[] $caps Array of existing capabilities.
* @return array[] Updated array of capabilities.
*/
public function filter_ure_caps( array $caps ) {
$caps['view_query_monitor'] = array(
'custom',
'query_monitor',
);
return $caps;
}
public static function init( $file = null ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QueryMonitor( $file );
}
return $instance;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Timer that collects timing and memory usage.
*
* @package query-monitor
*/
class QM_Timer {
protected $start = null;
protected $end = null;
protected $trace = null;
protected $laps = array();
public function start( array $data = null ) {
$this->trace = new QM_Backtrace();
$this->start = array(
'time' => microtime( true ),
'memory' => memory_get_usage(),
'data' => $data,
);
return $this;
}
public function stop( array $data = null ) {
$this->end = array(
'time' => microtime( true ),
'memory' => memory_get_usage(),
'data' => $data,
);
return $this;
}
public function lap( array $data = null, $name = null ) {
$lap = array(
'time' => microtime( true ),
'memory' => memory_get_usage(),
'data' => $data,
);
if ( ! isset( $name ) ) {
/* translators: %d: Timing lap number */
$i = sprintf( __( 'Lap %d', 'query-monitor' ), count( $this->laps ) + 1 );
} else {
$i = $name;
}
$this->laps[ $i ] = $lap;
return $this;
}
public function get_laps() {
$laps = array();
$prev = $this->start;
foreach ( $this->laps as $lap_id => $lap ) {
$lap['time_used'] = $lap['time'] - $prev['time'];
$lap['memory_used'] = $lap['memory'] - $prev['memory'];
$laps[ $lap_id ] = $lap;
$prev = $lap;
}
return $laps;
}
public function get_time() {
return $this->end['time'] - $this->start['time'];
}
public function get_memory() {
return $this->end['memory'] - $this->start['memory'];
}
public function get_start_time() {
return $this->start['time'];
}
public function get_start_memory() {
return $this->start['memory'];
}
public function get_end_time() {
return $this->end['time'];
}
public function get_end_memory() {
return $this->end['memory'];
}
public function get_trace() {
return $this->trace;
}
public function end( array $data = null ) {
return $this->stop( $data );
}
}

View File

@@ -0,0 +1,518 @@
<?php
/**
* General utilities class.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Util' ) ) {
class QM_Util {
protected static $file_components = array();
protected static $file_dirs = array();
protected static $abspath = null;
protected static $contentpath = null;
protected static $sort_field = null;
private function __construct() {}
public static function convert_hr_to_bytes( $size ) {
# Annoyingly, wp_convert_hr_to_bytes() is defined in a file that's only
# loaded in the admin area, so we'll use our own version.
# See also http://core.trac.wordpress.org/ticket/17725
$bytes = (float) $size;
if ( $bytes ) {
$last = strtolower( substr( $size, -1 ) );
$pos = strpos( ' kmg', $last, 1 );
if ( $pos ) {
$bytes *= pow( 1024, $pos );
}
$bytes = round( $bytes );
}
return $bytes;
}
public static function standard_dir( $dir, $path_replace = null ) {
$dir = self::normalize_path( $dir );
if ( is_string( $path_replace ) ) {
if ( ! self::$abspath ) {
self::$abspath = self::normalize_path( ABSPATH );
self::$contentpath = self::normalize_path( dirname( WP_CONTENT_DIR ) . '/' );
}
$dir = str_replace( array(
self::$abspath,
self::$contentpath,
), $path_replace, $dir );
}
return $dir;
}
public static function normalize_path( $path ) {
if ( function_exists( 'wp_normalize_path' ) ) {
$path = wp_normalize_path( $path );
} else {
$path = str_replace( '\\', '/', $path );
$path = str_replace( '//', '/', $path );
}
return $path;
}
public static function get_file_dirs() {
if ( empty( self::$file_dirs ) ) {
/**
* Filters the absolute directory paths that correlate to components.
*
* Note that this filter is applied before QM adds its built-in list of components. This is
* so custom registered components take precedence during component detection.
*
* See the corresponding `qm/component_name/{$type}` filter for specifying the component name.
*
* @since 3.6.0
*
* @param string[] $dirs Array of absolute directory paths keyed by component identifier.
*/
self::$file_dirs = apply_filters( 'qm/component_dirs', self::$file_dirs );
self::$file_dirs['plugin'] = WP_PLUGIN_DIR;
self::$file_dirs['mu-vendor'] = WPMU_PLUGIN_DIR . '/vendor';
self::$file_dirs['go-plugin'] = WPMU_PLUGIN_DIR . '/shared-plugins';
self::$file_dirs['mu-plugin'] = WPMU_PLUGIN_DIR;
self::$file_dirs['vip-plugin'] = get_theme_root() . '/vip/plugins';
if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) {
self::$file_dirs['vip-client-mu-plugin'] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR;
}
self::$file_dirs['theme'] = null;
self::$file_dirs['stylesheet'] = get_stylesheet_directory();
self::$file_dirs['template'] = get_template_directory();
self::$file_dirs['other'] = WP_CONTENT_DIR;
self::$file_dirs['core'] = ABSPATH;
self::$file_dirs['unknown'] = null;
foreach ( self::$file_dirs as $type => $dir ) {
self::$file_dirs[ $type ] = self::standard_dir( $dir );
}
}
return self::$file_dirs;
}
public static function get_file_component( $file ) {
# @TODO turn this into a class (eg QM_File_Component)
$file = self::standard_dir( $file );
if ( isset( self::$file_components[ $file ] ) ) {
return self::$file_components[ $file ];
}
foreach ( self::get_file_dirs() as $type => $dir ) {
// this slash makes paths such as plugins-mu match mu-plugin not plugin
if ( $dir && ( 0 === strpos( $file, trailingslashit( $dir ) ) ) ) {
break;
}
}
$context = $type;
switch ( $type ) {
case 'plugin':
case 'mu-plugin':
case 'mu-vendor':
$plug = str_replace( '/vendor/', '/', $file );
$plug = plugin_basename( $plug );
if ( strpos( $plug, '/' ) ) {
$plug = explode( '/', $plug );
$plug = reset( $plug );
} else {
$plug = basename( $plug );
}
if ( 'plugin' !== $type ) {
/* translators: %s: Plugin name */
$name = sprintf( __( 'MU Plugin: %s', 'query-monitor' ), $plug );
} else {
/* translators: %s: Plugin name */
$name = sprintf( __( 'Plugin: %s', 'query-monitor' ), $plug );
}
$context = $plug;
break;
case 'go-plugin':
case 'vip-plugin':
case 'vip-client-mu-plugin':
$plug = str_replace( self::$file_dirs[ $type ], '', $file );
$plug = trim( $plug, '/' );
if ( strpos( $plug, '/' ) ) {
$plug = explode( '/', $plug );
$plug = reset( $plug );
} else {
$plug = basename( $plug );
}
if ( 'vip-client-mu-plugin' === $type ) {
/* translators: %s: Plugin name */
$name = sprintf( __( 'VIP Client MU Plugin: %s', 'query-monitor' ), $plug );
} else {
/* translators: %s: Plugin name */
$name = sprintf( __( 'VIP Plugin: %s', 'query-monitor' ), $plug );
}
$context = $plug;
break;
case 'stylesheet':
if ( is_child_theme() ) {
$name = __( 'Child Theme', 'query-monitor' );
} else {
$name = __( 'Theme', 'query-monitor' );
}
$type = 'theme';
break;
case 'template':
$name = __( 'Parent Theme', 'query-monitor' );
$type = 'theme';
break;
case 'other':
// Anything else that's within the content directory should appear as
// `wp-content/{dir}` or `wp-content/{file}`
$name = self::standard_dir( $file );
$name = str_replace( dirname( self::$file_dirs['other'] ), '', $name );
$parts = explode( '/', trim( $name, '/' ) );
$name = $parts[0] . '/' . $parts[1];
$context = $file;
break;
case 'core':
$name = __( 'Core', 'query-monitor' );
break;
case 'unknown':
default:
$name = __( 'Unknown', 'query-monitor' );
/**
* Filters the name of a custom or unknown component.
*
* The dynamic portion of the hook name, `$type`, refers to the component identifier.
*
* See the corresponding `qm/component_dirs` filter for specifying the component directories.
*
* @since 3.6.0
*
* @param string $name The component name.
* @param string $file The full file path for the file within the component.
*/
$name = apply_filters( "qm/component_name/{$type}", $name, $file );
break;
}
self::$file_components[ $file ] = (object) compact( 'type', 'name', 'context' );
return self::$file_components[ $file ];
}
public static function populate_callback( array $callback ) {
if ( is_string( $callback['function'] ) && ( false !== strpos( $callback['function'], '::' ) ) ) {
$callback['function'] = explode( '::', $callback['function'] );
}
if ( isset( $callback['class'] ) ) {
$callback['function'] = array(
$callback['class'],
$callback['function'],
);
}
try {
if ( is_array( $callback['function'] ) ) {
if ( is_object( $callback['function'][0] ) ) {
$class = get_class( $callback['function'][0] );
$access = '->';
} else {
$class = $callback['function'][0];
$access = '::';
}
$callback['name'] = self::shorten_fqn( $class . $access . $callback['function'][1] ) . '()';
$ref = new ReflectionMethod( $class, $callback['function'][1] );
} elseif ( is_object( $callback['function'] ) ) {
if ( is_a( $callback['function'], 'Closure' ) ) {
$ref = new ReflectionFunction( $callback['function'] );
$file = self::standard_dir( $ref->getFileName(), '' );
if ( 0 === strpos( $file, '/' ) ) {
$file = basename( $ref->getFileName() );
}
/* translators: 1: Line number, 2: File name */
$callback['name'] = sprintf( __( 'Closure on line %1$d of %2$s', 'query-monitor' ), $ref->getStartLine(), $file );
} else {
// the object should have a __invoke() method
$class = get_class( $callback['function'] );
$callback['name'] = self::shorten_fqn( $class ) . '->__invoke()';
$ref = new ReflectionMethod( $class, '__invoke' );
}
} else {
$callback['name'] = self::shorten_fqn( $callback['function'] ) . '()';
$ref = new ReflectionFunction( $callback['function'] );
}
$callback['file'] = $ref->getFileName();
$callback['line'] = $ref->getStartLine();
// https://github.com/facebook/hhvm/issues/5856
$name = trim( $ref->getName() );
if ( '__lambda_func' === $name || 0 === strpos( $name, 'lambda_' ) ) {
if ( preg_match( '|(?P<file>.*)\((?P<line>[0-9]+)\)|', $callback['file'], $matches ) ) {
$callback['file'] = $matches['file'];
$callback['line'] = $matches['line'];
$file = trim( self::standard_dir( $callback['file'], '' ), '/' );
/* translators: 1: Line number, 2: File name */
$callback['name'] = sprintf( __( 'Anonymous function on line %1$d of %2$s', 'query-monitor' ), $callback['line'], $file );
} else {
// https://github.com/facebook/hhvm/issues/5807
unset( $callback['line'], $callback['file'] );
$callback['name'] = $name . '()';
$callback['error'] = new WP_Error( 'unknown_lambda', __( 'Unable to determine source of lambda function', 'query-monitor' ) );
}
}
if ( ! empty( $callback['file'] ) ) {
$callback['component'] = self::get_file_component( $callback['file'] );
} else {
$callback['component'] = (object) array(
'type' => 'php',
'name' => 'PHP',
'context' => '',
);
}
} catch ( ReflectionException $e ) {
$callback['error'] = new WP_Error( 'reflection_exception', $e->getMessage() );
}
return $callback;
}
public static function is_ajax() {
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return true;
}
return false;
}
public static function is_async() {
if ( self::is_ajax() ) {
return true;
}
if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && 'xmlhttprequest' === strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) { // phpcs:ignore
return true;
}
return false;
}
public static function get_admins() {
if ( is_multisite() ) {
return false;
} else {
return get_role( 'administrator' );
}
}
public static function is_multi_network() {
global $wpdb;
if ( function_exists( 'is_multi_network' ) ) {
return is_multi_network();
}
if ( ! is_multisite() ) {
return false;
}
// phpcs:disable
$num_sites = $wpdb->get_var( "
SELECT COUNT(*)
FROM {$wpdb->site}
" );
// phpcs:enable
return ( $num_sites > 1 );
}
public static function get_client_version( $client ) {
$client = intval( $client );
$hello = $client % 10000;
$major = intval( floor( $client / 10000 ) );
$minor = intval( floor( $hello / 100 ) );
$patch = intval( $hello % 100 );
return compact( 'major', 'minor', 'patch' );
}
public static function get_query_type( $sql ) {
// Trim leading whitespace and brackets
$sql = ltrim( $sql, ' \t\n\r\0\x0B(' );
if ( 0 === strpos( $sql, '/*' ) ) {
// Strip out leading comments such as `/*NO_SELECT_FOUND_ROWS*/` before calculating the query type
$sql = preg_replace( '|^/\*[^\*/]+\*/|', '', $sql );
}
$words = preg_split( '/\b/', trim( $sql ), 2, PREG_SPLIT_NO_EMPTY );
$type = strtoupper( $words[0] );
return $type;
}
public static function display_variable( $value ) {
if ( is_string( $value ) ) {
return $value;
} elseif ( is_bool( $value ) ) {
return ( $value ) ? 'true' : 'false';
} elseif ( is_scalar( $value ) ) {
return $value;
} elseif ( is_object( $value ) ) {
$class = get_class( $value );
switch ( true ) {
case ( $value instanceof WP_Post ):
case ( $value instanceof WP_User ):
return sprintf( '%s (ID: %s)', $class, $value->ID );
break;
case ( $value instanceof WP_Term ):
return sprintf( '%s (term_id: %s)', $class, $value->term_id );
break;
case ( $value instanceof WP_Comment ):
return sprintf( '%s (comment_ID: %s)', $class, $value->comment_ID );
break;
case ( $value instanceof WP_Error ):
return sprintf( '%s (%s)', $class, $value->get_error_code() );
break;
case ( $value instanceof WP_Role ):
case ( $value instanceof WP_Post_Type ):
case ( $value instanceof WP_Taxonomy ):
return sprintf( '%s (%s)', $class, $value->name );
break;
case ( $value instanceof WP_Network ):
return sprintf( '%s (id: %s)', $class, $value->id );
break;
case ( $value instanceof WP_Site ):
return sprintf( '%s (blog_id: %s)', $class, $value->blog_id );
break;
case ( $value instanceof WP_Theme ):
return sprintf( '%s (%s)', $class, $value->get_stylesheet() );
break;
default:
return $class;
break;
}
} else {
return gettype( $value );
}
}
/**
* Shortens a fully qualified name to reduce the length of the names of long namespaced symbols.
*
* This initialises portions that do not form the first or last portion of the name. For example:
*
* Inpsyde\Wonolog\HookListener\HookListenersRegistry->hook_callback()
*
* becomes:
*
* Inpsyde\W\H\HookListenersRegistry->hook_callback()
*
* @param string $fqn A fully qualified name.
* @return string A shortened version of the name.
*/
public static function shorten_fqn( $fqn ) {
return preg_replace_callback( '#\\\\[a-zA-Z0-9_\\\\]{4,}\\\\#', function( array $matches ) {
preg_match_all( '#\\\\([a-zA-Z0-9_])#', $matches[0], $m );
return '\\' . implode( '\\', $m[1] ) . '\\';
}, $fqn );
}
/**
* Helper function for JSON encoding data and formatting it in a consistent and compatible manner.
*
* @param mixed $data The data to be JSON encoded.
* @return string The JSON encoded data.
*/
public static function json_format( $data ) {
$json_options = JSON_PRETTY_PRINT;
if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
$json_options |= JSON_UNESCAPED_SLASHES;
}
$json = json_encode( $data, $json_options );
if ( ! defined( 'JSON_UNESCAPED_SLASHES' ) ) {
$json = wp_unslash( $json );
}
return $json;
}
public static function is_stringy( $data ) {
return ( is_string( $data ) || ( is_object( $data ) && method_exists( $data, '__toString' ) ) );
}
public static function sort( array &$array, $field ) {
self::$sort_field = $field;
usort( $array, array( __CLASS__, '_sort' ) );
}
public static function rsort( array &$array, $field ) {
self::$sort_field = $field;
usort( $array, array( __CLASS__, '_rsort' ) );
}
private static function _rsort( $a, $b ) {
$field = self::$sort_field;
if ( $a[ $field ] === $b[ $field ] ) {
return 0;
} else {
return ( $a[ $field ] > $b[ $field ] ) ? -1 : 1;
}
}
private static function _sort( $a, $b ) {
$field = self::$sort_field;
if ( $a[ $field ] === $b[ $field ] ) {
return 0;
} else {
return ( $a[ $field ] > $b[ $field ] ) ? 1 : -1;
}
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Mock 'Debug Bar' plugin class.
*
* @package query-monitor
*/
class Debug_Bar {
public $panels = array();
public function __construct() {
add_action( 'wp_head', array( $this, 'ensure_ajaxurl' ), 1 );
$this->enqueue();
$this->init_panels();
}
public function enqueue() {
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_register_style( 'debug-bar', false, array(
'query-monitor',
) );
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_register_script( 'debug-bar', false, array(
'query-monitor',
) );
/**
* Fires after scripts have been enqueued. This mimics the same action fired in the Debug Bar plugin.
*
* @since 2.7.0
*/
do_action( 'debug_bar_enqueue_scripts' );
}
public function init_panels() {
require_once 'debug_bar_panel.php';
/**
* Filters the debug bar panel list. This mimics the same filter called in the Debug Bar plugin.
*
* @since 2.7.0
*
* @param Debug_Bar_Panel[] $panels Array of Debug Bar panel instances.
*/
$this->panels = apply_filters( 'debug_bar_panels', array() );
}
public function ensure_ajaxurl() {
$dispatcher = QM_Dispatchers::get( 'html' );
if ( $this->panels && $dispatcher::user_can_view() ) {
?>
<script type="text/javascript">
var ajaxurl = '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>';
</script>
<?php
}
}
public function Debug_Bar() {
self::__construct();
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Mock 'Debug Bar' panel class.
*
* @package query-monitor
*/
abstract class Debug_Bar_Panel {
public $_title = '';
public $_visible = true;
public function __construct( $title = '' ) {
$this->title( $title );
if ( $this->init() === false ) {
$this->set_visible( false );
return;
}
# @TODO convert to QM classes
add_filter( 'debug_bar_classes', array( $this, 'debug_bar_classes' ) );
}
/**
* Initializes the panel.
*/
public function init() {}
public function prerender() {}
/**
* Renders the panel.
*/
public function render() {}
public function is_visible() {
return $this->_visible;
}
public function set_visible( $visible ) {
$this->_visible = $visible;
}
public function title( $title = null ) {
if ( ! isset( $title ) ) {
return $this->_title;
}
$this->_title = $title;
}
public function debug_bar_classes( $classes ) {
return $classes;
}
public function Debug_Bar_Panel( $title = '' ) {
self::__construct( $title );
}
}

View File

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

View File

@@ -0,0 +1,259 @@
<?php
/**
* Enqueued scripts and styles collector.
*
* @package query-monitor
*/
abstract class QM_Collector_Assets extends QM_Collector {
public function __construct() {
parent::__construct();
add_action( 'admin_print_footer_scripts', array( $this, 'action_print_footer_scripts' ) );
add_action( 'wp_print_footer_scripts', array( $this, 'action_print_footer_scripts' ) );
add_action( 'admin_head', array( $this, 'action_head' ), 9999 );
add_action( 'wp_head', array( $this, 'action_head' ), 9999 );
add_action( 'login_head', array( $this, 'action_head' ), 9999 );
add_action( 'embed_head', array( $this, 'action_head' ), 9999 );
}
abstract public function get_dependency_type();
public function action_head() {
$type = $this->get_dependency_type();
$this->data['header'] = $GLOBALS[ "wp_{$type}" ]->done;
}
public function action_print_footer_scripts() {
if ( empty( $this->data['header'] ) ) {
return;
}
$type = $this->get_dependency_type();
$this->data['footer'] = array_diff( $GLOBALS[ "wp_{$type}" ]->done, $this->data['header'] );
}
public function process() {
if ( empty( $this->data['header'] ) && empty( $this->data['footer'] ) ) {
return;
}
$this->data['is_ssl'] = is_ssl();
$this->data['host'] = wp_unslash( $_SERVER['HTTP_HOST'] );
$this->data['default_version'] = get_bloginfo( 'version' );
$home_url = home_url();
$positions = array(
'missing',
'broken',
'header',
'footer',
);
$this->data['counts'] = array(
'missing' => 0,
'broken' => 0,
'header' => 0,
'footer' => 0,
'total' => 0,
);
$type = $this->get_dependency_type();
foreach ( array( 'header', 'footer' ) as $position ) {
if ( empty( $this->data[ $position ] ) ) {
$this->data[ $position ] = array();
}
}
$raw = $GLOBALS[ "wp_{$type}" ];
$broken = array_values( array_diff( $raw->queue, $raw->done ) );
$missing = array_values( array_diff( $raw->queue, array_keys( $raw->registered ) ) );
// A broken asset is one which has been deregistered without also being dequeued
if ( ! empty( $broken ) ) {
foreach ( $broken as $key => $handle ) {
$item = $raw->query( $handle );
if ( $item ) {
$broken = array_merge( $broken, self::get_broken_dependencies( $item, $raw ) );
} else {
unset( $broken[ $key ] );
$missing[] = $handle;
}
}
if ( ! empty( $broken ) ) {
$this->data['broken'] = array_unique( $broken );
}
}
// A missing asset is one which has been enqueued with dependencies that don't exist
if ( ! empty( $missing ) ) {
$this->data['missing'] = array_unique( $missing );
foreach ( $this->data['missing'] as $handle ) {
$raw->add( $handle, false );
$key = array_search( $handle, $raw->done, true );
if ( false !== $key ) {
unset( $raw->done[ $key ] );
}
}
}
$all_dependencies = array();
$all_dependents = array();
$missing_dependencies = array();
foreach ( $positions as $position ) {
if ( empty( $this->data[ $position ] ) ) {
continue;
}
foreach ( $this->data[ $position ] as $handle ) {
$dependency = $raw->query( $handle );
if ( ! $dependency ) {
continue;
}
$all_dependencies = array_merge( $all_dependencies, $dependency->deps );
$dependents = $this->get_dependents( $dependency, $raw );
$all_dependents = array_merge( $all_dependents, $dependents );
list( $host, $source, $local ) = $this->get_dependency_data( $dependency );
if ( empty( $dependency->ver ) ) {
$ver = '';
} else {
$ver = $dependency->ver;
}
$warning = ! in_array( $handle, $raw->done, true );
if ( is_wp_error( $source ) ) {
$display = $source->get_error_message();
} else {
$display = ltrim( str_replace( $home_url, '', remove_query_arg( 'ver', $source ) ), '/' );
}
$dependencies = $dependency->deps;
foreach ( $dependencies as & $dep ) {
if ( ! $raw->query( $dep ) ) {
// A missing dependency is a dependecy on an asset that doesn't exist
$missing_dependencies[ $dep ] = true;
}
}
$this->data['assets'][ $position ][ $handle ] = array(
'host' => $host,
'source' => $source,
'local' => $local,
'ver' => $ver,
'warning' => $warning,
'display' => $display,
'dependents' => $dependents,
'dependencies' => $dependencies,
);
$this->data['counts'][ $position ]++;
$this->data['counts']['total']++;
}
}
unset( $this->data[ $position ] );
$all_dependencies = array_unique( $all_dependencies );
sort( $all_dependencies );
$this->data['dependencies'] = $all_dependencies;
$all_dependents = array_unique( $all_dependents );
sort( $all_dependents );
$this->data['dependents'] = $all_dependents;
$this->data['missing_dependencies'] = $missing_dependencies;
}
protected static function get_broken_dependencies( _WP_Dependency $item, WP_Dependencies $dependencies ) {
$broken = array();
foreach ( $item->deps as $handle ) {
$dep = $dependencies->query( $handle );
if ( $dep ) {
$broken = array_merge( $broken, self::get_broken_dependencies( $dep, $dependencies ) );
} else {
$broken[] = $item->handle;
}
}
return $broken;
}
public function get_dependents( _WP_Dependency $dependency, WP_Dependencies $dependencies ) {
$dependents = array();
$handles = array_unique( array_merge( $dependencies->queue, $dependencies->done ) );
foreach ( $handles as $handle ) {
$item = $dependencies->query( $handle );
if ( $item ) {
if ( in_array( $dependency->handle, $item->deps, true ) ) {
$dependents[] = $handle;
}
}
}
sort( $dependents );
return $dependents;
}
public function get_dependency_data( _WP_Dependency $dependency ) {
$data = $this->get_data();
$loader = rtrim( $this->get_dependency_type(), 's' );
$src = $dependency->src;
if ( null === $dependency->ver ) {
$ver = '';
} else {
$ver = $dependency->ver ? $dependency->ver : $this->data['default_version'];
}
if ( ! empty( $src ) && ! empty( $ver ) ) {
$src = add_query_arg( 'ver', $ver, $src );
}
/** This filter is documented in wp-includes/class.wp-scripts.php */
$source = apply_filters( "{$loader}_loader_src", $src, $dependency->handle );
$host = (string) parse_url( $source, PHP_URL_HOST );
$scheme = (string) parse_url( $source, PHP_URL_SCHEME );
$http_host = $data['host'];
if ( empty( $host ) && ! empty( $http_host ) ) {
$host = $http_host;
}
if ( $scheme && $data['is_ssl'] && ( 'https' !== $scheme ) && ( 'localhost' !== $host ) ) {
$source = new WP_Error( 'qm_insecure_content', __( 'Insecure content', 'query-monitor' ), array(
'src' => $source,
) );
}
if ( is_wp_error( $source ) ) {
$error_data = $source->get_error_data();
if ( $error_data && isset( $error_data['src'] ) ) {
$host = (string) parse_url( $error_data['src'], PHP_URL_HOST );
}
} elseif ( empty( $source ) ) {
$source = '';
$host = '';
}
$local = ( $http_host === $host );
return array( $host, $source, $local );
}
}

View File

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

View File

@@ -0,0 +1,30 @@
<?php
/**
* Enqueued styles collector.
*
* @package query-monitor
*/
class QM_Collector_Assets_Styles extends QM_Collector_Assets {
public $id = 'assets_styles';
public function get_dependency_type() {
return 'styles';
}
public function get_concerned_filters() {
return array(
'print_styles_array',
'style_loader_src',
'style_loader_tag',
);
}
}
function register_qm_collector_assets_styles( array $collectors, QueryMonitor $qm ) {
$collectors['assets_styles'] = new QM_Collector_Assets_Styles();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_assets_styles', 10, 2 );

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
<?php
/**
* Database query calling function collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Callers extends QM_Collector {
public $id = 'db_callers';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
if ( isset( $dbq->data['times'] ) ) {
$this->data['times'] = $dbq->data['times'];
QM_Util::rsort( $this->data['times'], 'ltime' );
}
if ( isset( $dbq->data['types'] ) ) {
$this->data['types'] = $dbq->data['types'];
}
}
}
}
function register_qm_collector_db_callers( array $collectors, QueryMonitor $qm ) {
$collectors['db_callers'] = new QM_Collector_DB_Callers();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_callers', 20, 2 );

View File

@@ -0,0 +1,34 @@
<?php
/**
* Database query calling component collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Components extends QM_Collector {
public $id = 'db_components';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
if ( isset( $dbq->data['component_times'] ) ) {
$this->data['times'] = $dbq->data['component_times'];
QM_Util::rsort( $this->data['times'], 'ltime' );
}
if ( isset( $dbq->data['types'] ) ) {
$this->data['types'] = $dbq->data['types'];
}
}
}
}
function register_qm_collector_db_components( array $collectors, QueryMonitor $qm ) {
$collectors['db_components'] = new QM_Collector_DB_Components();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_components', 20, 2 );

View File

@@ -0,0 +1,103 @@
<?php
/**
* Duplicate database query collector.
*
* @package query-monitor
*/
class QM_Collector_DB_Dupes extends QM_Collector {
public $id = 'db_dupes';
public function process() {
$dbq = QM_Collectors::get( 'db_queries' );
if ( ! $dbq ) {
return;
}
if ( ! isset( $dbq->data['dupes'] ) ) {
return;
}
// Filter out SQL queries that do not have dupes
$this->data['dupes'] = array_filter( $dbq->data['dupes'], array( $this, '_filter_dupe_queries' ) );
// Ignore dupes from `WP_Query->set_found_posts()`
unset( $this->data['dupes']['SELECT FOUND_ROWS()'] );
$stacks = array();
$tops = array();
$callers = array();
$components = array();
// Loop over all SQL queries that have dupes
foreach ( $this->data['dupes'] as $sql => $query_ids ) {
// Loop over each query
foreach ( $query_ids as $query_id ) {
if ( isset( $dbq->data['dbs']['$wpdb']->rows[ $query_id ]['trace'] ) ) {
$trace = $dbq->data['dbs']['$wpdb']->rows[ $query_id ]['trace'];
$stack = wp_list_pluck( $trace->get_filtered_trace(), 'id' );
$component = $trace->get_component();
// Populate the component counts for this query
if ( isset( $components[ $sql ][ $component->name ] ) ) {
$components[ $sql ][ $component->name ]++;
} else {
$components[ $sql ][ $component->name ] = 1;
}
} else {
$stack = array_reverse( explode( ', ', $dbq->data['dbs']['$wpdb']->rows[ $query_id ]['stack'] ) );
}
// Populate the caller counts for this query
if ( isset( $callers[ $sql ][ $stack[0] ] ) ) {
$callers[ $sql ][ $stack[0] ]++;
} else {
$callers[ $sql ][ $stack[0] ] = 1;
}
// Populate the stack for this query
$stacks[ $sql ][] = $stack;
}
// Get the callers which are common to all stacks for this query
$common = call_user_func_array( 'array_intersect', $stacks[ $sql ] );
// Remove callers which are common to all stacks for this query
foreach ( $stacks[ $sql ] as $i => $stack ) {
$stacks[ $sql ][ $i ] = array_values( array_diff( $stack, $common ) );
// No uncommon callers within the stack? Just use the topmost caller.
if ( empty( $stacks[ $sql ][ $i ] ) ) {
$stacks[ $sql ][ $i ] = array_keys( $callers[ $sql ] );
}
}
// Wave a magic wand
$sources[ $sql ] = array_count_values( wp_list_pluck( $stacks[ $sql ], 0 ) );
}
if ( ! empty( $sources ) ) {
$this->data['dupe_sources'] = $sources;
$this->data['dupe_callers'] = $callers;
$this->data['dupe_components'] = $components;
}
}
public function _filter_dupe_queries( $queries ) {
return ( count( $queries ) > 1 );
}
}
function register_qm_collector_db_dupes( array $collectors, QueryMonitor $qm ) {
$collectors['db_dupes'] = new QM_Collector_DB_Dupes();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_dupes', 25, 2 );

View File

@@ -0,0 +1,241 @@
<?php
/**
* Database query collector.
*
* @package query-monitor
*/
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
if ( ! defined( 'QM_DB_EXPENSIVE' ) ) {
define( 'QM_DB_EXPENSIVE', 0.05 );
}
if ( SAVEQUERIES && property_exists( $GLOBALS['wpdb'], 'save_queries' ) ) {
$GLOBALS['wpdb']->save_queries = true;
}
class QM_Collector_DB_Queries extends QM_Collector {
public $id = 'db_queries';
public $db_objects = array();
public function get_errors() {
if ( ! empty( $this->data['errors'] ) ) {
return $this->data['errors'];
}
return false;
}
public function get_expensive() {
if ( ! empty( $this->data['expensive'] ) ) {
return $this->data['expensive'];
}
return false;
}
public static function is_expensive( array $row ) {
return $row['ltime'] > QM_DB_EXPENSIVE;
}
public function process() {
$this->data['total_qs'] = 0;
$this->data['total_time'] = 0;
$this->data['errors'] = array();
/**
* Filters the `wpdb` instances that are exposed to QM.
*
* This allows Query Monitor to display multiple instances of `wpdb` on one page load.
*
* @since 2.7.0
*
* @param wpdb[] $db_objects Array of `wpdb` instances, keyed by their name.
*/
$this->db_objects = apply_filters( 'qm/collect/db_objects', array(
'$wpdb' => $GLOBALS['wpdb'],
) );
foreach ( $this->db_objects as $name => $db ) {
if ( is_a( $db, 'wpdb' ) ) {
$this->process_db_object( $name, $db );
} else {
unset( $this->db_objects[ $name ] );
}
}
}
protected function log_caller( $caller, $ltime, $type ) {
if ( ! isset( $this->data['times'][ $caller ] ) ) {
$this->data['times'][ $caller ] = array(
'caller' => $caller,
'ltime' => 0,
'types' => array(),
);
}
$this->data['times'][ $caller ]['ltime'] += $ltime;
if ( isset( $this->data['times'][ $caller ]['types'][ $type ] ) ) {
$this->data['times'][ $caller ]['types'][ $type ]++;
} else {
$this->data['times'][ $caller ]['types'][ $type ] = 1;
}
}
public function process_db_object( $id, wpdb $db ) {
global $EZSQL_ERROR, $wp_the_query;
// With SAVEQUERIES defined as false, `wpdb::queries` is empty but `wpdb::num_queries` is not.
if ( empty( $db->queries ) ) {
$this->data['total_qs'] += $db->num_queries;
return;
}
$rows = array();
$types = array();
$total_time = 0;
$has_result = false;
$has_trace = false;
$i = 0;
$request = trim( $wp_the_query->request );
if ( method_exists( $db, 'remove_placeholder_escape' ) ) {
$request = $db->remove_placeholder_escape( $request );
}
foreach ( (array) $db->queries as $query ) {
# @TODO: decide what I want to do with this:
if ( false !== strpos( $query[2], 'wp_admin_bar' ) and !isset( $_REQUEST['qm_display_admin_bar'] ) ) { // phpcs:ignore
continue;
}
$sql = $query[0];
$ltime = $query[1];
$stack = $query[2];
$has_start = isset( $query[3] );
$has_trace = isset( $query['trace'] );
$has_result = isset( $query['result'] );
if ( $has_result ) {
$result = $query['result'];
} else {
$result = null;
}
$total_time += $ltime;
if ( $has_trace ) {
$trace = $query['trace'];
$component = $query['trace']->get_component();
$caller = $query['trace']->get_caller();
$caller_name = $caller['display'];
$caller = $caller['display'];
} else {
$trace = null;
$component = null;
$callers = explode( ',', $stack );
$caller = trim( end( $callers ) );
if ( false !== strpos( $caller, '(' ) ) {
$caller_name = substr( $caller, 0, strpos( $caller, '(' ) ) . '()';
} else {
$caller_name = $caller;
}
}
$sql = trim( $sql );
$type = QM_Util::get_query_type( $sql );
$this->log_type( $type );
$this->log_caller( $caller_name, $ltime, $type );
$this->maybe_log_dupe( $sql, $i );
if ( $component ) {
$this->log_component( $component, $ltime, $type );
}
if ( ! isset( $types[ $type ]['total'] ) ) {
$types[ $type ]['total'] = 1;
} else {
$types[ $type ]['total']++;
}
if ( ! isset( $types[ $type ]['callers'][ $caller ] ) ) {
$types[ $type ]['callers'][ $caller ] = 1;
} else {
$types[ $type ]['callers'][ $caller ]++;
}
$is_main_query = ( $request === $sql && ( false !== strpos( $stack, ' WP->main,' ) ) );
$row = compact( 'caller', 'caller_name', 'sql', 'ltime', 'result', 'type', 'component', 'trace', 'is_main_query' );
if ( ! isset( $trace ) ) {
$row['stack'] = $stack;
}
if ( is_wp_error( $result ) ) {
$this->data['errors'][] = $row;
}
if ( self::is_expensive( $row ) ) {
$this->data['expensive'][] = $row;
}
$rows[ $i ] = $row;
$i++;
}
if ( '$wpdb' === $id && ! $has_result && ! empty( $EZSQL_ERROR ) && is_array( $EZSQL_ERROR ) ) {
// Fallback for displaying database errors when wp-content/db.php isn't in place
foreach ( $EZSQL_ERROR as $error ) {
$row = array(
'caller' => null,
'caller_name' => null,
'stack' => '',
'sql' => $error['query'],
'ltime' => 0,
'result' => new WP_Error( 'qmdb', $error['error_str'] ),
'type' => '',
'component' => false,
'trace' => null,
'is_main_query' => false,
);
$this->data['errors'][] = $row;
}
}
$total_qs = count( $rows );
$this->data['total_qs'] += $total_qs;
$this->data['total_time'] += $total_time;
$has_main_query = wp_list_filter( $rows, array(
'is_main_query' => true,
) );
# @TODO put errors in here too:
# @TODO proper class instead of (object)
$this->data['dbs'][ $id ] = (object) compact( 'rows', 'types', 'has_result', 'has_trace', 'total_time', 'total_qs', 'has_main_query' );
}
}
function register_qm_collector_db_queries( array $collectors, QueryMonitor $qm ) {
$collectors['db_queries'] = new QM_Collector_DB_Queries();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_db_queries', 10, 2 );

View File

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

View File

@@ -0,0 +1,309 @@
<?php
/**
* Environment data collector.
*
* @package query-monitor
*/
class QM_Collector_Environment extends QM_Collector {
public $id = 'environment';
protected $php_vars = array(
'max_execution_time',
'memory_limit',
'upload_max_filesize',
'post_max_size',
'display_errors',
'log_errors',
);
public function __construct() {
global $wpdb;
parent::__construct();
# If QM_DB is in place then we'll use the values which were
# caught early before any plugins had a chance to alter them
foreach ( $this->php_vars as $setting ) {
if ( isset( $wpdb->qm_php_vars ) && isset( $wpdb->qm_php_vars[ $setting ] ) ) {
$val = $wpdb->qm_php_vars[ $setting ];
} else {
$val = ini_get( $setting );
}
$this->data['php']['variables'][ $setting ]['before'] = $val;
}
}
protected static function get_error_levels( $error_reporting ) {
$levels = array(
'E_ERROR' => false,
'E_WARNING' => false,
'E_PARSE' => false,
'E_NOTICE' => false,
'E_CORE_ERROR' => false,
'E_CORE_WARNING' => false,
'E_COMPILE_ERROR' => false,
'E_COMPILE_WARNING' => false,
'E_USER_ERROR' => false,
'E_USER_WARNING' => false,
'E_USER_NOTICE' => false,
'E_STRICT' => false,
'E_RECOVERABLE_ERROR' => false,
'E_DEPRECATED' => false,
'E_USER_DEPRECATED' => false,
'E_ALL' => false,
);
foreach ( $levels as $level => $reported ) {
if ( defined( $level ) ) {
$c = constant( $level );
if ( $error_reporting & $c ) {
$levels[ $level ] = true;
}
}
}
return $levels;
}
public function process() {
global $wp_version;
$mysql_vars = array(
'key_buffer_size' => true, # Key cache size limit
'max_allowed_packet' => false, # Individual query size limit
'max_connections' => false, # Max number of client connections
'query_cache_limit' => true, # Individual query cache size limit
'query_cache_size' => true, # Total cache size limit
'query_cache_type' => 'ON', # Query cache on or off
);
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
foreach ( $dbq->db_objects as $id => $db ) {
if ( method_exists( $db, 'db_version' ) ) {
$server = $db->db_version();
// query_cache_* deprecated since MySQL 5.7.20
if ( version_compare( $server, '5.7.20', '>=' ) ) {
unset( $mysql_vars['query_cache_limit'], $mysql_vars['query_cache_size'], $mysql_vars['query_cache_type'] );
}
} else {
$server = null;
}
$variables = $db->get_results( "
SHOW VARIABLES
WHERE Variable_name IN ( '" . implode( "', '", array_keys( $mysql_vars ) ) . "' )
" );
if ( is_resource( $db->dbh ) ) {
# Old mysql extension
$extension = 'mysql';
} elseif ( is_object( $db->dbh ) ) {
# mysqli or PDO
$extension = get_class( $db->dbh );
} else {
# Who knows?
$extension = null;
}
if ( isset( $db->use_mysqli ) && $db->use_mysqli ) {
$client = mysqli_get_client_version();
$info = mysqli_get_server_info( $db->dbh );
} else {
// Please do not report this code as a PHP 7 incompatibility. Observe the surrounding logic.
// phpcs:ignore
if ( preg_match( '|[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}|', mysql_get_client_info(), $matches ) ) {
$client = $matches[0];
} else {
$client = null;
}
// Please do not report this code as a PHP 7 incompatibility. Observe the surrounding logic.
// phpcs:ignore
$info = mysql_get_server_info( $db->dbh );
}
if ( $client ) {
$client_version = implode( '.', QM_Util::get_client_version( $client ) );
$client_version = sprintf( '%s (%s)', $client, $client_version );
} else {
$client_version = null;
}
$info = array(
'server-version' => $server,
'extension' => $extension,
'client-version' => $client_version,
'user' => $db->dbuser,
'host' => $db->dbhost,
'database' => $db->dbname,
);
$this->data['db'][ $id ] = array(
'info' => $info,
'vars' => $mysql_vars,
'variables' => $variables,
);
}
}
$this->data['php']['version'] = phpversion();
$this->data['php']['sapi'] = php_sapi_name();
$this->data['php']['user'] = self::get_current_user();
$this->data['php']['old'] = version_compare( $this->data['php']['version'], 7.2, '<' );
foreach ( $this->php_vars as $setting ) {
$this->data['php']['variables'][ $setting ]['after'] = ini_get( $setting );
}
if ( defined( 'SORT_FLAG_CASE' ) ) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants
$sort_flags = SORT_STRING | SORT_FLAG_CASE;
} else {
$sort_flags = SORT_STRING;
}
if ( is_callable( 'get_loaded_extensions' ) ) {
$extensions = get_loaded_extensions();
sort( $extensions, $sort_flags );
$this->data['php']['extensions'] = array_combine( $extensions, array_map( array( $this, 'get_extension_version' ), $extensions ) );
} else {
$this->data['php']['extensions'] = array();
}
$this->data['php']['error_reporting'] = error_reporting();
$this->data['php']['error_levels'] = self::get_error_levels( $this->data['php']['error_reporting'] );
$this->data['wp']['version'] = $wp_version;
$constants = array(
'WP_DEBUG' => self::format_bool_constant( 'WP_DEBUG' ),
'WP_DEBUG_DISPLAY' => self::format_bool_constant( 'WP_DEBUG_DISPLAY' ),
'WP_DEBUG_LOG' => self::format_bool_constant( 'WP_DEBUG_LOG' ),
'SCRIPT_DEBUG' => self::format_bool_constant( 'SCRIPT_DEBUG' ),
'WP_CACHE' => self::format_bool_constant( 'WP_CACHE' ),
'CONCATENATE_SCRIPTS' => self::format_bool_constant( 'CONCATENATE_SCRIPTS' ),
'COMPRESS_SCRIPTS' => self::format_bool_constant( 'COMPRESS_SCRIPTS' ),
'COMPRESS_CSS' => self::format_bool_constant( 'COMPRESS_CSS' ),
'WP_ENVIRONMENT_TYPE' => self::format_bool_constant( 'WP_ENVIRONMENT_TYPE' ),
);
if ( function_exists( 'wp_get_environment_type' ) ) {
$this->data['wp']['environment_type'] = wp_get_environment_type();
}
$this->data['wp']['constants'] = apply_filters( 'qm/environment-constants', $constants );
if ( is_multisite() ) {
$this->data['wp']['constants']['SUNRISE'] = self::format_bool_constant( 'SUNRISE' );
}
if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
$server = explode( ' ', wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) );
$server = explode( '/', reset( $server ) );
} else {
$server = array( '' );
}
if ( isset( $server[1] ) ) {
$server_version = $server[1];
} else {
$server_version = null;
}
if ( isset( $_SERVER['SERVER_ADDR'] ) ) {
$address = wp_unslash( $_SERVER['SERVER_ADDR'] );
} else {
$address = null;
}
$this->data['server'] = array(
'name' => $server[0],
'version' => $server_version,
'address' => $address,
'host' => null,
'OS' => null,
);
if ( function_exists( 'php_uname' ) ) {
$this->data['server']['host'] = php_uname( 'n' );
$this->data['server']['OS'] = php_uname( 's' ) . ' ' . php_uname( 'r' );
}
}
public function get_extension_version( $extension ) {
// Nothing is simple in PHP. The exif and mysqlnd extensions (and probably others) add a bunch of
// crap to their version number, so we need to pluck out the first numeric value in the string.
$version = trim( phpversion( $extension ) );
if ( ! $version ) {
return $version;
}
$parts = explode( ' ', $version );
foreach ( $parts as $part ) {
if ( $part && is_numeric( $part[0] ) ) {
$version = $part;
break;
}
}
return $version;
}
protected static function get_current_user() {
$php_u = null;
if ( function_exists( 'posix_getpwuid' ) && function_exists( 'posix_getuid' ) && function_exists( 'posix_getgrgid' ) ) {
$u = posix_getpwuid( posix_getuid() );
if ( ! empty( $u ) && isset( $u['gid']) ) {
$g = posix_getgrgid( $u['gid'] );
if ( ! empty( $g ) && isset( $u['name'], $g['name'] ) ) {
$php_u = $u['name'] . ':' . $g['name'];
}
}
}
if ( empty( $php_u ) && isset( $_ENV['APACHE_RUN_USER'] ) ) {
$php_u = $_ENV['APACHE_RUN_USER'];
if ( isset( $_ENV['APACHE_RUN_GROUP'] ) ) {
$php_u .= ':' . $_ENV['APACHE_RUN_GROUP'];
}
}
if ( empty( $php_u ) && isset( $_SERVER['USER'] ) ) {
$php_u = wp_unslash( $_SERVER['USER'] );
}
if ( empty( $php_u ) && function_exists( 'exec' ) ) {
$php_u = exec( 'whoami' ); // phpcs:ignore
}
if ( empty( $php_u ) && function_exists( 'getenv' ) ) {
$php_u = getenv( 'USERNAME' );
}
return $php_u;
}
}
function register_qm_collector_environment( array $collectors, QueryMonitor $qm ) {
$collectors['environment'] = new QM_Collector_Environment();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_environment', 20, 2 );

View File

@@ -0,0 +1,57 @@
<?php
/**
* Hooks and actions collector.
*
* @package query-monitor
*/
class QM_Collector_Hooks extends QM_Collector {
public $id = 'hooks';
protected static $hide_core;
public function process() {
global $wp_actions, $wp_filter;
self::$hide_qm = self::hide_qm();
self::$hide_core = ( defined( 'QM_HIDE_CORE_ACTIONS' ) && QM_HIDE_CORE_ACTIONS );
$hooks = array();
$all_parts = array();
$components = array();
if ( has_filter( 'all' ) ) {
$hooks[] = QM_Hook::process( 'all', $wp_filter, self::$hide_qm, self::$hide_core );
}
if ( defined( 'QM_SHOW_ALL_HOOKS' ) && QM_SHOW_ALL_HOOKS ) {
// Show all hooks
$hook_names = array_keys( $wp_filter );
} else {
// Only show action hooks that have been called at least once
$hook_names = array_keys( $wp_actions );
}
foreach ( $hook_names as $name ) {
$hook = QM_Hook::process( $name, $wp_filter, self::$hide_qm, self::$hide_core );
$hooks[] = $hook;
$all_parts = array_merge( $all_parts, $hook['parts'] );
$components = array_merge( $components, $hook['components'] );
}
$this->data['hooks'] = $hooks;
$this->data['parts'] = array_unique( array_filter( $all_parts ) );
$this->data['components'] = array_unique( array_filter( $components ) );
usort( $this->data['parts'], 'strcasecmp' );
usort( $this->data['components'], 'strcasecmp' );
}
}
# Load early to catch all hooks
QM_Collectors::add( new QM_Collector_Hooks() );

View File

@@ -0,0 +1,285 @@
<?php
/**
* HTTP API request collector.
*
* @package query-monitor
*/
class QM_Collector_HTTP extends QM_Collector {
public $id = 'http';
private $transport = null;
private $info = null;
public function __construct() {
parent::__construct();
add_filter( 'http_request_args', array( $this, 'filter_http_request_args' ), 9999, 2 );
add_filter( 'pre_http_request', array( $this, 'filter_pre_http_request' ), 9999, 3 );
add_action( 'http_api_debug', array( $this, 'action_http_api_debug' ), 9999, 5 );
add_action( 'requests-curl.before_request', array( $this, 'action_curl_before_request' ), 9999 );
add_action( 'requests-curl.after_request', array( $this, 'action_curl_after_request' ), 9999, 2 );
add_action( 'requests-fsockopen.before_request', array( $this, 'action_fsockopen_before_request' ), 9999 );
add_action( 'requests-fsockopen.after_request', array( $this, 'action_fsockopen_after_request' ), 9999, 2 );
}
public function get_concerned_actions() {
$actions = array(
'http_api_curl',
'requests-multiple.request.complete',
'requests-request.progress',
'requests-transport.internal.parse_error',
'requests-transport.internal.parse_response',
);
$transports = array(
'requests',
'curl',
'fsockopen',
);
foreach ( $transports as $transport ) {
$actions[] = "requests-{$transport}.after_headers";
$actions[] = "requests-{$transport}.after_multi_exec";
$actions[] = "requests-{$transport}.after_request";
$actions[] = "requests-{$transport}.after_send";
$actions[] = "requests-{$transport}.before_multi_add";
$actions[] = "requests-{$transport}.before_multi_exec";
$actions[] = "requests-{$transport}.before_parse";
$actions[] = "requests-{$transport}.before_redirect";
$actions[] = "requests-{$transport}.before_redirect_check";
$actions[] = "requests-{$transport}.before_request";
$actions[] = "requests-{$transport}.before_send";
$actions[] = "requests-{$transport}.remote_host_path";
$actions[] = "requests-{$transport}.remote_socket";
}
return $actions;
}
public function get_concerned_filters() {
return array(
'block_local_requests',
'http_request_args',
'http_response',
'https_local_ssl_verify',
'https_ssl_verify',
'pre_http_request',
'use_curl_transport',
'use_streams_transport',
);
}
public function get_concerned_constants() {
return array(
'WP_PROXY_HOST',
'WP_PROXY_PORT',
'WP_PROXY_USERNAME',
'WP_PROXY_PASSWORD',
'WP_PROXY_BYPASS_HOSTS',
'WP_HTTP_BLOCK_EXTERNAL',
'WP_ACCESSIBLE_HOSTS',
);
}
/**
* Filter the arguments used in an HTTP request.
*
* Used to log the request, and to add the logging key to the arguments array.
*
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return array HTTP request arguments.
*/
public function filter_http_request_args( array $args, $url ) {
$trace = new QM_Backtrace();
if ( isset( $args['_qm_key'] ) ) {
// Something has triggered another HTTP request from within the `pre_http_request` filter
// (eg. WordPress Beta Tester does this). This allows for one level of nested queries.
$args['_qm_original_key'] = $args['_qm_key'];
$start = $this->data['http'][ $args['_qm_key'] ]['start'];
} else {
$start = microtime( true );
}
$key = microtime( true ) . $url;
$this->data['http'][ $key ] = array(
'url' => $url,
'args' => $args,
'start' => $start,
'trace' => $trace,
);
$args['_qm_key'] = $key;
return $args;
}
/**
* Log the HTTP request's response if it's being short-circuited by another plugin.
* This is necessary due to https://core.trac.wordpress.org/ticket/25747
*
* $response should be one of boolean false, an array, or a `WP_Error`, but be aware that plugins
* which short-circuit the request using this filter may (incorrectly) return data of another type.
*
* @param bool|array|WP_Error $response The preemptive HTTP response. Default false.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return bool|array|WP_Error The preemptive HTTP response.
*/
public function filter_pre_http_request( $response, array $args, $url ) {
// All is well:
if ( false === $response ) {
return $response;
}
// Something's filtering the response, so we'll log it
$this->log_http_response( $response, $args, $url );
return $response;
}
/**
* Debugging action for the HTTP API.
*
* @param mixed $response A parameter which varies depending on $action.
* @param string $action The debug action. Currently one of 'response' or 'transports_list'.
* @param string $class The HTTP transport class name.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
*/
public function action_http_api_debug( $response, $action, $class, $args, $url ) {
switch ( $action ) {
case 'response':
if ( ! empty( $class ) ) {
$this->data['http'][ $args['_qm_key'] ]['transport'] = str_replace( 'wp_http_', '', strtolower( $class ) );
} else {
$this->data['http'][ $args['_qm_key'] ]['transport'] = null;
}
$this->log_http_response( $response, $args, $url );
break;
case 'transports_list':
# Nothing
break;
}
}
public function action_curl_before_request() {
$this->transport = 'curl';
}
public function action_curl_after_request( $headers, array $info = null ) {
$this->info = $info;
}
public function action_fsockopen_before_request() {
$this->transport = 'fsockopen';
}
public function action_fsockopen_after_request( $headers, array $info = null ) {
$this->info = $info;
}
/**
* Log an HTTP response.
*
* @param array|WP_Error $response The HTTP response.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
*/
public function log_http_response( $response, array $args, $url ) {
$this->data['http'][ $args['_qm_key'] ]['end'] = microtime( true );
$this->data['http'][ $args['_qm_key'] ]['response'] = $response;
$this->data['http'][ $args['_qm_key'] ]['args'] = $args;
if ( isset( $args['_qm_original_key'] ) ) {
$this->data['http'][ $args['_qm_original_key'] ]['end'] = $this->data['http'][ $args['_qm_original_key'] ]['start'];
$this->data['http'][ $args['_qm_original_key'] ]['response'] = new WP_Error( 'http_request_not_executed', sprintf(
/* translators: %s: Hook name */
__( 'Request not executed due to a filter on %s', 'query-monitor' ),
'pre_http_request'
) );
}
$this->data['http'][ $args['_qm_key'] ]['info'] = $this->info;
$this->data['http'][ $args['_qm_key'] ]['transport'] = $this->transport;
$this->info = null;
$this->transport = null;
}
public function process() {
$this->data['ltime'] = 0;
if ( ! isset( $this->data['http'] ) ) {
return;
}
/**
* List of HTTP API error codes to ignore.
*
* @since 2.7.0
*
* @param array $http_errors Array of HTTP errors.
*/
$silent = apply_filters( 'qm/collect/silent_http_errors', array(
'http_request_not_executed',
'airplane_mode_enabled',
) );
foreach ( $this->data['http'] as $key => & $http ) {
if ( ! isset( $http['response'] ) ) {
// Timed out
$http['response'] = new WP_Error( 'http_request_timed_out', __( 'Request timed out', 'query-monitor' ) );
$http['end'] = floatval( $http['start'] + $http['args']['timeout'] );
}
if ( is_wp_error( $http['response'] ) ) {
if ( ! in_array( $http['response']->get_error_code(), $silent, true ) ) {
$this->data['errors']['alert'][] = $key;
}
$http['type'] = -1;
} elseif ( ! $http['args']['blocking'] ) {
$http['type'] = -2;
} else {
$http['type'] = intval( wp_remote_retrieve_response_code( $http['response'] ) );
if ( $http['type'] >= 400 ) {
$this->data['errors']['warning'][] = $key;
}
}
$http['ltime'] = ( $http['end'] - $http['start'] );
if ( isset( $http['info'] ) ) {
if ( isset( $http['info']['total_time'] ) ) {
$http['ltime'] = $http['info']['total_time'];
}
if ( ! empty( $http['info']['url'] ) ) {
if ( rtrim( $http['url'], '/' ) !== rtrim( $http['info']['url'], '/' ) ) {
$http['redirected_to'] = $http['info']['url'];
}
}
}
$this->data['ltime'] += $http['ltime'];
$http['component'] = $http['trace']->get_component();
$this->log_type( $http['type'] );
$this->log_component( $http['component'], $http['ltime'], $http['type'] );
}
}
}
# Load early in case a plugin is doing an HTTP request when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_HTTP() );

View File

@@ -0,0 +1,168 @@
<?php
/**
* Language and locale collector.
*
* @package query-monitor
*/
class QM_Collector_Languages extends QM_Collector {
public $id = 'languages';
public function __construct() {
parent::__construct();
add_filter( 'override_load_textdomain', array( $this, 'log_file_load' ), 9999, 3 );
add_filter( 'load_script_translation_file', array( $this, 'log_script_file_load' ), 9999, 3 );
}
public function get_concerned_actions() {
return array(
'load_textdomain',
'unload_textdomain',
);
}
public function get_concerned_filters() {
return array(
'determine_locale',
'gettext',
'gettext_with_context',
'load_script_textdomain_relative_path',
'load_script_translation_file',
'load_script_translations',
'load_textdomain_mofile',
'locale',
'ngettext',
'ngettext_with_context',
'override_load_textdomain',
'override_unload_textdomain',
'plugin_locale',
'pre_determine_locale',
'pre_load_script_translations',
'theme_locale',
);
}
public function get_concerned_options() {
return array(
'WPLANG',
);
}
public function get_concerned_constants() {
return array(
'WPLANG',
);
}
public function process() {
$this->data['locale'] = get_locale();
$this->data['user_locale'] = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale();
ksort( $this->data['languages'] );
foreach ( $this->data['languages'] as & $mofiles ) {
foreach ( $mofiles as & $mofile ) {
$mofile['found_formatted'] = $mofile['found'] ? size_format( $mofile['found'] ) : '';
}
}
}
/**
* Store log data.
*
* @param bool $override Whether to override the text domain. Default false.
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the MO file.
* @return bool
*/
public function log_file_load( $override, $domain, $mofile ) {
if ( 'query-monitor' === $domain && self::hide_qm() ) {
return $override;
}
$trace = new QM_Backtrace();
$filtered = $trace->get_filtered_trace();
$caller = array();
foreach ( $filtered as $i => $item ) {
if ( in_array( $item['function'], array(
'load_muplugin_textdomain',
'load_plugin_textdomain',
'load_theme_textdomain',
'load_child_theme_textdomain',
'load_default_textdomain',
), true ) ) {
$caller = $item;
$display = $i + 1;
if ( isset( $filtered[ $display ] ) ) {
$caller['display'] = $filtered[ $display ]['display'];
}
break;
}
}
if ( empty( $caller ) ) {
if ( isset( $filtered[1] ) ) {
$caller = $filtered[1];
} else {
$caller = $filtered[0];
}
}
if ( ! isset( $caller['file'] ) && isset( $filtered[0]['file'] ) && isset( $filtered[0]['line'] ) ) {
$caller['file'] = $filtered[0]['file'];
$caller['line'] = $filtered[0]['line'];
}
$found = file_exists( $mofile ) ? filesize( $mofile ) : false;
$this->data['languages'][ $domain ][] = array(
'caller' => $caller,
'domain' => $domain,
'file' => $mofile,
'found' => $found,
'handle' => null,
'type' => 'gettext',
);
return $override;
}
/**
* Filters the file path for loading script translations for the given script handle and textdomain.
*
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The textdomain.
*
* @return string|false Path to the translation file to load. False if there isn't one.
*/
public function log_script_file_load( $file, $handle, $domain ) {
$trace = new QM_Backtrace();
$filtered = $trace->get_filtered_trace();
$caller = $filtered[0];
$found = ( $file && file_exists( $file ) ) ? filesize( $file ) : false;
$this->data['languages'][ $domain ][] = array(
'caller' => $caller,
'domain' => $domain,
'file' => $file,
'found' => $found,
'handle' => $handle,
'type' => 'jed',
);
return $file;
}
}
# Load early to catch early errors
QM_Collectors::add( new QM_Collector_Languages() );

View File

@@ -0,0 +1,162 @@
<?php
/**
* PSR-3 compatible logging collector.
*
* @package query-monitor
*/
class QM_Collector_Logger extends QM_Collector {
public $id = 'logger';
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
public function __construct() {
parent::__construct();
foreach ( $this->get_levels() as $level ) {
add_action( "qm/{$level}", array( $this, $level ), 10, 2 );
}
add_action( 'qm/log', array( $this, 'log' ), 10, 3 );
}
public function emergency( $message, array $context = array() ) {
$this->store( self::EMERGENCY, $message, $context );
}
public function alert( $message, array $context = array() ) {
$this->store( self::ALERT, $message, $context );
}
public function critical( $message, array $context = array() ) {
$this->store( self::CRITICAL, $message, $context );
}
public function error( $message, array $context = array() ) {
$this->store( self::ERROR, $message, $context );
}
public function warning( $message, array $context = array() ) {
$this->store( self::WARNING, $message, $context );
}
public function notice( $message, array $context = array() ) {
$this->store( self::NOTICE, $message, $context );
}
public function info( $message, array $context = array() ) {
$this->store( self::INFO, $message, $context );
}
public function debug( $message, array $context = array() ) {
$this->store( self::DEBUG, $message, $context );
}
public function log( $level, $message, array $context = array() ) {
if ( ! in_array( $level, $this->get_levels(), true ) ) {
throw new InvalidArgumentException( __( 'Unsupported log level', 'query-monitor' ) );
}
$this->store( $level, $message, $context );
}
protected function store( $level, $message, array $context = array() ) {
$type = 'string';
$trace = new QM_Backtrace( array(
'ignore_frames' => 2,
) );
if ( is_wp_error( $message ) ) {
$type = 'wp_error';
$message = sprintf(
'WP_Error: %s (%s)',
$message->get_error_message(),
$message->get_error_code()
);
}
if ( ( $message instanceof Exception ) || ( $message instanceof Throwable ) ) {
$type = 'throwable';
$message = get_class( $message ) . ': ' . $message->getMessage();
}
if ( ! QM_Util::is_stringy( $message ) ) {
$type = 'dump';
$message = print_r( $message, true );
}
$this->data['logs'][] = array(
'message' => self::interpolate( $message, $context ),
'context' => $context,
'trace' => $trace,
'level' => $level,
'type' => $type,
);
}
protected static function interpolate( $message, array $context = array() ) {
// build a replacement array with braces around the context keys
$replace = array();
foreach ( $context as $key => $val ) {
// check that the value can be casted to string
if ( is_bool( $val ) ) {
$replace[ "{{$key}}" ] = ( $val ? 'true' : 'false' );
} elseif ( is_scalar( $val ) || QM_Util::is_stringy( $val ) ) {
$replace[ "{{$key}}" ] = $val;
}
}
// interpolate replacement values into the message and return
return strtr( $message, $replace );
}
public function process() {
if ( empty( $this->data['logs'] ) ) {
return;
}
$components = array();
foreach ( $this->data['logs'] as $row ) {
$component = $row['trace']->get_component();
$components[ $component->name ] = $component->name;
}
$this->data['components'] = $components;
}
public function get_levels() {
return array(
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
self::NOTICE,
self::INFO,
self::DEBUG,
);
}
public function get_warning_levels() {
return array(
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
);
}
}
# Load early in case a plugin wants to log a message early in the bootstrap process
QM_Collectors::add( new QM_Collector_Logger() );

View File

@@ -0,0 +1,65 @@
<?php
/**
* General overview collector.
*
* @package query-monitor
*/
class QM_Collector_Overview extends QM_Collector {
public $id = 'overview';
public function process() {
$this->data['time_taken'] = self::timer_stop_float();
$this->data['time_limit'] = ini_get( 'max_execution_time' );
$this->data['time_start'] = $GLOBALS['timestart'];
if ( ! empty( $this->data['time_limit'] ) ) {
$this->data['time_usage'] = ( 100 / $this->data['time_limit'] ) * $this->data['time_taken'];
} else {
$this->data['time_usage'] = 0;
}
if ( function_exists( 'memory_get_peak_usage' ) ) {
$this->data['memory'] = memory_get_peak_usage();
} elseif ( function_exists( 'memory_get_usage' ) ) {
$this->data['memory'] = memory_get_usage();
} else {
$this->data['memory'] = 0;
}
if ( is_user_logged_in() ) {
$this->data['current_user'] = self::format_user( wp_get_current_user() );
} else {
$this->data['current_user'] = false;
}
if ( function_exists( 'current_user_switched' ) && current_user_switched() ) {
$this->data['switched_user'] = self::format_user( current_user_switched() );
} else {
$this->data['switched_user'] = false;
}
$this->data['memory_limit'] = QM_Util::convert_hr_to_bytes( ini_get( 'memory_limit' ) );
if ( $this->data['memory_limit'] > 0 ) {
$this->data['memory_usage'] = ( 100 / $this->data['memory_limit'] ) * $this->data['memory'];
} else {
$this->data['memory_usage'] = 0;
}
$this->data['display_time_usage_warning'] = ( $this->data['time_usage'] >= 75 );
$this->data['display_memory_usage_warning'] = ( $this->data['memory_usage'] >= 75 );
$this->data['is_admin'] = is_admin();
}
}
function register_qm_collector_overview( array $collectors, QueryMonitor $qm ) {
$collectors['overview'] = new QM_Collector_Overview();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_overview', 1, 2 );

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
<?php
/**
* HTTP redirect collector.
*
* @package query-monitor
*/
class QM_Collector_Redirects extends QM_Collector {
public $id = 'redirects';
public function __construct() {
parent::__construct();
add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 9999, 2 );
}
public function filter_wp_redirect( $location, $status ) {
if ( ! $location ) {
return $location;
}
$trace = new QM_Backtrace();
$this->data['trace'] = $trace;
$this->data['location'] = $location;
$this->data['status'] = $status;
return $location;
}
}
# Load early in case a plugin is doing a redirect when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Redirects() );

View File

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

View File

@@ -0,0 +1,280 @@
<?php
/**
* Template and theme collector.
*
* @package query-monitor
*/
class QM_Collector_Theme extends QM_Collector {
public $id = 'response';
protected $got_theme_compat = false;
protected $query_templates = array();
public function __construct() {
parent::__construct();
add_filter( 'body_class', array( $this, 'filter_body_class' ), 9999 );
add_filter( 'timber/output', array( $this, 'filter_timber_output' ), 9999, 3 );
add_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
add_action( 'get_template_part', array( $this, 'action_get_template_part' ), 10, 3 );
}
public function get_concerned_actions() {
return array(
'template_redirect',
);
}
public function get_concerned_filters() {
$filters = array(
'stylesheet',
'stylesheet_directory',
'template',
'template_directory',
'template_include',
);
foreach ( self::get_query_template_names() as $template => $conditional ) {
// @TODO this isn't correct for post type archives
$filter = str_replace( '_', '', $template );
$filters[] = "{$filter}_template_hierarchy";
$filters[] = "{$filter}_template";
}
return $filters;
}
public function get_concerned_options() {
return array(
'stylesheet',
'template',
);
}
public static function get_query_template_names() {
$names = array();
$names['embed'] = 'is_embed';
$names['404'] = 'is_404';
$names['search'] = 'is_search';
$names['front_page'] = 'is_front_page';
$names['home'] = 'is_home';
if ( function_exists( 'is_privacy_policy' ) ) {
$names['privacy_policy'] = 'is_privacy_policy';
}
$names['post_type_archive'] = 'is_post_type_archive';
$names['taxonomy'] = 'is_tax';
$names['attachment'] = 'is_attachment';
$names['single'] = 'is_single';
$names['page'] = 'is_page';
$names['singular'] = 'is_singular';
$names['category'] = 'is_category';
$names['tag'] = 'is_tag';
$names['author'] = 'is_author';
$names['date'] = 'is_date';
$names['archive'] = 'is_archive';
$names['index'] = '__return_true';
return $names;
}
// https://core.trac.wordpress.org/ticket/14310
public function action_template_redirect() {
add_filter( 'template_include', array( $this, 'filter_template_include' ), PHP_INT_MAX );
foreach ( self::get_query_template_names() as $template => $conditional ) {
// If a matching theme-compat file is found, further conditional checks won't occur in template-loader.php
if ( $this->got_theme_compat ) {
break;
}
$get_template = "get_{$template}_template";
if ( function_exists( $conditional ) && function_exists( $get_template ) && call_user_func( $conditional ) ) {
$filter = str_replace( '_', '', $template );
add_filter( "{$filter}_template_hierarchy", array( $this, 'filter_template_hierarchy' ), PHP_INT_MAX );
$loaded_template = call_user_func( $get_template );
$default_template = locate_template( $this->query_templates );
if ( $default_template !== $loaded_template ) {
$this->data['template_altered'] = true;
}
remove_filter( "{$filter}_template_hierarchy", array( $this, 'filter_template_hierarchy' ), PHP_INT_MAX );
}
}
}
/**
* Fires before a template part is loaded.
*
* @param string $slug The slug name for the generic template.
* @param string $name The name of the specialized template.
* @param string[] $templates Array of template files to search for, in order.
*/
public function action_get_template_part( $slug, $name, $templates ) {
$data = compact( 'slug', 'name', 'templates' );
$data['trace'] = new QM_Backtrace( array(
'ignore_frames' => 4,
) );
$this->data['requested_template_parts'][] = $data;
}
public function filter_template_hierarchy( array $templates ) {
$this->query_templates = $templates;
if ( ! isset( $this->data['template_hierarchy'] ) ) {
$this->data['template_hierarchy'] = array();
}
foreach ( $templates as $template_name ) {
if ( file_exists( ABSPATH . WPINC . '/theme-compat/' . $template_name ) ) {
$this->got_theme_compat = true;
break;
}
}
$this->data['template_hierarchy'] = array_merge( $this->data['template_hierarchy'], $templates );
return $templates;
}
public function filter_body_class( array $class ) {
$this->data['body_class'] = $class;
return $class;
}
public function filter_template_include( $template_path ) {
$this->data['template_path'] = $template_path;
return $template_path;
}
public function filter_timber_output( $output, $data = null, $file = null ) {
if ( $file ) {
$this->data['timber_files'][] = $file;
}
return $output;
}
public function process() {
$stylesheet_directory = QM_Util::standard_dir( get_stylesheet_directory() );
$template_directory = QM_Util::standard_dir( get_template_directory() );
$theme_directory = QM_Util::standard_dir( get_theme_root() );
if ( isset( $this->data['template_hierarchy'] ) ) {
$this->data['template_hierarchy'] = array_unique( $this->data['template_hierarchy'] );
}
$this->data['has_template_part_action'] = function_exists( 'wp_body_open' );
if ( $this->data['has_template_part_action'] ) {
// Since WP 5.2, the `get_template_part` action populates this data nicely:
if ( ! empty( $this->data['requested_template_parts'] ) ) {
$this->data['template_parts'] = array();
$this->data['theme_template_parts'] = array();
$this->data['count_template_parts'] = array();
foreach ( $this->data['requested_template_parts'] as $part ) {
$file = locate_template( $part['templates'] );
$part['caller'] = $part['trace']->get_caller();
unset( $part['trace'] );
if ( ! $file ) {
$this->data['unsuccessful_template_parts'][] = $part;
continue;
}
$file = QM_Util::standard_dir( $file );
if ( isset( $this->data['count_template_parts'][ $file ] ) ) {
$this->data['count_template_parts'][ $file ]++;
continue;
}
$this->data['count_template_parts'][ $file ] = 1;
$filename = str_replace( array(
$stylesheet_directory,
$template_directory,
), '', $file );
$slug = trim( str_replace( '.php', '', $filename ), '/' );
$display = trim( $filename, '/' );
$theme_display = trim( str_replace( $theme_directory, '', $file ), '/' );
$this->data['template_parts'][ $file ] = $display;
$this->data['theme_template_parts'][ $file ] = $theme_display;
}
}
} else {
// Prior to WP 5.2, we need to look into `get_included_files()` and do our best to figure out
// if each one is a template part:
foreach ( get_included_files() as $file ) {
$file = QM_Util::standard_dir( $file );
$filename = str_replace( array(
$stylesheet_directory,
$template_directory,
), '', $file );
if ( $filename !== $file ) {
$slug = trim( str_replace( '.php', '', $filename ), '/' );
$display = trim( $filename, '/' );
$theme_display = trim( str_replace( $theme_directory, '', $file ), '/' );
$count = did_action( "get_template_part_{$slug}" );
if ( $count ) {
$this->data['template_parts'][ $file ] = $display;
$this->data['theme_template_parts'][ $file ] = $theme_display;
$this->data['count_template_parts'][ $file ] = $count;
} else {
$slug = trim( preg_replace( '|\-[^\-]+$|', '', $slug ), '/' );
$count = did_action( "get_template_part_{$slug}" );
if ( $count ) {
$this->data['template_parts'][ $file ] = $display;
$this->data['theme_template_parts'][ $file ] = $theme_display;
$this->data['count_template_parts'][ $file ] = $count;
}
}
}
}
}
if ( ! empty( $this->data['template_path'] ) ) {
$template_path = QM_Util::standard_dir( $this->data['template_path'] );
$template_file = str_replace( array( $stylesheet_directory, $template_directory, ABSPATH ), '', $template_path );
$template_file = ltrim( $template_file, '/' );
$theme_template_file = str_replace( array( $theme_directory, ABSPATH ), '', $template_path );
$theme_template_file = ltrim( $theme_template_file, '/' );
$this->data['template_path'] = $template_path;
$this->data['template_file'] = $template_file;
$this->data['theme_template_file'] = $theme_template_file;
}
$this->data['stylesheet'] = get_stylesheet();
$this->data['template'] = get_template();
$this->data['is_child_theme'] = ( $this->data['stylesheet'] !== $this->data['template'] );
if ( isset( $this->data['body_class'] ) ) {
asort( $this->data['body_class'] );
}
}
}
function register_qm_collector_theme( array $collectors, QueryMonitor $qm ) {
$collectors['response'] = new QM_Collector_Theme();
return $collectors;
}
if ( ! is_admin() ) {
add_filter( 'qm/collectors', 'register_qm_collector_theme', 10, 2 );
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Timing and profiling collector.
*
* @package query-monitor
*/
class QM_Collector_Timing extends QM_Collector {
public $id = 'timing';
private $track_timer = array();
private $start = array();
private $stop = array();
public function __construct() {
parent::__construct();
add_action( 'qm/start', array( $this, 'action_function_time_start' ), 10, 1 );
add_action( 'qm/stop', array( $this, 'action_function_time_stop' ), 10, 1 );
add_action( 'qm/lap', array( $this, 'action_function_time_lap' ), 10, 2 );
}
public function action_function_time_start( $function ) {
$this->track_timer[ $function ] = new QM_Timer();
$this->start[ $function ] = $this->track_timer[ $function ]->start();
}
public function action_function_time_stop( $function ) {
if ( ! isset( $this->track_timer[ $function ] ) ) {
$trace = new QM_Backtrace();
$this->data['warning'][] = array(
'function' => $function,
'message' => __( 'Timer not started', 'query-monitor' ),
'trace' => $trace,
);
return;
}
$this->stop[ $function ] = $this->track_timer[ $function ]->stop();
$this->calculate_time( $function );
}
public function action_function_time_lap( $function, $name = null ) {
if ( ! isset( $this->track_timer[ $function ] ) ) {
$trace = new QM_Backtrace();
$this->data['warning'][] = array(
'function' => $function,
'message' => __( 'Timer not started', 'query-monitor' ),
'trace' => $trace,
);
return;
}
$this->track_timer[ $function ]->lap( array(), $name );
}
public function calculate_time( $function ) {
$trace = $this->track_timer[ $function ]->get_trace();
$function_time = $this->track_timer[ $function ]->get_time();
$function_memory = $this->track_timer[ $function ]->get_memory();
$function_laps = $this->track_timer[ $function ]->get_laps();
$start_time = $this->track_timer[ $function ]->get_start_time();
$end_time = $this->track_timer[ $function ]->get_end_time();
$this->data['timing'][] = array(
'function' => $function,
'function_time' => $function_time,
'function_memory' => $function_memory,
'laps' => $function_laps,
'trace' => $trace,
'start_time' => ( $start_time - $GLOBALS['timestart'] ),
'end_time' => ( $end_time - $GLOBALS['timestart'] ),
);
}
public function process() {
foreach ( $this->start as $function => $value ) {
if ( ! isset( $this->stop[ $function ] ) ) {
$trace = $this->track_timer[ $function ]->get_trace();
$this->data['warning'][] = array(
'function' => $function,
'message' => __( 'Timer not stopped', 'query-monitor' ),
'trace' => $trace,
);
}
}
if ( ! empty( $this->data['timing'] ) ) {
usort( $this->data['timing'], array( $this, 'sort_by_start_time' ) );
}
}
public function sort_by_start_time( array $a, array $b ) {
if ( $a['start_time'] === $b['start_time'] ) {
return 0;
} else {
return ( $a['start_time'] > $b['start_time'] ) ? 1 : -1;
}
}
}
# Load early in case a plugin is setting the function to be checked when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Timing() );

View File

@@ -0,0 +1,81 @@
<?php
/**
* Transient storage collector.
*
* @package query-monitor
*/
class QM_Collector_Transients extends QM_Collector {
public $id = 'transients';
public function __construct() {
parent::__construct();
add_action( 'setted_site_transient', array( $this, 'action_setted_site_transient' ), 10, 3 );
add_action( 'setted_transient', array( $this, 'action_setted_blog_transient' ), 10, 3 );
}
public function tear_down() {
parent::tear_down();
remove_action( 'setted_site_transient', array( $this, 'action_setted_site_transient' ), 10 );
remove_action( 'setted_transient', array( $this, 'action_setted_blog_transient' ), 10 );
}
public function action_setted_site_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'site', $value, $expiration );
}
public function action_setted_blog_transient( $transient, $value, $expiration ) {
$this->setted_transient( $transient, 'blog', $value, $expiration );
}
public function setted_transient( $transient, $type, $value, $expiration ) {
$trace = new QM_Backtrace( array(
'ignore_frames' => 1, # Ignore the action_setted_(site|blog)_transient method
) );
$name = str_replace( array(
'_site_transient_',
'_transient_',
), '', $transient );
$size = strlen( maybe_serialize( $value ) );
$this->data['trans'][] = array(
'name' => $name,
'trace' => $trace,
'type' => $type,
'value' => $value,
'expiration' => $expiration,
'exp_diff' => ( $expiration ? human_time_diff( 0, $expiration ) : '' ),
'size' => $size,
'size_formatted' => size_format( $size ),
);
}
public function process() {
$this->data['has_type'] = is_multisite();
if ( empty( $this->data['trans'] ) ) {
return;
}
foreach ( $this->data['trans'] as $i => $transient ) {
$filtered_trace = $transient['trace']->get_display_trace();
array_shift( $filtered_trace ); // remove do_action('setted_(site_)?transient')
array_shift( $filtered_trace ); // remove set_(site_)?transient()
$component = $transient['trace']->get_component();
$this->data['trans'][ $i ]['filtered_trace'] = $filtered_trace;
$this->data['trans'][ $i ]['component'] = $component;
unset( $this->data['trans'][ $i ]['trace'] );
}
}
}
# Load early in case a plugin is setting transients when it initialises instead of after the `plugins_loaded` hook
QM_Collectors::add( new QM_Collector_Transients() );

View File

@@ -0,0 +1,103 @@
<?php
/**
* Ajax request dispatcher.
*
* @package query-monitor
*/
class QM_Dispatcher_AJAX extends QM_Dispatcher {
public $id = 'ajax';
public function __construct( QM_Plugin $qm ) {
parent::__construct( $qm );
add_action( 'shutdown', array( $this, 'dispatch' ), 0 );
}
public function init() {
if ( ! self::user_can_view() ) {
return;
}
if ( QM_Util::is_ajax() ) {
ob_start();
}
parent::init();
}
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
$this->before_output();
foreach ( $this->get_outputters( 'headers' ) as $id => $output ) {
$output->output();
}
$this->after_output();
}
protected function before_output() {
require_once $this->qm->plugin_path( 'output/Headers.php' );
foreach ( glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
require_once $file;
}
}
protected function after_output() {
# flush once, because we're nice
if ( ob_get_length() ) {
ob_flush();
}
}
public function is_active() {
if ( ! QM_Util::is_ajax() ) {
return false;
}
if ( ! self::user_can_view() ) {
return false;
}
# If the headers have already been sent then we can't do anything about it
if ( headers_sent() ) {
return false;
}
# Don't process if the minimum required actions haven't fired:
if ( is_admin() ) {
if ( ! did_action( 'admin_init' ) ) {
return false;
}
} else {
if ( ! did_action( 'wp' ) ) {
return false;
}
}
return true;
}
}
function register_qm_dispatcher_ajax( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['ajax'] = new QM_Dispatcher_AJAX( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_ajax', 10, 2 );

View File

@@ -0,0 +1,726 @@
<?php
/**
* General HTML request dispatcher.
*
* @package query-monitor
*/
class QM_Dispatcher_Html extends QM_Dispatcher {
/**
* Outputter instances.
*
* @var QM_Output_Html[] Array of outputters.
*/
protected $outputters = array();
public $id = 'html';
public $did_footer = false;
protected $admin_bar_menu = array();
protected $panel_menu = array();
public function __construct( QM_Plugin $qm ) {
add_action( 'admin_bar_menu', array( $this, 'action_admin_bar_menu' ), 999 );
add_action( 'wp_ajax_qm_auth_on', array( $this, 'ajax_on' ) );
add_action( 'wp_ajax_qm_auth_off', array( $this, 'ajax_off' ) );
add_action( 'wp_ajax_qm_editor_set', array( $this, 'ajax_editor_set' ) );
add_action( 'wp_ajax_nopriv_qm_auth_off', array( $this, 'ajax_off' ) );
add_action( 'shutdown', array( $this, 'dispatch' ), 0 );
add_action( 'wp_footer', array( $this, 'action_footer' ) );
add_action( 'admin_footer', array( $this, 'action_footer' ) );
add_action( 'login_footer', array( $this, 'action_footer' ) );
add_action( 'embed_footer', array( $this, 'action_footer' ) );
add_action( 'gp_footer', array( $this, 'action_footer' ) );
parent::__construct( $qm );
}
public function action_footer() {
$this->did_footer = true;
}
/**
* Helper function. Should the authentication cookie be secure?
*
* @return bool Should the authentication cookie be secure?
*/
public static function secure_cookie() {
return ( is_ssl() && ( 'https' === parse_url( home_url(), PHP_URL_SCHEME ) ) );
}
public function ajax_on() {
if ( ! current_user_can( 'view_query_monitor' ) || ! check_ajax_referer( 'qm-auth-on', 'nonce', false ) ) {
wp_send_json_error();
}
$expiration = time() + ( 2 * DAY_IN_SECONDS );
$secure = self::secure_cookie();
$cookie = wp_generate_auth_cookie( get_current_user_id(), $expiration, 'logged_in' );
setcookie( QM_COOKIE, $cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure, false );
wp_send_json_success();
}
public function ajax_off() {
if ( ! self::user_verified() || ! check_ajax_referer( 'qm-auth-off', 'nonce', false ) ) {
wp_send_json_error();
}
$expiration = time() - 31536000;
setcookie( QM_COOKIE, ' ', $expiration, COOKIEPATH, COOKIE_DOMAIN );
wp_send_json_success();
}
public function ajax_editor_set() {
if ( ! current_user_can( 'view_query_monitor' ) || ! check_ajax_referer( 'qm-editor-set', 'nonce', false ) ) {
wp_send_json_error();
}
$expiration = time() + ( 2 * YEAR_IN_SECONDS );
$secure = self::secure_cookie();
$editor = wp_unslash( $_POST['editor'] );
setcookie( QM_EDITOR_COOKIE, $editor, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure, false );
wp_send_json_success( $editor );
}
public function action_admin_bar_menu( WP_Admin_Bar $wp_admin_bar ) {
if ( ! self::user_can_view() ) {
return;
}
$title = __( 'Query Monitor', 'query-monitor' );
$wp_admin_bar->add_node( array(
'id' => 'query-monitor',
'title' => esc_html( $title ),
'href' => '#qm-overview',
) );
$wp_admin_bar->add_node( array(
'parent' => 'query-monitor',
'id' => 'query-monitor-placeholder',
'title' => esc_html( $title ),
'href' => '#qm-overview',
) );
}
public function init() {
if ( ! self::user_can_view() ) {
return;
}
if ( ! file_exists( $this->qm->plugin_path( 'assets/query-monitor.css' ) ) ) {
add_action( 'admin_notices', array( $this, 'build_warning' ) );
}
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), -9999 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ), -9999 );
add_action( 'login_enqueue_scripts', array( $this, 'enqueue_assets' ), -9999 );
add_action( 'enqueue_embed_scripts', array( $this, 'enqueue_assets' ), -9999 );
add_action( 'gp_head', array( $this, 'manually_print_assets' ), 11 );
parent::init();
}
public function manually_print_assets() {
wp_print_scripts( array(
'query-monitor',
) );
wp_print_styles( array(
'query-monitor',
) );
}
public function build_warning() {
printf(
'<div id="qm-built-nope" class="notice notice-error"><p>%s</p></div>',
sprintf(
/* translators: 1: CLI command to run, 2: plugin directory name */
esc_html__( 'Asset files for Query Monitor need to be built. Run %1$s from the %2$s directory.', 'query-monitor' ),
'<code>npm i && npm run build</code>',
sprintf(
'<code>%s</code>',
esc_html( QM_Util::standard_dir( untrailingslashit( $this->qm->plugin_path() ), '' ) )
)
)
);
}
public function enqueue_assets() {
global $wp_locale, $wp_version;
$deps = array(
'jquery',
);
if ( defined( 'QM_NO_JQUERY' ) && QM_NO_JQUERY ) {
$deps = array();
}
$css = 'query-monitor';
if ( method_exists( 'Dark_Mode', 'is_using_dark_mode' ) && is_user_logged_in() ) {
if ( Dark_Mode::is_using_dark_mode() ) {
$css .= '-dark';
}
} elseif ( defined( 'QM_DARK_MODE' ) && QM_DARK_MODE ) {
$css .= '-dark';
}
wp_enqueue_style(
'query-monitor',
$this->qm->plugin_url( "assets/{$css}.css" ),
array( 'dashicons' ),
$this->qm->plugin_ver( "assets/{$css}.css" )
);
wp_enqueue_script(
'query-monitor',
$this->qm->plugin_url( 'assets/query-monitor.js' ),
$deps,
$this->qm->plugin_ver( 'assets/query-monitor.js' ),
false
);
wp_localize_script(
'query-monitor',
'qm_number_format',
$wp_locale->number_format
);
wp_localize_script(
'query-monitor',
'qm_l10n',
array(
'ajax_error' => __( 'PHP Errors in Ajax Response', 'query-monitor' ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'auth_nonce' => array(
'on' => wp_create_nonce( 'qm-auth-on' ),
'off' => wp_create_nonce( 'qm-auth-off' ),
'editor-set' => wp_create_nonce( 'qm-editor-set' ),
),
'fatal_error' => __( 'PHP Fatal Error', 'query-monitor' ),
)
);
/**
* Fires when assets for QM's HTML have been enqueued.
*
* @since 3.6.0
*
* @param \QM_Dispatcher_Html $this The HTML dispatcher.
*/
do_action( 'qm/output/enqueued-assets', $this );
}
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
$switched_locale = function_exists( 'switch_to_locale' ) && switch_to_locale( get_user_locale() );
$this->before_output();
foreach ( $this->outputters as $id => $output ) {
$timer = new QM_Timer();
$timer->start();
printf(
"\n" . '<!-- Begin %1$s output -->' . "\n" . '<div class="qm-panel-container" id="qm-%1$s-container">' . "\n",
esc_html( $id )
);
$output->output();
printf(
"\n" . '</div>' . "\n" . '<!-- End %s output -->' . "\n",
esc_html( $id )
);
$output->set_timer( $timer->stop() );
}
$this->after_output();
if ( $switched_locale ) {
restore_previous_locale();
}
}
protected function before_output() {
require_once $this->qm->plugin_path( 'output/Html.php' );
foreach ( glob( $this->qm->plugin_path( 'output/html/*.php' ) ) as $file ) {
require_once $file;
}
$this->outputters = $this->get_outputters( 'html' );
/**
* Filters the menu items shown in Query Monitor's admin toolbar menu.
*
* @since 3.0.0
*
* @param array $menus Array of menus.
*/
$this->admin_bar_menu = apply_filters( 'qm/output/menus', array() );
/**
* Filters the menu items shown in the panel navigation menu in Query Monitor's output.
*
* @since 3.0.0
*
* @param array $admin_bar_menu Array of menus.
*/
$this->panel_menu = apply_filters( 'qm/output/panel_menus', $this->admin_bar_menu );
foreach ( $this->outputters as $output_id => $output ) {
$collector = $output->get_collector();
if ( ( ! empty( $collector->concerned_filters ) || ! empty( $collector->concerned_actions ) ) && isset( $this->panel_menu[ 'qm-' . $output_id ] ) ) {
$this->panel_menu[ 'qm-' . $output_id ]['children'][ 'qm-' . $output_id . '-concerned_hooks' ] = array(
'href' => esc_attr( '#' . $collector->id() . '-concerned_hooks' ),
'title' => __( 'Hooks in Use', 'query-monitor' ),
);
}
}
$class = array(
'qm-no-js',
);
if ( did_action( 'wp_head' ) ) {
$class[] = sprintf( 'qm-theme-%s', get_template() );
$class[] = sprintf( 'qm-theme-%s', get_stylesheet() );
}
if ( ! is_admin_bar_showing() ) {
$class[] = 'qm-peek';
}
$json = array(
'menu' => $this->js_admin_bar_menu(),
'ajax_errors' => array(), # @TODO move this into the php_errors collector
);
echo '<!-- Begin Query Monitor output -->' . "\n\n";
echo '<script type="text/javascript">' . "\n\n";
echo 'var qm = ' . json_encode( $json ) . ';' . "\n\n";
echo '</script>' . "\n\n";
echo '<div id="query-monitor-main" class="' . implode( ' ', array_map( 'esc_attr', $class ) ) . '" dir="ltr">';
echo '<div id="qm-side-resizer" class="qm-resizer"></div>';
echo '<div id="qm-title" class="qm-resizer">';
echo '<h1 class="qm-title-heading">' . esc_html__( 'Query Monitor', 'query-monitor' ) . '</h1>';
echo '<div class="qm-title-heading">';
echo '<select>';
printf(
'<option value="%1$s">%2$s</option>',
'#qm-overview',
esc_html__( 'Overview', 'query-monitor' )
);
foreach ( $this->panel_menu as $menu ) {
printf(
'<option value="%1$s">%2$s</option>',
esc_attr( $menu['href'] ),
esc_html( $menu['title'] )
);
if ( ! empty( $menu['children'] ) ) {
foreach ( $menu['children'] as $child ) {
printf(
'<option value="%1$s">└ %2$s</option>',
esc_attr( $child['href'] ),
esc_html( $child['title'] )
);
}
}
}
printf(
'<option value="%1$s">%2$s</option>',
'#qm-settings',
esc_html__( 'Settings', 'query-monitor' )
);
echo '</select>';
echo '</div>';
echo '<button class="qm-title-button qm-button-container-settings" aria-label="' . esc_attr__( 'Settings', 'query-monitor' ) . '"><span class="dashicons dashicons-admin-generic" aria-hidden="true"></span></button>';
echo '<button class="qm-title-button qm-button-container-position" aria-label="' . esc_html__( 'Toggle panel position', 'query-monitor' ) . '"><span class="dashicons dashicons-image-rotate-left" aria-hidden="true"></span></button>';
echo '<button class="qm-title-button qm-button-container-close" aria-label="' . esc_attr__( 'Close Panel', 'query-monitor' ) . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button>';
echo '</div>'; // #qm-title
echo '<div id="qm-wrapper">';
echo '<nav id="qm-panel-menu" aria-labelledby="qm-panel-menu-caption">';
echo '<h2 class="qm-screen-reader-text" id="qm-panel-menu-caption">' . esc_html__( 'Query Monitor Menu', 'query-monitor' ) . '</h2>';
echo '<ul role="tablist">';
printf(
'<li role="presentation"><button role="tab" data-qm-href="%1$s">%2$s</button></li>',
'#qm-overview',
esc_html__( 'Overview', 'query-monitor' )
);
foreach ( $this->panel_menu as $id => $menu ) {
$this->do_panel_menu_item( $id, $menu );
}
echo '</ul>';
echo '</nav>'; // #qm-panel-menu
echo '<div id="qm-panels">';
}
protected function do_panel_menu_item( $id, array $menu ) {
printf(
'<li role="presentation"><button role="tab" data-qm-href="%1$s">%2$s</button>',
esc_attr( $menu['href'] ),
esc_html( $menu['title'] )
);
if ( ! empty( $menu['children'] ) ) {
echo '<ul role="presentation">';
foreach ( $menu['children'] as $child_id => $child ) {
$this->do_panel_menu_item( $child_id, $child );
}
echo '</ul>';
}
echo '</li>';
}
protected function after_output() {
$state = self::user_verified() ? 'on' : 'off';
$editor = self::editor_cookie();
$text = array(
'on' => __( 'Clear authentication cookie', 'query-monitor' ),
'off' => __( 'Set authentication cookie', 'query-monitor' ),
);
echo '<div class="qm qm-non-tabular" id="qm-settings" data-qm-state="' . esc_attr( $state ) . '">';
echo '<h2 class="qm-screen-reader-text">' . esc_html__( 'Settings', 'query-monitor' ) . '</h2>';
echo '<div class="qm-boxed">';
echo '<section>';
echo '<h3>' . esc_html__( 'Authentication', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html__( 'You can set an authentication cookie which allows you to view Query Monitor output when you&rsquo;re not logged in, or when you&rsquo;re logged in as a different user.', 'query-monitor' ) . '</p>';
echo '<p><button class="qm-auth qm-button" data-qm-text-on="' . esc_attr( $text['on'] ) . '" data-qm-text-off="' . esc_attr( $text['off'] ) . '">' . esc_html( $text[ $state ] ) . '</button></p>';
echo '<p data-qm-state-visibility="on"><span class="dashicons dashicons-yes qm-dashicons-yes"></span> ' . esc_html__( 'Authentication cookie is set', 'query-monitor' ) . '</p>';
echo '</section>';
echo '</div>';
echo '<div class="qm-boxed">';
echo '<section class="qm-editor">';
echo '<h3>' . esc_html__( 'Editor', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html__( 'You can set your editor here, so that when you click on stack trace links the file opens in your editor.', 'query-monitor' ) . '</p>';
echo '<p>';
echo '<select id="qm-editor-select" name="qm-editor-select" class="qm-filter">';
$editors = array(
'Default/Xdebug' => '',
'Atom' => 'atom',
'Netbeans' => 'netbeans',
'PhpStorm' => 'phpstorm',
'Sublime Text' => 'sublime',
'TextMate' => 'textmate',
'Visual Studio Code' => 'vscode',
);
foreach ( $editors as $name => $value ) {
echo '<option value="' . esc_attr( $value ) . '" ' . selected( $value, $editor, false ) . '>' . esc_html( $name ) . '</option>';
}
echo '</select>';
echo '</p><p>';
echo '<button class="qm-editor-button qm-button">' . esc_html__( 'Set editor cookie', 'query-monitor' ) . '</button>';
echo '</p>';
echo '<p id="qm-editor-save-status"><span class="dashicons dashicons-yes qm-dashicons-yes"></span> ' . esc_html__( 'Saved! Reload to apply changes.', 'query-monitor' ) . '</p>';
echo '</section>';
echo '</div>';
echo '<div class="qm-boxed">';
$constants = array(
'QM_DARK_MODE' => array(
'label' => __( 'Enable dark mode for Query Monitor\'s interface.', 'query-monitor' ),
'default' => false,
),
'QM_DB_EXPENSIVE' => array(
'label' => __( 'If an individual database query takes longer than this time to execute, it\'s considered "slow" and triggers a warning.', 'query-monitor' ),
'default' => 0.05,
),
'QM_DISABLED' => array(
'label' => __( 'Disable Query Monitor entirely.', 'query-monitor' ),
'default' => false,
),
'QM_DISABLE_ERROR_HANDLER' => array(
'label' => __( 'Disable the handling of PHP errors.', 'query-monitor' ),
'default' => false,
),
'QM_ENABLE_CAPS_PANEL' => array(
'label' => __( 'Enable the Capability Checks panel.', 'query-monitor' ),
'default' => false,
),
'QM_HIDE_CORE_ACTIONS' => array(
'label' => __( 'Hide WordPress core on the Hooks & Actions panel.', 'query-monitor' ),
'default' => false,
),
'QM_HIDE_SELF' => array(
'label' => __( 'Hide Query Monitor itself from various panels. Set to false if you want to see how Query Monitor hooks into WordPress.', 'query-monitor' ),
'default' => true,
),
'QM_NO_JQUERY' => array(
'label' => __( 'Don\'t specify jQuery as a dependency of Query Monitor. If jQuery isn\'t enqueued then Query Monitor will still operate, but with some reduced functionality.', 'query-monitor' ),
'default' => false,
),
'QM_SHOW_ALL_HOOKS' => array(
'label' => __( 'In the Hooks & Actions panel, show every hook that has an action or filter attached (instead of every action hook that fired during the request).', 'query-monitor' ),
'default' => false,
),
);
echo '<section>';
echo '<h3>' . esc_html__( 'Configuration', 'query-monitor' ) . '</h3>';
echo '<p>';
printf(
/* translators: %s: Name of the config file */
esc_html__( 'The following PHP constants can be defined in your %s file in order to control the behavior of Query Monitor:', 'query-monitor' ),
'<code>wp-config.php</code>'
);
echo '</p>';
echo '<dl>';
foreach ( $constants as $name => $constant ) {
echo '<dt><code>' . esc_html( $name ) . '</code></dt>';
echo '<dd>';
printf(
esc_html( $constant['label'] ),
'<code>' . esc_html( $constant['default'] ) . '</code>'
);
$default_value = $constant['default'];
if ( is_bool( $default_value ) ) {
$default_value = ( $default_value ? 'true' : 'false' );
}
echo '<br><span class="qm-info">';
printf(
/* translators: %s: Default value for a PHP constant */
esc_html__( 'Default value: %s', 'query-monitor' ),
'<code>' . esc_html( $default_value ) . '</code>'
);
echo '</span>';
if ( defined( $name ) ) {
$current_value = constant( $name );
if ( is_bool( $current_value ) ) {
$current_value = QM_Collector::format_bool_constant( $name );
}
}
if ( defined( $name ) && ( constant( $name ) !== $constant['default'] ) ) {
echo '<br><span class="qm-info">';
printf(
/* translators: %s: Current value for a PHP constant */
esc_html__( 'Current value: %s', 'query-monitor' ),
'<code>' . esc_html( $current_value ) . '</code>'
);
echo '</span>';
}
echo '</dd>';
}
echo '</dl>';
echo '</section>';
echo '</div>';
echo '</div>'; // #qm-settings
/**
* Fires after settings but before the panel closing tag.
*
* @since 3.1.0
*
* @param QM_Dispatcher_Html $this The HTML dispatcher instance.
* @param QM_Output_Html[] $this->outputters Array of outputters.
*/
do_action( 'qm/output/after', $this, $this->outputters );
echo '</div>'; // #qm-panels
echo '</div>'; // #qm-wrapper
echo '</div>'; // #query-monitor-main
echo '<script type="text/javascript">' . "\n\n";
?>
window.addEventListener('load', function() {
if ( ( 'undefined' === typeof QM_i18n ) || ( 'undefined' === typeof jQuery ) || ! window.jQuery ) {
/* Fallback for worst case scenario */
document.getElementById( 'query-monitor-main' ).className += ' qm-broken';
console.error( document.getElementById( 'qm-broken' ).textContent );
if ( 'undefined' === typeof QM_i18n ) {
console.error( 'QM error from page: undefined QM_i18n' );
}
if ( 'undefined' === typeof jQuery ) {
console.error( 'QM error from page: undefined jQuery' );
}
if ( ! window.jQuery ) {
console.error( 'QM error from page: no jQuery' );
}
var menu_item = document.getElementById( 'wp-admin-bar-query-monitor' );
if ( menu_item ) {
menu_item.addEventListener( 'click', function() {
document.getElementById( 'query-monitor-main' ).className += ' qm-show';
} );
}
} else if ( ! document.getElementById( 'wpadminbar' ) ) {
document.getElementById( 'query-monitor-main' ).className += ' qm-peek';
}
} );
<?php
echo '</script>' . "\n\n";
echo '<!-- End Query Monitor output -->' . "\n\n";
}
public static function size( $var ) {
$start_memory = memory_get_usage();
try {
$var = unserialize( serialize( $var ) ); // phpcs:ignore
} catch ( Exception $e ) {
return $e;
}
return memory_get_usage() - $start_memory - ( PHP_INT_SIZE * 8 );
}
public function js_admin_bar_menu() {
/**
* Filters the CSS class names used on Query Monitor's admin toolbar menu.
*
* @since 2.7.0
*
* @param array $menu_classes Array of menu classes.
*/
$class = implode( ' ', apply_filters( 'qm/output/menu_class', array() ) );
if ( false === strpos( $class, 'qm-' ) ) {
$class .= ' qm-all-clear';
}
/**
* Filters the title used in Query Monitor's admin toolbar menu.
*
* @since 2.7.0
*
* @param array $output_title List of titles.
*/
$title = implode( '&nbsp;&nbsp;&nbsp;', apply_filters( 'qm/output/title', array() ) );
if ( empty( $title ) ) {
$title = esc_html__( 'Query Monitor', 'query-monitor' );
}
$admin_bar_menu = array(
'top' => array(
'title' => sprintf(
'<span class="ab-icon">QM</span><span class="ab-label">%s</span>',
$title
),
'classname' => $class,
),
'sub' => array(),
);
foreach ( $this->admin_bar_menu as $menu ) {
$admin_bar_menu['sub'][ $menu['id'] ] = $menu;
}
return $admin_bar_menu;
}
public function is_active() {
if ( ! self::user_can_view() ) {
return false;
}
if ( ! $this->did_footer ) {
return false;
}
// Don't dispatch if this is an async request and not a customizer preview:
if ( QM_Util::is_async() && ( ! function_exists( 'is_customize_preview' ) || ! is_customize_preview() ) ) {
return false;
}
// Don't dispatch if the minimum required actions haven't fired:
if ( is_admin() ) {
if ( ! did_action( 'admin_init' ) ) {
return false;
}
} else {
if ( ! ( did_action( 'wp' ) || did_action( 'login_init' ) || did_action( 'gp_head' ) ) ) {
return false;
}
}
// Don't dispatch during an iframed request, eg the plugin info modal or an upgrader action:
if ( defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST ) {
return false;
}
/** Back-compat filter. Please use `qm/dispatch/html` instead */
if ( ! apply_filters( 'qm/process', true, is_admin_bar_showing() ) ) {
return false;
}
return true;
}
}
function register_qm_dispatcher_html( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['html'] = new QM_Dispatcher_Html( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_html', 10, 2 );

View File

@@ -0,0 +1,81 @@
<?php
/**
* REST API request dispatcher.
*
* @package query-monitor
*/
class QM_Dispatcher_REST extends QM_Dispatcher {
public $id = 'rest';
public function __construct( QM_Plugin $qm ) {
parent::__construct( $qm );
add_filter( 'rest_post_dispatch', array( $this, 'filter_rest_post_dispatch' ), 1, 3 );
}
/**
* Filters a REST API response in order to add QM's headers.
*
* @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
* @return WP_HTTP_Response Result to send to the client.
*/
public function filter_rest_post_dispatch( WP_HTTP_Response $result, WP_REST_Server $server, WP_REST_Request $request ) {
if ( ! $this->should_dispatch() ) {
return $result;
}
$this->before_output();
/* @var QM_Output_Headers[] */
foreach ( $this->get_outputters( 'headers' ) as $id => $output ) {
$output->output();
}
$this->after_output();
return $result;
}
protected function before_output() {
require_once $this->qm->plugin_path( 'output/Headers.php' );
foreach ( glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
include_once $file;
}
}
public function is_active() {
# If the headers have already been sent then we can't do anything about it
if ( headers_sent() ) {
return false;
}
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
return false;
}
if ( ! self::user_can_view() ) {
return false;
}
return true;
}
}
function register_qm_dispatcher_rest( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['rest'] = new QM_Dispatcher_REST( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_rest', 10, 2 );

View File

@@ -0,0 +1,86 @@
<?php
/**
* HTTP redirect dispatcher.
*
* @package query-monitor
*/
class QM_Dispatcher_Redirect extends QM_Dispatcher {
public $id = 'redirect';
public function __construct( QM_Plugin $qm ) {
parent::__construct( $qm );
add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 9999, 2 );
}
/**
* Filters a redirect location in order to output QM's headers.
*
* @param string $location The path to redirect to.
* @param int $status Status code to use.
*/
public function filter_wp_redirect( $location, $status ) {
if ( ! $this->should_dispatch() ) {
return $location;
}
$this->before_output();
/* @var QM_Output_Headers[] */
foreach ( $this->get_outputters( 'headers' ) as $id => $output ) {
$output->output();
}
$this->after_output();
return $location;
}
protected function before_output() {
require_once $this->qm->plugin_path( 'output/Headers.php' );
foreach ( glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
require_once $file;
}
}
public function is_active() {
if ( ! self::user_can_view() ) {
return false;
}
# If the headers have already been sent then we can't do anything about it
if ( headers_sent() ) {
return false;
}
# Don't process if the minimum required actions haven't fired:
if ( is_admin() ) {
if ( ! did_action( 'admin_init' ) ) {
return false;
}
} else {
if ( ! ( did_action( 'wp' ) || did_action( 'login_init' ) ) ) {
return false;
}
}
return true;
}
}
function register_qm_dispatcher_redirect( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['redirect'] = new QM_Dispatcher_Redirect( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_redirect', 10, 2 );

View File

@@ -0,0 +1,162 @@
<?php
/**
* Dispatcher for output that gets added to `wp_die()` calls.
*
* @package query-monitor
*/
class QM_Dispatcher_WP_Die extends QM_Dispatcher {
public $id = 'wp_die';
public $trace = null;
protected $outputters = array();
public function __construct( QM_Plugin $qm ) {
add_action( 'shutdown', array( $this, 'dispatch' ), 0 );
add_filter( 'wp_die_handler', array( $this, 'filter_wp_die_handler' ) );
parent::__construct( $qm );
}
public function filter_wp_die_handler( $handler ) {
$this->trace = new QM_Backtrace( array(
'ignore_frames' => 1,
) );
return $handler;
}
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
require_once $this->qm->plugin_path( 'output/Html.php' );
$switched_locale = function_exists( 'switch_to_locale' ) && switch_to_locale( get_user_locale() );
$stack = array();
$filtered_trace = $this->trace->get_display_trace();
// Ignore the `apply_filters('wp_die_handler')` stack frame:
array_shift( $filtered_trace );
foreach ( $filtered_trace as $i => $item ) {
$stack[] = QM_Output_Html::output_filename( $item['display'], $item['file'], $item['line'] );
}
if ( isset( $filtered_trace[ $i - 1 ] ) ) {
$culprit = $filtered_trace[ $i - 1 ];
} else {
$culprit = $filtered_trace[ $i ];
}
$component = QM_Backtrace::get_frame_component( $culprit );
printf(
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
'<link rel="stylesheet" href="%s" media="all" />',
esc_url( includes_url( 'css/dashicons.css' ) )
);
?>
<style>
#query-monitor {
position: absolute;
margin: 0.9em 0 1em;
box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.13 );
background: #fff;
padding-top: 1em;
max-width: 700px;
z-index: -1;
}
#query-monitor h2 {
font-size: 12px;
font-weight: normal;
padding: 5px;
background: #f3f3f3;
margin: 0;
border-top: 1px solid #ddd;
}
#query-monitor ol,
#query-monitor p {
font-size: 12px;
padding: 0;
margin: 1em 2em;
}
#query-monitor ol {
padding: 0 0 1em 1em;
}
#query-monitor li {
margin: 0 0 0.7em;
list-style: none;
}
#query-monitor .qm-info {
color: #666;
}
#query-monitor .dashicons-info {
color: #0071a1;
vertical-align: bottom;
margin-right: 5px;
}
</style>
<?php
echo '<div id="query-monitor">';
echo '<p>';
echo '<span class="dashicons dashicons-info" aria-hidden="true"></span>';
if ( $component ) {
$name = ( 'plugin' === $component->type ) ? $component->context : $component->name;
printf(
/* translators: %s: Plugin or theme name */
esc_html__( 'This message was triggered by %s.', 'query-monitor' ),
'<b>' . esc_html( $name ) . '</b>'
);
}
echo '</p>';
echo '<p>' . esc_html__( 'Call stack:', 'query-monitor' ) . '</p>';
echo '<ol>';
echo '<li>' . implode( '</li><li>', $stack ) . '</li>'; // WPCS: XSS ok.
echo '</ol>';
echo '<h2>' . esc_html__( 'Query Monitor', 'query-monitor' ) . '</h2>';
echo '</div>';
if ( $switched_locale ) {
restore_previous_locale();
}
}
public function is_active() {
if ( ! $this->trace ) {
return false;
}
if ( ! self::user_can_view() ) {
return false;
}
return true;
}
}
function register_qm_dispatcher_wp_die( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['wp_die'] = new QM_Dispatcher_WP_Die( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_wp_die', 10, 2 );

View File

@@ -0,0 +1,24 @@
<?php
/**
* Abstract output class for HTTP headers.
*
* @package query-monitor
*/
abstract class QM_Output_Headers extends QM_Output {
public function output() {
$id = $this->collector->id;
foreach ( $this->get_output() as $key => $value ) {
if ( is_scalar( $value ) ) {
header( sprintf( 'X-QM-%s-%s: %s', $id, $key, $value ) );
} else {
header( sprintf( 'X-QM-%s-%s: %s', $id, $key, json_encode( $value ) ) );
}
}
}
}

View File

@@ -0,0 +1,517 @@
<?php
/**
* Abstract output class for HTML pages.
*
* @package query-monitor
*/
abstract class QM_Output_Html extends QM_Output {
protected static $file_link_format = null;
protected $current_id = null;
protected $current_name = null;
public function name() {
_deprecated_function(
esc_html( get_class( $this->collector ) . '::name()' ),
'3.5',
esc_html( get_class( $this ) . '::name()' )
);
return $this->collector->name();
}
public function admin_menu( array $menu ) {
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( $this->name() ),
) );
return $menu;
}
public function get_output() {
ob_start();
// compat until I convert all the existing outputters to use `get_output()`
$this->output();
$out = ob_get_clean();
return $out;
}
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 )
);
}
protected function after_tabular_output() {
echo '</table>';
echo '</div>';
$this->output_concerns();
}
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 )
);
}
protected function after_non_tabular_output() {
echo '</div>';
echo '</div>';
$this->output_concerns();
}
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__( '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 );
}
echo '</tbody>';
echo '</table>';
echo '</div>';
}
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 )
);
}
protected function after_debug_bar_output() {
echo '</div>';
}
protected function build_notice( $notice ) {
$return = '<section>';
$return .= '<div class="qm-notice">';
$return .= '<p>';
$return .= $notice;
$return .= '</p>';
$return .= '</div>';
$return .= '</section>';
return $return;
}
public static function output_inner( $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[] $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 array $prepend Associative array of options to prepend to the list of values.
* @type array $append Associative array of options to append to the list of values.
* }
* @return string Markup for the table filter controls.
*/
protected function build_filter( $name, array $values, $label, $args = array() ) {
if ( empty( $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(),
), $args );
$core_val = __( '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-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_x( 'All', '"All" option for filters', 'query-monitor' ) . '</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 .= '<label 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 .= '<span class="qm-sort-arrow" aria-hidden="true"></span>';
$out .= '</button>';
$out .= '</label>';
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;
}
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|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|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)|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 ) ? $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 );
}
}
$link = sprintf( self::get_file_link_format(), rawurlencode( $file ), intval( $link_line ) );
if ( $is_filename ) {
$format = '<a href="%s" class="qm-edit-link">%s</a>';
} else {
$format = '<a href="%s" class="qm-edit-link"><code>%s</code></a>';
}
return sprintf(
$format,
esc_attr( $link ),
esc_html( $text )
);
}
/**
* Provides a protocol URL for edit links in QM stack traces for various editors.
*
* @param string $editor the chosen code editor
* @param string $default_format a format to use if no editor is found
*
* @return string a protocol URL format
*/
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';
default:
return $default_format;
}
}
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 $format The format of the clickable file link.
*/
$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;
}
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 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() );
}
public static function has_clickable_links() {
return ( false !== self::get_file_link_format() );
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* General overview output for HTTP headers.
*
* @package query-monitor
*/
class QM_Output_Headers_Overview extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_Overview Collector.
*/
protected $collector;
public function get_output() {
$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 kilobytes */
__( '%s kB', 'query-monitor' ),
number_format_i18n( $data['memory'] / 1024 )
);
$headers['memory_usage'] = sprintf(
/* translators: 1: Percentage of memory limit used, 2: Memory limit in kilobytes */
__( '%1$s%% of %2$s kB limit', 'query-monitor' ),
number_format_i18n( $data['memory_usage'], 1 ),
number_format_i18n( $data['memory_limit'] / 1024 )
);
}
return $headers;
}
}
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,77 @@
<?php
/**
* PHP error output for HTTP headers.
*
* @package query-monitor
*/
class QM_Output_Headers_PHP_Errors extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_PHP_Errors Collector.
*/
protected $collector;
public function get_output() {
$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++;
# @TODO we should calculate the component during process() so we don't need to do it
# separately in each output.
if ( $error['trace'] ) {
$component = $error['trace']->get_component()->name;
$stack = $error['trace']->get_stack();
} else {
$component = __( 'Unknown', 'query-monitor' );
$stack = array();
}
$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' => $component,
);
$key = sprintf( 'error-%d', $count );
$headers[ $key ] = json_encode( $output_error );
}
}
return array_merge(
array(
'error-count' => $count,
),
$headers
);
}
}
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,40 @@
<?php
/**
* HTTP redirects output for HTTP headers.
*
* @package query-monitor
*/
class QM_Output_Headers_Redirects extends QM_Output_Headers {
/**
* Collector instance.
*
* @var QM_Collector_Redirects Collector.
*/
protected $collector;
public function get_output() {
$data = $this->collector->get_data();
$headers = array();
if ( empty( $data['trace'] ) ) {
return array();
}
$headers['Redirect-Trace'] = implode( ', ', $data['trace']->get_stack() );
return $headers;
}
}
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,123 @@
<?php
/**
* Admin screen output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Admin Screen', 'query-monitor' );
}
public function output() {
$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 ( $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();
}
}
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,240 @@
<?php
/**
* Scripts and styles output for HTML pages.
*
* @package query-monitor
*/
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' ) );
}
abstract public function get_type_labels();
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['assets'] ) ) {
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_label = $this->get_type_labels();
$this->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( $this->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( $this->type . '-dependencies', $data['dependencies'], __( 'Dependencies', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
echo '<th scope="col" class="qm-filterable-column">';
echo $this->build_filter( $this->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();
}
protected function dependency_row( $handle, array $asset, $label ) {
$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 ] ) ) {
$dependency_output[] = sprintf(
'<span style="white-space:nowrap"><span class="dashicons dashicons-warning" aria-hidden="true"></span>%s</span>',
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';
}
echo '<tr data-qm-subject="' . esc_attr( $this->type . '-' . $handle ) . '" data-qm-' . esc_attr( $this->type ) . '-host="' . esc_attr( $qm_host ) . '" data-qm-' . esc_attr( $this->type ) . '-dependents="' . esc_attr( $dependents_list ) . '" data-qm-' . esc_attr( $this->type ) . '-dependencies="' . esc_attr( $dependencies_list ) . '" class="' . esc_attr( $class ) . '">';
echo '<td class="qm-nowrap">';
if ( $asset['warning'] ) {
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
}
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 );
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 ( is_wp_error( $asset['source'] ) ) {
$error_data = $asset['source']->get_error_data();
if ( $error_data && isset( $error_data['src'] ) ) {
printf(
'<span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>%1$s:</span><br><a href="%2$s" class="qm-link">%2$s</a>',
esc_html( $asset['source']->get_error_message() ),
esc_url( $error_data['src'] )
);
} else {
printf(
'<span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>%s</span>',
esc_html( $asset['source']->get_error_message() )
);
}
} elseif ( ! empty( $asset['source'] ) ) {
printf(
'<a href="%s" class="qm-link">%s</a>',
esc_url( $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>';
}
public function _prefix_type( $val ) {
return $this->type . '-' . $val;
}
public function admin_class( array $class ) {
$data = $this->collector->get_data();
if ( ! empty( $data['broken'] ) || ! empty( $data['missing'] ) ) {
$class[] = 'qm-error';
}
return $class;
}
public function admin_menu( array $menu ) {
$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,41 @@
<?php
/**
* Enqueued scripts output for HTML pages.
*
* @package query-monitor
*/
class QM_Output_Html_Assets_Scripts extends QM_Output_Html_Assets {
/**
* Collector instance.
*
* @var QM_Collector_Assets_Scripts Collector.
*/
protected $collector;
public function name() {
return __( 'Scripts', 'query-monitor' );
}
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' ),
);
}
}
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,41 @@
<?php
/**
* Enqueued styles output for HTML pages.
*
* @package query-monitor
*/
class QM_Output_Html_Assets_Styles extends QM_Output_Html_Assets {
/**
* Collector instance.
*
* @var QM_Collector_Assets_Styles Collector.
*/
protected $collector;
public function name() {
return __( 'Styles', 'query-monitor' );
}
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' ),
);
}
}
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,283 @@
<?php
/**
* Block editor data output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Blocks', 'query-monitor' );
}
public function output() {
$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>';
echo '<th scope="col">' . esc_html__( 'Render Callback', 'query-monitor' ) . '</th>';
if ( isset( $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>';
printf(
'<td colspan="6">%s</td>',
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();
}
protected static function render_block( $i, array $block, array $data ) {
$block_error = false;
$row_class = '';
$referenced_post = null;
$referenced_type = 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-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>'
);
}
}
}
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>';
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
echo $error_message; // WPCS: XSS ok;
}
if ( ! empty( $referenced_post ) && ! empty( $referenced_pto ) ) {
echo '<br>';
echo '<a href="' . esc_url( get_edit_post_link( $referenced_post ) ) . '" class="qm-link">' . esc_html( $referenced_pto->labels->edit_item ) . '</a>';
}
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 ( 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'] ) ) {
echo '<br><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
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 );
}
}
}
public function admin_menu( array $menu ) {
$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;
}
}
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,223 @@
<?php
/**
* User capability checks output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Capability Checks', 'query-monitor' );
}
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;
}
$data = $this->collector->get_data();
if ( ! empty( $data['caps'] ) ) {
$this->before_tabular_output();
$results = array(
'true',
'false',
);
$show_user = ( count( $data['users'] ) > 1 );
$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'];
usort( $users, 'strcasecmp' );
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 $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>';
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();
}
}
public function admin_menu( array $menu ) {
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => $this->name(),
) );
return $menu;
}
}
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,107 @@
<?php
/**
* Template conditionals output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Conditionals', 'query-monitor' );
}
public function output() {
$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();
}
public function admin_menu( array $menu ) {
$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;
}
public function panel_menu( array $menu ) {
$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;
}
}
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,131 @@
<?php
/**
* Database query calling function output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Queries by Caller', 'query-monitor' );
}
public function output() {
$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" aria-sort="none">';
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"><button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="caller" data-qm-value="' . esc_attr( $row['caller'] ) . '"><code>' . esc_html( $row['caller'] ) . '</code></button></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( $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();
}
}
public function panel_menu( array $menu ) {
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
$dbq_data = $dbq->get_data();
if ( isset( $dbq_data['times'] ) ) {
$menu['qm-db_queries-$wpdb']['children'][] = $this->menu( array(
'title' => esc_html__( 'Queries by Caller', 'query-monitor' ),
) );
}
}
return $menu;
}
}
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,128 @@
<?php
/**
* Database query calling component output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Queries by Component', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['types'] ) || empty( $data['times'] ) ) {
return;
}
$total_time = 0;
$span = count( $data['types'] ) + 2;
$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" aria-sort="none">';
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"><button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="component" data-qm-value="' . esc_attr( $row['component'] ) . '">' . esc_html( $row['component'] ) . '</button></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">&nbsp;</td>';
}
}
echo '<td class="qm-num" data-qm-sort-weight="' . esc_attr( $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();
}
public function panel_menu( array $menu ) {
$data = $this->collector->get_data();
if ( empty( $data['types'] ) || empty( $data['times'] ) ) {
return $menu;
}
$dbq = QM_Collectors::get( 'db_queries' );
if ( $dbq ) {
$dbq_data = $dbq->get_data();
if ( isset( $dbq_data['component_times'] ) ) {
$menu['qm-db_queries-$wpdb']['children'][] = $this->menu( array(
'title' => esc_html__( 'Queries by Component', 'query-monitor' ),
) );
}
}
return $menu;
}
}
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,161 @@
<?php
/**
* Duplicate database query output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Duplicate Queries', 'query-monitor' );
}
public function output() {
$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">' . 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 );
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-row-caller qm-nowrap qm-ltr">';
foreach ( $data['dupe_callers'][ $sql ] as $caller => $calls ) {
printf(
'<button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="caller" data-qm-value="%s"><code>%s</code></button><br><span class="qm-info qm-supplemental">%s</span><br>',
esc_attr( $caller ),
esc_html( $caller ),
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();
}
public function admin_menu( array $menu ) {
$dbq = QM_Collectors::get( 'db_dupes' );
if ( $dbq ) {
$dbq_data = $dbq->get_data();
if ( isset( $dbq_data['dupes'] ) && count( $dbq_data['dupes'] ) ) {
$count = count( $dbq_data['dupes'] );
$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;
}
public function panel_menu( array $menu ) {
$id = $this->collector->id();
if ( isset( $menu[ $id ] ) ) {
$menu[ $id ]['title'] = $menu[ $id ]['title'];
$menu['qm-db_queries-$wpdb']['children'][] = $menu[ $id ];
unset( $menu[ $id ] );
}
return $menu;
}
}
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,618 @@
<?php
/**
* Database query output for HTML pages.
*
* @package query-monitor
*/
class QM_Output_Html_DB_Queries extends QM_Output_Html {
/**
* Collector instance.
*
* @var QM_Collector_DB_Queries Collector.
*/
protected $collector;
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' ) );
}
public function name() {
return __( 'Database Queries', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['dbs'] ) ) {
$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'] );
}
foreach ( $data['dbs'] as $name => $db ) {
$this->output_queries( $name, $db, $data );
}
}
protected function output_empty_queries() {
$id = sprintf(
'%s-wpdb',
$this->collector->id()
);
$this->before_non_tabular_output( $id );
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();
}
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();
}
protected function output_expensive_queries( array $expensive ) {
$dp = strlen( substr( strrchr( (string) QM_DB_EXPENSIVE, '.' ), 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();
}
protected function output_queries( $name, stdClass $db, array $data ) {
$this->query_row = 0;
$span = 4;
if ( $db->has_result ) {
$span++;
}
if ( $db->has_trace ) {
$span++;
}
$panel_id = sprintf(
'%s-%s',
$this->collector->id(),
sanitize_title_with_dashes( $name )
);
$panel_name = sprintf(
/* translators: %s: Name of database controller */
__( '%s Queries', 'query-monitor' ),
$name
);
if ( ! empty( $db->rows ) ) {
$this->before_tabular_output( $panel_id, $panel_name );
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 && ( '$wpdb' === $name ) ) {
echo '<tr>';
echo '<th colspan="' . intval( $span ) . '" class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
if ( file_exists( WP_CONTENT_DIR . '/db.php' ) ) {
/* translators: 1: Symlink file name, 2: URL to wiki page */
$message = __( 'Extended query information such as the component and affected rows is not available. A conflicting %1$s file is present. <a href="%2$s" target="_blank" class="qm-external-link">See this wiki page for more information.</a>', 'query-monitor' );
} else {
/* translators: 1: Symlink file name, 2: URL to wiki page */
$message = __( 'Extended query information such as the component and affected rows is not available. Query Monitor was unable to symlink its %1$s file into place. <a href="%2$s" target="_blank" class="qm-external-link">See this wiki page for more information.</a>', 'query-monitor' );
}
echo wp_kses( sprintf(
$message,
'<code>db.php</code>',
'https://github.com/johnbillion/query-monitor/wiki/db.php-Symlink'
), array(
'a' => array(
'href' => array(),
'target' => array(),
'class' => array(),
),
) );
echo '</th>';
echo '</tr>';
}
$types = array_keys( $db->types );
$prepend = array();
$callers = wp_list_pluck( $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 = wp_list_pluck( $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" aria-sort="none">';
echo $this->build_sorter( __( 'Rows', 'query-monitor' ) ); // WPCS: XSS ok.
echo '</th>';
}
echo '<th scope="col" class="qm-num qm-sortable-column" role="columnheader" aria-sort="none">';
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( $panel_id, $panel_name );
$notice = __( 'No queries! Nice work.', 'query-monitor' );
echo $this->build_notice( $notice ); // WPCS: XSS ok.
$this->after_non_tabular_output();
}
}
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 = 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_display_trace();
array_shift( $filtered_trace );
foreach ( $filtered_trace as $item ) {
$stack[] = self::output_filename( $item['display'], $item['calling_file'], $item['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 = explode( ', ', $row['stack'] );
$stack = array_reverse( $stack );
array_shift( $stack );
$stack = array_map( function( $item ) {
return '<code>' . esc_html( $item ) . '</code>';
}, $stack );
}
$row_attr = array();
if ( is_wp_error( $row['result'] ) ) {
$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 ( is_wp_error( $row['result'] ) ) {
echo "<td class='qm-row-result qm-row-error'><span class='dashicons dashicons-warning' aria-hidden='true'></span>" . 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'] ) && is_wp_error( $row['result'] ) ) {
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 ) {
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
}
echo esc_html( $stime );
echo "</td>\n";
}
echo '</tr>';
}
public function admin_title( array $existing ) {
$data = $this->collector->get_data();
if ( isset( $data['dbs'] ) ) {
foreach ( $data['dbs'] as $key => $db ) {
/* translators: %s: Database query time in seconds */
$text = _nx( '%s S', '%s S', $db->total_time, 'Query time', 'query-monitor' );
// Avoid a potentially blank translation for the plural form.
// @see https://meta.trac.wordpress.org/ticket/5377
if ( '' === $text ) {
$text = '%s S';
}
$title[] = sprintf(
esc_html( '%s' . $text ),
( count( $data['dbs'] ) > 1 ? '&bull;&nbsp;&nbsp;&nbsp;' : '' ),
number_format_i18n( $db->total_time, 4 )
);
/* translators: %s: Number of database queries */
$text = _nx( '%s Q', '%s Q', $db->total_qs, 'Query count', '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[] = sprintf(
esc_html( $text ),
number_format_i18n( $db->total_qs )
);
}
} elseif ( isset( $data['total_qs'] ) ) {
/* translators: %s: Number of database queries */
$text = _nx( '%s Q', '%s Q', $data['total_qs'], 'Query count', '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[] = sprintf(
/* translators: %s: Number of database queries */
esc_html( $text ),
number_format_i18n( $data['total_qs'] )
);
}
foreach ( $title as &$t ) {
$t = preg_replace( '#\s?([^0-9,\.]+)#', '<small>$1</small>', $t );
}
$title = array_merge( $existing, $title );
return $title;
}
public function admin_class( array $class ) {
if ( $this->collector->get_errors() ) {
$class[] = 'qm-error';
}
if ( $this->collector->get_expensive() ) {
$class[] = 'qm-expensive';
}
return $class;
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
$errors = $this->collector->get_errors();
$expensive = $this->collector->get_expensive();
if ( isset( $data['dbs'] ) && count( $data['dbs'] ) > 1 ) {
foreach ( $data['dbs'] as $name => $db ) {
$name_attr = sanitize_title_with_dashes( $name );
$id = $this->collector->id() . '-' . $name_attr;
$menu[ $id ] = $this->menu( array(
'id' => esc_attr( sprintf( 'query-monitor-%s-db-%s', $this->collector->id(), $name_attr ) ),
'title' => esc_html( sprintf(
/* translators: %s: Name of database controller */
__( 'Queries: %s', 'query-monitor' ),
$name
) ),
'href' => esc_attr( sprintf( '#%s-%s', $this->collector->id(), $name_attr ) ),
) );
}
} else {
$id = $this->collector->id() . '-$wpdb';
$menu[ $id ] = $this->menu( array(
'title' => esc_html__( 'Queries', 'query-monitor' ),
'href' => esc_attr( sprintf( '#%s-wpdb', $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;
}
public function panel_menu( array $menu ) {
foreach ( array( 'errors', 'expensive' ) as $sub ) {
$id = $this->collector->id() . '-' . $sub;
if ( isset( $menu[ $id ] ) ) {
$menu[ $id ]['title'] = $menu[ $id ]['title'];
$menu['qm-db_queries-$wpdb']['children'][] = $menu[ $id ];
unset( $menu[ $id ] );
}
}
return $menu;
}
}
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,91 @@
<?php
/**
* 'Debug Bar' output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
$title = $this->collector->get_panel()->title();
return sprintf(
/* translators: Debug Bar add-on name */
__( 'Debug Bar: %s', 'query-monitor' ),
$title
);
}
public function output() {
$target = sanitize_html_class( get_class( $this->collector->get_panel() ) );
$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 = 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();
}
}
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 ) {
$panel_id = strtolower( sanitize_html_class( get_class( $panel ) ) );
$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,321 @@
<?php
/**
* Environment data output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Environment', 'query-monitor' );
}
public function output() {
$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 ) {
echo '<span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
}
echo esc_html( $data['php']['version'] );
echo $append; // WPCS: XSS ok.
echo '</td>';
echo '</tr>';
echo '<tr>';
echo '<th scope="row">SAPI</th>';
echo '<td>' . esc_html( $data['php']['sapi'] ) . '</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 ) {
$class = '';
$warners = array(
'max_execution_time',
'memory_limit',
);
if ( ! $val && in_array( $key, $warners, true ) ) {
$class = 'qm-warn';
}
echo '<tr class="' . esc_attr( $class ) . '">';
echo '<th scope="row">' . esc_html( $key ) . '</th>';
echo '<td>';
if ( 'qm-warn' === $class ) {
echo '<span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
}
echo esc_html( $val['after'] );
if ( $val['after'] !== $val['before'] ) {
printf(
'<br><span class="qm-info qm-supplemental">%s</span>',
esc_html( sprintf(
/* translators: %s: Original value of a variable */
__( 'Overridden at runtime from %s', 'query-monitor' ),
$val['before']
) )
);
}
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( $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'] ) ) {
foreach ( $data['db'] as $id => $db ) {
if ( 1 === count( $data['db'] ) ) {
$name = __( 'Database', 'query-monitor' );
} else {
/* translators: %s: Name of database controller */
$name = sprintf( __( 'Database: %s', 'query-monitor' ), $id );
}
echo '<section>';
echo '<h3>' . esc_html( $name ) . '</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( $db['info'][ $field ] ) ) {
echo '<td><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . esc_html__( 'Unknown', 'query-monitor' ) . '</span></td>';
} else {
echo '<td>' . esc_html( $db['info'][ $field ] ) . '</td>';
}
echo '</tr>';
}
foreach ( $db['variables'] as $setting ) {
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$key = $setting->Variable_name;
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$val = $setting->Value;
$append = '';
if ( is_numeric( $val ) && ( $val >= ( 1024 * 1024 ) ) ) {
$append .= sprintf(
'&nbsp;<span class="qm-info">(~%s)</span>',
esc_html( 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>';
}
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' => __( 'Address', 'query-monitor' ),
'host' => __( 'Host', 'query-monitor' ),
'OS' => __( 'OS', '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();
}
}
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,117 @@
<?php
/**
* Request and response headers output for HTML pages.
*
* @package query-monitor
*/
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' );
}
public function output() {
$this->output_request();
$this->output_response();
}
public function output_request() {
$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();
}
public function output_response() {
$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();
}
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>';
}
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;
}
}
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,203 @@
<?php
/**
* Hooks and actions output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Hooks & Actions', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['hooks'] ) ) {
return;
}
$this->before_tabular_output();
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 scope="col">' . esc_html__( 'Priority', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Action', 'query-monitor' ) . '</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'] );
echo '</tbody>';
$this->after_tabular_output();
}
public static function output_hook_table( array $hooks ) {
$core = __( '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'] );
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'] ) {
echo '<br><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
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 ( 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>';
// phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound
} elseif ( defined( 'PHP_INT_MIN' ) && 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'] ) ) {
echo '<br><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
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>';
echo '</tr>';
}
}
}
}
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,398 @@
<?php
/**
* HTTP API request output for HTML pages.
*
* @package query-monitor
*/
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' ) );
}
public function name() {
return __( 'HTTP API Calls', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( ! empty( $data['http'] ) ) {
$statuses = array_keys( $data['types'] );
$components = wp_list_pluck( $data['component_times'], 'component' );
usort( $statuses, 'strcasecmp' );
usort( $components, 'strcasecmp' );
$status_output = array();
foreach ( $statuses as $key => $status ) {
if ( -1 === $status ) {
$status_output[-1] = __( 'Error', 'query-monitor' );
} elseif ( -2 === $status ) {
/* translators: A non-blocking HTTP API request */
$status_output[-2] = __( '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">' . esc_html__( 'URL', 'query-monitor' ) . '</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__( '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 $key => $row ) {
$ltime = $row['ltime'];
$i++;
$is_error = false;
$row_attr = array();
$css = '';
if ( is_wp_error( $row['response'] ) ) {
$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'] ) && empty( $row['args']['local'] ) ) {
$info .= '<span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . 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['trace']->get_display_trace();
$filtered_trace = array_filter( $filtered_trace, function( $item ) {
// @TODO This should happen during collection.
if ( isset( $item['class'] ) ) {
return ! in_array( $item['class'], array(
'WP_Http',
), true );
}
if ( isset( $item['function'] ) ) {
return ! in_array( $item['function'], array(
'wp_safe_remote_request',
'wp_safe_remote_get',
'wp_safe_remote_post',
'wp_safe_remote_head',
'wp_remote_request',
'wp_remote_get',
'wp_remote_post',
'wp_remote_head',
'wp_remote_fopen',
'download_url',
'vip_safe_wp_remote_get',
'wpcom_vip_file_get_contents',
), true );
}
return true;
} );
foreach ( $filtered_trace as $item ) {
$stack[] = self::output_filename( $item['display'], $item['calling_file'], $item['calling_line'] );
}
$row_attr['data-qm-component'] = $component->name;
$row_attr['data-qm-type'] = $row['type'];
$row_attr['data-qm-time'] = $row['ltime'];
if ( 'core' !== $component->context ) {
$row_attr['data-qm-component'] .= ' non-core';
}
$attr = '';
foreach ( $row_attr as $a => $v ) {
$attr .= ' ' . $a . '="' . esc_attr( $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"><span class="dashicons dashicons-warning" aria-hidden="true"></span>%1$s</span><br>%2$s',
/* 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['transport'] ) && ! empty( $row['info'] ) );
echo '<td class="qm-has-toggle qm-col-status">';
if ( $is_error ) {
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
}
echo esc_html( $response );
if ( $show_toggle ) {
echo self::build_toggler(); // WPCS: XSS ok;
echo '<ul class="qm-toggled">';
}
if ( ! empty( $row['transport'] ) ) {
$transport = sprintf(
/* translators: %s HTTP API transport name */
__( 'HTTP API Transport: %s', 'query-monitor' ),
$row['transport']
);
printf(
'<li><span class="qm-info qm-supplemental">%s</span></li>',
esc_html( $transport )
);
}
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 ) )
);
}
$size_fields = array(
'size_download' => __( 'Response Size', 'query-monitor' ),
);
foreach ( $size_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( size_format( $row['info'][ $key ] ) )
);
}
$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 )
);
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="6">%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();
}
}
public function admin_class( array $class ) {
$data = $this->collector->get_data();
if ( isset( $data['errors']['alert'] ) ) {
$class[] = 'qm-alert';
}
if ( isset( $data['errors']['warning'] ) ) {
$class[] = 'qm-warning';
}
return $class;
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
$count = isset( $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;
}
}
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,124 @@
<?php
/**
* Language and locale output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Languages', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['languages'] ) ) {
return;
}
$this->before_tabular_output();
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>';
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'] ) {
echo esc_html( QM_Util::standard_dir( $mofile['file'], '' ) );
} else {
echo '<em>' . esc_html__( 'None', 'query-monitor' ) . '</em>';
}
echo '</td>';
echo '<td class="qm-nowrap">';
if ( $mofile['found'] ) {
echo esc_html( $mofile['found_formatted'] );
} else {
echo esc_html__( 'Not Found', 'query-monitor' );
}
echo '</td>';
echo '</tr>';
}
}
echo '</tbody>';
$this->after_tabular_output();
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
$args = array(
'title' => esc_html( $this->name() ),
);
$menu[ $this->collector->id() ] = $this->menu( $args );
return $menu;
}
}
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,198 @@
<?php
/**
* PSR-3 compatible logging output for HTML pages.
*
* @package query-monitor
*/
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' ), 12 );
add_filter( 'qm/output/menu_class', array( $this, 'admin_class' ) );
}
public function name() {
return __( 'Logger', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['logs'] ) ) {
return;
}
$levels = array_map( 'ucfirst', $this->collector->get_levels() );
$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__( '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['trace']->get_component();
$row_attr = array();
$row_attr['data-qm-component'] = $component->name;
$row_attr['data-qm-type'] = ucfirst( $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 scope="row" class="qm-nowrap">';
if ( $is_warning ) {
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
} else {
echo '<span class="dashicons" aria-hidden="true"></span>';
}
echo esc_html( ucfirst( $row['level'] ) );
echo '</td>';
if ( 'dump' === $row['type'] ) {
printf(
'<td><pre>%s</pre></td>',
esc_html( $row['message'] )
);
} else {
printf(
'<td>%s</td>',
esc_html( $row['message'] )
);
}
$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();
}
public function admin_class( array $class ) {
$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;
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
if ( empty( $data['logs'] ) ) {
return $menu;
}
$key = 'log';
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' );
$menu[ $this->collector->id() ] = $this->menu( array(
'id' => "query-monitor-logger-{$key}",
'title' => esc_html( sprintf(
$label,
number_format_i18n( $count )
) ),
) );
return $menu;
}
}
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,297 @@
<?php
/**
* General overview output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Overview', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
$db_query_num = null;
$db_queries = QM_Collectors::get( 'db_queries' );
if ( $db_queries ) {
# @TODO: make this less derpy:
$db_queries_data = $db_queries->get_data();
if ( isset( $db_queries_data['types'] ) && isset( $db_queries_data['total_time'] ) ) {
$db_query_num = $db_queries_data['types'];
$db_query_time = $db_queries_data['total_time'];
}
}
$raw_request = QM_Collectors::get( 'raw_request' );
$cache = QM_Collectors::get( 'cache' );
$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">';
echo '<p class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . esc_html( $qm_broken ) . '</p>';
echo '</section>';
echo '<section id="qm-ajax-errors">';
echo '<p class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . esc_html( $ajax_errors ) . '</p>';
echo '</section>';
if ( $raw_request ) {
echo '<section id="qm-overview-raw-request">';
$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-boxed">';
echo '<section>';
echo '<h3>' . esc_html__( 'Page Generation Time', 'query-monitor' ) . '</h3>';
echo '<p>';
echo esc_html( number_format_i18n( $data['time_taken'], 4 ) );
if ( $data['time_limit'] > 0 ) {
if ( $data['display_time_usage_warning'] ) {
echo '<br><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
} 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 {
echo '<br><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
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: %s: Memory used in kilobytes */
__( '%s kB', 'query-monitor' ),
number_format_i18n( $data['memory'] / 1024 )
) );
if ( $data['memory_limit'] > 0 ) {
if ( $data['display_memory_usage_warning'] ) {
echo '<br><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
} else {
echo '<br><span class="qm-info">';
}
echo esc_html( sprintf(
/* translators: 1: Percentage of memory limit used, 2: Memory limit in kilobytes */
__( '%1$s%% of %2$s kB limit', 'query-monitor' ),
number_format_i18n( $data['memory_usage'], 1 ),
number_format_i18n( $data['memory_limit'] / 1024 )
) );
echo '</span>';
} else {
echo '<br><span class="qm-warn"><span class="dashicons dashicons-warning" aria-hidden="true"></span>';
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>';
if ( isset( $db_queries_data ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Database Query Time', 'query-monitor' ) . '</h3>';
echo '<p>';
echo esc_html( number_format_i18n( $db_queries_data['total_time'], 4 ) );
echo '</p>';
echo '</section>';
}
if ( isset( $db_query_num ) && isset( $db_queries_data ) ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Database Queries', 'query-monitor' ) . '</h3>';
echo '<p>';
if ( ! isset( $db_query_num['SELECT'] ) || count( $db_query_num ) > 1 ) {
foreach ( $db_query_num as $type_name => $type_count ) {
printf(
'<button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="type" data-qm-value="%1$s">%2$s: %3$s</button><br>',
esc_attr( $type_name ),
esc_html( $type_name ),
esc_html( number_format_i18n( $type_count ) )
);
}
}
printf(
'<button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="type" data-qm-value="">%1$s: %2$s</button>',
esc_html( _x( 'Total', 'database queries', 'query-monitor' ) ),
esc_html( number_format_i18n( $db_queries_data['total_qs'] ) )
);
echo '</p>';
echo '</section>';
}
if ( $cache ) {
echo '<section>';
echo '<h3>' . esc_html__( 'Object Cache', 'query-monitor' ) . '</h3>';
$cache_data = $cache->get_data();
if ( isset( $cache_data['stats'] ) && isset( $cache_data['cache_hit_percentage'] ) ) {
$cache_hit_percentage = $cache_data['cache_hit_percentage'];
}
if ( isset( $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>';
} else {
echo '<p>';
echo esc_html__( 'Object cache statistics are not available', 'query-monitor' );
echo '</p>';
}
if ( $cache_data['has_object_cache'] ) {
echo '<p><span class="qm-info">';
printf(
'<a href="%s" class="qm-link">%s</a>',
esc_url( network_admin_url( 'plugins.php?plugin_status=dropins' ) ),
esc_html__( 'External object cache in use', 'query-monitor' )
);
echo '</span></p>';
} else {
echo '<p>';
echo esc_html__( 'External object cache not in use', 'query-monitor' );
echo '</p>';
$potentials = array_filter( $cache_data['object_cache_extensions'] );
if ( ! empty( $potentials ) ) {
foreach ( $potentials as $name => $value ) {
echo '<p>';
echo esc_html( sprintf(
/* translators: %s: PHP extension name */
__( 'The %s extension for PHP is installed but is not in use by WordPress', 'query-monitor' ),
$name
) );
echo '</p>';
}
}
}
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>';
}
}
echo '</section>';
}
$this->after_non_tabular_output();
}
public function admin_title( array $existing ) {
$data = $this->collector->get_data();
if ( empty( $data['memory'] ) ) {
$memory = '??';
} else {
$memory = number_format_i18n( ( $data['memory'] / 1024 ), 0 );
}
$title[] = sprintf(
/* translators: %s: Page load time in seconds with a decimal fraction */
esc_html_x( '%s S', 'Page load time', 'query-monitor' ),
number_format_i18n( $data['time_taken'], 2 )
);
$title[] = sprintf(
/* translators: %s: Memory usage in kilobytes */
esc_html_x( '%s kB', 'Memory usage', 'query-monitor' ),
$memory
);
foreach ( $title as &$t ) {
$t = preg_replace( '#\s?([^0-9,\.]+)#', '<small>$1</small>', $t );
}
$title = array_merge( $existing, $title );
return $title;
}
}
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,304 @@
<?php
/**
* PHP error output for HTML pages.
*
* @package query-monitor
*/
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' ) );
}
public function name() {
return __( 'PHP Errors', 'query-monitor' );
}
public function output() {
$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'];
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" class="qm-num">' . esc_html__( 'Count', 'query-monitor' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Location', '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 ) {
$row_attr = array();
$row_attr['data-qm-type'] = ucfirst( $type );
$row_attr['data-qm-key'] = $error_key;
if ( $error['trace'] ) {
$component = $error['trace']->get_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 scope="row" class="qm-nowrap">';
if ( $is_warning ) {
echo '<span class="dashicons dashicons-warning" aria-hidden="true"></span>';
} else {
echo '<span class="dashicons" aria-hidden="true"></span>';
}
echo esc_html( $title );
echo '</td>';
echo '<td class="qm-ltr">' . esc_html( $error['message'] ) . '</td>';
echo '<td class="qm-num">' . esc_html( number_format_i18n( $error['calls'] ) ) . '</td>';
$stack = array();
if ( $error['trace'] ) {
$filtered_trace = $error['trace']->get_display_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'] ) && isset( $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>';
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>';
$this->after_tabular_output();
}
public function admin_class( array $class ) {
$data = $this->collector->get_data();
if ( ! empty( $data['errors'] ) ) {
foreach ( $data['errors'] as $type => $errors ) {
$class[] = 'qm-' . $type;
}
}
return $class;
}
public function admin_menu( array $menu ) {
$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 += array_sum( wp_list_pluck( $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;
}
public function panel_menu( array $menu ) {
if ( ! isset( $menu[ $this->collector->id() ] ) ) {
return $menu;
}
$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( wp_list_pluck( $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;
}
}
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,228 @@
<?php
/**
* Request data output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Request', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
$db_queries = QM_Collectors::get( 'db_queries' );
$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 qm-boxed-wrap">';
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['dbs']['$wpdb']->has_main_query ) ) {
printf(
'<p><button class="qm-filter-trigger" data-qm-target="db_queries-wpdb" data-qm-filter="caller" data-qm-value="qm-main-query">%s</button></p>',
esc_html__( 'View Main Query', 'query-monitor' )
);
}
}
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'] ) ) {
printf( // WPCS: XSS ok.
'<p>%1$s (%2$s)</p>',
esc_html( $data['queried_object']['title'] ),
esc_html( get_class( $data['queried_object']['data'] ) )
);
} 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 ) ) {
$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();
}
public function admin_menu( array $menu ) {
$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;
}
}
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,220 @@
<?php
/**
* Template and theme output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Theme', 'query-monitor' );
}
public function output() {
$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 ( $data['is_child_theme'] ) {
echo '<h3>' . esc_html__( 'Parent Theme', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html( $data['template'] ) . '</p>';
}
echo '</section>';
echo '<section>';
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 = false;
}
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_altered'] ) ) {
printf(
'<p><button class="qm-filter-trigger qm-filter-info" data-qm-target="response-concerned_hooks">%s</button></p>',
esc_html__( 'Template Hooks in Use', 'query-monitor' )
);
}
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 ( 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 ( $data['has_template_part_action'] ) {
echo '<h4>' . esc_html__( 'Not Loaded', 'query-monitor' ) . '</h4>';
if ( ! empty( $data['unsuccessful_template_parts'] ) ) {
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>';
} elseif ( $data['has_template_part_action'] ) {
echo '<p><em>' . esc_html__( 'None', 'query-monitor' ) . '</em></p>';
}
}
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();
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
if ( 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;
}
public function panel_menu( array $menu ) {
if ( isset( $menu[ $this->collector->id() ] ) ) {
$menu[ $this->collector->id() ]['title'] = __( 'Template', 'query-monitor' );
}
return $menu;
}
}
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,207 @@
<?php
/**
* Timing and profiling output for HTML pages.
*
* @package query-monitor
*/
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' ), 15 );
}
public function name() {
return __( 'Timing', 'query-monitor' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data['timing'] ) && empty( $data['warning'] ) ) {
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['trace']->get_component();
$trace = $row['trace']->get_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['trace']->get_component();
$trace = $row['trace']->get_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"><span class="dashicons dashicons-warning" aria-hidden="true"></span>%s</td>',
esc_html( $row['message'] )
);
printf(
'<td class="qm-nowrap">%s</td>',
esc_html( $component->name )
);
}
}
echo '</tbody>';
$this->after_tabular_output();
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
if ( ! empty( $data['timing'] ) || ! empty( $data['warning'] ) ) {
$count = 0;
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' );
$menu[ $this->collector->id() ] = $this->menu( array(
'title' => esc_html( sprintf(
$label,
number_format_i18n( $count )
) ),
) );
}
return $menu;
}
}
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,156 @@
<?php
/**
* Transient storage output for HTML pages.
*
* @package query-monitor
*/
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 );
}
public function name() {
return __( 'Transients', 'query-monitor' );
}
public function output() {
$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( $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 $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>';
}
$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();
}
}
public function admin_menu( array $menu ) {
$data = $this->collector->get_data();
$count = isset( $data['trans'] ) ? count( $data['trans'] ) : 0;
$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;
}
}
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,74 @@
<?php
/**
* Query Monitor plugin for WordPress
*
* @package query-monitor
* @link https://github.com/johnbillion/query-monitor
* @author John Blackbourn <john@johnblackbourn.com>
* @copyright 2009-2019 John Blackbourn
* @license GPL v2 or later
*
* Plugin Name: Query Monitor
* Description: The Developer Tools Panel for WordPress.
* Version: 3.6.4
* Plugin URI: https://querymonitor.com/
* Author: John Blackbourn
* Author URI: https://querymonitor.com/
* Text Domain: query-monitor
* Domain Path: /languages/
* Requires PHP: 5.3.6
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
defined( 'ABSPATH' ) || die();
$qm_dir = dirname( __FILE__ );
require_once "{$qm_dir}/classes/Plugin.php";
if ( ! QM_Plugin::php_version_met() ) {
add_action( 'admin_notices', 'QM_Plugin::php_version_nope' );
return;
}
# No autoloaders for us. See https://github.com/johnbillion/query-monitor/issues/7
foreach ( array( 'Activation', 'Util', 'QM' ) as $qm_class ) {
require_once "{$qm_dir}/classes/{$qm_class}.php";
}
QM_Activation::init( __FILE__ );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once "{$qm_dir}/classes/CLI.php";
QM_CLI::init( __FILE__ );
}
if ( defined( 'QM_DISABLED' ) && QM_DISABLED ) {
return;
}
if ( 'cli' === php_sapi_name() && ! defined( 'QM_TESTS' ) ) {
# For the time being, let's not load QM when using the CLI because we've no persistent storage and no means of
# outputting collected data on the CLI. This will hopefully change in a future version of QM.
return;
}
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
# Let's not load QM during cron events for the same reason as above.
return;
}
foreach ( array( 'QueryMonitor', 'Backtrace', 'Collectors', 'Collector', 'Dispatchers', 'Dispatcher', 'Hook', 'Output', 'Timer' ) as $qm_class ) {
require_once "{$qm_dir}/classes/{$qm_class}.php";
}
QueryMonitor::init( __FILE__ );

View File

@@ -0,0 +1,462 @@
=== Query Monitor ===
Contributors: johnbillion
Tags: debug, debug-bar, debugging, development, developer, performance, profiler, queries, query monitor, rest-api
Requires at least: 3.7
Tested up to: 5.5
Stable tag: 3.6.4
License: GPLv2 or later
Requires PHP: 5.3
Query Monitor is the developer tools panel for WordPress.
== Description ==
Query Monitor is the developer tools panel for WordPress. It enables debugging of database queries, PHP errors, hooks and actions, block editor blocks, enqueued scripts and stylesheets, HTTP API calls, and more.
It includes some advanced features such as debugging of Ajax calls, REST API calls, and user capability checks. It includes the ability to narrow down much of its output by plugin or theme, allowing you to quickly determine poorly performing plugins, themes, or functions.
Query Monitor focuses heavily on presenting its information in a useful manner, for example by showing aggregate database queries grouped by the plugins, themes, or functions that are responsible for them. It adds an admin toolbar menu showing an overview of the current page, with complete debugging information shown in panels once you select a menu item.
For complete information, please see [the Query Monitor website](https://querymonitor.com/).
Here's an overview of what's shown for each page load:
* Database queries, including notifications for slow, duplicate, or erroneous queries. Allows filtering by query type (`SELECT`, `UPDATE`, `DELETE`, etc), responsible component (plugin, theme, WordPress core), and calling function, and provides separate aggregate views for each.
* The template filename, the complete template hierarchy, and names of all template parts that were loaded or not loaded.
* PHP errors presented nicely along with their responsible component and call stack, and a visible warning in the admin toolbar.
* Blocks and associated properties in post content when using WordPress 5.0+ or the Gutenberg plugin.
* Matched rewrite rules, associated query strings, and query vars.
* Enqueued scripts and stylesheets, along with their dependencies, dependents, and alerts for broken dependencies.
* Language settings and loaded translation files (MO files) for each text domain.
* HTTP API requests, with response code, responsible component, and time taken, with alerts for failed or erroneous requests.
* User capability checks, along with the result and any parameters passed to the capability check.
* Environment information, including detailed information about PHP, the database, WordPress, and the web server.
* The values of all WordPress conditional functions such as `is_single()`, `is_home()`, etc.
* Transients that were updated.
In addition:
* Whenever a redirect occurs, Query Monitor adds an HTTP header containing the call stack, so you can use your favourite HTTP inspector or browser developer tools to trace what triggered the redirect.
* The response from any jQuery-initiated Ajax request on the page will contain various debugging information in its headers. PHP errors also get output to the browser's developer console.
* The response from an authenticated WordPress REST API request will contain various debugging information in its headers, as long as the authenticated user has permission to view Query Monitor's output.
By default, Query Monitor's output is only shown to Administrators on single-site installations, and Super Admins on Multisite installations.
In addition to this, you can set an authentication cookie which allows you to view Query Monitor output when you're not logged in (or if you're logged in as a non-Administrator). See the Settings panel for details.
= Privacy Statement =
Query Monitor is private by default and always will be. It does not persistently store any of the data that it collects. It does not send data to any third party, nor does it include any third party resources.
[Query Monitor's full privacy statement can be found here](https://github.com/johnbillion/query-monitor/wiki/Privacy-Statement).
== Screenshots ==
1. Admin Toolbar Menu
2. Aggregate Database Queries by Component
3. Capability Checks
4. Database Queries
5. Hooks and Actions
6. HTTP API Requests
7. Aggregate Database Queries by Calling Function
== Frequently Asked Questions ==
= Who can see Query Monitor's output? =
By default, Query Monitor's output is only shown to Administrators on single-site installations, and Super Admins on Multisite installations.
In addition to this, you can set an authentication cookie which allows you to view Query Monitor output when you're not logged in, or when you're logged in as a user who cannot usually see Query Monitor's output. See the Settings panel for details.
= Does Query Monitor itself impact the page generation time or memory usage? =
Short answer: Yes, but only a little.
Long answer: Query Monitor has a small impact on page generation time because it hooks into WordPress in the same way that other plugins do. The impact is low; typically between 10ms and 100ms depending on the complexity of your site.
Query Monitor's memory usage typically accounts for around 10% of the total memory used to generate the page.
= Are there any add-on plugins for Query Monitor? =
[A list of add-on plugins for Query Monitor can be found here.](https://github.com/johnbillion/query-monitor/wiki/Query-Monitor-Add-on-Plugins)
In addition, Query Monitor transparently supports add-ons for the Debug Bar plugin. If you have any Debug Bar add-ons installed, just deactivate Debug Bar and the add-ons will show up in Query Monitor's menu.
= Where can I suggest a new feature or report a bug? =
Please use [the issue tracker on Query Monitor's GitHub repo](https://github.com/johnbillion/query-monitor/issues) as it's easier to keep track of issues there, rather than on the wordpress.org support forums.
= Is Query Monitor available on WordPress.com VIP Go? =
Yes, it's included as part of the VIP Go platform. However, a user needs to be granted the `view_query_monitor` capability to see Query Monitor even if they're an administrator.
= I'm using multiple instances of `wpdb`. How do I get my additional instances to show up in Query Monitor? =
You'll need to hook into the `qm/collect/db_objects` filter and add an item to the array containing your `wpdb` instance. For example:
`
add_filter( 'qm/collect/db_objects', function( $objects ) {
$objects['my_db'] = $GLOBALS['my_db'];
return $objects;
} );
`
Your `wpdb` instance will then show up as a separate panel, and the query time and query count will show up separately in the admin toolbar menu. Aggregate information (queries by caller and component) will not be separated.
= Can I click on stack traces to open the file in my editor? =
Yes. You can enable this on the Settings panel.
= Do you accept donations? =
No, I do not accept donations. If you like the plugin, I'd love for you to [leave a review](https://wordpress.org/support/view/plugin-reviews/query-monitor). Tell all your friends about the plugin too!
## Changelog ##
### 3.6.4 ###
* Correct an error introduced in 3.6.3 with the extra early error handling (ironic).
### 3.6.3 ###
* Correct the size of the close icon.
### 3.6.2 ###
* Capture and display the most recent PHP error that occurred before QM loaded.
* Add support for the environment type added in WP 5.5.
* Avoid a potentially blank translation for some plural forms.
* Increase some contrast in dark mode.
* Combine the response-related sections of the Request panel.
* Add extra sanity checking when attempting to fetch the posix user information.
### 3.6.1 ###
* Adjust the bottom margin when the QM panel is open so QM doesn't cover the bottom of the page. Works more often than not.
* Prevent QM from triggering a fatal itself if a fatal occurs before the HTML dispatcher is loaded.
* Add an informational message to the template output when template hooks are in use.
* Fix errors caused by missing user or group IDs when collecting environment data.
* Add TextMate to list of supported editors.
* Demote some cache warnings to informational messages.
* Support passing backtrace to `QM_Backtrace`.
### 3.6.0 ###
* Improvements to the UI when a fatal error occurs, including an admin toolbar warning.
* Improvements to the UI when QM is running in "broken" mode due to missing jQuery or an unrecoverable JavaScript error.
* Don't display fatal errors if error display is off and the user cannot view QM.
* Improvements to the visual appearance of the `wp_die()` output.
* Simplify re-throwing a caught exception so QM doesn't get the blame for fatal errors, eg. in the WordPress core fatal error handler.
* Add support for logging a variable of any type in the logger, as a replacement for var dumping.
* Don't show a message for errors in Ajax calls that have already occurred on the main page load.
* Don't dispatch QM during an iframed request, eg the plugin info modal or an upgrader action.
* Hide QM itself from various panels by default to remove noise. Can be controlled via the existing `QM_HIDE_SELF` configuration constant.
* Support for the new `is_favicon()` conditional added in WP 5.4.
* Fix the side panel resizing functionality.
* Add a WP-CLI command for creating the symlink to the db file.
* Add filters to `QM_Util::get_file_dirs()` and `get_file_component()` to allow support for non-standard plugin and theme locations.
* Add an action that fires when QM enqueues its assets, so add-on plugins can enqueue theirs only when necessary.
### 3.5.2 ###
* Add support for exposing [Full Site Editing](https://github.com/WordPress/gutenberg/issues?q=label%3A%22%5BFeature%5D+Full+Site+Editing%22) blocks in the Block Editor panel.
### 3.5.1 ###
* Defer population of the `found_formatted` property because this can fire before WPML has initialised its locale proxy. Fixes #485.
* Ensure all error types are accounted for when populating the panel menu error count. Fixes #486.
### 3.5.0 ###
* Add an editor selection UI on the Settings panel.
* Improve the output of missing asset dependencies.
* Improve the output of unsuccessful template parts.
* Handle non-boolean constants such as `WP_DEBUG_LOG`, which now accepts a path too.
* Add support for touch devices when resizing the panel. (Works alright-ish, probably needs some animation frame work.)
* Apply the same styles to notices, deprecated, and strict errors.
* Some more style resets for compatibility with popular themes.
* Style changes to bring QM inline with WP 5.3's improved button and focus styles.
* More colour contrast and dark mode tweaks.
* Add permalink-related filters to the concerned filters for the Request panel.
* Fix and improve the admin toolbar menu hover colours.
* Add the error count to the panel menu.
* Remove unnecessary use of plural forms added in 3.4.0.
* More CSS resets to avoid overly tall filters in Firefox.
* Improved styling for warning rows.
* Display the log count in the menu item.
### 3.4.0 ###
* Introduce an exception handler so a stack trace can be shown for fatal errors in PHP >= 7.
* Add separate persistence of QM window for front-end and admin area.
* Add the request and response HTTP headers to the Request panel.
* Introduce Started and Stopped columns in the Timings panel.
* By popular demand, revert back to closest first ordering of stack traces so they're inline with most other dev tools out there.
* Show the script handle in addition to the text domain in the Languages panel.
* Improve the panel menu highlighting colours.
* Better presentation of the default and current values for the settings constants.
* Truncate long host names in the Scripts and Styles panels.
* Add some more of the admin screen globals to the admin collector.
* Switch back to using a monospace font in numeric data cells.
* Allow dark mode to be enabled with `QM_DARK_MODE`.
* Display the total query count even when `SAVEQUERIES` is defined as false.
* Allow proper plural forms to be used wherever a phrase includes a numeric value.
* More style resetting for compatibility with Twenty Twenty.
* Avoid a division by zero when cache hits is 0.
* Switch to (mostly) CSS for the child menu item marker.
### 3.3.7 ###
* Expose instances where a requested template part was not loaded.
* Update the docs for multiple `wpdb` instances.
* Various accessibility improvements.
* Remove the RDBMS info as it's not at all reliable.
### 3.3.6 ###
* Fix a compatibility issue where QM and the fatal error protection in WordPress 5.2+ were handling syntax errors differently.
* Fix some bugs with the icons for the panel controls.
### 3.3.5 ###
* Add support for the new `get_template_part` action in WP 5.2.
* Add a friendly error message when the PHP version requirement isn't met.
* Add support for the new privacy policy conditional in WP 5.2.
* Add support for the new privacy policy template in WP 5.2.
### 3.3.4 ###
* Updated CSS to avoid conflicts with themes using `ul`, `nav`, and `li` styling.
* Don't define `ajaxurl` if there are no Debug Bar panels to show.
* New icon for QM! By [Tubagus Didin Asrori](https://www.instagram.com/asrorigus/).
* Push the close button a bit further away from the edge of the screen to avoid scrollbar interference on macOS.
* Fix clash with object cache plugins that keep their hit and miss stats private.
* Add missing asset position counters.
### 3.3.3 ###
* Add scripts and styles counts to admin menu items.
* Group the cache logic together to avoid calling cache related functionality when it's not available. Fixes #418.
* Switch to installing the test suite as Composer dependencies.
### 3.3.2 ###
* Improve the accuracy of the `ver` parameter for enqueued scripts and styles.
* Separate and simplify the output for the object cache and opcode cache statuses. Fixes #413.
* Better formatting when no object cache stats are available.
### 3.3.1 ###
* Move the hook processing into its own class and out of the collector, so it can be re-used even if the Hooks collector isn't in use. Fixes #399.
* Increase the sidebar layout to 100% height when there's no admin toolbar.
* Update the QM element ID in the "worst case scenario" JS. Fixes #398.
* Improve the layout of the Settings panel.
* Force the `Core` and `Non-Core` filter items to the bottom of the list, so plugins and themes takes precedence.
* Add an entry for the Settings screen to the narrow view nav menu.
* Add the admin notice hooks to the list of concerned actions for the Admin Screen panel.
### 3.3.0 ###
New features! Read about them here: https://querymonitor.com/blog/2019/02/new-features-in-query-monitor-3-3/
* Introduce sub-menus for displaying Hooks in Use for each panel.
* Output the call stack and responsible component when `wp_die()` is called.
* Support for JavaScript (Jed) translations in WordPress 5.0+.
* Add render timing for blocks using the new hooks introduced in WordPress 5.1.
* Introduce a toggle to display QM on the side of the window.
* Allow non-string values to be used in the logger message. They'll be presented as JSON formatted strings.
* Allow boolean values to be used in log message contexts.
* Add some margin to the Close button so it doesn't get covered up so much by scroll bars.
* Prefix QM's cookie name with `wp-` to ensure interoperability with caches and proxies.
* Separate the Scripts and Styles collector and outputter so they're actually two separate panels.
* Add support for opcode cache detection separate from the object cache detection.
* Rename the main QM container to get around the fact that its name clashes with the plugin rows in older versions of WordPress.
* Avoid using `wp_parse_url()` as it was only introduced in WP 4.4.
### 3.2.2 ###
* Support for nested content blocks (eg. in columns).
* Hide long innerHTML content of blocks behind a toggle.
* Add validation of the referenced media file in media blocks.
* Ensure asset URLs include the `ver` query arg.
* Tweak the warning colours.
* Coding standards.
* Layout tweaks.
### 3.2.1 ###
* Fix a fatal error for < 5.0 sites that are not running the Gutenberg plugin.
### 3.2.0 ###
* Add a new `Blocks` panel for debugging blocks in post content. Supports WordPress 5.0 and the Gutenberg plugin.
* Display the number of times that each template part was included.
* Allow the scripts and styles output to be filtered based on Dependencies and Dependents.
* Remove the `Pin` button in favour of always pinning QM when it's open.
* Add a "Settings" link to the Plugins screen that opens the settings panel.
* Add a link to the Add-ons page on the wiki.
* Add some more verbose and visible error notices for suboptimal PHP configuration directives.
* Add support for identifying any RDBMS, not just MySQL and MariaDB.
* Perform the PHP version check earlier on so that fewer parts of QM need to be compatible with PHP 5.2.
* Highlight plain `http` requests to the HTTP API as insecure.
* Ensure the `Template` admin menu is always shown, even if the template file name isn't known.
* Adjust the JS and CSS asset source to not include the host.
* Add a warning for insecure JS and CSS assets.
* Remove before and after pseudo-elements in the style reset.
* Show as much theme and template information as possible, even if QM doesn't know the template name.
* Highlight non-core rows when filtering the Hooks & Actions panel by Non-Core.
* Add a filter for environment constants.
* Min width CSS for buttons.
* First pass at documenting filters and hooks.
* More coding standards updates.
### 3.1.1 ###
* Add a dark mode for the UI which is used via the Dark Mode plugin.
* Display Query Monitor's output in the user's selected language, instead of the site language.
* Add extended support for the Members and User Role Editor plugins.
* Fix link hover and focus styles.
* Reset some more CSS styles.
### 3.1.0 ###
**Main changes:**
* Lots of accessibility improvements.
* Switch to system default fonts to match the WordPress admin area fonts.
* [Implement a PSR-3 compatible logger](https://querymonitor.com/blog/2018/07/profiling-and-logging/).
* UI improvements for mobile/touch/narrow devices.
* Various improvements to the layout of the Scripts and Styles panels.
* Prevent the "overscroll" behaviour that causes the main page to scroll when scrolling to the end of a panel.
* Remove the second table footer when filtering tables.
* Add a settings panel with information about all of the available configuration constants.
**All other changes:**
* Show a warning message in the Overview panel when a PHP error is trigger during an Ajax request.
* Display a warning when time or memory usage is above 75% of the respective limit.
* Template Part file string normalization so template parts are correctly shown on Windows systems.
* Don't output toggle links or a blank HTTP API transport if not necessary.
* Add a human readable representation of transient timeouts, and prevent some wrapping.
* Add a tear down for the capability checks collector so that cap checks performed between QM's processing and output don't break things.
* Remove the ability to sort the HTTP API Calls table. This removes a column, increasing the available horizontal space.
* Handle a bunch more known object types when displaying parameter values.
* Allow PHP errors to be filtered by level.
* Shorten the displayed names of long namespaced symbols by initialising the inner portions of the name.
* Combine the Location and Caller columns for PHP Errors to save some horizontal space.
* Don't wrap text in the PHP error type column.
* Improve the authentication cookie toggle so it dynamically reflects the current state.
* For now, force QM to use ltr text direction.
* Clarify terminology around the number of enqueued assets.
* Add fallback support for `wp_cache_get_stats()` to fetch cache stats.
* Improve the message shown when no queries are performed.
* Pluck stats from cache controllers that implement a `getStats()` method and return a nested array of stats for each server.
* Rename the `QM_HIDE_CORE_HOOKS` configuration constant to `QM_HIDE_CORE_ACTIONS`.
* Better handling of environments with unlimited execution time or memory limit. Adds a warning for both.
* When an external cache isn't in use, provide some helpful info if an appropriate extension is installed.
### 3.0.1 ###
* Add even more hardening to the JS handling to prevent problems when jQuery is broken.
* Remove the old `no-js` styles which don't work well with the new UI.
* Correct the logic for showing the `Non-Core` component filter option.
* Add another VIP function to the list of functions that call the HTTP API.
* Add an inline warning highlight to capability checks that are empty or of a non-string type.
* Add support for WordPress.com VIP Client MU plugins.
* Add support for displaying laps as part of the timing information.
* Add full support for namespaced Debug Bar add-on panels.
* Switch back to depending on `jquery` instead of `jquery-core`.
* Don't assume `php_uname()` is always callable. Add info about the host OS too.
* Reset inline height attribute when the panel is closed.
### 3.0.0 ###
* Brand new UI that resembles familiar web developer tools. Lots of related improvements and fixes.
* Introduce some basic timing functionality in a Timings panel. See #282 for usage.
* Introduce a `QM_NO_JQUERY` constant for running QM without jQuery as a dependency.
* Greater resilience to JavaScript errors.
* Allow the Scripts and Styles panel to be filtered by host name.
* Expose information about redirects that occurred in HTTP API requests.
* Expose more debugging information for HTTP API requests.
* Don't enable the Capability Checks panel by default as it's very memory intensive.
* Allow PHP errors to be silenced according to their component. See `qm/collect/php_error_levels` and `qm/collect/hide_silenced_php_errors` filters.
* Hide all file paths and stack traces behind toggles by default.
* Remove support for the AMP for WordPress plugin.
* Add associative keys to the array passed to the `qm/built-in-collectors` filter.
* Drop support for PHP 5.2.
* Generally improve performance and reduce memory usage.
### 2.17.0 ###
* Add the current user object to the Request panel.
* A few improvements to the appearance of the overall layout.
* Use relative positioning in place of the nasty absolute position hack needed for some themes.
* Ensure the `get_*_template()` function exists before calling it.
* Add a `QM_DISABLE_ERROR_HANDLER` constant to disable QM's error handling.
* Switch to runtime filtering of user capabilities instead of granting the `view_query_monitor` cap upon activation.
* Correct a bunch of inline docs and code standards.
### 2.16.2 ###
* Correctly handle re-selection of filters with a saved value that contains special characters.
* Show the correct caller for Super Admin capability checks.
### 2.16.1 ###
* Update the plugin version number (no functional changes from 2.16.0).
### 2.16.0 ###
* Introduce a new panel for displaying user capability checks that have been performed during the page load.
* Remember the picked value in all the filters. Uses localStorage in the browser.
* Add a "Non-Core" filter to the Component filter control in all panels.
* Add a "Non-SELECT" filter to the query type filter control in the Queries panel.
* Display collapsed stack traces by default in all panels.
* Add the error code to the Database Errors output.
* Improve the visual appearance of the column sorting controls.
* Improved display for parameter values in call stacks.
* Any files within `wp-content` which don't have a component are now grouped by the root directory or file name.
### 2.15.0 ###
* Reverse the order of stack traces so they're in natural order, and improve styling.
* Enable query types to be clicked in the Overview.
* Add a highlight to the currently applied table filter.
* Improve table row highlighting when the row header spans multiple rows.
* Expose a link to the main query from the Request panel.
* Better stack traces for transient sets and HTTP API requests.
* Group and sort the Languages output by textdomain.
* Log and expose PHP extensions, and improve styling for error reporting level.
* Better highlighting of PHP warnings and QM errors.
* Add support for a `vendor` directory in the root of the `mu-plugins` directory when detecting components.
* Log the size of the value of updated transients.
* Add a help link when query components aren't available.
* Make the Hooks table output reusable by other components.
* Add a bit of vertical breathing room.
* Various improvements to terminology.
* Coding standards.
### 2.14.0 ###
* Some more inline documentation about clickable stack traces.
* Output a more complete list of error levels and their status.
* Internationalisation fixes.
* Add some wrapping to the Request and Theme output so posts with long unbroken slugs don't break the layout.
* PHP error handler: Add new hook `qm/collect/new_php_error`
* Built-in collectors: Add new `qm/built-in-collectors` filter on files before including them
* More defensive CSS.
* Fix the size of the expand/contract buttons.
* Avoid showing two unnecessary functions in the call stack for textdomain loading.

View File

@@ -0,0 +1,139 @@
<?php
/**
* Plugin Name: Query Monitor Database Class
*
* *********************************************************************
*
* Ensure this file is symlinked to your wp-content directory to provide
* additional database query information in Query Monitor's output.
*
* *********************************************************************
*
* @package query-monitor
*/
defined( 'ABSPATH' ) || die();
if ( defined( 'QM_DISABLED' ) && QM_DISABLED ) {
return;
}
if ( 'cli' === php_sapi_name() && ! defined( 'QM_TESTS' ) ) {
# For the time being, let's not load QM when using the CLI because we've no persistent storage and no means of
# outputting collected data on the CLI. This will hopefully change in a future version of QM.
return;
}
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
# Let's not load QM during cron events for the same reason as above.
return;
}
# No autoloaders for us. See https://github.com/johnbillion/query-monitor/issues/7
$qm_dir = dirname( dirname( __FILE__ ) );
$plugin = "{$qm_dir}/classes/Plugin.php";
if ( ! is_readable( $plugin ) ) {
return;
}
require_once $plugin;
if ( ! QM_Plugin::php_version_met() ) {
return;
}
$backtrace = "{$qm_dir}/classes/Backtrace.php";
if ( ! is_readable( $backtrace ) ) {
return;
}
require_once $backtrace;
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
class QM_DB extends wpdb {
public $qm_php_vars = array(
'max_execution_time' => null,
'memory_limit' => null,
'upload_max_filesize' => null,
'post_max_size' => null,
'display_errors' => null,
'log_errors' => null,
);
/**
* Class constructor
*/
public function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) {
foreach ( $this->qm_php_vars as $setting => &$val ) {
$val = ini_get( $setting );
}
parent::__construct( $dbuser, $dbpassword, $dbname, $dbhost );
}
/**
* Perform a MySQL database query, using current database connection.
*
* @see wpdb::query()
*
* @param string $query Database query
* @return int|false Number of rows affected/selected or false on error
*/
public function query( $query ) {
if ( ! $this->ready ) {
if ( isset( $this->check_current_query ) ) {
// This property was introduced in WP 4.2
$this->check_current_query = true;
}
return false;
}
if ( $this->show_errors ) {
$this->hide_errors();
}
$result = parent::query( $query );
if ( ! SAVEQUERIES ) {
return $result;
}
$i = $this->num_queries - 1;
$this->queries[ $i ]['trace'] = new QM_Backtrace( array(
'ignore_frames' => 1,
) );
if ( ! isset( $this->queries[ $i ][3] ) ) {
$this->queries[ $i ][3] = $this->time_start;
}
if ( $this->last_error ) {
$code = 'qmdb';
if ( $this->use_mysqli ) {
if ( $this->dbh instanceof mysqli ) {
$code = mysqli_errno( $this->dbh );
}
} else {
if ( is_resource( $this->dbh ) ) {
// Please do not report this code as a PHP 7 incompatibility. Observe the surrounding logic.
// phpcs:ignore
$code = mysql_errno( $this->dbh );
}
}
$this->queries[ $i ]['result'] = new WP_Error( $code, $this->last_error );
} else {
$this->queries[ $i ]['result'] = $result;
}
return $result;
}
}
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wpdb = new QM_DB( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );