├── .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 |
` + row.url + `
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 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
URLStatusRuntimeDate Added
30 |
31 |
32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 |

43 |
44 |
45 | 62 |
63 |
64 |
65 |
66 | 67 |
68 | --------------------------------------------------------------------------------