PluginProbe
Redirection / 5.5.2
Redirection v5.5.2
5.10.0 5.9.0 5.8.1 5.8.0 3.7.2 3.7.3 4.0 4.0.1 4.1 4.1.1 4.2 4.2.1 4.2.2 4.2.3 4.3 4.3.1 4.3.2 4.3.3 4.4 4.4.1 4.4.2 4.5 4.5.1 4.6.2 4.7.1 All 130 releases
redirection / models / log / log.php

log.php in Redirection 5.5.2, at models/log/log.php

523 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once __DIR__ . '/log-404.php';
4 require_once __DIR__ . '/log-redirect.php';
5
6 /**
7 * Base log class
8 */
9 abstract class Red_Log {
10 const MAX_IP_LENGTH = 45;
11 const MAX_DOMAIN_LENGTH = 255;
12 const MAX_URL_LENGTH = 2000;
13 const MAX_AGENT_LENGTH = 255;
14 const MAX_REFERRER_LENGTH = 255;
15
16 /**
17 * Supported HTTP methods
18 *
19 * @var array
20 */
21 protected static $supported_methods = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH' ];
22
23 /**
24 * Log ID
25 *
26 * @var integer
27 */
28 protected $id = 0;
29
30 /**
31 * Created date time
32 *
33 * @var integer
34 */
35 protected $created = 0;
36
37 /**
38 * Requested URL
39 *
40 * @var string
41 */
42 protected $url = '';
43
44 /**
45 * Client user agent
46 *
47 * @var string
48 */
49 protected $agent = '';
50
51 /**
52 * Client referrer
53 *
54 * @var string
55 */
56 protected $referrer = '';
57
58 /**
59 * Client IP
60 *
61 * @var string
62 */
63 protected $ip = '';
64
65 /**
66 * Requested domain
67 *
68 * @var string
69 */
70 protected $domain = '';
71
72 /**
73 * Response HTTP code
74 *
75 * @var integer
76 */
77 protected $http_code = 0;
78
79 /**
80 * Request method
81 *
82 * @var string
83 */
84 protected $request_method = '';
85
86 /**
87 * Additional request data
88 *
89 * @var string
90 */
91 protected $request_data = '';
92
93 /**
94 * Constructor
95 *
96 * @param array $values Array of log values.
97 */
98 final public function __construct( array $values ) {
99 foreach ( $values as $key => $value ) {
100 $this->$key = $value;
101 }
102
103 if ( isset( $values['created'] ) ) {
104 $converted = mysql2date( 'U', $values['created'] );
105
106 if ( $converted ) {
107 $this->created = intval( $converted, 10 );
108 }
109 }
110 }
111
112 /**
113 * Get's the table name for this log object
114 *
115 * @param Object $wpdb WPDB object.
116 * @return string
117 */
118 protected static function get_table_name( $wpdb ) {
119 return '';
120 }
121
122 /**
123 * Get a log item by ID
124 *
125 * @param integer $id Log ID.
126 * @return Red_Log|false
127 */
128 public static function get_by_id( $id ) {
129 global $wpdb;
130
131 $table = static::get_table_name( $wpdb );
132
133 // Table name is internally generated.
134 // phpcs:ignore
135 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id=%d", $id ), ARRAY_A );
136 if ( $row ) {
137 return new static( $row );
138 }
139
140 return false;
141 }
142
143 /**
144 * Delete a log entry
145 *
146 * @param integer $id Log ID.
147 * @return integer|false
148 */
149 public static function delete( $id ) {
150 global $wpdb;
151
152 return $wpdb->delete( static::get_table_name( $wpdb ), [ 'id' => $id ] );
153 }
154
155 /**
156 * Delete all matching log entries
157 *
158 * @param array $params Array of filter parameters.
159 * @return integer|false
160 */
161 public static function delete_all( array $params = [] ) {
162 global $wpdb;
163
164 $query = self::get_query( $params );
165 $table = static::get_table_name( $wpdb );
166
167 $sql = "DELETE FROM {$table} {$query['where']}";
168
169 // phpcs:ignore
170 return $wpdb->query( $sql );
171 }
172
173 /**
174 * Convert a log entry to JSON
175 *
176 * @return array
177 */
178 public function to_json() {
179 return [
180 'id' => intval( $this->id, 10 ),
181 'created' => date_i18n( get_option( 'date_format' ), $this->created ),
182 'created_time' => gmdate( get_option( 'time_format' ), $this->created ),
183 'url' => $this->url,
184 'agent' => $this->agent,
185 'referrer' => $this->referrer,
186 'domain' => $this->domain,
187 'ip' => $this->ip,
188 'http_code' => intval( $this->http_code, 10 ),
189 'request_method' => $this->request_method,
190 'request_data' => $this->request_data ? json_decode( $this->request_data, true ) : '',
191 ];
192 }
193
194 /**
195 * Get filtered log entries
196 *
197 * @param array $params Filters.
198 * @return array{items: Array, total: integer}
199 */
200 public static function get_filtered( array $params ) {
201 global $wpdb;
202
203 $query = self::get_query( $params );
204 $table = static::get_table_name( $wpdb );
205
206 $sql = "SELECT * FROM {$table} {$query['where']}";
207
208 // Already escaped
209 // phpcs:ignore
210 $sql .= $wpdb->prepare( ' ORDER BY ' . $query['orderby'] . ' ' . $query['direction'] . ' LIMIT %d,%d', $query['offset'], $query['limit'] );
211
212 // Already escaped
213 // phpcs:ignore
214 $rows = $wpdb->get_results( $sql, ARRAY_A );
215
216 // Already escaped
217 // phpcs:ignore
218 $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table " . $query['where'] );
219 $items = array();
220
221 foreach ( $rows as $row ) {
222 $item = new static( $row );
223 $items[] = $item->to_json();
224 }
225
226 return [
227 'items' => $items,
228 'total' => intval( $total_items, 10 ),
229 ];
230 }
231
232 /**
233 * Get grouped log entries
234 *
235 * @param string $group Group type.
236 * @param array $params Filter params.
237 * @return array{items: mixed, total: integer}
238 */
239 public static function get_grouped( $group, array $params ) {
240 global $wpdb;
241
242 $table = static::get_table_name( $wpdb );
243 $query = self::get_query( $params );
244
245 if ( ! in_array( $group, [ 'ip', 'url', 'agent' ], true ) ) {
246 $group = 'url';
247 }
248
249 // Already escaped
250 // phpcs:ignore
251 $sql = $wpdb->prepare( "SELECT COUNT(*) as count,$group FROM {$table} {$query['where']} GROUP BY $group ORDER BY count {$query['direction']}, $group LIMIT %d,%d", $query['offset'], $query['limit'] );
252
253 // Already escaped
254 // phpcs:ignore
255 $rows = $wpdb->get_results( $sql );
256
257 // Already escaped
258 // phpcs:ignore
259 $total_items = $wpdb->get_var( "SELECT COUNT(DISTINCT $group) FROM {$table} {$query['where']}" );
260
261 foreach ( $rows as $row ) {
262 $row->count = intval( $row->count, 10 );
263
264 if ( isset( $row->url ) ) {
265 $row->id = $row->url;
266 } elseif ( isset( $row->ip ) ) {
267 $row->id = $row->ip;
268 } elseif ( isset( $row->agent ) ) {
269 $row->id = $row->agent;
270 }
271 }
272
273 return array(
274 'items' => $rows,
275 'total' => intval( $total_items, 10 ),
276 );
277 }
278
279 /**
280 * Convert a set of filters to a SQL query.
281 *
282 * @param array $params Filters.
283 * @return array{orderby: string, direction: string, limit: integer, offset: integer, where: string}
284 */
285 public static function get_query( array $params ) {
286 $query = [
287 'orderby' => 'id',
288 'direction' => 'DESC',
289 'limit' => RED_DEFAULT_PER_PAGE,
290 'offset' => 0,
291 'where' => '',
292 ];
293
294 if ( isset( $params['orderby'] ) && in_array( $params['orderby'], array( 'ip', 'url' ), true ) ) {
295 $query['orderby'] = $params['orderby'];
296 }
297
298 if ( isset( $params['direction'] ) && in_array( strtoupper( $params['direction'] ), array( 'ASC', 'DESC' ), true ) ) {
299 $query['direction'] = strtoupper( $params['direction'] );
300 }
301
302 if ( isset( $params['per_page'] ) ) {
303 $limit = intval( $params['per_page'], 10 );
304 if ( $limit >= 5 && $limit <= RED_MAX_PER_PAGE ) {
305 $query['limit'] = $limit;
306 }
307 }
308
309 if ( isset( $params['page'] ) ) {
310 $offset = intval( $params['page'], 10 );
311
312 if ( $offset >= 0 ) {
313 $query['offset'] = $offset * $query['limit'];
314 }
315 }
316
317 if ( isset( $params['filterBy'] ) && is_array( $params['filterBy'] ) ) {
318 $where = static::get_query_filter( $params['filterBy'] );
319
320 if ( count( $where ) > 0 ) {
321 $query['where'] = 'WHERE ' . implode( ' AND ', $where );
322 }
323 }
324
325 return $query;
326 }
327
328 /**
329 * Get query filters as a SQL `WHERE` statement. SQL will be sanitized
330 *
331 * @param array $filter Array of filter params.
332 * @return array
333 */
334 protected static function get_query_filter( array $filter ) {
335 global $wpdb;
336
337 $where = [];
338
339 if ( isset( $filter['ip'] ) ) {
340 // phpcs:ignore
341 $ip = @inet_pton( trim( $filter['ip'] ) );
342
343 if ( $ip !== false ) {
344 // Full IP match
345 // phpcs:ignore
346 $ip = @inet_ntop( $ip ); // Convert back to string
347 $where[] = $wpdb->prepare( 'ip = %s', $ip );
348 } else {
349 // Partial IP match
350 $where[] = $wpdb->prepare( 'ip LIKE %s', '%' . $wpdb->esc_like( trim( $filter['ip'] ) ) . '%' );
351 }
352 }
353
354 if ( isset( $filter['domain'] ) ) {
355 $where[] = $wpdb->prepare( 'domain LIKE %s', '%' . $wpdb->esc_like( trim( $filter['domain'] ) ) . '%' );
356 }
357
358 if ( isset( $filter['url-exact'] ) ) {
359 $where[] = $wpdb->prepare( 'url = %s', $filter['url-exact'] );
360 } elseif ( isset( $filter['url'] ) ) {
361 $where[] = $wpdb->prepare( 'url LIKE %s', '%' . $wpdb->esc_like( trim( $filter['url'] ) ) . '%' );
362 }
363
364 if ( isset( $filter['referrer'] ) ) {
365 $where[] = $wpdb->prepare( 'referrer LIKE %s', '%' . $wpdb->esc_like( trim( $filter['referrer'] ) ) . '%' );
366 }
367
368 if ( isset( $filter['agent'] ) ) {
369 $agent = trim( $filter['agent'] );
370
371 if ( empty( $agent ) ) {
372 $where[] = $wpdb->prepare( 'agent = %s', $agent );
373 } else {
374 $where[] = $wpdb->prepare( 'agent LIKE %s', '%' . $wpdb->esc_like( $agent ) . '%' );
375 }
376 }
377
378 if ( isset( $filter['http'] ) ) {
379 $where[] = $wpdb->prepare( 'http_code = %d', $filter['http'] );
380 }
381
382 if ( isset( $filter['method'] ) && in_array( strtoupper( $filter['method'] ), static::$supported_methods, true ) ) {
383 $where[] = $wpdb->prepare( 'request_method = %s', strtoupper( $filter['method'] ) );
384 }
385
386 return $where;
387 }
388
389 /**
390 * Sanitize a new log entry
391 *
392 * @param string $domain Requested Domain.
393 * @param string $url Requested URL.
394 * @param string $ip Client IP. This is assumed to be a valid IP and won't be checked.
395 * @param array $details Extra log details.
396 * @return array
397 */
398 protected static function sanitize_create( $domain, $url, $ip, array $details = [] ) {
399 $url = urldecode( $url );
400 $insert = [
401 'url' => substr( sanitize_text_field( $url ), 0, self::MAX_URL_LENGTH ),
402 'domain' => substr( sanitize_text_field( $domain ), 0, self::MAX_DOMAIN_LENGTH ),
403 'ip' => substr( sanitize_text_field( $ip ), 0, self::MAX_IP_LENGTH ),
404 'created' => current_time( 'mysql' ),
405 ];
406
407 // Unfortunatley these names dont match up
408 $allowed = [
409 'agent' => 'agent',
410 'referrer' => 'referrer',
411 'request_method' => 'request_method',
412 'http_code' => 'http_code',
413 'request_data' => 'request_data',
414 ];
415
416 foreach ( $allowed as $name => $replace ) {
417 if ( ! empty( $details[ $name ] ) ) {
418 $insert[ $replace ] = $details[ $name ];
419 }
420 }
421
422 if ( isset( $insert['agent'] ) ) {
423 $insert['agent'] = substr( sanitize_text_field( $insert['agent'] ), 0, self::MAX_AGENT_LENGTH );
424 }
425
426 if ( isset( $insert['referrer'] ) ) {
427 $insert['referrer'] = substr( sanitize_text_field( $insert['referrer'] ), 0, self::MAX_REFERRER_LENGTH );
428 }
429
430 if ( isset( $insert['request_data'] ) ) {
431 $insert['request_data'] = wp_json_encode( $insert['request_data'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK );
432 }
433
434 if ( isset( $insert['http_code'] ) ) {
435 $insert['http_code'] = intval( $insert['http_code'], 10 );
436 }
437
438 if ( isset( $insert['request_method'] ) ) {
439 $insert['request_method'] = strtoupper( sanitize_text_field( $insert['request_method'] ) );
440
441 if ( ! in_array( $insert['request_method'], static::$supported_methods, true ) ) {
442 $insert['request_method'] = '';
443 }
444 }
445
446 return $insert;
447 }
448
449 /**
450 * Get the CSV filename for this log object
451 *
452 * @return string
453 */
454 public static function get_csv_filename() {
455 return '';
456 }
457
458 /**
459 * Get the CSV headers for this log object
460 *
461 * @return array
462 */
463 public static function get_csv_header() {
464 return [];
465 }
466
467 /**
468 * Get the CSV headers for this log object
469 *
470 * @param object $row Log row.
471 * @return array
472 */
473 public static function get_csv_row( $row ) {
474 return [];
475 }
476
477 /**
478 * Export the log entry to CSV
479 *
480 * @return void
481 */
482 public static function export_to_csv() {
483 $filename = static::get_csv_filename() . '-' . date_i18n( get_option( 'date_format' ) ) . '.csv';
484
485 header( 'Content-Type: text/csv' );
486 header( 'Cache-Control: no-cache, must-revalidate' );
487 header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
488 header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
489
490 // phpcs:ignore
491 $stdout = fopen( 'php://output', 'w' );
492 if ( ! $stdout ) {
493 return;
494 }
495
496 fputcsv( $stdout, static::get_csv_header() );
497
498 global $wpdb;
499
500 $table = static::get_table_name( $wpdb );
501 // phpcs:ignore
502 $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table" );
503 $exported = 0;
504
505 $limit = 100;
506
507 while ( $exported < $total_items ) {
508 // phpcs:ignore
509 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table LIMIT %d,%d", $exported, $limit ) );
510 $exported += count( $rows );
511
512 foreach ( $rows as $row ) {
513 $csv = static::get_csv_row( $row );
514 fputcsv( $stdout, $csv );
515 }
516
517 if ( count( $rows ) < $limit ) {
518 break;
519 }
520 }
521 }
522 }
523