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

View File

@ -0,0 +1,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.

View File

@ -0,0 +1 @@
<path d="M18 12h-2.18c-0.17 0.7-0.44 1.35-0.81 1.93l1.54 1.54-2.1 2.1-1.54-1.54c-0.58 0.36-1.23 0.63-1.91 0.79v2.18h-3v-2.18c-0.68-0.16-1.33-0.43-1.91-0.79l-1.54 1.54-2.12-2.12 1.54-1.54c-0.36-0.58-0.63-1.23-0.79-1.91h-2.18v-2.97h2.17c0.16-0.7 0.44-1.35 0.8-1.94l-1.54-1.54 2.1-2.1 1.54 1.54c0.58-0.37 1.24-0.64 1.93-0.81v-2.18h3v2.18c0.68 0.16 1.33 0.43 1.91 0.79l1.54-1.54 2.12 2.12-1.54 1.54c0.36 0.59 0.64 1.24 0.8 1.94h2.17v2.97zM9.5 13.5c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3z"></path>

View File

@ -0,0 +1 @@
<path d="M15 8l-4.030 6-3.97-6h8z"></path>

View File

@ -0,0 +1 @@
<path d="M13.89 3.39l2.71 2.72c0.46 0.46 0.42 1.24 0.030 1.64l-8.010 8.020-5.56 1.16 1.16-5.58s7.6-7.63 7.99-8.030c0.39-0.39 1.22-0.39 1.68 0.070zM11.16 6.18l-5.59 5.61 1.11 1.11 5.54-5.65zM8.19 14.41l5.58-5.6-1.070-1.080-5.59 5.6z"></path>

View File

@ -0,0 +1 @@
<path d="M9 3h8v8l-2-1v-3.080l-5.6 5.59-1.41-1.41 6.090-6.1h-4.080zM12 15v-3l2-2v7h-11v-11h8l-2 2h-4v7h7z"></path>

View File

@ -0,0 +1 @@
<path d="M3 4.5v-2s3.34-1 7-1 7 1 7 1v2l-5 7.030v6.97s-1.22-0.090-2.25-0.59-1.75-1.41-1.75-1.41v-4.97z"></path>

View File

@ -0,0 +1 @@
<path d="M7 5h-1.95c0-1.74 0.85-2.9 2.95-2.9v-2.1c-3.15 0-5.040 2.11-5.040 5h-1.78l2.62 3.39zM20 1v14h-5v5h-14v-10h9v-9h10zM18 3h-6v7h3v3h3v-10zM13 12h-10v6h10v-6z"></path>

View File

@ -0,0 +1 @@
<path d="M15.95 5h-1.95l3.2 3.39 2.62-3.39h-1.78c0-2.89-1.89-5-5.040-5v2.1c2.1 0 2.95 1.16 2.95 2.9zM1 1h10v9h9v10h-14v-5h-5v-14zM3 3v10h3v-3h3v-7h-6zM8 12v6h10v-6h-10z"></path>

View File

@ -0,0 +1 @@
<path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zM11 6c0-0.55-0.45-1-1-1s-1 0.45-1 1 0.45 1 1 1 1-0.45 1-1zM11 15v-6h-2v6h2z"></path>

View File

@ -0,0 +1 @@
<path d="M14.95 6.46l-3.54 3.54 3.54 3.54-1.41 1.41-3.54-3.53-3.53 3.53-1.42-1.42 3.53-3.53-3.53-3.53 1.42-1.42 3.53 3.53 3.54-3.53z"></path>

View File

@ -0,0 +1 @@
<path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zM11.13 11.38l0.35-6.46h-2.96l0.35 6.46h2.26zM11.040 14.74c0.24-0.23 0.37-0.55 0.37-0.96 0-0.42-0.12-0.74-0.36-0.97s-0.59-0.35-1.060-0.35-0.82 0.12-1.070 0.35-0.37 0.55-0.37 0.97c0 0.41 0.13 0.73 0.38 0.96 0.26 0.23 0.61 0.34 1.060 0.34s0.8-0.11 1.050-0.34z"></path>

View File

@ -0,0 +1,2 @@
<path d="M10,2c-4.42,0-8,3.58-8,8s3.58,8,8,8s8-3.58,8-8S14.42,2,10,2z M9.385,14.66h-1.34l-3.24-4.54l1.341-1.25l2.569,2.4
l5.141-5.931l1.34,0.94L9.385,14.66z"/>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,791 @@
/**
* 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').length ? $('#wpadminbar').outerHeight() : 0;
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');
var theme = localStorage.getItem( 'qm-theme' );
if ( theme ) {
container.attr('data-theme', theme);
$('.qm-theme-toggle[value="' + theme + '"]').prop('checked', true);
}
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.trigger('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 ) {
sessionStorage.setItem( key, $(this).val() );
} else {
sessionStorage.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');
var filtered_count = 0;
var total_count = 0;
matches.each(function(i){
var row_time = $(this).attr('data-qm-time');
if ( row_time ) {
time += parseFloat( row_time );
}
var row_count = $(this).attr('data-qm-count');
if ( row_count ) {
filtered_count += parseFloat( row_count );
} else {
filtered_count++;
}
});
if ( time ) {
time = QM_i18n.number_format( time, 4 );
}
tr.each(function(i){
var row_count = $(this).attr('data-qm-count');
if ( row_count ) {
total_count += parseFloat( row_count );
} else {
total_count++;
}
});
if ( table.find('.qm-filtered').length ) {
var count = filtered_count + ' / ' + total_count;
} else {
var count = filtered_count;
}
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 = sessionStorage.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).trigger('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').trigger('change');
$('#qm-' + target).find('[data-filter="' + filter + '"]').val(value).addClass('qm-highlight').trigger('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(150,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(150);
}
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 = JSON.parse( 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-theme-toggle').on('click',function(e){
container.attr('data-theme',$(this).val());
localStorage.setItem('qm-theme',$(this).val());
});
$.qm.tableSort({target: $('.qm-sortable')});
var startY, startX, resizerHeight;
$(document).on('mousedown touchstart', '.qm-resizer', function(event) {
event.stopPropagation();
resizerHeight = $(this).outerHeight() - 1;
startY = container.outerHeight() + ( event.clientY || event.originalEvent.targetTouches[0].pageY );
startX = container.outerWidth() + ( event.clientX || event.originalEvent.targetTouches[0].pageX );
if ( ! container.hasClass('qm-show-right') ) {
$(document).on('mousemove touchmove', qm_do_resizer_drag_vertical);
} else {
$(document).on('mousemove touchmove', qm_do_resizer_drag_horizontal);
}
$(document).on('mouseup touchend', qm_stop_resizer_drag);
});
function qm_do_resizer_drag_vertical(event) {
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 )' );
}
}
function qm_do_resizer_drag_horizontal(event) {
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_vertical);
$(document).off('mousemove touchmove', qm_do_resizer_drag_horizontal);
$(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').on('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"]').on('click',function(){
show_panel( '#qm-settings' );
$('#qm-settings').focus();
});
$('.qm-button-container-position').on('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').on('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 columns.
// @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').removeAttr('aria-sort');
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() {
var main = document.getElementById( 'query-monitor-main' );
var ceased = document.getElementById( 'query-monitor-ceased' );
var broken = document.getElementById( 'qm-broken' );
var menu_item = document.getElementById( 'wp-admin-bar-query-monitor' );
if ( ( 'undefined' === typeof jQuery ) || ! window.jQuery ) {
/* Fallback for running without jQuery (`QM_NO_JQUERY`) or when jQuery is broken */
if ( main ) {
main.className += ' qm-broken';
}
if ( broken ) {
console.error( broken.textContent );
}
if ( 'undefined' === typeof jQuery ) {
console.error( 'QM error from JS: undefined jQuery' );
} else if ( ! window.jQuery ) {
console.error( 'QM error from JS: no jQuery' );
}
if ( menu_item && main ) {
menu_item.addEventListener( 'click', function() {
main.className += ' qm-show';
} );
}
}
if ( ! main ) {
if ( ceased ) {
// QM was ceased
console.info( 'QM: collection and output was ceased' );
} else {
// QM's output has disappeared
console.error( 'QM error from JS: QM output does not exist' );
}
}
} );

View File

@ -0,0 +1,134 @@
<?php declare(strict_types = 1);
/**
* Plugin activation handler.
*
* @package query-monitor
*/
class QM_Activation extends QM_Plugin {
/**
* @param string $file
*/
protected function __construct( $file ) {
# 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 );
}
/**
* @param bool $sitewide
* @return void
*/
public function activate( $sitewide = false ) {
$db = WP_CONTENT_DIR . '/db.php';
$create_symlink = defined( 'QM_DB_SYMLINK' ) ? QM_DB_SYMLINK : true;
if ( $create_symlink && ! 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' ) );
}
}
/**
* @return void
*/
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', false ) ) {
unlink( WP_CONTENT_DIR . '/db.php' ); // phpcs:ignore
}
}
/**
* @param array<int, string> $plugins
* @return array<int, string>
*/
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() ), '/' );
$qm = preg_grep( '/' . $f . '$/', $plugins );
$notqm = preg_grep( '/' . $f . '$/', $plugins, PREG_GREP_INVERT );
if ( false === $qm || false === $notqm ) {
return $plugins;
}
return array_merge(
$qm,
$notqm
);
}
/**
* @param array<string, int> $plugins
* @return array<string, int>
*/
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;
}
}
/**
* @param string $file
* @return self
*/
public static function init( $file ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QM_Activation( $file );
}
return $instance;
}
}

View File

