PluginProbe
Custom 404 Pro / trunk
Custom 404 Pro vtrunk
3.15.2 3.15.4 3.15.5 3.15.6 3.16.0 3.15.1 3.15.0 3.14.1 3.14.0 3.13.0 trunk 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1.5 1.1.6 1.2.0 1.3.10 1.3.12 1.3.5 All 100 releases
custom-404-pro / admin / class-logsclass.php

class-logsclass.php in Custom 404 Pro trunk, at admin/class-logsclass.php

341 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Logs list table class.
4 *
5 * @package Custom_404_Pro
6 */
7
8 if ( ! class_exists( 'WP_List_Table' ) ) {
9 include_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
10 }
11
12 /**
13 * Logs class.
14 */
15 class LogsClass extends WP_List_Table {
16
17 /**
18 * Number of log rows shown per page.
19 *
20 * @since 3.15.4
21 * @var int
22 */
23 const PER_PAGE = 50;
24
25 /**
26 * Maps an `orderby` query argument to a real database column.
27 *
28 * Both the current column keys and the short legacy keys ('i', 'p', 'r', 'u')
29 * are accepted so that bookmarked sort URLs from older versions keep working.
30 * Any value outside this map is discarded rather than interpolated into SQL.
31 *
32 * @since 3.15.4
33 * @var array<string, string>
34 */
35 const SORTABLE_COLUMN_MAP = array(
36 'created' => 'created',
37 'ip' => 'ip',
38 'path' => 'path',
39 'referer' => 'referer',
40 'user_agent' => 'user_agent',
41 'i' => 'ip',
42 'p' => 'path',
43 'r' => 'referer',
44 'u' => 'user_agent',
45 );
46
47 /**
48 * Constructor.
49 */
50 public function __construct() {
51 parent::__construct(
52 array(
53 'singular' => 'log',
54 'plural' => 'logs',
55 'ajax' => false,
56 )
57 );
58 }
59
60 /**
61 * Add stuff on top or bottom of the Logs Table.
62 *
63 * @param string $which top/bottom.
64 * @return void
65 */
66 public function extra_tablenav( $which ) {
67 // No additional navigation content needed.
68 }
69
70 /**
71 * Prepares the list of items for displaying.
72 *
73 * The query is assembled in SQL order — WHERE, then ORDER BY, then LIMIT —
74 * and pagination is applied by the database. Only the rows for the current
75 * page are ever read into PHP, so the screen stays usable on sites with very
76 * large log tables.
77 *
78 * @since 3.15.4 Pagination moved into SQL; clause order corrected.
79 */
80 public function prepare_items() {
81 global $wpdb;
82
83 $columns = $this->get_columns();
84 $hidden = array();
85 $sortable = $this->get_sortable_columns();
86 $this->_column_headers = array( $columns, $hidden, $sortable );
87
88 $helpers = Helpers::singleton();
89 $table = $wpdb->prefix . $helpers->table_logs;
90
91 // WHERE must be built first: appending ORDER BY before it is invalid SQL.
92 $where = '';
93 if ( array_key_exists( 's', $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
94 $search = sanitize_text_field( wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
95 if ( '' !== $search ) {
96 $where = $this->build_search_clause( $search );
97 }
98 }
99
100 // Count every matching row so pagination reports the real total.
101 $total_items = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $table . $where ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
102
103 $sql = 'SELECT * FROM ' . $table . $where;
104
105 $order_by = '';
106 $order = '';
107 if ( array_key_exists( 'orderby', $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
108 $order_by = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
109 $order = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
110 }
111 $sql .= $this->build_order_clause( $order_by, $order );
112
113 $per_page = self::PER_PAGE;
114 $current_page = max( 1, $this->get_pagenum() );
115
116 $this->set_pagination_args(
117 array(
118 'total_items' => $total_items,
119 'per_page' => $per_page,
120 )
121 );
122
123 $sql .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $per_page, ( $current_page - 1 ) * $per_page );
124
125 $sql_data = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
126
127 $data = array();
128 foreach ( (array) $sql_data as $row ) {
129 $data[] = array(
130 'id' => (int) $row->id,
131 'ip' => sanitize_text_field( $row->ip ),
132 'path' => sanitize_text_field( $row->path ),
133 'referer' => sanitize_text_field( $row->referer ),
134 'user_agent' => sanitize_text_field( $row->user_agent ),
135 'created' => sanitize_text_field( $row->created ),
136 );
137 }
138
139 $this->items = $data;
140 }
141
142 /**
143 * Appends an ORDER BY clause for a recognised sortable column.
144 *
145 * The column name is resolved through SORTABLE_COLUMN_MAP and the direction
146 * is narrowed to the literal 'ASC' or 'DESC', so neither value can carry
147 * user input into the query. An unrecognised column is ignored entirely.
148 *
149 * @since 3.15.4 Column names are whitelisted; unknown values no longer emit a bare direction.
150 * @param string $order_by Requested orderby query argument.
151 * @param string $order Requested sort direction.
152 * @param string $sql SQL query string built so far.
153 * @return string Modified SQL query string.
154 */
155 public function manage_sorting( $order_by, $order, $sql ) {
156 if ( ! isset( self::SORTABLE_COLUMN_MAP[ $order_by ] ) ) {
157 return $sql;
158 }
159
160 return $sql . $this->build_order_clause( $order_by, $order );
161 }
162
163 /**
164 * Builds the ORDER BY clause for the row query.
165 *
166 * Always returns a clause, and always ends on `id`.
167 *
168 * Two reasons the sort must be total rather than just correct. LIMIT/OFFSET
169 * has no defined row order in SQL unless the ORDER BY distinguishes every
170 * row, so a sort that ties leaves the database free to return a row on two
171 * different pages and omit another entirely. `created` has one-second
172 * resolution and a bot crawl writes dozens of rows inside the same second,
173 * so ties here are routine rather than exotic. `id` is the primary key and
174 * breaks every tie.
175 *
176 * With no sort requested the rows come back in `id` order, which is the
177 * order the screen showed before pagination moved into SQL.
178 *
179 * @since 3.15.4
180 * @param string $order_by Requested orderby query argument.
181 * @param string $order Requested sort direction.
182 * @return string ORDER BY clause, including the leading space.
183 */
184 private function build_order_clause( $order_by, $order ) {
185 $direction = ( 'DESC' === strtoupper( (string) $order ) ) ? 'DESC' : 'ASC';
186
187 if ( ! isset( self::SORTABLE_COLUMN_MAP[ $order_by ] ) ) {
188 return ' ORDER BY id ASC';
189 }
190
191 return ' ORDER BY ' . self::SORTABLE_COLUMN_MAP[ $order_by ] . ' ' . $direction . ', id ' . $direction;
192 }
193
194 /**
195 * Builds the prepared WHERE clause for a search term.
196 *
197 * @since 3.15.4
198 * @param string $search Search string.
199 * @return string Prepared WHERE clause, including the leading space.
200 */
201 private function build_search_clause( $search ) {
202 global $wpdb;
203 $like = '%' . $wpdb->esc_like( $search ) . '%';
204 return $wpdb->prepare(
205 ' WHERE (ip LIKE %s OR path LIKE %s OR referer LIKE %s OR user_agent LIKE %s OR created LIKE %s)',
206 $like,
207 $like,
208 $like,
209 $like,
210 $like
211 );
212 }
213
214 /**
215 * Handles search filtering of the logs table.
216 *
217 * @param string $search Search string.
218 * @param string $sql SQL query string.
219 * @return string Modified SQL query string.
220 */
221 public function manage_search( $search, $sql ) {
222 return $sql . $this->build_search_clause( $search );
223 }
224
225 /**
226 * Returns the columns for the logs table.
227 *
228 * @return array Column definitions.
229 */
230 public function get_columns() {
231 $columns = array(
232 'cb' => "<input type='checkbox' />",
233 'ip' => esc_html__( 'IP', 'custom-404-pro' ),
234 'path' => esc_html__( 'Path', 'custom-404-pro' ),
235 'referer' => esc_html__( 'Referer', 'custom-404-pro' ),
236 'user_agent' => esc_html__( 'User Agent', 'custom-404-pro' ),
237 'created' => esc_html__( 'Created', 'custom-404-pro' ),
238 );
239 return $columns;
240 }
241
242 /**
243 * Renders the default column value.
244 *
245 * @param array $item Row data.
246 * @param string $column_name Column name.
247 * @return mixed Column value.
248 */
249 public function column_default( $item, $column_name ) {
250 switch ( $column_name ) {
251 case 'cb':
252 return (int) $item['id'];
253 case 'ip':
254 case 'path':
255 case 'referer':
256 case 'user_agent':
257 case 'created':
258 // WP_List_Table echoes column values verbatim, so escape here.
259 return esc_html( $item[ $column_name ] );
260 }
261 }
262
263 /**
264 * Returns the sortable columns.
265 *
266 * @return array Sortable column definitions.
267 */
268 public function get_sortable_columns() {
269 $sortable_columns = array(
270 'ip' => 'ip',
271 'path' => 'path',
272 'referer' => 'referer',
273 'user_agent' => 'user_agent',
274 'created' => array( 'created', true ),
275 );
276 return $sortable_columns;
277 }
278
279 /**
280 * Renders the IP column with row actions.
281 *
282 * @param array $item Row data.
283 * @return string Column HTML.
284 */
285 public function column_ip( $item ) {
286 $page_slug = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
287 $delete_link = wp_nonce_url(
288 add_query_arg(
289 array(
290 'page' => $page_slug,
291 'action' => 'c4p-logs--delete',
292 'path' => (int) $item['id'],
293 ),
294 admin_url( 'admin.php' )
295 ),
296 'c4p-logs--delete'
297 );
298 $actions = array(
299 'c4p-logs--delete' => sprintf(
300 '<a href="%1$s">%2$s</a>',
301 esc_url( $delete_link ),
302 esc_html__( 'Delete', 'custom-404-pro' )
303 ),
304 );
305 return sprintf(
306 '%1$s %2$s',
307 esc_html( $item['ip'] ),
308 $this->row_actions( $actions )
309 );
310 }
311
312 /**
313 * Renders the checkbox column.
314 *
315 * @param array $item Row data.
316 * @return string Column HTML.
317 */
318 public function column_cb( $item ) {
319 return sprintf(
320 '<input type="checkbox" name="path[]" value="%1$d" aria-label="%2$s" />',
321 (int) $item['id'],
322 /* translators: %s: the 404 path recorded in this log row */
323 esc_attr( sprintf( __( 'Select log entry for %s', 'custom-404-pro' ), $item['path'] ) )
324 );
325 }
326
327 /**
328 * Returns the bulk actions for the logs table.
329 *
330 * @return array Bulk action definitions.
331 */
332 public function get_bulk_actions() {
333 $actions = array(
334 'c4p-logs--delete' => esc_html__( 'Delete', 'custom-404-pro' ),
335 'c4p-logs--delete-all' => esc_html__( 'Delete All', 'custom-404-pro' ),
336 'c4p-logs--export-csv' => esc_html__( 'Export All (.csv)', 'custom-404-pro' ),
337 );
338 return $actions;
339 }
340 }
341