PluginProbe
Redirection / 5.3.7
Redirection v5.3.7
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.3.7, at models/log/log.php

512 lines 11.9 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 $where[] = $wpdb->prepare( 'agent LIKE %s', '%' . $wpdb->esc_like( trim( $filter['agent'] ) ) . '%' );
370 }
371
372 if ( isset( $filter['http'] ) ) {
373 $where[] = $wpdb->prepare( 'http_code = %d', $filter['http'] );
374 }
375
376 if ( isset( $filter['method'] ) && in_array( strtoupper( $filter['method'] ), static::$supported_methods, true ) ) {
377 $where[] = $wpdb->prepare( 'request_method = %s', strtoupper( $filter['method'] ) );
378 }
379
380 return $where;
381 }
382
383 /**
384 * Sanitize a new log entry
385 *
386 * @param string $domain Requested Domain.
387 * @param string $url Requested URL.
388 * @param string $ip Client IP. This is assumed to be a valid IP and won't be checked.
389 * @param array $details Extra log details.
390 * @return array
391 */
392 protected static function sanitize_create( $domain, $url, $ip, array $details = [] ) {
393 $insert = [
394 'url' => substr( sanitize_text_field( $url ), 0, self::MAX_URL_LENGTH ),
395 'domain' => substr( sanitize_text_field( $domain ), 0, self::MAX_DOMAIN_LENGTH ),
396 'ip' => substr( sanitize_text_field( $ip ), 0, self::MAX_IP_LENGTH ),
397 'created' => current_time( 'mysql' ),
398 ];
399
400 // Unfortunatley these names dont match up
401 $allowed = [
402 'agent' => 'agent',
403 'referrer' => 'referrer',
404 'request_method' => 'request_method',
405 'http_code' => 'http_code',
406 'request_data' => 'request_data',
407 ];
408
409 foreach ( $allowed as $name => $replace ) {
410 if ( ! empty( $details[ $name ] ) ) {
411 $insert[ $replace ] = $details[ $name ];
412 }
413 }
414
415 if ( isset( $insert['agent'] ) ) {
416 $insert['agent'] = substr( sanitize_text_field( $insert['agent'] ), 0, self::MAX_AGENT_LENGTH );
417 }
418
419 if ( isset( $insert['referrer'] ) ) {
420 $insert['referrer'] = substr( sanitize_text_field( $insert['referrer'] ), 0, self::MAX_REFERRER_LENGTH );
421 }
422
423 if ( isset( $insert['request_data'] ) ) {
424 $insert['request_data'] = wp_json_encode( $insert['request_data'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK );
425 }
426
427 if ( isset( $insert['http_code'] ) ) {
428 $insert['http_code'] = intval( $insert['http_code'], 10 );
429 }
430
431 if ( isset( $insert['request_method'] ) ) {
432 $insert['request_method'] = strtoupper( sanitize_text_field( $insert['request_method'] ) );
433
434 if ( ! in_array( $insert['request_method'], static::$supported_methods, true ) ) {
435 $insert['request_method'] = '';
436 }
437 }
438
439 return $insert;
440 }
441
442 /**
443 * Get the CSV filename for this log object
444 *
445 * @return string
446 */
447 public static function get_csv_filename() {
448 return '';
449 }
450
451 /**
452 * Get the CSV headers for this log object
453 *
454 * @return array
455 */
456 public static function get_csv_header() {
457 return [];
458 }
459
460 /**
461 * Get the CSV headers for this log object
462 *
463 * @param object $row Log row.
464 * @return array
465 */
466 public static function get_csv_row( $row ) {
467 return [];
468 }
469
470 /**
471 * Export the log entry to CSV
472 *
473 * @return void
474 */
475 public static function export_to_csv() {
476 $filename = static::get_csv_filename() . '-' . date_i18n( get_option( 'date_format' ) ) . '.csv';
477
478 header( 'Content-Type: text/csv' );
479 header( 'Cache-Control: no-cache, must-revalidate' );
480 header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
481 header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
482
483 // phpcs:ignore
484 $stdout = fopen( 'php://output', 'w' );
485 fputcsv( $stdout, static::get_csv_header() );
486
487 global $wpdb;
488
489 $table = static::get_table_name( $wpdb );
490 // phpcs:ignore
491 $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table" );
492 $exported = 0;
493
494 $limit = 100;
495
496 while ( $exported < $total_items ) {
497 // phpcs:ignore
498 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table LIMIT %d,%d", $exported, $limit ) );
499 $exported += count( $rows );
500
501 foreach ( $rows as $row ) {
502 $csv = static::get_csv_row( $row );
503 fputcsv( $stdout, $csv );
504 }
505
506 if ( count( $rows ) < $limit ) {
507 break;
508 }
509 }
510 }
511 }
512