@ -0,0 +1,558 @@
<?php declare(strict_types = 1);
/**
* Function call backtrace container.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'QM_Backtrace' ) ) {
class QM_Backtrace {
/**
* @var array<string, bool>
*/
protected static $ignore_class = array(
'wpdb' => true,
'hyperdb' => true,
'LudicrousDB' => true,
'QueryMonitor' => true,
'W3_Db' => true,
'Debug_Bar_PHP' => true,
'WP_Hook' => true,
'Altis\Cloud\DB' => true,
'Yoast\WP\Lib\ORM' => true,
'Perflab_SQLite_DB' => true,
);
/**
* @var array<string, array<string, bool>>
*/
protected static $ignore_method = array();
/**
* @var array<string, bool>
*/
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_constructor' => true,
'_deprecated_file' => true,
'_deprecated_function' => true,
'_deprecated_hook' => true,
'dbDelta' => true,
);
/**
* @var array<string, int|string>
*/
protected static $show_args = array(
'do_action' => 1,
'apply_filters' => 1,
'do_action_ref_array' => 1,
'apply_filters_ref_array' => 1,
'do_action_deprecated' => 1,
'apply_filters_deprecated' => 1,
'get_query_template' => 1,
'resolve_block_template' => 1,
'get_template_part' => 2,
'get_extended_template_part' => 2,
'ai_get_template_part' => 2,
'load_template' => 'dir',
'dynamic_sidebar' => 1,
'get_header' => 1,
'get_sidebar' => 1,
'get_footer' => 1,
'get_transient' => 1,
'set_transient' => 1,
'class_exists' => 2,
'current_user_can' => 3,
'user_can' => 4,
'current_user_can_for_blog' => 4,
'author_can' => 4,
);
/**
* @var array<string, bool>
*/
protected static $ignore_hook = array();
/**
* @var bool
*/
protected static $filtered = false;
/**
* @var array<string, mixed[]>
*/
protected $args = array();
/**
* @var mixed[]
*/
protected $trace;
/**
* @var mixed[]|null
*/
protected $filtered_trace = null;
/**
* @var int
*/
protected $calling_line = 0;
/**
* @var string
*/
protected $calling_file = '';
/**
* @var QM_Component|null
*/
protected $component = null;
/**
* @var mixed[]|null
*/
protected $top_frame = null;
/**
* @param array<string, mixed[]> $args
* @param mixed[] $trace
*/
public function __construct( array $args = array(), array $trace = null ) {
$this->trace = $trace ?? debug_backtrace( 0 );
$this->args = array_merge( array(
'ignore_class' => array(),
'ignore_method' => array(),
'ignore_func' => array(),
'ignore_hook' => array(),
'show_args' => array(),
), $args );
foreach ( $this->trace as & $frame ) {
if ( ! isset( $frame['args'] ) ) {
continue;
}
if ( isset( $frame['function'], self::$show_args[ $frame['function'] ] ) ) {
$show = self::$show_args[ $frame['function'] ];
if ( ! is_int( $show ) ) {
$show = 1;
}
$frame['args'] = array_slice( $frame['args'], 0, $show );
} else {
unset( $frame['args'] );
}
}
}
/**
* @param mixed[] $frame
* @return void
*/
public function push_frame( array $frame ) {
$this->top_frame = $frame;
}
/**
* @return array<int, string>
*/
public function get_stack() {
$trace = $this->get_filtered_trace();
$stack = array_column( $trace, 'display' );
return $stack;
}
/**
* @return mixed[]|false
*/
public function get_caller() {
$trace = $this->get_filtered_trace();
return reset( $trace );
}
/**
* @return QM_Component
*/
public function get_component() {
if ( isset( $this->component ) ) {
return $this->component;
}
$components = array();
$frames = $this->get_filtered_trace();
if ( $this->top_frame ) {
array_unshift( $frames, $this->top_frame );
}
foreach ( $frames 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.
$this->component = $component;
return $this->component;
}
$components[ $component->type ] = $component;
}
}
foreach ( QM_Util::get_file_dirs() as $type => $dir ) {
if ( isset( $components[ $type ] ) ) {
$this->component = $components[ $type ];
return $this->component;
}
}
$component = new QM_Component();
$component->type = 'unknown';
$component->name = __( 'Unknown', 'query-monitor' );
$component->context = 'unknown';
return $component;
}
/**
* Attempts to determine the component responsible for a given frame.
*
* @param mixed[] $frame A single frame from a trace.
* @phpstan-param array{
* class?: class-string,
* function?: string,
* file?: string,
* } $frame
* @return QM_Component|null An object representing the component, or null if
* the component cannot be determined.
*/
public static function get_frame_component( array $frame ) {
try {
if ( isset( $frame['class'], $frame['function'] ) ) {
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;
}
if ( ! $file ) {
return null;
}
return QM_Util::get_file_component( $file );
} catch ( ReflectionException $e ) {
return null;
}
}
/**
* @return mixed[]
*/
public function get_trace() {
return $this->trace;
}
/**
* @deprecated Use the `::get_filtered_trace()` method instead.
*
* @return mixed[]
*/
public function get_display_trace() {
return $this->get_filtered_trace();
}
/**
* @return array<int, array<string, mixed>>
* @phpstan-return list<array{
* file: string,
* line: int,
* display: string,
* }>
*/
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'] );
// When a PHP error is triggered which doesn't have a stack trace, for example a
// deprecated error, QM will blame itself due to its error handler. This prevents that.
if ( false === strpos( $file, 'query-monitor/collectors/php_errors.php' ) ) {
$trace[0] = $lowest;
}
}
$this->filtered_trace = $trace;
}
return $this->filtered_trace;
}
/**
* @param array<int, string> $stack
* @return array<int, string>
*/
public static function get_filtered_stack( array $stack ) {
$trace = new self( array(), array() );
$return = array();
foreach ( $stack as $i => $item ) {
$frame = array(
'function' => $item,
);
if ( false !== strpos( $item, '->' ) ) {
list( $class, $function ) = explode( '->', $item );
$frame = array(
'class' => $class,
'type' => '->',
'function' => $function,
);
}
if ( false !== strpos( $item, '::' ) ) {
list( $class, $function ) = explode( '::', $item );
$frame = array(
'class' => $class,
'type' => '::',
'function' => $function,
);
}
$frame['args'] = array();
if ( $trace->filter_trace( $frame ) ) {
$return[] = $item;
}
}
return $return;
}
/**
* @deprecated Use the `ignore_class`, `ignore_method`, `ignore_func`, and `ignore_hook` arguments instead.
*
* @param int $num
* @return self
*/
public function ignore( $num ) {
for ( $i = 0; $i < $num; $i++ ) {
unset( $this->trace[ $i ] );
}
$this->trace = array_values( $this->trace );
return $this;
}
/**
* @param mixed[] $frame
* @return mixed[]|null
*/
public function filter_trace( array $frame ) {
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 array<string, 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 (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 array<string, array<string, bool>> $ignore_method Array of method names to ignore. The top level array keys are
* class names, the second level array keys are method names, and
* the array values are whether to ignore the method (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 array<string, 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 (usually true).
*/
self::$ignore_func = apply_filters( 'qm/trace/ignore_func', self::$ignore_func );
/**
* Filters which action and filter names to ignore when constructing user-facing call stacks.
*
* @since 3.8.0
*
* @param array<string, bool> $ignore_hook Array of hook names to ignore. The array keys are hook names to ignore,
* the array values are whether to ignore the hook (usually true).
*/
self::$ignore_hook = apply_filters( 'qm/trace/ignore_hook', self::$ignore_hook );
/**
* Filters the number of argument values to show for the given function name when constructing user-facing
* call stacks.
*
* @since 2.7.0
*
* @param array<string,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 = $frame;
$ignore_class = array_filter( array_merge( self::$ignore_class, $this->args['ignore_class'] ) );
$ignore_method = array_filter( array_merge( self::$ignore_method, $this->args['ignore_method'] ) );
$ignore_func = array_filter( array_merge( self::$ignore_func, $this->args['ignore_func'] ) );
$ignore_hook = array_filter( array_merge( self::$ignore_hook, $this->args['ignore_hook'] ) );
$show_args = array_merge( self::$show_args, $this->args['show_args'] );
$hook_functions = array(
'apply_filters' => true,
'do_action' => true,
'apply_filters_ref_array' => true,
'do_action_ref_array' => true,
'apply_filters_deprecated' => true,
'do_action_deprecated' => true,
);
if ( ! isset( $frame['function'] ) ) {
$frame['function'] = '(unknown)';
}
if ( isset( $frame['class'] ) ) {
if ( isset( $ignore_class[ $frame['class'] ] ) ) {
$return = null;
} elseif ( isset( $ignore_method[ $frame['class'] ][ $frame['function'] ] ) ) {
$return = null;
} elseif ( 0 === strpos( $frame['class'], 'QM' ) ) {
$return = null;
} else {
$return['id'] = $frame['class'] . $frame['type'] . $frame['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $frame['class'] . $frame['type'] . $frame['function'] ) . '()';
}
} else {
if ( isset( $ignore_func[ $frame['function'] ] ) ) {
$return = null;
} elseif ( isset( $show_args[ $frame['function'] ] ) ) {
$show = $show_args[ $frame['function'] ];
if ( 'dir' === $show ) {
if ( isset( $frame['args'][0] ) ) {
$arg = QM_Util::standard_dir( $frame['args'][0], '' );
$return['id'] = $frame['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $frame['function'] ) . "('{$arg}')";
}
} else {
if ( isset( $hook_functions[ $frame['function'] ], $frame['args'][0] ) && is_string( $frame['args'][0] ) && isset( $ignore_hook[ $frame['args'][0] ] ) ) {
$return = null;
} else {
$args = array();
for ( $i = 0; $i < $show; $i++ ) {
if ( isset( $frame['args'] ) && array_key_exists( $i, $frame['args'] ) ) {
if ( is_string( $frame['args'][ $i ] ) ) {
$args[] = '\'' . $frame['args'][ $i ] . '\'';
} else {
$args[] = QM_Util::display_variable( $frame['args'][ $i ] );
}
}
}
$return['id'] = $frame['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $frame['function'] ) . '(' . implode( ',', $args ) . ')';
}
}
} else {
$return['id'] = $frame['function'] . '()';
$return['display'] = QM_Util::shorten_fqn( $frame['function'] ) . '()';
}
}
if ( $return ) {
$return['calling_file'] = $this->calling_file;
$return['calling_line'] = $this->calling_line;
if ( ! isset( $return['file'] ) ) {
$return['file'] = $this->calling_file;
}
if ( ! isset( $return['line'] ) ) {
$return['line'] = $this->calling_line;
}
}
if ( isset( $frame['line'] ) ) {
$this->calling_line = $frame['line'];
}
if ( isset( $frame['file'] ) ) {
$this->calling_file = $frame['file'];
}
return $return;
}
}
} else {
add_action( 'init', 'QueryMonitor::symlink_warning' );
}

View File

@ -0,0 +1,75 @@
<?php declare(strict_types = 1);
/**
* Plugin CLI command.
*
* @package query-monitor
*/
class QM_CLI extends QM_Plugin {
/**
* @param string $file
*/
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
*
* @return void
*/
public function enable() {
$drop_in = WP_CONTENT_DIR . '/db.php';
if ( file_exists( $drop_in ) ) {
$contents = file_get_contents( $drop_in );
if ( false !== $contents && false !== strpos( $contents, '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 is already in place' );
}
}
if ( defined( 'QM_DB_SYMLINK' ) && ! QM_DB_SYMLINK ) {
WP_CLI::warning( 'Creation of symlink prevented by QM_DB_SYMLINK constant.' );
exit( 0 );
}
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' );
}
}
/**
* @param string $file
* @return self
*/
public static function init( $file = null ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QM_CLI( $file );
}
return $instance;
}
}

View File

@ -0,0 +1,389 @@
<?php declare(strict_types = 1);
/**
* Abstract data collector.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Collector' ) ) {
abstract class QM_Collector {
/**
* @var QM_Timer|null
*/
protected $timer;
/**
* @var QM_Data
*/
protected $data;
/**
* @var bool|null
*/
protected static $hide_qm = null;
/**
* @var array<string, array<string, mixed>>
*/
public $concerned_actions = array();
/**
* @var array<string, array<string, mixed>>
*/
public $concerned_filters = array();
/**
* @var array<string, array<string, mixed>>
*/
public $concerned_constants = array();
/**
* @var array<int, string>
*/
public $tracked_hooks = array();
/**
* @var string
*/
public $id = '';
public function __construct() {
$this->data = $this->get_storage();
}
/**
* @return void
*/
public function set_up() {}
/**
* @return string
*/
final public function id() {
return "qm-{$this->id}";
}
/**
* @param string $type
* @return void
*/
protected function log_type( $type ) {
if ( isset( $this->data->types[ $type ] ) ) {
$this->data->types[ $type ]++;
} else {
$this->data->types[ $type ] = 1;
}
}
/**
* @param QM_Component $component
* @param float $ltime
* @param string|int $type
* @return void
*/
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;
}
}
/**
* @return float
*/
public static function timer_stop_float() {
return microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'];
}
/**
* @param string $constant
* @return string
*/
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';
}
}
/**
* @return QM_Data
*/
public function get_data() {
return $this->data;
}
/**
* @return QM_Data
*/
public function get_storage(): QM_Data {
return new QM_Data_Fallback();
}
/**
* @return void
*/
final public function discard_data() {
$this->data = $this->get_storage();
}
/**
* @param string $id
* @return void
*/
final public function set_id( $id ) {
$this->id = $id;
}
/**
* @return void
*/
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 array<int, 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 array<int, 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 array<int, 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 array<int, 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, 'action', $wp_filter, true, false );
}
$tracked[] = $action;
}
foreach ( $concerned_filters as $filter ) {
if ( has_filter( $filter ) ) {
$this->concerned_filters[ $filter ] = QM_Hook::process( $filter, 'filter', $wp_filter, true, false );
}
$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, 'filter', $wp_filter, true, false );
}
$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;
}
/**
* @param array<string, mixed> $concerns
* @return bool
*/
public function filter_concerns( $concerns ) {
return ! empty( $concerns['actions'] );
}
/**
* @param WP_User $user_object
* @return array<string, mixed>
*/
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;
}
/**
* @return bool
*/
public static function enabled() {
return true;
}
/**
* @return bool
*/
public static function hide_qm() {
if ( ! defined( 'QM_HIDE_SELF' ) ) {
return false;
}
if ( null === self::$hide_qm ) {
self::$hide_qm = QM_HIDE_SELF;
}
return self::$hide_qm;
}
/**
* @param array<string, mixed> $item
* @phpstan-param array{
* component: QM_Component,
* } $item
* @return bool
*/
public function filter_remove_qm( array $item ) {
return ( 'query-monitor' !== $item['component']->context );
}
/**
* @param mixed[] $items
* @return bool
*/
public function filter_dupe_items( $items ) {
return ( count( $items ) > 1 );
}
/**
* @return void
*/
public function process() {}
/**
* @return void
*/
public function post_process() {}
/**
* @return void
*/
public function tear_down() {}
/**
* @return QM_Timer|null
*/
public function get_timer() {
return $this->timer;
}
/**
* @param QM_Timer $timer
* @return void
*/
public function set_timer( QM_Timer $timer ) {
$this->timer = $timer;
}
/**
* @return array<int, string>
*/
public function get_concerned_actions() {
return array();
}
/**
* @return array<int, string>
*/
public function get_concerned_filters() {
return array();
}
/**
* @return array<int, string>
*/
public function get_concerned_options() {
return array();
}
/**
* @return array<int, string>
*/
public function get_concerned_constants() {
return array();
}
}
}

