├── .gitignore
├── assets
├── css
│ └── admin.css
└── js
│ └── admin.js
├── includes
├── class-query.php
└── class-upgrade.php
├── log-http-requests.php
├── readme.txt
└── templates
└── page-settings.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .svn/
2 |
3 |
--------------------------------------------------------------------------------
/assets/css/admin.css:
--------------------------------------------------------------------------------
1 | /* table */
2 |
3 | .widefat td {
4 | font-size: 12px;
5 | }
6 |
7 | .lhr-listing tbody tr:nth-child(even) td {
8 | background-color: #f8f8f8;
9 | }
10 |
11 | .widefat .field-status-code,
12 | .widefat .field-runtime,
13 | .widefat .field-date {
14 | width: 100px;
15 | }
16 |
17 | .widefat .field-args,
18 | .widefat .field-response {
19 | display: none;
20 | }
21 |
22 | .widefat .field-url {
23 | word-break: break-word;
24 | }
25 |
26 | .widefat .field-runtime.warn {
27 | background-color: rgba(255, 235, 59, 0.2);
28 | }
29 |
30 | .widefat .field-runtime.error {
31 | background-color: rgba(244, 67, 54, 0.2);
32 | }
33 |
34 | .http-request-args,
35 | .http-response {
36 | max-height: 300px;
37 | font-family: monospace;
38 | white-space: pre;
39 | overflow: auto;
40 | }
41 |
42 | /* pager */
43 |
44 | .lhr-pager {
45 | padding: 10px 0;
46 | text-align: right;
47 | }
48 |
49 | .lhr-page {
50 | display: inline-block;
51 | padding: 0px 4px;
52 | margin-right: 6px;
53 | cursor: pointer;
54 | }
55 |
56 | .lhr-page.active {
57 | font-weight: bold;
58 | cursor: default;
59 | }
60 |
61 | /* grid */
62 |
63 | .wrapper {
64 | display: grid;
65 | grid-template-columns: 50% 50%;
66 | grid-gap: 10px;
67 | }
68 |
69 | /* modal */
70 |
71 | .media-modal,
72 | .media-modal-backdrop {
73 | display: none;
74 | }
75 |
76 | .media-modal.open,
77 | .media-modal-backdrop.open {
78 | display: block;
79 | }
80 |
81 | .media-frame-title,
82 | .media-frame-content {
83 | left: 0;
84 | }
85 |
86 | .media-frame-router {
87 | left: 10px;
88 | }
89 |
90 | .media-frame-content {
91 | top: 48px;
92 | bottom: 0;
93 | overflow: auto;
94 | }
95 |
96 | .button-link.media-modal-close {
97 | cursor: pointer;
98 | text-decoration: none;
99 | }
100 |
101 | .button-link.media-modal-close.prev {
102 | margin-right: 60px;
103 | }
104 |
105 | .button-link.media-modal-close.next {
106 | margin-right: 30px;
107 | }
108 |
109 | .media-modal-close.prev .media-modal-icon::before {
110 | content: "\f342";
111 | }
112 |
113 | .media-modal-close.next .media-modal-icon::before {
114 | content: "\f346";
115 | }
116 |
117 | .modal-content-wrap {
118 | padding: 16px;
119 | }
--------------------------------------------------------------------------------
/assets/js/admin.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | $(function() {
3 |
4 | // Refresh
5 | LHR.refresh = function() {
6 | $('.lhr-refresh').text('Refreshing...').attr('disabled', 'disabled');
7 |
8 | $.post(ajaxurl, {
9 | 'action': 'lhr_query',
10 | '_wpnonce': LHR.nonce,
11 | 'data': LHR.query_args
12 | }, function(data) {
13 | LHR.response = data;
14 | LHR.query_args.page = 1;
15 |
16 | var html = '';
17 | $.each(data.rows, function(idx, row) {
18 | var runtime = parseFloat(row.runtime);
19 | var css_class = (runtime > 1) ? ' warn' : '';
20 | css_class = (runtime > 2) ? ' error' : css_class;
21 | html += `
22 |
23 |
24 |
25 | |
26 | ` + row.status_code + ` |
27 | ` + row.runtime + ` |
28 | ` + row.date_added + ` |
29 |
30 | `;
31 | });
32 | $('.lhr-listing tbody').html(html);
33 | $('.lhr-pager').html(data.pager);
34 | $('.lhr-refresh').text('Refresh').removeAttr('disabled');
35 | }, 'json');
36 | }
37 |
38 | // Clear
39 | LHR.clear = function() {
40 | $('.lhr-clear').text('Clearing...').attr('disabled', 'disabled');
41 |
42 | $.post(ajaxurl, {
43 | 'action': 'lhr_clear',
44 | '_wpnonce': LHR.nonce
45 | }, function(data) {
46 | $('.lhr-listing tbody').html('');
47 | $('.lhr-clear').text('Clear log').removeAttr('disabled');
48 | }, 'json');
49 | }
50 |
51 | LHR.show_details = function(action) {
52 | var id = LHR.active_id;
53 |
54 | if ('next' == action && id < LHR.response.rows.length - 1) {
55 | id = id + 1;
56 | }
57 | else if ('prev' == action && id > 0) {
58 | id = id - 1;
59 | }
60 |
61 | LHR.active_id = id;
62 |
63 | var data = LHR.response.rows[id];
64 | $('.http-url').text(data.url);
65 | $('.http-request-id').text(id);
66 | $('.http-request-args').text(JSON.stringify(JSON.parse(data.request_args), null, 2));
67 | $('.http-response').text(JSON.stringify(JSON.parse(data.response), null, 2));
68 | $('.media-modal').addClass('open');
69 | $('.media-modal-backdrop').addClass('open');
70 | }
71 |
72 | // Page change
73 | $(document).on('click', '.lhr-page:not(.active)', function() {
74 | LHR.query_args.page = parseInt($(this).attr('data-page'));
75 | LHR.refresh();
76 | });
77 |
78 | // Open detail modal
79 | $(document).on('click', '.field-url a', function() {
80 | LHR.active_id = parseInt($(this).attr('data-id'));
81 | LHR.show_details('curr');
82 | });
83 |
84 | // Close modal window
85 | $(document).on('click', '.media-modal-close', function() {
86 | var $this = $(this);
87 |
88 | if ($this.hasClass('prev') || $this.hasClass('next')) {
89 | var action = $this.hasClass('prev') ? 'prev' : 'next';
90 | LHR.show_details(action);
91 | return;
92 | }
93 |
94 | $('.media-modal').removeClass('open');
95 | $('.media-modal-backdrop').removeClass('open');
96 | $(document).off('keydown.lhr-modal-close');
97 | });
98 |
99 | $(document).keydown(function(e) {
100 |
101 | if (! $('.media-modal').hasClass('open')) {
102 | return;
103 | }
104 |
105 | if (-1 < $.inArray(e.keyCode, [27, 38, 40])) {
106 | e.preventDefault();
107 |
108 | if (27 == e.keyCode) { // esc
109 | $('.media-modal-close').click();
110 | }
111 | else if (38 == e.keyCode) { // up
112 | $('.media-modal-close.prev').click();
113 | }
114 | else if (40 == e.keyCode) { // down
115 | $('.media-modal-close.next').click();
116 | }
117 | }
118 | });
119 |
120 | // Ajax
121 | LHR.refresh();
122 | });
123 | })(jQuery);
124 |
--------------------------------------------------------------------------------
/includes/class-query.php:
--------------------------------------------------------------------------------
1 | wpdb = $GLOBALS['wpdb'];
12 | }
13 |
14 |
15 | function get_results( $args ) {
16 | $defaults = [
17 | 'page' => 1,
18 | 'per_page' => 50,
19 | 'orderby' => 'date_added',
20 | 'order' => 'DESC',
21 | 'search' => '',
22 | ];
23 |
24 | $args = array_merge( $defaults, $args );
25 |
26 | $output = [];
27 | $orderby = in_array( $args['orderby'], [ 'url', 'runtime', 'date_added' ] ) ? $args['orderby'] : 'date_added';
28 | $order = in_array( $args['order'], [ 'ASC', 'DESC' ] ) ? $args['order'] : 'DESC';
29 | $page = (int) $args['page'];
30 | $per_page = (int) $args['per_page'];
31 | $limit = ( ( $page - 1 ) * $per_page ) . ',' . $per_page;
32 |
33 | $this->sql = "
34 | SELECT
35 | SQL_CALC_FOUND_ROWS
36 | id, url, request_args, response, runtime, date_added
37 | FROM {$this->wpdb->prefix}lhr_log
38 | ORDER BY $orderby $order, id DESC
39 | LIMIT $limit
40 | ";
41 | $results = $this->wpdb->get_results( $this->sql, ARRAY_A );
42 |
43 | $total_rows = (int) $this->wpdb->get_var( "SELECT FOUND_ROWS()" );
44 | $total_pages = ceil( $total_rows / $per_page );
45 |
46 | $this->pager_args = [
47 | 'page' => $page,
48 | 'per_page' => $per_page,
49 | 'total_rows' => $total_rows,
50 | 'total_pages' => $total_pages,
51 | ];
52 |
53 | foreach ( $results as $row ) {
54 | $row['status_code'] = '-';
55 | $response = json_decode( $row['response'], true );
56 | if ( ! empty( $response['response']['code'] ) ) {
57 | $row['status_code'] = (int) $response['response']['code'];
58 | }
59 | $row['runtime'] = round( $row['runtime'], 4 );
60 | $row['date_raw'] = $row['date_added'];
61 | $row['date_added'] = LHR()->time_since( $row['date_added'] );
62 | $row['url'] = esc_url( $row['url'] );
63 | $output[] = $row;
64 | }
65 |
66 | return $output;
67 | }
68 |
69 |
70 | function truncate_table() {
71 | $this->wpdb->query( "TRUNCATE TABLE {$this->wpdb->prefix}lhr_log" );
72 | }
73 |
74 |
75 | function paginate() {
76 | $params = $this->pager_args;
77 |
78 | $output = '';
79 | $page = (int) $params['page'];
80 | $per_page = (int) $params['per_page'];
81 | $total_rows = (int) $params['total_rows'];
82 | $total_pages = (int) $params['total_pages'];
83 |
84 | // Only show pagination when > 1 page
85 | if ( 1 < $total_pages ) {
86 |
87 | if ( 3 < $page ) {
88 | $output .= '<<';
89 | }
90 | if ( 1 < ( $page - 10 ) ) {
91 | $output .= '' . ($page - 10) . '';
92 | }
93 | for ( $i = 2; $i > 0; $i-- ) {
94 | if ( 0 < ( $page - $i ) ) {
95 | $output .= '' . ($page - $i) . '';
96 | }
97 | }
98 |
99 | // Current page
100 | $output .= '' . $page . '';
101 |
102 | for ( $i = 1; $i <= 2; $i++ ) {
103 | if ( $total_pages >= ( $page + $i ) ) {
104 | $output .= '' . ($page + $i) . '';
105 | }
106 | }
107 | if ( $total_pages > ( $page + 10 ) ) {
108 | $output .= '' . ($page + 10) . '';
109 | }
110 | if ( $total_pages > ( $page + 2 ) ) {
111 | $output .= '>>';
112 | }
113 | }
114 |
115 | return $output;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/includes/class-upgrade.php:
--------------------------------------------------------------------------------
1 | version = LHR_VERSION;
10 | $this->last_version = get_option( 'lhr_version' );
11 |
12 | if ( version_compare( $this->last_version, $this->version, '<' ) ) {
13 | if ( version_compare( $this->last_version, '0.1.0', '<' ) ) {
14 | require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
15 | $this->clean_install();
16 | }
17 | else {
18 | $this->run_upgrade();
19 | }
20 |
21 | update_option( 'lhr_version', $this->version );
22 | }
23 | }
24 |
25 |
26 | private function clean_install() {
27 | global $wpdb;
28 |
29 | $sql = "
30 | CREATE TABLE IF NOT EXISTS {$wpdb->prefix}lhr_log (
31 | id BIGINT unsigned not null auto_increment,
32 | url TEXT,
33 | request_args MEDIUMTEXT,
34 | response MEDIUMTEXT,
35 | runtime VARCHAR(64),
36 | date_added DATETIME,
37 | PRIMARY KEY (id)
38 | ) DEFAULT CHARSET=utf8mb4";
39 | $wpdb->query( $sql );
40 | }
41 |
42 |
43 | private function run_upgrade() {
44 | global $wpdb;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/log-http-requests.php:
--------------------------------------------------------------------------------
1 | .
23 | */
24 |
25 | defined( 'ABSPATH' ) or exit;
26 |
27 | class Log_HTTP_Requests
28 | {
29 | public $query;
30 | public $start_time;
31 | public static $instance;
32 |
33 |
34 | function __construct() {
35 |
36 | // setup variables
37 | define( 'LHR_VERSION', '1.4.1' );
38 | define( 'LHR_DIR', dirname( __FILE__ ) );
39 | define( 'LHR_URL', plugins_url( '', __FILE__ ) );
40 | define( 'LHR_BASENAME', plugin_basename( __FILE__ ) );
41 |
42 | add_action( 'init', [ $this, 'init' ] );
43 | add_action( 'admin_menu', [ $this, 'admin_menu' ] );
44 | add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
45 | add_filter( 'http_request_args', [ $this, 'start_timer' ] );
46 | add_action( 'http_api_debug', [ $this, 'capture_request' ], 10, 5 );
47 | add_action( 'lhr_cleanup_cron', [ $this, 'cleanup' ] );
48 | add_action( 'wp_ajax_lhr_query', [ $this, 'lhr_query' ] );
49 | add_action( 'wp_ajax_lhr_clear', [ $this, 'lhr_clear' ] );
50 | }
51 |
52 |
53 | public static function instance() {
54 | if ( ! isset( self::$instance ) ) {
55 | self::$instance = new self;
56 | }
57 | return self::$instance;
58 | }
59 |
60 |
61 | function init() {
62 | include( LHR_DIR . '/includes/class-upgrade.php' );
63 | include( LHR_DIR . '/includes/class-query.php' );
64 |
65 | new LHR_Upgrade();
66 | $this->query = new LHR_Query();
67 |
68 | if ( ! wp_next_scheduled( 'lhr_cleanup_cron' ) ) {
69 | wp_schedule_single_event( time() + 86400, 'lhr_cleanup_cron' );
70 | }
71 | }
72 |
73 |
74 | function cleanup() {
75 | global $wpdb;
76 |
77 | $now = current_time( 'timestamp' );
78 | $expires = apply_filters( 'lhr_expiration_days', 1 );
79 | $expires = date( 'Y-m-d H:i:s', strtotime( '-' . $expires . ' days', $now ) );
80 | $wpdb->query( "DELETE FROM {$wpdb->prefix}lhr_log WHERE date_added < '$expires'" );
81 | }
82 |
83 |
84 | function admin_menu() {
85 | add_management_page( 'Log HTTP Requests', 'Log HTTP Requests', 'manage_options', 'log-http-requests', [ $this, 'settings_page' ] );
86 | }
87 |
88 |
89 | function settings_page() {
90 | include( LHR_DIR . '/templates/page-settings.php' );
91 | }
92 |
93 |
94 | function admin_scripts( $hook ) {
95 | if ( 'tools_page_log-http-requests' == $hook ) {
96 | wp_enqueue_script( 'lhr', LHR_URL . '/assets/js/admin.js', [ 'jquery' ] );
97 | wp_enqueue_style( 'lhr', LHR_URL . '/assets/css/admin.css' );
98 | wp_enqueue_style( 'media-views' );
99 | }
100 | }
101 |
102 |
103 | function validate() {
104 | if ( ! current_user_can( 'manage_options' ) ) {
105 | wp_die();
106 | }
107 |
108 | check_ajax_referer( 'lhr_nonce' );
109 | }
110 |
111 |
112 | function lhr_query() {
113 | $this->validate();
114 |
115 | $output = [
116 | 'rows' => LHR()->query->get_results( $_POST['data'] ),
117 | 'pager' => LHR()->query->paginate()
118 | ];
119 |
120 | wp_send_json( $output );
121 | }
122 |
123 |
124 | function lhr_clear() {
125 | $this->validate();
126 |
127 | LHR()->query->truncate_table();
128 | }
129 |
130 |
131 | function start_timer( $args ) {
132 | $this->start_time = microtime( true );
133 | return $args;
134 | }
135 |
136 |
137 | function capture_request( $response, $context, $transport, $args, $url ) {
138 | global $wpdb;
139 |
140 | if ( false !== strpos( $url, 'doing_wp_cron' ) ) {
141 | return;
142 | }
143 |
144 | // False to ignore current row
145 | $log_data = apply_filters( 'lhr_log_data', [
146 | 'url' => $url,
147 | 'request_args' => json_encode( $args ),
148 | 'response' => json_encode( $response ),
149 | 'runtime' => ( microtime( true ) - $this->start_time ),
150 | 'date_added' => current_time( 'mysql' )
151 | ]);
152 |
153 | if ( false !== $log_data ) {
154 | $wpdb->insert( $wpdb->prefix . 'lhr_log', $log_data );
155 | }
156 | }
157 |
158 |
159 | function time_since( $time ) {
160 | $time = current_time( 'timestamp' ) - strtotime( $time );
161 | $time = ( $time < 1 ) ? 1 : $time;
162 | $tokens = array (
163 | 31536000 => 'year',
164 | 2592000 => 'month',
165 | 604800 => 'week',
166 | 86400 => 'day',
167 | 3600 => 'hour',
168 | 60 => 'minute',
169 | 1 => 'second'
170 | );
171 |
172 | foreach ( $tokens as $unit => $text ) {
173 | if ( $time < $unit ) continue;
174 | $numberOfUnits = floor( $time / $unit );
175 | return $numberOfUnits . ' ' . $text . ( ( $numberOfUnits > 1 ) ? 's' : '' );
176 | }
177 | }
178 | }
179 |
180 |
181 | function LHR() {
182 | return Log_HTTP_Requests::instance();
183 | }
184 |
185 |
186 | LHR();
187 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === Log HTTP Requests ===
2 | Contributors: mgibbs189
3 | Tags: log, wp_http, requests, update checks, api
4 | Requires at least: 5.0
5 | Tested up to: 6.2.2
6 | Stable tag: trunk
7 | License: GPLv2
8 |
9 | Log and view all WP HTTP requests
10 |
11 | == Description ==
12 |
13 | = Log and view all WP HTTP requests =
14 |
15 | How long do [core / plugin / theme] update checks take to run? What data about my site is being sent out? What about all those ajax requests? The answers to these questions are just a few clicks away.
16 |
17 | This plugin logs all WP_HTTP requests and displays them in a table listing for easy viewing. It also stores the runtime of each HTTP request.
18 |
19 | = Available Hooks =
20 | Customize the length (in days) before older log items are removed:
21 |
22 |
23 | add_filter( 'lhr_expiration_days', function( $days ) {
24 | return 7; // default = 1
25 | });
26 |
27 |
28 | Don't log items from a specific hostname:
29 |
30 |
31 | add_filter( 'lhr_log_data', function( $data ) {
32 | if ( false !== strpos( $data['url'], 'wordpress.org' ) ) {
33 | return false;
34 | }
35 | return $data;
36 | });
37 |
38 |
39 | In the above example, the `$data` array keys correspond to columns within the `lhr_log` database table.
40 |
41 | = Important Links =
42 | * [Github →](https://github.com/FacetWP/log-http-requests)
43 |
44 | == Installation ==
45 |
46 | 1. Download and activate the plugin.
47 | 2. Browse to `Tools > Log HTTP Requests` to view log entries.
48 |
49 | == Changelog ==
50 |
51 | = 1.4.1
52 | * Fixed PHP8 deprecation notices
53 |
54 | = 1.4 =
55 | * Added extra ajax role validation (props pluginvulnerabilities.com)
56 |
57 | = 1.3.2 =
58 | * Escaped URL field to prevent possible XSS (props Bishop Fox)
59 |
60 | = 1.3.1 =
61 | * Ensured compatibility with WP 5.8
62 |
63 | = 1.3 =
64 | * Minor PHP cleanup
65 | * Ensured compatibility with WP 5.7
66 |
67 | = 1.2 =
68 | * Moved "Log HTTP Requests" to the `Tools` menu (props @aaemnnosttv)
69 | * Added "Status" column to show HTTP response code (props @danielbachhuber)
70 | * Added prev/next browsing to the detail modal (props @marcissimus)
71 | * Added keyboard support (up, down, esc) to the detail modal (props @marcissimus)
72 | * Added raw timestamp to "Date Added" column on hover
73 | * Added hook docs to the readme
74 |
75 | = 1.1 =
76 | * Added `lhr_log_data` hook to customize logged data (return FALSE to skip logging)
77 | * Added `lhr_expiration_days` hook
78 |
79 | = 1.0.4 =
80 | * Minor styling tweak
81 |
82 | = 1.0.3 =
83 | * Better visibility for long URLs
84 |
85 | = 1.0.2 =
86 | * Minor design tweaks
87 | * Replaced `json_encode` with `wp_send_json`
88 |
89 | = 1.0.1 =
90 | * Tested compatibility against WP 4.9.4
91 |
92 | = 1.0.0 =
93 | * Initial release
94 |
--------------------------------------------------------------------------------
/templates/page-settings.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
Log HTTP Requests
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | URL |
23 | Status |
24 | Runtime |
25 | Date Added |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
66 |
67 |
68 |
--------------------------------------------------------------------------------