View File

@ -0,0 +1,340 @@
<?php declare(strict_types = 1);
/**
* Enqueued scripts and styles collector.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* @extends QM_DataCollector<QM_Data_Assets>
*/
abstract class QM_Collector_Assets extends QM_DataCollector {
public function get_storage(): QM_Data {
return new QM_Data_Assets();
}
/**
* @return void
*/
public function set_up() {
parent::set_up();
add_action( 'admin_print_footer_scripts', array( $this, 'action_print_footer_scripts' ), 9999 );
add_action( 'wp_print_footer_scripts', array( $this, 'action_print_footer_scripts' ), 9999 );
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 );
}
/**
* @return void
*/
public function tear_down() {
remove_action( 'admin_print_footer_scripts', array( $this, 'action_print_footer_scripts' ), 9999 );
remove_action( 'wp_print_footer_scripts', array( $this, 'action_print_footer_scripts' ), 9999 );
remove_action( 'admin_head', array( $this, 'action_head' ), 9999 );
remove_action( 'wp_head', array( $this, 'action_head' ), 9999 );
remove_action( 'login_head', array( $this, 'action_head' ), 9999 );
remove_action( 'embed_head', array( $this, 'action_head' ), 9999 );
parent::tear_down();
}
/**
* @return string
*/
abstract public function get_dependency_type();
/**
* @return void
*/
public function action_head() {
$type = $this->get_dependency_type();
/** @var WP_Dependencies $dependencies */
$dependencies = $GLOBALS[ "wp_{$type}" ];
$this->data->header = $dependencies->done;
}
/**
* @return void
*/
public function action_print_footer_scripts() {
if ( empty( $this->data->header ) ) {
return;
}
$type = $this->get_dependency_type();
/** @var WP_Dependencies $dependencies */
$dependencies = $GLOBALS[ "wp_{$type}" ];
$this->data->footer = array_diff( $dependencies->done, $this->data->header );
}
/**
* @return void
*/
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' );
$this->data->port = (string) parse_url( $this->data->host, PHP_URL_PORT );
$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();
}
}
/** @var WP_Dependencies $raw */
$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 ) {
/** @var _WP_Dependency|false $item */
$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;
}
/** @var string $handle */
foreach ( $this->data->{$position} as $handle ) {
/** @var _WP_Dependency|false $dependency */
$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, $port ) = $this->get_dependency_data( $dependency );
if ( empty( $dependency->ver ) ) {
$ver = '';
} else {
$ver = $dependency->ver;
}
$warning = ! in_array( $handle, $raw->done, true );
if ( $source instanceof WP_Error ) {
$display = $source->get_error_message();
} else {
$display = ltrim( preg_replace( '#https?://' . preg_quote( $this->data->host, '#' ) . '#', '', remove_query_arg( 'ver', $source ) ), '/' );
}
$dependencies = $dependency->deps;
foreach ( $dependencies as $dep ) {
if ( ! $raw->query( $dep ) ) {
// A missing dependency is a dependency on an asset that doesn't exist
$missing_dependencies[ $dep ] = true;
}
}
$this->data->assets[ $position ][ $handle ] = array(
'host' => $host,
'port' => $port,
'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;
}
/**
* @param _WP_Dependency $item
* @param WP_Dependencies $dependencies
* @return array<int, string>
*/
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 instanceof _WP_Dependency ) {
$broken = array_merge( $broken, self::get_broken_dependencies( $dep, $dependencies ) );
} else {
$broken[] = $item->handle;
}
}
return $broken;
}
/**
* @param _WP_Dependency $dependency
* @param WP_Dependencies $dependencies
* @return array<int, string>
*/
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 instanceof _WP_Dependency ) {
if ( in_array( $dependency->handle, $item->deps, true ) ) {
$dependents[] = $handle;
}
}
}
sort( $dependents );
return $dependents;
}
/**
* @param _WP_Dependency $dependency
* @return mixed[]
* @phpstan-return array{
* 0: string,
* 1: string|WP_Error,
* 2: bool,
* 3: string,
* }
*/
public function get_dependency_data( _WP_Dependency $dependency ) {
/** @var QM_Data_Assets */
$data = $this->get_data();
$loader = rtrim( $this->get_dependency_type(), 's' );
$src = $dependency->src;
$host = '';
$scheme = '';
$port = '';
if ( null === $dependency->ver ) {
$ver = '';
} else {
$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 );
if ( is_string( $source ) ) {
$host = (string) parse_url( $source, PHP_URL_HOST );
$scheme = (string) parse_url( $source, PHP_URL_SCHEME );
$port = (string) parse_url( $source, PHP_URL_PORT );
}
$http_host = $data->host;
$http_port = $data->port;
if ( empty( $host ) && ! empty( $http_host ) ) {
$host = $http_host;
$port = $http_port;
}
if ( $scheme && $data->is_ssl && ( 'https' !== $scheme ) && ( 'localhost' !== $host ) ) {
$source = new WP_Error( 'qm_insecure_content', __( 'Insecure content', 'query-monitor' ), array(
'src' => $source,
) );
}
if ( $source instanceof WP_Error ) {
$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, $port );
}
}

View File

@ -0,0 +1,112 @@
<?php declare(strict_types = 1);
/**
* Container for data collectors.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Collectors' ) ) {
/**
* @implements \IteratorAggregate<string, QM_Collector>
*/
class QM_Collectors implements IteratorAggregate {
/**
* @var array<string, QM_Collector>
*/
private $items = array();
/**
* @var boolean
*/
private $processed = false;
/**
* @return ArrayIterator<string, QM_Collector>
*/
#[\ReturnTypeWillChange]
public function getIterator() {
return new ArrayIterator( $this->items );
}
/**
* @param QM_Collector $collector
* @return void
*/
public static function add( QM_Collector $collector ) {
$collectors = self::init();
$collector->set_up();
$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();
return $collectors->items[ $id ] ?? null;
}
/**
* @return self
*/
public static function init() {
static $instance;
if ( ! $instance ) {
$instance = new QM_Collectors();
}
return $instance;
}
/**
* @return void
*/
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;
}
/**
* @return void
*/
public static function cease() {
$collectors = self::init();
$collectors->processed = true;
/** @var QM_Collector $collector */
foreach ( $collectors as $collector ) {
$collector->tear_down();
$collector->discard_data();
}
}
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types = 1);
/**
* Class representing a component.
*
* @package query-monitor
*/
class QM_Component {
/**
* @var string
*/
public $type;
/**
* @var string
*/
public $name;
/**
* @var string
*/
public $context;
}

View File

@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
/**
* Database class used by the database dropin.
*
* @package query-monitor
*/
class QM_DB extends wpdb {
/**
* @var float
*/
public $time_start;
/**
* Performs a MySQL database query, using current database connection.
*
* @see wpdb::query()
*
* @param string $query Database query
* @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
* affected/selected for all other queries. Boolean false on error.
*/
public function query( $query ) {
if ( $this->show_errors ) {
$this->hide_errors();
}
$result = parent::query( $query );
$i = $this->num_queries - 1;
if ( did_action( 'qm/cease' ) ) {
// It's not possible to prevent the parent class from logging queries because it reads
// the `SAVEQUERIES` constant and I don't want to override more methods than necessary.
$this->queries = array();
}
if ( ! isset( $this->queries[ $i ] ) ) {
return $result;
}
$this->queries[ $i ]['trace'] = new QM_Backtrace();
if ( ! isset( $this->queries[ $i ][3] ) ) {
$this->queries[ $i ][3] = $this->time_start;
}
if ( $this->last_error ) {
$code = 'qmdb';
// This needs to remain in place to account for a user still on PHP 5. Don't want to kill their site.
if ( $this->dbh instanceof mysqli ) {
$code = mysqli_errno( $this->dbh );
}
$this->queries[ $i ]['result'] = new WP_Error( $code, $this->last_error );
} else {
$this->queries[ $i ]['result'] = $result;
}
return $result;
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
/**
* Abstract data transfer object.
*/
/**
* @implements ArrayAccess<string,mixed>
*/
#[AllowDynamicProperties]
abstract class QM_Data implements \ArrayAccess {
/**
* @var array<string, mixed>
*/
public $types = array();
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* component: string,
* ltime: float,
* types: array<array-key, int>,
* }>
*/
public $component_times = array();
/**
* @param mixed $offset
* @param mixed $value
* @return void
*/
#[ReturnTypeWillChange]
final public function offsetSet( $offset, $value ) {
if ( is_string( $offset ) ) {
$this->$offset = $value;
}
}
/**
* @param mixed $offset
* @return bool
*/
#[ReturnTypeWillChange]
final public function offsetExists( $offset ) {
return is_string( $offset ) && isset( $this->$offset );
}
/**
* @param mixed $offset
* @return void
*/
#[ReturnTypeWillChange]
final public function offsetUnset( $offset ) {
// @TODO might be able to no-op this
if ( is_string( $offset ) ) {
unset( $this->$offset );
}
}
/**
* @param mixed $offset
* @return mixed
*/
#[ReturnTypeWillChange]
final public function offsetGet( $offset ) {
return ( is_string( $offset ) && isset( $this->$offset ) ) ? $this->$offset : null;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
/**
* Abstract data collector for structured data.
*
* @package query-monitor
*/
/**
* @phpstan-template T of QM_Data
*/
abstract class QM_DataCollector extends QM_Collector {
/**
* @var QM_Data
* @phpstan-var T
*/
protected $data;
/**
* @return QM_Data
* @phpstan-return T
*/
final public function get_data() {
return $this->data;
}
}

View File

@ -0,0 +1,239 @@
<?php declare(strict_types = 1);
/**
* Abstract dispatcher.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Dispatcher' ) ) {
abstract class QM_Dispatcher {
/**
* Outputter instances.
*
* @var array<string, QM_Output> Array of outputters.
*/
protected $outputters = array();
/**
* Query Monitor plugin instance.
*
* @var QM_Plugin Plugin instance.
*/
protected $qm;
/**
* @var string
*/
public $id = '';
/**
* @var bool
*/
protected $ceased = false;
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' ) );
}
/**
* @return bool
*/
abstract public function is_active();
/**
* @return bool
*/
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.
*
* Possible filter names include:
*
* - `qm/dispatch/html`
* - `qm/dispatch/ajax`
* - `qm/dispatch/redirect`
* - `qm/dispatch/rest`
* - `qm/dispatch/wp_die`
*
* @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();
}
/**
* @return void
*/
public function cease() {
$this->ceased = true;
add_filter( "qm/dispatch/{$this->id}", '__return_false' );
}
/**
* Processes and fetches the outputters for this dispatcher.
*
* @param string $outputter_id The outputter ID.
* @return array<string, 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 array<string, 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;
}
/**
* @return void
*/
public function init() {
if ( ! self::user_can_view() ) {
do_action( 'qm/cease' );
return;
}
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', 1 );
}
add_action( 'send_headers', 'nocache_headers' );
}
/**
* @return void
*/
protected function before_output() {
}
/**
* @return void
*/
protected function after_output() {
}
/**
* @return bool
*/
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();
}
/**
* @return bool
*/
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;
}
/**
* @return string
*/
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 '';
}
/**
* @param string $value
* @return bool
*/
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;
}
/**
* Attempts to switch to the given locale.
*
* This is a wrapper around `switch_to_locale()` which is safe to call at any point, even
* before the `$wp_locale_switcher` global is initialised or if the function does not exist.
*
* @param string $locale The locale.
* @return bool True on success, false on failure.
*/
public static function switch_to_locale( $locale ) {
global $wp_locale_switcher;
if ( function_exists( 'switch_to_locale' ) && ( $wp_locale_switcher instanceof WP_Locale_Switcher ) ) {
return switch_to_locale( $locale );
}
return false;
}
/**
* Attempts to restore the previous locale.
*
* This is a wrapper around `restore_previous_locale()` which is safe to call at any point, even
* before the `$wp_locale_switcher` global is initialised or if the function does not exist.
*
* @return string|false Locale on success, false on error.
*/
public static function restore_previous_locale() {
global $wp_locale_switcher;
if ( function_exists( 'restore_previous_locale' ) && ( $wp_locale_switcher instanceof WP_Locale_Switcher ) ) {
return restore_previous_locale();
}
return false;
}
}
}

View File

@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
/**
* Container for dispatchers.
*
* @package query-monitor
*/
/**
* @implements \IteratorAggregate<string, QM_Dispatcher>
*/
class QM_Dispatchers implements IteratorAggregate {
/**
* @var array<string, QM_Dispatcher>
*/
private $items = array();
/**
* @return ArrayIterator<string, QM_Dispatcher>
*/
#[\ReturnTypeWillChange]
public function getIterator() {
return new ArrayIterator( $this->items );
}
/**
* @param QM_Dispatcher $dispatcher
* @return void
*/
public static function add( QM_Dispatcher $dispatcher ) {
$dispatchers = self::init();
$dispatchers->items[ $dispatcher->id ] = $dispatcher;
}
/**
* @param string $id
* @return QM_Dispatcher|false
*/
public static function get( $id ) {
$dispatchers = self::init();
return $dispatchers->items[ $id ] ?? false;
}
/**
* @return void
*/
public static function cease() {
$dispatchers = self::init();
/** @var QM_Dispatcher $dispatcher */
foreach ( $dispatchers as $dispatcher ) {
$dispatcher->cease();
}
}
/**
* @return self
*/
public static function init() {
static $instance;
if ( ! $instance ) {
$instance = new QM_Dispatchers();
}
return $instance;
}
}

View File

@ -0,0 +1,77 @@
<?php declare(strict_types = 1);
/**
* Hook processor.
*
* @package query-monitor
*/
class QM_Hook {
/**
* @param string $name
* @param string $type
* @param array<string, WP_Hook> $wp_filter
* @param bool $hide_qm
* @param bool $hide_core
* @return array<int, array<string, mixed>>
* @phpstan-param 'action'|'filter' $type
* @phpstan-return array{
* name: string,
* type: 'action'|'filter',
* actions: list<array{
* priority: int,
* callback: array<string, mixed>,
* }>,
* parts: list<string>,
* components: array<string, string>,
* }
*/
public static function process( $name, string $type, 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 $cb ) {
$callback = QM_Util::populate_callback( $cb );
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;
}
$actions[] = array(
'priority' => $priority,
'callback' => $callback,
);
}
}
}
$parts = array_values( array_filter( (array) preg_split( '#[_/.-]#', $name ) ) );
return array(
'name' => $name,
'type' => $type,
'actions' => $actions,
'parts' => $parts,
'components' => $components,
);
}
}

View File

@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
/**
* 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|null Timer.
*/
protected $timer;
public function __construct( QM_Collector $collector ) {
$this->collector = $collector;
}
/**
* @return mixed
*/
abstract public function get_output();
/**
* @return void
*/
public function output() {
// nothing
}
/**
* @return QM_Collector
*/
public function get_collector() {
return $this->collector;
}
/**
* @return QM_Timer|null
*/
final public function get_timer() {
return $this->timer;
}
/**
* @param QM_Timer $timer
* @return void
*/
final public function set_timer( QM_Timer $timer ) {
$this->timer = $timer;
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* PHP version compatibility functionality.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_PHP' ) ) {
class QM_PHP {
/**
* @var string
*/
public static $minimum_version = '7.4.0';
/**
* @return bool
*/
public static function version_met() {
return version_compare( PHP_VERSION, self::$minimum_version, '>=' );
}
/**
* @return void
*/
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_version,
PHP_VERSION,
'https://wordpress.org/support/update-php/'
),
array(
'a' => array(
'href' => array(),
),
)
)
);
}
/**
* @return void
*/
public static function vendor_nope() {
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__( 'Dependencies for Query Monitor need to be installed. Run %1$s from the %2$s directory.', 'query-monitor' ),
'<code>composer install --no-dev</code>',
sprintf(
'<code>%s</code>',
esc_html( dirname( dirname( __FILE__ ) ) )
)
)
);
}
}
}

View File

@ -0,0 +1,109 @@
<?php declare(strict_types = 1);
/**
* Abstract plugin wrapper.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Plugin' ) ) {
abstract class QM_Plugin {
/**
* @var array<string, string>
*/
private $plugin = array();
/**
* @var string
*/
public $file = '';
/**
* Class constructor
*
* @param string $file
*/
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 QM_VERSION;
}
/**
* 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.
*
* @param string $item
* @param string $file
* @return string
*/
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, '/' );
}
/**
* @param string $name Icon name.
* @return string Icon HTML.
*/
public static function icon( $name ) {
if ( 'blank' === $name ) {
return '<span class="qm-icon qm-icon-blank"></span>';
}
return sprintf(
'<svg class="qm-icon qm-icon-%1$s" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20"><use href="#qm-icon-%1$s" /></svg>',
esc_attr( $name )
);
}
}
}

View File

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

View File

@ -0,0 +1,281 @@
<?php declare(strict_types = 1);
/**
* The main Query Monitor plugin class.
*
* @package query-monitor
*/
class QueryMonitor extends QM_Plugin {
/**
* @return void
*/
public function set_up() {
# 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' ) );
add_action( 'qm/cease', array( $this, 'action_cease' ) );
# 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' ) );
add_filter( 'plugin_row_meta', array( $this, 'filter_plugin_row_meta' ), 10, 2 );
# Load and register built-in collectors:
$collectors = array();
$files = glob( $this->plugin_path( 'collectors/*.php' ) );
if ( $files ) {
foreach ( $files as $file ) {
$key = basename( $file, '.php' );
$collectors[ $key ] = $file;
}
}
/**
* Allow filtering of built-in collector files.
*
* @since 2.14.0
*
* @param array<string, string> $collectors Array of file paths to be loaded, keyed by the base
* name of the file.
*/
foreach ( apply_filters( 'qm/built-in-collectors', $collectors ) as $file ) {
include_once $file;
}
}
/**
* @param array<string, string> $actions
* @return array<string, string>
*/
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 );
}
/**
* Filters the array of row meta for each plugin in the Plugins list table.
*
* @param array<int, string> $plugin_meta An array of the plugin's metadata.
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
* @return array<int, string> Updated array of the plugin's metadata.
*/
public function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
if ( 'query-monitor/query-monitor.php' !== $plugin_file ) {
return $plugin_meta;
}
$plugin_meta[] = sprintf(
'<a href="%1$s"><span class="dashicons dashicons-star-filled" aria-hidden="true" style="font-size:14px;line-height:1.3"></span>%2$s</a>',
'https://github.com/sponsors/johnbillion',
esc_html_x( 'Sponsor', 'verb', 'query-monitor' )
);
return $plugin_meta;
}
/**
* 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 array<string, 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 array<int, string> $required_caps Required primitive capabilities for the requested capability.
* @param mixed[] $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters.
* }
* @phpstan-param array{
* 0: string,
* 1: int,
* } $args
* @param WP_User $user Concerned user object.
* @return array<string, 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;
}
/**
* @return void
*/
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 array<int, 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 ( (array) glob( $this->plugin_path( 'dispatchers/*.php' ) ) as $file ) {
include_once $file;
}
/**
* Filters the dispatchers that are being added.
*
* @since 2.11.2
*
* @param array<int, 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 );
}
}
/**
* @return void
*/
public function action_init() {
load_plugin_textdomain( 'query-monitor', false, dirname( $this->plugin_base() ) . '/languages' );
}
/**
* @return void
*/
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/
*
* @return void
*/
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/
*
* @return void
*/
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<string, array<string, mixed>> $groups Array of existing groups.
* @return array<string, array<string, mixed>> 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<string, array<int, string>> $caps Array of existing capabilities.
* @return array<string, array<int, string>> Updated array of capabilities.
*/
public function filter_ure_caps( array $caps ) {
$caps['view_query_monitor'] = array(
'custom',
'query_monitor',
);
return $caps;
}
/**
* @return void
*/
public function action_cease() {
// iterate collectors, call tear_down
// discard all collected data
QM_Collectors::cease();
// remove dispatchers or prevent them from doing anything
QM_Dispatchers::cease();
}
/**
* @param string $file
* @return self
*/
public static function init( $file = null ) {
static $instance = null;
if ( ! $instance ) {
$instance = new QueryMonitor( $file );
}
return $instance;
}
}

View File

@ -0,0 +1,183 @@
<?php declare(strict_types = 1);
/**
* Timer that collects timing and memory usage.
*
* @package query-monitor
*/
class QM_Timer {
/**
* @var array<string, mixed>
* @phpstan-var array{
* time: float,
* memory: int,
* data: mixed[]|null,
* }
*/
protected $start;
/**
* @var array<string, mixed>|null
* @phpstan-var array{
* time: float,
* memory: int,
* data: mixed[]|null,
* }|null
*/
protected $end = null;
/**
* @var QM_Backtrace
*/
protected $trace;
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* time: float,
* memory: int,
* data: mixed[]|null,
* }>
*/
protected $laps = array();
/**
* @param mixed[] $data
* @return self
*/
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;
}
/**
* @param mixed[] $data
* @return self
*/
public function stop( array $data = null ) {
$this->end = array(
'time' => microtime( true ),
'memory' => memory_get_usage(),
'data' => $data,
);
return $this;
}
/**
* @param mixed[] $data
* @param string $name
* @return self
*/
public function lap( array $data = null, $name = null ) {
$lap = array(
'time' => microtime( true ),
'memory' => memory_get_usage(),
'data' => $data,
);
if ( ! isset( $name ) ) {
$i = sprintf(
/* translators: %s: Timing lap number */
__( 'Lap %s', 'query-monitor' ),
number_format_i18n( count( $this->laps ) + 1 )
);
} else {
$i = $name;
}
$this->laps[ $i ] = $lap;
return $this;
}
/**
* @return mixed[]
*/
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;
}
/**
* @return float
*/
public function get_time() {
return $this->end['time'] - $this->start['time'];
}
/**
* @return int
*/
public function get_memory() {
return $this->end['memory'] - $this->start['memory'];
}
/**
* @return float
*/
public function get_start_time() {
return $this->start['time'];
}
/**
* @return int
*/
public function get_start_memory() {
return $this->start['memory'];
}
/**
* @return float
*/
public function get_end_time() {
return $this->end['time'];
}
/**
* @return int
*/
public function get_end_memory() {
return $this->end['memory'];
}
/**
* @return QM_Backtrace
*/
public function get_trace() {
return $this->trace;
}
/**
* @param mixed[] $data
* @return self
*/
public function end( array $data = null ) {
return $this->stop( $data );
}
}

View File

@ -0,0 +1,672 @@
<?php declare(strict_types = 1);
/**
* General utilities class.
*
* @package query-monitor
*/
if ( ! class_exists( 'QM_Util' ) ) {
class QM_Util {
/**
* @var array<string, QM_Component>
*/
protected static $file_components = array();
/**
* @var array<string, string|null>
*/
protected static $file_dirs = array();
/**
* @var string|null
*/
protected static $abspath = null;
/**
* @var string|null
*/
protected static $contentpath = null;
private function __construct() {}
/**
* @param string $size
* @return float
*/
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;
}
/**
* @param string $dir
* @param string $path_replace
* @return string
*/
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;
}
/**
* @param string $path
* @return string
*/
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;
}
/**
* @return array<string, string|null>
*/
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 also the corresponding filters:
*
* - `qm/component_context/{$type}`
* - `qm/component_name/{$type}`
*
* @since 3.6.0
*
* @param array<string, string|null> $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;
}
if ( defined( '\Altis\ROOT_DIR' ) ) {
self::$file_dirs['altis-vendor'] = \Altis\ROOT_DIR . '/vendor';
}
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 ) {
if ( null === $dir ) {
continue;
}
self::$file_dirs[ $type ] = self::standard_dir( $dir );
}
}
return self::$file_dirs;
}
/**
* Attempts to determine the component responsible for a given file name.
*
* @param string $file An absolute file path.
* @return QM_Component An object representing the component.
*/
public static function get_file_component( $file ) {
$file = self::standard_dir( $file );
$type = '';
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 'altis-vendor':
$plug = str_replace( \Altis\ROOT_DIR . '/vendor/', '', $file );
$plug = explode( '/', $plug, 3 );
$plug = $plug[0] . '/' . $plug[1];
/* translators: %s: Dependency name */
$name = sprintf( __( 'Dependency: %s', 'query-monitor' ), $plug );
break;
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 = __( 'WordPress Core', 'query-monitor' );
break;
case 'unknown':
default:
$name = __( 'Unknown', 'query-monitor' );
/**
* Filters the type of a custom or unknown component.
*
* The dynamic portion of the hook name, `$type`, refers to the component identifier.
*
* See also the corresponding filters:
*
* - `qm/component_dirs`
* - `qm/component_name/{$type}`
* - `qm/component_context/{$type}`
*
* @since 3.8.1
*
* @param string $type The component type.
* @param string $file The full file path for the file within the component.
* @param string $name The component name.
* @param string $context The context for the component.
*/
$type = apply_filters( "qm/component_type/{$type}", $type, $file, $name, $context );
/**
* Filters the name of a custom or unknown component.
*
* The dynamic portion of the hook name, `$type`, refers to the component identifier.
*
* See also the corresponding filters:
*
* - `qm/component_dirs`
* - `qm/component_type/{$type}`
* - `qm/component_context/{$type}`
*
* @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 );
/**
* Filters the context for a custom or unknown component. The context is usually a
* representation of its type more specific to the individual component.
*
* The dynamic portion of the hook name, `$type`, refers to the component identifier.
*
* See also the corresponding filters:
*
* - `qm/component_dirs`
* - `qm/component_type/{$type}`
* - `qm/component_name/{$type}`
*
* @since 3.8.0
*
* @param string $context The context for the component.
* @param string $file The full file path for the file within the component.
* @param string $name The component name.
*/
$context = apply_filters( "qm/component_context/{$type}", $context, $file, $name );
break;
}
$component = new QM_Component();
$component->type = $type;
$component->name = $name;
$component->context = $context;
self::$file_components[ $file ] = $component;
return self::$file_components[ $file ];
}
/**
* @param array<string, mixed> $callback
* @return array<string, mixed>
* @phpstan-return array{
* name?: string,
* file?: string|false,
* line?: string|false,
* error?: WP_Error,
* component?: QM_Component,
* }
*/
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 ( $callback['function'] instanceof Closure ) {
$ref = new ReflectionFunction( $callback['function'] );
$filename = $ref->getFileName();
if ( $filename ) {
$file = self::standard_dir( $filename, '' );
if ( 0 === strpos( $file, '/' ) ) {
$file = basename( $filename );
}
$callback['name'] = sprintf(
/* translators: A closure is an anonymous PHP function. 1: Line number, 2: File name */
__( 'Closure on line %1$d of %2$s', 'query-monitor' ),
$ref->getStartLine(),
$file
);
} else {
/* translators: A closure is an anonymous PHP function */
$callback['name'] = __( 'Unknown closure', 'query-monitor' );
}
} 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 ( $callback['file'] && 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'] = new QM_Component();
$callback['component']->type = 'php';
$callback['component']->name = 'PHP';
$callback['component']->context = '';
}
} catch ( ReflectionException $e ) {
$callback['error'] = new WP_Error( 'reflection_exception', $e->getMessage() );
}
unset( $callback['function'], $callback['class'] );
return $callback;
}
/**
* @return bool
*/
public static function is_ajax() {
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return true;
}
return false;
}
/**
* @return bool
*/
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;
}
/**
* @return WP_Role|false
*/
public static function get_admins() {
if ( is_multisite() ) {
return false;
} else {
return get_role( 'administrator' );
}
}
/**
* @return bool
*/
public static function is_multi_network() {
return ( function_exists( 'is_multi_network' ) && is_multi_network() );
}
/**
* @param int|string $client
* @return array<string, int>
* @phpstan-return array{
* major: int,
* minor: int,
* patch: int,
* }
*/
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' );
}
/**
* @param string $sql
* @return string
*/
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 = 'Unknown';
if ( is_array( $words ) && isset( $words[0] ) ) {
$type = strtoupper( $words[0] );
}
return $type;
}
/**
* @param mixed $value
* @return string|float|int
*/
public static function display_variable( $value ) {
if ( is_string( $value ) ) {
return $value;
} elseif ( $value === null ) {
return 'null';
} 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 ):
$class = sprintf( '%s (ID: %s)', $class, $value->ID );
break;
case ( $value instanceof WP_Term ):
$class = sprintf( '%s (term_id: %s)', $class, $value->term_id );
break;
case ( $value instanceof WP_Comment ):
$class = sprintf( '%s (comment_ID: %s)', $class, $value->comment_ID );
break;
case ( $value instanceof WP_Error ):
$class = 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 ):
$class = sprintf( '%s (%s)', $class, $value->name );
break;
case ( $value instanceof WP_Network ):
$class = sprintf( '%s (id: %s)', $class, $value->id );
break;
case ( $value instanceof WP_Site ):
$class = sprintf( '%s (blog_id: %s)', $class, $value->blog_id );
break;
case ( $value instanceof WP_Theme ):
$class = sprintf( '%s (%s)', $class, $value->get_stylesheet() );
break;
}
return $class;
} 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 ) {
if ( substr_count( $fqn, '\\' ) < 3 ) {
return $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 manner.
*
* @param mixed $data The data to be JSON encoded.
* @return string The JSON encoded data.
*/
public static function json_format( $data ) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
$json_options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
$json = json_encode( $data, $json_options );
if ( false === $json ) {
return '';
}
return $json;
}
/**
* Returns the site editor URL for a given template or template part name.
*
* @param string $template The site template name, for example `twentytwentytwo//header-small-dark`.
* @param string $type The template type, either 'wp_template_part' or 'wp_template'.
* @return string The admin URL for editing the site template.
*/
public static function get_site_editor_url( string $template, string $type = 'wp_template_part' ): string {
return add_query_arg(
array(
'postType' => $type,
'postId' => urlencode( $template ),
'canvas' => 'edit',
),
admin_url( 'site-editor.php' )
);
}
/**
* @deprecated
* @param mixed $data
* @return bool
*/
public static function is_stringy( $data ) {
return ( is_string( $data ) || ( is_object( $data ) && method_exists( $data, '__toString' ) ) );
}
/**
* @param mixed[] $array
* @param string $field
* @return void
*/
public static function sort( array &$array, $field ) {
usort( $array, function( array $a, array $b ) use ( $field ): int {
return $a[ $field ] <=> $b[ $field ];
} );
}
/**
* @param mixed[] $array
* @param string $field
* @return void
*/
public static function rsort( array &$array, $field ) {
usort( $array, function( array $a, array $b ) use ( $field ): int {
return $b[ $field ] <=> $a[ $field ];
} );
}
}
}

View File

@ -0,0 +1,78 @@
<?php declare(strict_types = 1);
/**
* Mock 'Debug Bar' plugin class.
*
* @package query-monitor
*/
class Debug_Bar {
/**
* @var array<int, Debug_Bar_Panel>
*/
public $panels = array();
public function __construct() {
add_action( 'wp_head', array( $this, 'ensure_ajaxurl' ), 1 );
$this->enqueue();
$this->init_panels();
}
/**
* @return void
*/
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' );
}
/**
* @return void
*/
public function init_panels() {
/**
* Filters the debug bar panel list. This mimics the same filter called in the Debug Bar plugin.
*
* @since 2.7.0
*
* @param array<int, Debug_Bar_Panel> $panels Array of Debug Bar panel instances.
*/
$this->panels = apply_filters( 'debug_bar_panels', array() );
}
/**
* @return void
*/
public function ensure_ajaxurl() {
$dispatcher = QM_Dispatchers::get( 'html' );
if ( $this->panels && $dispatcher && $dispatcher::user_can_view() ) {
?>
<script type="text/javascript">
var ajaxurl = '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>';
</script>
<?php
}
}
/**
* @return void
*/
public function Debug_Bar() {
self::__construct();
}
}

View File

@ -0,0 +1,95 @@
<?php declare(strict_types = 1);
/**
* Mock 'Debug Bar' panel class.
*
* @package query-monitor
*/
abstract class Debug_Bar_Panel {
/**
* @var string
*/
public $_title = '';
/**
* @var bool
*/
public $_visible = true;
/**
* @param string $title
*/
public function __construct( $title = '' ) {
$this->title( $title );
if ( $this->init() === false ) {
$this->set_visible( false );
return;
}
add_filter( 'debug_bar_classes', array( $this, 'debug_bar_classes' ) );
}
/**
* Initializes the panel.
*
* @return false|void
*/
public function init() {}
/**
* @return void
*/
public function prerender() {}
/**
* Renders the panel.
*
* @return void
*/
public function render() {}
/**
* @return bool
*/
public function is_visible() {
return $this->_visible;
}
/**
* @param bool $visible
* @return void
*/
public function set_visible( $visible ) {
$this->_visible = $visible;
}
/**
* @param string|null $title
* @return string|void
*/
public function title( $title = null ) {
if ( ! isset( $title ) ) {
return $this->_title;
}
$this->_title = $title;
}
/**
* @param array<int, string> $classes
* @return array<int, string>
*/
public function debug_bar_classes( $classes ) {
return $classes;
}
/**
* @param string $title
* @return void
*/
public function Debug_Bar_Panel( $title = '' ) {
self::__construct( $title );
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,109 @@
{
"name": "johnbillion/query-monitor",
"description": "The Developer Tools panel for WordPress.",
"license": "GPL-2.0-or-later",
"type": "wordpress-plugin",
"authors": [
{
"name": "John Blackbourn",
"homepage": "https://johnblackbourn.com/"
}
],
"homepage": "https://github.com/johnbillion/query-monitor/",
"support": {
"issues": "https://github.com/johnbillion/query-monitor/issues",
"forum": "https://wordpress.org/support/plugin/query-monitor",
"source": "https://github.com/johnbillion/query-monitor"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/johnbillion"
}
],
"require": {
"php": ">=7.4.0",
"composer/installers": "^1.0 || ^2.0"
},
"require-dev": {
"codeception/module-asserts": "^1.0",
"codeception/module-db": "^1.0",
"codeception/module-webdriver": "^1.0",
"codeception/util-universalframework": "^1.0",
"dealerdirect/phpcodesniffer-composer-installer": "0.7.2",
"ergebnis/composer-normalize": "^2",
"johnbillion/plugin-infrastructure": "dev-trunk",
"lucatume/wp-browser": "^3.0.21",
"phpcompatibility/phpcompatibility-wp": "2.1.4",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"roots/wordpress": "*",
"squizlabs/php_codesniffer": "3.7.1",
"szepeviktor/phpstan-wordpress": "1.3.0",
"wp-coding-standards/wpcs": "2.3.0"
},
"autoload": {
"classmap": [
"classes",
"data",
"output"
]
},
"autoload-dev": {
"psr-4": {
"QM\\Tests\\": "tests/integration"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"ergebnis/composer-normalize": true,
"roots/wordpress-core-installer": true,
"composer/installers": true
},
"classmap-authoritative": true,
"preferred-install": "dist",
"prepend-autoloader": false,
"sort-packages": true
},
"extra": {
"wordpress-install-dir": "vendor/roots/wordpress"
},
"scripts": {
"build-vendor": [
"build-vendor"
],
"test": [
"@composer validate --strict --no-check-lock",
"@composer normalize --dry-run",
"@test:phpstan",
"@test:phpcs",
"@test:integration",
"@test:acceptance"
],
"test:acceptance": [
"npm run build",
"acceptance-tests"
],
"test:destroy": [
"COMPOSE_PROJECT_NAME=query-monitor docker-compose down --volumes --remove-orphans"
],
"test:integration": [
"integration-tests"
],
"test:phpcs": [
"phpcs -nps --colors --report-code --report-summary --report-width=80 --cache=tests/cache/phpcs.json --basepath='./' --standard=phpcs53.xml",
"phpcs -nps --colors --report-code --report-summary --report-width=80 --cache=tests/cache/phpcs.json --basepath='./' ."
],
"test:phpstan": [
"codecept build",
"phpstan analyze --memory-limit=1024M"
],
"test:start": [
"COMPOSE_PROJECT_NAME=query-monitor docker-compose up -d"
],
"test:stop": [
"COMPOSE_PROJECT_NAME=query-monitor docker-compose down"
]
}
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
/**
* Admin screen data transfer object.
*
* @package query-monitor
*/
class QM_Data_Admin extends QM_Data {
/**
* @var ?WP_Screen
*/
public $current_screen;
/**
* @var string
*/
public $hook_suffix;
/**
* @var array<string, string>
* @phpstan-var array{}|array{
* columns_filter: string,
* sortables_filter: string,
* column_action: string,
* class_name?: string,
* }
*/
public $list_table;
/**
* @var string
*/
public $pagenow;
/**
* @var string
*/
public $taxnow;
/**
* @var string
*/
public $typenow;
}

View File

@ -0,0 +1,73 @@
<?php declare(strict_types = 1);
/**
* Asset data transfer object.
*
* @package query-monitor
*/
class QM_Data_Assets extends QM_Data {
/**
* @var array<string, array<string, array<string, mixed>>>
*/
public $assets;
/**
* @var array<int, string>
*/
public $broken;
/**
* @var array<string, int>
*/
public $counts;
/**
* @var string
*/
public $default_version;
/**
* @var array<int, string>
*/
public $dependencies;
/**
* @var array<int, string>
*/
public $dependents;
/**
* @var array<int, string>
*/
public $footer;
/**
* @var array<int, string>
*/
public $header;
/**
* @var string
*/
public $host;
/**
* @var bool
*/
public $is_ssl;
/**
* @var array<int, string>
*/
public $missing;
/**
* @var array<string, true>
*/
public $missing_dependencies;
/**
* @var string
*/
public $port;
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
/**
* Block editor data transfer object.
*
* @package query-monitor
*/
class QM_Data_Block_Editor extends QM_Data {
/**
* @var array<int, string>
*/
public $all_dynamic_blocks;
/**
* @var bool
*/
public $block_editor_enabled;
/**
* @var bool
*/
public $has_block_context;
/**
* @var bool
*/
public $has_block_timing;
/**
* @var array<int, mixed>|null
*/
public $post_blocks;
/**
* @var bool
*/
public $post_has_blocks;
/**
* @var int
*/
public $total_blocks;
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
/**
* Cache data transfer object.
*
* @package query-monitor
*/
class QM_Data_Cache extends QM_Data {
/**
* @var bool
*/
public $has_object_cache;
/**
* @var bool
*/
public $display_hit_rate_warning;
/**
* @var bool
*/
public $has_opcode_cache;
/**
* @var int
*/
public $cache_hit_percentage;
/**
* @var array<string, mixed>
*/
public $stats;
/**
* @var array<string, bool>
*/
public $object_cache_extensions;
/**
* @var array<string, bool>
*/
public $opcode_cache_extensions;
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
/**
* Cache data transfer object.
*
* @package query-monitor
*/
class QM_Data_Caps extends QM_Data {
/**
* @var array<int, array<string, mixed>>
* @phpstan-var list<array{
* args: list<mixed>,
* filtered_trace: list<array<string, mixed>>,
* component: QM_Component,
* result: bool,
* parts: list<string>,
* name: string,
* user: string,
* }>
*/
public $caps;
/**
* @var array<int, string>
*/
public $parts;
/**
* @var array<int, int>
*/
public $users;
/**
* @var array<string, string>
*/
public $components;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
/**
* Conditionals data transfer object.
*
* @package query-monitor
*/
class QM_Data_Conditionals extends QM_Data {
/**
* @var array<string, array<int, string>>
* @phpstan-var array{
* true: list<string>,
* false: list<string>,
* na: list<string>,
* }
*/
public $conds;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
/**
* Database query callers data transfer object.
*
* @package query-monitor
*/
class QM_Data_DB_Callers extends QM_Data {
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* caller: string,
* ltime: float,
* types: array<string, int>,
* }>
*/
public $times = array();
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
/**
* Database query components data transfer object.
*
* @package query-monitor
*/
class QM_Data_DB_Components extends QM_Data {
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* ltime: float,
* types: array<string, int>,
* component: string,
* }>
*/
public $times;
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types = 1);
/**
* Duplicate database queries data transfer object.
*
* @package query-monitor
*/
class QM_Data_DB_Dupes extends QM_Data {
/**
* @var int
*/
public $total_qs;
/**
* @var array<string, array<string, int>>
*/
public $dupe_sources;
/**
* @var array<string, array<string, int>>
*/
public $dupe_callers;
/**
* @var array<string, array<string, int>>
*/
public $dupe_components;
/**
* @var array<string, array<int, int>>
*/
public $dupes;
/**
* @var array<string, float>
*/
public $dupe_times;
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types = 1);
/**
* Database queries data transfer object.
*
* @package query-monitor
*/
class QM_Data_DB_Queries extends QM_Data {
/**
* @var int
*/
public $total_qs;
/**
* @var float
*/
public $total_time;
/**
* @var array<int, array<string, mixed>>
*/
public $errors;
/**
* @var ?array<int, array<string, mixed>>
*/
public $expensive;
/**
* @var ?stdClass
*/
public $wpdb;
/**
* @var ?array<string, array<string, mixed>>
* @phpstan-var ?array<string, array{
* caller: string,
* ltime: float,
* types: array<string, int>,
* }>
*/
public $times = array();
/**
* @var ?array<string, array<int, int>>
*/
public $dupes;
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types = 1);
/**
* Doing it Wrong data transfer object.
*
* @package query-monitor
*/
class QM_Data_Doing_It_Wrong extends QM_Data {
/**
* @var array<int, array<string, mixed>>
* @phpstan-var array<int, array{
* hook: string,
* filtered_trace: list<array<string, mixed>>,
* message: string,
* component: QM_Component,
* }>
*/
public $actions;
}

View File

@ -0,0 +1,68 @@
<?php declare(strict_types = 1);
/**
* Environment data transfer object.
*
* @package query-monitor
*/
class QM_Data_Environment extends QM_Data {
/**
* @TODO data class
* @var array<string, mixed>
* @phpstan-var array{
* variables: array<string, string|null>,
* version: string|false,
* sapi: string|false,
* user: string,
* old: bool,
* extensions: array<string, string>,
* error_reporting: int,
* error_levels: array<string, bool>,
* }
*/
public $php;
/**
* @TODO data class
* @var array<string, mixed>
* @phpstan-var array{
* info: array{
* server-version: string,
* extension: string|null,
* client-version: string|null,
* user: string,
* host: string,
* database: string,
* },
* vars: array<string, bool|string>,
* variables: list<stdClass>,
* }
*/
public $db;
/**
* @TODO data class
* @var array<string, mixed>
* @phpstan-var array{
* version: string,
* environment_type?: string,
* development_mode?: string,
* constants: array<string, string>,
* }>
*/
public $wp;
/**
* @TODO data class
* @var array<string, mixed>
* @phpstan-var array{
* name: string,
* version: string|null,
* address: string|null,
* host: string|null,
* OS: string|null,
* arch: string|null,
* }>
*/
public $server;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
/**
* Fallback data transfer object for third-party collectors that don't extend
* the new QM_DataCollector class.
*
* @package query-monitor
*/
#[AllowDynamicProperties]
class QM_Data_Fallback extends QM_Data {}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
/**
* Hooks data transfer object.
*
* @package query-monitor
*/
class QM_Data_Hooks extends QM_Data {
/**
* @var array<int, array<string, mixed>>
* @phpstan-var list<array{
* name: string,
* actions: list<array{
* priority: int,
* callback: array<string, mixed>,
* }>,
* parts: list<string>,
* components: array<string, string>,
* }>
*/
public $hooks;
/**
* @var array<int, string>
*/
public $parts;
/**
* @var array<string, string>
*/
public $components;
/**
* @var bool
*/
public $all_hooks;
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types = 1);
/**
* HTTP data transfer object.
*
* @package query-monitor
*/
class QM_Data_HTTP extends QM_Data {
/**
* @var array<string, array<string, mixed>>
* @phpstan-var array<string, array{
* args: array<string, mixed>,
* component: QM_Component,
* filtered_trace: list<array<string, mixed>>,
* info: array<string, mixed>|null,
* host: string,
* local: bool,
* ltime: float,
* redirected_to: string|null,
* response: mixed[]|WP_Error,
* type: string,
* url: string,
* }>
*/
public $http;
/**
* @var float
*/
public $ltime;
/**
* @var array<string, array<int, string>>
* @phpstan-var array{
* alert?: list<string>,
* warning?: list<string>,
* }
*/
public $errors;
}

View File

@ -0,0 +1,56 @@
<?php declare(strict_types = 1);
/**
* Languages data transfer object.
*
* @package query-monitor
*/
class QM_Data_Languages extends QM_Data {
/**
* @var array<string, array<string, array<string, mixed>>>
* @phpstan-var array<string, array<string, array{
* caller: mixed,
* domain: string,
* file: string|false,
* found: int|false,
* handle: string|null,
* type: 'gettext'|'jed',
* }>>
*/
public $languages;
/**
* @var string
*/
public $locale;
/**
* @var string
*/
public $user_locale;
/**
* @var string
*/
public $determined_locale;
/**
* @var string
*/
public $language_attributes;
/**
* @var string
*/
public $mlp_language;
/**
* @var string
*/
public $pll_language;
/**
* @var int
*/
public $total_size;
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
/**
* Logger data transfer object.
*
* @package query-monitor
*/
class QM_Data_Logger extends QM_Data {
/**
* @var array<string, int>
* @phpstan-var array<QM_Collector_Logger::*, int>
*/
public $counts;
/**
* @var array<int, array<string, mixed>>
* @phpstan-var list<array{
* message: string,
* filtered_trace: mixed[],
* component: QM_Component,
* level: QM_Collector_Logger::*,
* }>
*/
public $logs;
/**
* @var array<string, string>
*/
public $components;
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types = 1);
/**
* Multisite data transfer object.
*
* @package query-monitor
*/
class QM_Data_Multisite extends QM_Data {
/**
* @var array<int, array<string, mixed>>
* @phpstan-var list<array{
* new: int,
* prev: int,
* to: bool,
* trace: QM_Backtrace,
* }>
*/
public $switches;
}

View File

@ -0,0 +1,69 @@
<?php declare(strict_types = 1);
/**
* Overview data transfer object.
*
* @package query-monitor
*/
class QM_Data_Overview extends QM_Data {
/**
* @var ?float
*/
public $time_taken;
/**
* @var int
*/
public $time_limit;
/**
* @var float
*/
public $time_start;
/**
* @var int|float
*/
public $time_usage;
/**
* @var int
*/
public $memory;
/**
* @var float
*/
public $memory_limit;
/**
* @var int|float
*/
public $memory_usage;
/**
* @var ?array<string, mixed>
*/
public $current_user;
/**
* @var ?array<string, mixed>
*/
public $switched_user;
/**
* @var bool
*/
public $display_time_usage_warning;
/**
* @var bool
*/
public $display_memory_usage_warning;
/**
* @var bool
*/
public $is_admin;
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types = 1);
/**
* PHP errors data transfer object.
*
* @package query-monitor
*/
/**
* @phpstan-type errorObject array{
* errno: int,
* type: string,
* message: string,
* file: string|null,
* filename: string,
* line: int|null,
* filtered_trace: list<array<string, mixed>>|null,
* component: QM_Component,
* calls: int,
* }
* @phpstan-type errorObjects array<string, array<string, errorObject>>
*/
class QM_Data_PHP_Errors extends QM_Data {
/**
* @var array<string, string>
*/
public $components;
/**
* @var array<string, array<string, array<string, mixed>>>
* @phpstan-var errorObjects
*/
public $errors;
/**
* @var array<string, array<string, array<string, mixed>>>
* @phpstan-var errorObjects
*/
public $suppressed;
/**
* @var array<string, array<string, array<string, mixed>>>
* @phpstan-var errorObjects
*/
public $silenced;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
/**
* Raw request data transfer object.
*
* @package query-monitor
*/
class QM_Data_Raw_Request extends QM_Data {
/**
* @var array<string, mixed>
*/
public $request;
/**
* @var array<string, mixed>
*/
public $response;
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types = 1);
/**
* Redirect data transfer object.
*
* @package query-monitor
*/
class QM_Data_Redirect extends QM_Data {
/**
* @var ?QM_Backtrace
*/
public $trace;
/**
* @var ?string
*/
public $location;
/**
* @var ?int
*/
public $status;
}

View File

@ -0,0 +1,52 @@
<?php declare(strict_types = 1);
/**
* Request data transfer object.
*
* @package query-monitor
*/
class QM_Data_Request extends QM_Data {
/**
* @var array<string, mixed>
* @phpstan-var array{
* title: string,
* data: WP_User|false,
* }
*/
public $user;
/**
* @var array<string, array<string, mixed>>
*/
public $multisite;
/**
* @var array<string, mixed>
*/
public $request;
/**
* @var array<string, mixed>
*/
public $qvars;
/**
* @var array<string, mixed>
*/
public $plugin_qvars;
/**
* @var array<string, mixed>
*/
public $queried_object;
/**
* @var string
*/
public $request_method;
/**
* @var array<string, string>
*/
public $matching_rewrites;
}

View File

@ -0,0 +1,99 @@
<?php declare(strict_types = 1);
/**
* Theme data transfer object.
*
* @package query-monitor
*/
class QM_Data_Theme extends QM_Data {
/**
* @var bool
*/
public $is_child_theme;
/**
* @var string
*/
public $stylesheet_theme_json;
/**
* @var string
*/
public $template_theme_json;
/**
* @var WP_Block_Template|null
*/
public $block_template;
/**
* @var array<string, string>
*/
public $theme_dirs;
/**
* @var array<string, string>
*/
public $theme_folders;
/**
* @var string
*/
public $stylesheet;
/**
* @var string
*/
public $template;
/**
* @var string
*/
public $theme_template_file;
/**
* @var string
*/
public $template_path;
/**
* @var ?string
*/
public $template_file;
/**
* @var ?array<int, string>
*/
public $template_hierarchy;
/**
* @var ?array<int, string>
*/
public $timber_files;
/**
* @var ?array<int, string>
*/
public $body_class;
/**
* @var array<string|int, string>
*/
public $template_parts;
/**
* @var array<string|int, string>
*/
public $theme_template_parts;
/**
* @var array<string|int, int>
*/
public $count_template_parts;
/**
* @var array<int, array<string, mixed>>
*/
public $unsuccessful_template_parts;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
/**
* Timing data transfer object.
*
* @package query-monitor
*/
class QM_Data_Timing extends QM_Data {
/**
* @var array<int, array<string, mixed>>
*/
public $warning;
/**
* @var array<int, array<string, mixed>>
*/
public $timing;
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
/**
* Transients data transfer object.
*
* @package query-monitor
*/
class QM_Data_Transients extends QM_Data {
/**
* @var array<int, array{
* name: string,
* filtered_trace: mixed[],
* component: QM_Component,
* type: string,
* value: mixed,
* expiration: int,
* exp_diff: string,
* size: int,
* size_formatted: string,
* }>
*/
public $trans = array();
/**
* @var bool
*/
public $has_type;
}

View File

@ -0,0 +1,127 @@
<?php declare(strict_types = 1);
/**
* Ajax request dispatcher.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Dispatcher_AJAX extends QM_Dispatcher {
public $id = 'ajax';
public function __construct( QM_Plugin $qm ) {
parent::__construct( $qm );
// This dispatcher needs to run on a priority lower than 1 so it can output
// its headers before wp_ob_end_flush_all() flushes all the output buffers:
// https://github.com/WordPress/wordpress-develop/blob/0a3a3c5119897c6d551a42ae9b5dbfa4f576f2c9/src/wp-includes/default-filters.php#L382
add_action( 'shutdown', array( $this, 'dispatch' ), 0 );
}
/**
* @return void
*/
public function init() {
if ( ! self::user_can_view() ) {
return;
}
if ( QM_Util::is_ajax() ) {
// Start an output buffer for Ajax requests so headers can be output at the end:
ob_start();
}
parent::init();
}
/**
* @return void
*/
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
$this->before_output();
foreach ( $this->get_outputters( 'headers' ) as $id => $output ) {
$output->output();
}
$this->after_output();
}
/**
* @return void
*/
protected function before_output() {
foreach ( (array) glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
require_once $file;
}
}
/**
* @return void
*/
protected function after_output() {
# flush once, because we're nice
if ( ob_get_length() ) {
ob_flush();
}
}
/**
* @return bool
*/
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;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
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,941 @@
<?php declare(strict_types = 1);
/**
* General HTML request dispatcher.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Dispatcher_Html extends QM_Dispatcher {
/**
* Outputter instances.
*
* @var array<string, QM_Output_Html> Array of outputters.
*/
protected $outputters = array();
/**
* @var string
*/
public $id = 'html';
/**
* @var bool
*/
public $did_footer = false;
/**
* @var array<string, mixed[]>
*/
protected $admin_bar_menu = array();
/**
* @var array<string, mixed[]>
*/
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' ) );
// 9 is a magic number, it's the latest we can realistically use due to plugins
// which call `fastcgi_finish_request()` in a `shutdown` callback hooked on the
// default priority of 10, and QM needs to dispatch its output before those.
add_action( 'shutdown', array( $this, 'dispatch' ), 9 );
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( 'gp_footer', array( $this, 'action_footer' ) );
parent::__construct( $qm );
}
/**
* @return void
*/
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 ) ) );
}
/**
* @return void
*/
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' );
$domain = COOKIE_DOMAIN ?: '';
setcookie( QM_COOKIE, $cookie, $expiration, COOKIEPATH, $domain, $secure, false );
wp_send_json_success();
}
/**
* @return void
*/
public function ajax_off() {
if ( ! self::user_verified() || ! check_ajax_referer( 'qm-auth-off', 'nonce', false ) ) {
wp_send_json_error();
}
$expiration = time() - 31536000;
$domain = COOKIE_DOMAIN ?: '';
setcookie( QM_COOKIE, ' ', $expiration, COOKIEPATH, $domain );
wp_send_json_success();
}
/**
* @return void
*/
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'] );
$domain = COOKIE_DOMAIN ?: '';
setcookie( QM_EDITOR_COOKIE, $editor, $expiration, COOKIEPATH, $domain, $secure, false );
wp_send_json_success( $editor );
}
/**
* @param WP_Admin_Bar $wp_admin_bar
* @return void
*/
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',
) );
}
/**
* @return void
*/
public function init() {
if ( ! self::user_can_view() ) {
return;
}
if ( ! self::request_supported() ) {
return;
}
if ( ! file_exists( $this->qm->plugin_path( 'assets/query-monitor.css' ) ) ) {
add_action( 'admin_notices', array( $this, 'build_warning' ) );
return;
}
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();
}
/**
* @return void
*/
public function manually_print_assets() {
wp_print_scripts( array(
'query-monitor',
) );
wp_print_styles( array(
'query-monitor',
) );
}
/**
* @return void
*/
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() ), '' ) )
)
)
);
}
/**
* @return void
*/
public function enqueue_assets() {
global $wp_locale;
$deps = array(
'jquery',
);
if ( defined( 'QM_NO_JQUERY' ) && QM_NO_JQUERY ) {
$deps = array();
}
wp_enqueue_style(
'query-monitor',
$this->qm->plugin_url( 'assets/query-monitor.css' ),
array(),
QM_VERSION
);
wp_enqueue_script(
'query-monitor',
$this->qm->plugin_url( 'assets/query-monitor.js' ),
$deps,
QM_VERSION,
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 $dispatcher The HTML dispatcher.
*/
do_action( 'qm/output/enqueued-assets', $this );
}
/**
* @return void
*/
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
if ( $this->ceased ) {
$admin_bar_menu = array(
'top' => array(
'title' => 'Query Monitor',
),
'sub' => array(
'ceased' => array(
'title' => esc_html__( 'Data collection ceased', 'query-monitor' ),
'id' => 'query-monitor-ceased',
'href' => '#',
),
),
);
$json = array(
'menu' => $admin_bar_menu,
);
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-ceased"></div>';
echo '<!-- End Query Monitor output -->' . "\n\n";
return;
}
$switched_locale = self::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 ) {
self::restore_previous_locale();
}
}
/**
* @return void
*/
protected function before_output() {
foreach ( (array) glob( $this->qm->plugin_path( 'output/html/*.php' ) ) as $file ) {
require_once $file;
}
/** @var array<string, QM_Output_Html> $outputters */
$outputters = $this->get_outputters( 'html' );
$this->outputters = $outputters;
/**
* Filters the menu items shown in Query Monitor's admin toolbar menu.
*
* @since 3.0.0
*
* @param array<string, mixed[]> $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<string, mixed[]> $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 ] ) ) {
$count = count( $collector->concerned_filters ) + count( $collector->concerned_actions );
$this->panel_menu[ 'qm-' . $output_id ]['children'][ 'qm-' . $output_id . '-concerned_hooks' ] = array(
'href' => esc_attr( '#' . $collector->id() . '-concerned_hooks' ),
'title' => sprintf(
/* translators: %s: Number of hooks */
__( 'Hooks in Use (%s)', 'query-monitor' ),
number_format_i18n( $count )
),
);
}
}
$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 '<svg id="qm-icon-container">';
foreach ( (array) glob( $this->qm->plugin_path( 'assets/icons/*.svg' ) ) as $icon ) {
if ( ! $icon ) {
continue;
}
$icon_name = basename( $icon, '.svg' );
$contents = (string) file_get_contents( $icon );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo str_replace(
'<path ',
sprintf(
'<path id="qm-icon-%s" ',
$icon_name
),
$contents
);
}
echo '</svg>';
echo '<div id="query-monitor-main" data-theme="auto" 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>';
$settings = QueryMonitor::icon( 'admin-generic' );
$toggle = QueryMonitor::icon( 'image-rotate-left' );
$close = QueryMonitor::icon( 'no-alt' );
echo '</div>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<button class="qm-title-button qm-button-container-settings" aria-label="' . esc_attr__( 'Settings', 'query-monitor' ) . '">' . $settings . '</button>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<button class="qm-title-button qm-button-container-position" aria-label="' . esc_html__( 'Toggle panel position', 'query-monitor' ) . '">' . $toggle . '</button>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<button class="qm-title-button qm-button-container-close" aria-label="' . esc_attr__( 'Close Panel', 'query-monitor' ) . '">' . $close . '</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">';
}
/**
* @param string $id
* @param mixed[] $menu
* @return void
*/
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>';
}
/**
* @return void
*/
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-grid">';
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>';
$yes = QueryMonitor::icon( 'yes-alt' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p data-qm-state-visibility="on">' . $yes . ' ' . esc_html__( 'Authentication cookie is set', 'query-monitor' ) . '</p>';
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Editor', 'query-monitor' ) . '</h3>';
if ( ! has_filter( 'qm/output/file_link_format' ) ) {
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',
'Nova' => 'nova',
'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>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p id="qm-editor-save-status">' . $yes . ' ' . esc_html__( 'Saved! Reload to apply changes.', 'query-monitor' ) . '</p>';
} else {
printf(
/* translators: %s: Name of WordPress filter */
esc_html__( 'The file link format for your editor is set by the %s filter.', 'query-monitor' ),
'<code>qm/output/file_link_format</code>'
);
echo '</p>';
}
echo '</section>';
echo '<section>';
echo '<h3>' . esc_html__( 'Appearance', 'query-monitor' ) . '</h3>';
echo '<p>' . esc_html__( 'Your browser color scheme is respected by default. You can override it here.', 'query-monitor' ) . '</p>';
echo '<ul>';
echo '<li><label><input type="radio" class="qm-theme-toggle qm-radio" name="qm-theme" value="auto" checked/>' . esc_html_x( 'Auto', 'colour scheme', 'query-monitor' ) . '</label></li>';
echo '<li><label><input type="radio" class="qm-theme-toggle qm-radio" name="qm-theme" value="light"/>' . esc_html_x( 'Light', 'colour scheme', 'query-monitor' ) . '</label></li>';
echo '<li><label><input type="radio" class="qm-theme-toggle qm-radio" name="qm-theme" value="dark"/>' . esc_html_x( 'Dark', 'colour scheme', 'query-monitor' ) . '</label></li>';
echo '</ul>';
echo '</section>';
echo '</div>';
echo '<div class="qm-boxed">';
$constants = array(
'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,
),
'QM_DB_SYMLINK' => array(
'label' => __( 'Allow the wp-content/db.php file symlink to be put into place during activation. Set to false to prevent the symlink creation.', 'query-monitor' ),
'default' => true,
),
);
/**
* Filters which PHP constants for configuring Query Monitor are displayed on its settings panel.
*
* @since 3.12.0
*
* @param array $constants The displayed settings constants.
* @phpstan-param array<string, array{
* label: string,
* default: mixed,
* }> $constants
*/
$constants = apply_filters( 'qm/constants', $constants );
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>';
echo esc_html( $constant['label'] );
$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( (string) $default_value ) . '</code>'
);
echo '</span>';
if ( defined( $name ) && ( constant( $name ) !== $constant['default'] ) ) {
$current_value = constant( $name );
if ( is_bool( $current_value ) ) {
$current_value = QM_Collector::format_bool_constant( $name );
}
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 $dispatcher The HTML dispatcher instance.
* @param array<string, QM_Output_Html> $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() {
var main = document.getElementById( 'query-monitor-main' );
var broken = document.getElementById( 'qm-broken' );
var menu_item = document.getElementById( 'wp-admin-bar-query-monitor' );
var admin_bar = document.getElementById( 'wpadminbar' );
if ( ( 'undefined' === typeof QM_i18n ) && ( ( 'undefined' === typeof jQuery ) || ! window.jQuery ) ) {
/* Fallback for worst case scenario */
if ( 'undefined' === typeof QM_i18n ) {
console.error( 'QM error from page: undefined QM_i18n' );
}
if ( main ) {
main.className += ' qm-broken';
}
if ( broken ) {
console.error( broken.textContent );
}
if ( 'undefined' === typeof jQuery ) {
console.error( 'QM error from page: undefined jQuery' );
} else if ( ! window.jQuery ) {
console.error( 'QM error from page: no jQuery' );
}
if ( menu_item && main ) {
menu_item.addEventListener( 'click', function() {
main.className += ' qm-show';
} );
}
} else if ( main && ! admin_bar ) {
main.className += ' qm-peek';
}
} );
<?php
echo '</script>' . "\n\n";
echo '<!-- End Query Monitor output -->' . "\n\n";
}
/**
* @param mixed $var
* @return int|Exception
*/
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 );
}
/**
* @return array<string, mixed>
*/
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;', 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;
}
/**
* @return bool
*/
public static function request_supported() {
// Don't dispatch if this is an async request:
if ( QM_Util::is_async() ) {
return false;
}
// Don't dispatch during a Customizer preview request:
if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
return false;
}
// Don't dispatch during an iframed request, eg the plugin info modal, an upgrader action, or the Customizer:
if ( defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST ) {
return false;
}
// Don't dispatch inside the Site Editor:
if ( isset( $_SERVER['SCRIPT_NAME'] ) && '/wp-admin/site-editor.php' === $_SERVER['SCRIPT_NAME'] ) {
return false;
}
// Don't dispatch on the interim login screen:
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['interim-login'] ) ) {
return false;
}
return true;
}
/**
* @return bool
*/
public function is_active() {
if ( ! self::user_can_view() ) {
return false;
}
if ( ! $this->did_footer ) {
return false;
}
if ( ! self::request_supported() ) {
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;
}
}
/** Back-compat filter. Please use `qm/dispatch/html` instead */
if ( ! apply_filters( 'qm/process', true, is_admin_bar_showing() ) ) {
return false;
}
if ( ! file_exists( $this->qm->plugin_path( 'assets/query-monitor.css' ) ) ) {
return false;
}
return true;
}
/**
* Cease without deactivating the dispatcher.
*
* @return void
*/
public function cease() {
$this->ceased = true;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
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,93 @@
<?php declare(strict_types = 1);
/**
* REST API request dispatcher.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
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 );
}
/**
* 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.
* @return WP_HTTP_Response Result to send to the client.
*/
public function filter_rest_post_dispatch( WP_HTTP_Response $result ) {
if ( ! $this->should_dispatch() ) {
return $result;
}
$this->before_output();
/** @var array<string, QM_Output_Headers> $outputters */
$outputters = $this->get_outputters( 'headers' );
foreach ( $outputters as $output ) {
$output->output();
}
$this->after_output();
return $result;
}
/**
* @return void
*/
protected function before_output() {
foreach ( (array) glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
include_once $file;
}
}
/**
* @return bool
*/
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;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
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,80 @@
<?php declare(strict_types = 1);
/**
* REST API enveloped request dispatcher.
*
* @package query-monitor
*/
class QM_Dispatcher_REST_Envelope extends QM_Dispatcher {
public $id = 'rest_envelope';
public function __construct( QM_Plugin $qm ) {
parent::__construct( $qm );
add_filter( 'rest_envelope_response', array( $this, 'filter_rest_envelope_response' ), 999, 2 );
}
/**
* Filters the enveloped form of a REST API response to add QM's data.
*
* @param array<string, mixed> $envelope Envelope data.
* @param WP_REST_Response $response Original response data.
* @return array<string, mixed> Envelope data.
*/
public function filter_rest_envelope_response( array $envelope, WP_REST_Response $response ) {
if ( ! $this->should_dispatch() ) {
return $envelope;
}
$data = array();
$this->before_output();
/** @var array<string, QM_Output_Raw> $outputters */
$outputters = $this->get_outputters( 'raw' );
foreach ( $outputters as $id => $output ) {
$data[ $id ] = $output->get_output();
}
$this->after_output();
$envelope['qm'] = $data;
return $envelope;
}
/**
* @return void
*/
protected function before_output() {
foreach ( (array) glob( $this->qm->plugin_path( 'output/raw/*.php' ) ) as $file ) {
include_once $file;
}
}
/**
* @return bool
*/
public function is_active() {
if ( ! self::user_can_view() ) {
return false;
}
return true;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
function register_qm_dispatcher_rest_envelope( array $dispatchers, QM_Plugin $qm ) {
$dispatchers['rest_envelope'] = new QM_Dispatcher_REST_Envelope( $qm );
return $dispatchers;
}
add_filter( 'qm/dispatchers', 'register_qm_dispatcher_rest_envelope', 10, 2 );

View File

@ -0,0 +1,101 @@
<?php declare(strict_types = 1);
/**
* HTTP redirect dispatcher.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
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.
* @return string
*/
public function filter_wp_redirect( $location, $status ) {
if ( ! $this->should_dispatch() ) {
return $location;
}
$this->before_output();
/** @var array<string, QM_Output_Headers> $outputters */
$outputters = $this->get_outputters( 'headers' );
foreach ( $outputters as $output ) {
$output->output();
}
$this->after_output();
return $location;
}
/**
* @return void
*/
protected function before_output() {
foreach ( (array) glob( $this->qm->plugin_path( 'output/headers/*.php' ) ) as $file ) {
require_once $file;
}
}
/**
* @return bool
*/
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;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
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,168 @@
<?php declare(strict_types = 1);
/**
* Dispatcher for output that gets added to `wp_die()` calls.
*
* @package query-monitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class QM_Dispatcher_WP_Die extends QM_Dispatcher {
/**
* @var string
*/
public $id = 'wp_die';
/**
* @var QM_Backtrace|null
*/
public $trace = null;
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 );
}
/**
* @param callable $handler
* @return callable
*/
public function filter_wp_die_handler( $handler ) {
$this->trace = new QM_Backtrace( array(
'ignore_hook' => array(
current_filter() => true,
),
) );
return $handler;
}
/**
* @return void
*/
public function dispatch() {
if ( ! $this->should_dispatch() ) {
return;
}
$switched_locale = self::switch_to_locale( get_user_locale() );
$stack = array();
$filtered_trace = $this->trace->get_filtered_trace();
$component = $this->trace->get_component();
foreach ( $filtered_trace as $i => $item ) {
$stack[] = QM_Output_Html::output_filename( $item['display'], $item['file'], $item['line'] );
}
?>
<style>
#query-monitor {
position: absolute;
margin: 4em 0 1em -2em;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgb( 0 0 0 / 4% );
background: #fff;
padding-top: 1em;
max-width: 700px;
z-index: -1;
}
#query-monitor h2 {
font-size: 12px;
font-weight: normal;
padding: 7px;
background: #f3f3f3;
margin: 0;
border-top: 1px solid #ddd;
}
#query-monitor ol,
#query-monitor p {
font-size: 12px;
padding: 0;
margin: 1em 2.5em;
}
#query-monitor ol {
padding: 0 0 1em 0;
}
#query-monitor li {
margin: 0 0 0.7em;
list-style: none;
}
#query-monitor .qm-info {
color: #666;
}
#query-monitor a.qm-edit-link svg {
display: none !important;
}
</style>
<?php
echo '<div id="query-monitor">';
echo '<p>';
if ( 'unknown' !== $component->type ) {
$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 ) {
self::restore_previous_locale();
}
}
/**
* @return bool
*/
public function is_active() {
if ( ! $this->trace ) {
return false;
}
if ( ! self::user_can_view() ) {
return false;
}
return true;
}
}
/**
* @param array<string, QM_Dispatcher> $dispatchers
* @param QM_Plugin $qm
* @return array<string, QM_Dispatcher>
*/
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,32 @@
<?php declare(strict_types = 1);
/**
* Abstract output class for HTTP headers.
*
* @package query-monitor
*/
abstract class QM_Output_Headers extends QM_Output {
/**
* @return void
*/
public function output() {
$id = $this->collector->id;
foreach ( $this->get_output() as $key => $value ) {
if ( ! is_scalar( $value ) ) {
$value = json_encode( $value );
}
# Remove illegal characters (Header may not contain NUL bytes)
if ( is_string( $value ) ) {
$value = str_replace( chr( 0 ), '', $value );
}
header( sprintf( 'X-QM-%s-%s: %s', $id, $key, $value ) );
}
}
}

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More