· 5 years ago · Jun 05, 2020, 04:22 PM
1<?php
2
3/**
4 * Handles log entries by writing to database.
5 *
6 * @class WPF_Log_Handler
7 */
8
9class WPF_Log_Handler {
10
11 /**
12 * Log Levels
13 *
14 * Description of levels:.
15 * 'error': Error conditions.
16 * 'warning': Warning conditions.
17 * 'notice': Normal but significant condition.
18 * 'info': Informational messages.
19 *
20 * @see @link {https://tools.ietf.org/html/rfc5424}
21 */
22 const ERROR = 'error';
23 const WARNING = 'warning';
24 const NOTICE = 'notice';
25 const INFO = 'info';
26
27 /**
28 * Level strings mapped to integer severity.
29 *
30 * @var array
31 */
32 protected static $level_to_severity = array(
33 self::ERROR => 500,
34 self::WARNING => 400,
35 self::NOTICE => 300,
36 self::INFO => 200,
37 );
38
39 /**
40 * Severity integers mapped to level strings.
41 *
42 * This is the inverse of $level_severity.
43 *
44 * @var array
45 */
46 protected static $severity_to_level = array(
47 500 => self::ERROR,
48 400 => self::WARNING,
49 300 => self::NOTICE,
50 200 => self::INFO,
51 );
52
53 /**
54 * Constructor for the logger.
55 */
56 public function __construct() {
57
58 add_action( 'init', array( $this, 'init' ) );
59
60 }
61
62 /**
63 * Prepares logging functionalty if enabled
64 *
65 * @access public
66 * @return void
67 */
68
69 public function init() {
70
71 if ( wp_fusion()->settings->get( 'enable_logging' ) != true ) {
72 return;
73 }
74
75 add_filter( 'wpf_configure_sections', array( $this, 'configure_sections' ), 10, 2 );
76
77 add_action( 'admin_menu', array( $this, 'register_logger_subpage' ) );
78 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
79
80 // Screen options
81 add_action( 'load-tools_page_wpf-settings-logs', array( $this, 'add_screen_options' ) );
82 add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 );
83
84 // Error handling
85 add_action( 'shutdown', array( $this, 'shutdown' ) );
86
87 $this->create_update_table();
88
89 }
90
91 /**
92 * Adds standalone log management page
93 *
94 * @access public
95 * @return void
96 */
97
98 public function register_logger_subpage() {
99
100 $page = add_submenu_page(
101 'tools.php',
102 'WP Fusion Activity Logs',
103 'WP Fusion Logs',
104 'manage_options',
105 'wpf-settings-logs',
106 array( $this, 'show_logs_section' )
107 );
108
109 }
110
111 /**
112 * Enqueues logger styles
113 *
114 * @access public
115 * @return void
116 */
117
118 public function enqueue_scripts() {
119
120 $screen = get_current_screen();
121
122 if ( 'tools_page_wpf-settings-logs' !== $screen->id ) {
123 return;
124 }
125
126 wp_enqueue_style( 'wpf-options', WPF_DIR_URL . 'assets/css/wpf-options.css', array(), WP_FUSION_VERSION );
127 wp_enqueue_style( 'wpf-admin', WPF_DIR_URL . 'assets/css/wpf-admin.css', array(), WP_FUSION_VERSION );
128
129 }
130
131 /**
132 * Adds per-page screen option
133 *
134 * @access public
135 * @return void
136 */
137
138 public function add_screen_options() {
139
140 $args = array(
141 'label' => __( 'Entries per page', 'wp-fusion' ),
142 'default' => 20,
143 'option' => 'wpf_status_log_items_per_page',
144 );
145
146 add_screen_option( 'per_page', $args );
147
148 }
149
150 /**
151 * Save screen options
152 *
153 * @access public
154 * @return int Value
155 */
156
157 public function set_screen_option( $status, $option, $value ) {
158
159 if ( 'wpf_status_log_items_per_page' == $option ) {
160 return $value;
161 }
162
163 return $status;
164
165 }
166
167 /**
168 * Adds logging tab to main settings for access
169 *
170 * @access public
171 * @return array Page
172 */
173
174 public function configure_sections( $page, $options ) {
175
176 $page['sections'] = wp_fusion()->settings->insert_setting_after(
177 'advanced', $page['sections'], array(
178 'logs' => array(
179 'title' => __( 'Logs', 'wp-fusion' ),
180 'slug' => 'wpf-settings-logs',
181 ),
182 )
183 );
184
185 return $page;
186
187 }
188
189 /**
190 * Creates logging table if logging enabled
191 *
192 * @access public
193 * @return void
194 */
195
196 public function create_update_table() {
197
198 global $wpdb;
199 $table_name = $wpdb->prefix . 'wpf_logging';
200
201 if ( $wpdb->get_var( "show tables like '$table_name'" ) != $table_name ) {
202
203 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
204
205 $collate = '';
206
207 if ( $wpdb->has_cap( 'collation' ) ) {
208 $collate = $wpdb->get_charset_collate();
209 }
210
211 $sql = 'CREATE TABLE ' . $table_name . " (
212 log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
213 timestamp datetime NOT NULL,
214 level smallint(4) NOT NULL,
215 user bigint(8) NOT NULL,
216 source varchar(200) NOT NULL,
217 message longtext NOT NULL,
218 context longtext NULL,
219 PRIMARY KEY (log_id),
220 KEY level (level)
221 ) $collate;";
222
223 dbDelta( $sql );
224
225 }
226
227 }
228
229 /**
230 * Logging tab content
231 *
232 * @access public
233 * @return void
234 */
235
236 public function show_logs_section() {
237
238 include_once WPF_DIR_PATH . 'includes/admin/logging/class-log-table-list.php';
239
240 // Flush
241 if ( ! empty( $_REQUEST['flush-logs'] ) ) {
242 self::flush();
243 }
244
245 // Bulk actions
246 if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) {
247 self::log_table_bulk_actions();
248 }
249
250 $log_table_list = new WPF_Log_Table_List();
251 $log_table_list->prepare_items();
252
253 // Stop _wp_http_referer getting appended to the logs URL, so it doesn't get too long
254 add_filter(
255 'removable_query_args', function( $query_args ) {
256
257 $query_args[] = '_wp_http_referer';
258 return $query_args;
259
260 }
261 );
262
263 ?>
264
265 <div class="wrap">
266 <h1><?php _e( 'WP Fusion Activity Log', 'wp-fusion' ); ?></h1>
267
268 <form method="get" id="mainform">
269
270 <input type="hidden" name="page" value="wpf-settings-logs">
271
272 <?php $log_table_list->display(); ?>
273
274 <?php submit_button( __( 'Flush all logs', 'wp-fusion' ), 'delete', 'flush-logs' ); ?>
275 <?php wp_nonce_field( 'wp-fusion-status-logs' ); ?>
276
277 </form>
278 </div>
279
280 <?php
281
282 }
283
284
285 /**
286 * Validate a level string.
287 *
288 * @param string $level
289 * @return bool True if $level is a valid level.
290 */
291 public static function is_valid_level( $level ) {
292 return array_key_exists( strtolower( $level ), self::$level_to_severity );
293 }
294
295 /**
296 * Translate level string to integer.
297 *
298 * @param string $level emergency|alert|critical|error|warning|notice|info|debug
299 * @return int 100 (debug) - 800 (emergency) or 0 if not recognized
300 */
301 public static function get_level_severity( $level ) {
302 if ( self::is_valid_level( $level ) ) {
303 $severity = self::$level_to_severity[ strtolower( $level ) ];
304 } else {
305 $severity = 0;
306 }
307 return $severity;
308 }
309
310 /**
311 * Translate severity integer to level string.
312 *
313 * @param int $severity
314 * @return bool|string False if not recognized. Otherwise string representation of level.
315 */
316 public static function get_severity_level( $severity ) {
317 if ( array_key_exists( $severity, self::$severity_to_level ) ) {
318 return self::$severity_to_level[ $severity ];
319 } else {
320 return false;
321 }
322 }
323
324 /**
325 * Handle a log entry.
326 *
327 * @param int $timestamp Log timestamp.
328 * @param string $level emergency|alert|critical|error|warning|notice|info|debug
329 * @param string $message Log message.
330 * @param array $context {
331 * Additional information for log handlers.
332 *
333 * @type string $source Optional. Source will be available in log table.
334 * If no source is provided, attempt to provide sensible default.
335 * }
336 *
337 * @see WPF_Log_Handler::get_log_source() for default source.
338 *
339 * @return bool False if value was not handled and true if value was handled.
340 */
341 public function handle( $level, $user, $message, $context = array() ) {
342
343 $timestamp = current_time( 'timestamp' );
344
345 do_action( 'wpf_handle_log', $timestamp, $level, $user, $message, $context );
346
347 if ( wp_fusion()->settings->get( 'enable_logging' ) != true ) {
348 return;
349 }
350
351 if ( wp_fusion()->settings->get( 'logging_errors_only' ) == true && $level != 'error' ) {
352 return;
353 }
354
355 if ( isset( $context['source'] ) && $context['source'] ) {
356 $source = $context['source'];
357 } else {
358 $source = $this->get_log_source();
359 }
360
361 // Filter out irrelevant meta fields
362 if ( isset( $context['meta_array'] ) && $context['meta_array'] ) {
363
364 $contact_fields = wp_fusion()->settings->get( 'contact_fields' );
365
366 foreach ( $context['meta_array'] as $key => $data ) {
367
368 if ( ! isset( $contact_fields[ $key ] ) || $contact_fields[ $key ]['active'] == false ) {
369 unset( $context['meta_array'][ $key ] );
370 }
371 }
372 }
373
374 if ( empty( $user ) ) {
375 $user = 0;
376 }
377
378 // Don't log meta data pushes where no enabled fields are being synced
379 if ( isset( $context['meta_array'] ) && empty( $context['meta_array'] ) ) {
380 return;
381 }
382
383 do_action( 'wpf_log_handled', $timestamp, $level, $user, $message, $source, $context );
384
385 return $this->add( $timestamp, $level, $user, $message, $source, $context );
386 }
387
388 /**
389 * Add a log entry to chosen file.
390 *
391 * @param string $level emergency|alert|critical|error|warning|notice|info|debug
392 * @param string $message Log message.
393 * @param string $source Log source. Useful for filtering and sorting.
394 * @param array $context {
395 * Context will be serialized and stored in database.
396 * }
397 *
398 * @return bool True if write was successful.
399 */
400 protected static function add( $timestamp, $level, $user, $message, $source, $context ) {
401 global $wpdb;
402
403 $insert = array(
404 'timestamp' => date( 'Y-m-d H:i:s', $timestamp ),
405 'level' => self::get_level_severity( $level ),
406 'user' => $user,
407 'message' => $message,
408 'source' => $source,
409 );
410
411 $format = array(
412 '%s',
413 '%d',
414 '%d',
415 '%s',
416 '%s',
417 '%s', // possible serialized context
418 );
419
420 if ( ! empty( $context ) ) {
421 $insert['context'] = serialize( $context );
422 }
423
424 $result = $wpdb->insert( "{$wpdb->prefix}wpf_logging", $insert, $format );
425
426 if ( $result === false ) {
427 return false;
428 }
429
430 $rowcount = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wpf_logging" );
431
432 $max_log_size = apply_filters( 'wpf_log_max_entries', 10000 );
433
434 if ( $rowcount > $max_log_size ) {
435 $wpdb->query( "DELETE FROM {$wpdb->prefix}wpf_logging ORDER BY log_id ASC LIMIT 1" );
436 }
437
438 return $result;
439
440 }
441
442 /**
443 * Clear all logs from the DB.
444 *
445 * @return bool True if flush was successful.
446 */
447 public static function flush() {
448 global $wpdb;
449
450 return $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wpf_logging" );
451 }
452
453 /**
454 * Bulk DB log table actions.
455 *
456 * @since 3.0.0
457 */
458 private function log_table_bulk_actions() {
459
460 if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'wp-fusion-status-logs' ) ) {
461 wp_die( __( 'Action failed. Please refresh the page and retry.', 'wp-fusion' ) );
462 }
463
464 $log_ids = array_map( 'absint', (array) $_REQUEST['log'] );
465
466 if ( 'delete' === $_REQUEST['action'] || 'delete' === $_REQUEST['action2'] ) {
467 self::delete( $log_ids );
468 }
469 }
470
471 /**
472 * Delete selected logs from DB.
473 *
474 * @param int|string|array Log ID or array of Log IDs to be deleted.
475 *
476 * @return bool
477 */
478 public static function delete( $log_ids ) {
479 global $wpdb;
480
481 if ( ! is_array( $log_ids ) ) {
482 $log_ids = array( $log_ids );
483 }
484
485 $format = array_fill( 0, count( $log_ids ), '%d' );
486
487 $query_in = '(' . implode( ',', $format ) . ')';
488
489 $query = $wpdb->prepare(
490 "DELETE FROM {$wpdb->prefix}wpf_logging WHERE log_id IN {$query_in}",
491 $log_ids
492 );
493
494 return $wpdb->query( $query );
495 }
496
497
498 /**
499 * Get appropriate source based on file name.
500 *
501 * Try to provide an appropriate source in case none is provided.
502 *
503 * @return string Text to use as log source. "" (empty string) if none is found.
504 */
505
506 protected static function get_log_source() {
507
508 static $ignore_files = array( 'class-log-handler' );
509
510 /**
511 * PHP < 5.3.6 correct behavior
512 *
513 * @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters
514 */
515
516 if ( defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) {
517 $debug_backtrace_arg = DEBUG_BACKTRACE_IGNORE_ARGS;
518 } else {
519 $debug_backtrace_arg = false;
520 }
521
522 $full_trace = debug_backtrace( $debug_backtrace_arg );
523
524 $slugs = array( 'user-profile', 'api', 'access-control', 'class-auto-login', 'class-ajax' );
525
526 foreach ( wp_fusion()->get_integrations() as $slug => $integration ) {
527 $slugs[] = $slug;
528 }
529
530 $found_integrations = array();
531
532 foreach ( $full_trace as $i => $trace ) {
533
534 if ( isset( $trace['file'] ) ) {
535
536 foreach ( $slugs as $slug ) {
537
538 if ( empty( $slug ) ) {
539 continue;
540 }
541
542 if ( strpos( $trace['file'], $slug ) !== false ) {
543
544 $found_integrations[] = $slug;
545 }
546 }
547 }
548 }
549
550 // Figure out most likely integration
551 if ( ! empty( $found_integrations ) ) {
552
553 $source = serialize( array_reverse( array_unique( $found_integrations ) ) );
554
555 } else {
556 $source = 'unknown';
557 }
558
559 return $source;
560 }
561
562
563 /**
564 * Check for PHP errors on shutdown and log them
565 *
566 * @access public
567 * @return void
568 */
569 public function shutdown() {
570
571 $error = error_get_last();
572
573 if ( is_null( $error ) ) {
574 return;
575 }
576
577 if ( false !== strpos( $error['file'], 'wp-fusion' ) || false !== strpos( $error['message'], 'wp-fusion' ) ) {
578
579 if ( E_ERROR == $error['type'] || E_WARNING == $error['type'] ) {
580
581 // Get the source
582
583 $source = 'unknown';
584
585 $slugs = array( 'user-profile', 'api', 'access-control', 'class-auto-login', 'class-ajax', 'class-user' );
586
587 foreach ( wp_fusion()->get_integrations() as $slug => $integration ) {
588 $slugs[] = $slug;
589 }
590
591 foreach ( $slugs as $slug ) {
592
593 if ( empty( $slug ) ) {
594 continue;
595 }
596
597 if ( strpos( $error['file'], $slug ) !== false ) {
598
599 $source = $slug;
600 break;
601
602 }
603 }
604
605 if ( E_ERROR == $error['type'] ) {
606 $level = 'error';
607 } elseif ( E_WARNING == $error['type'] ) {
608 $level = 'warning';
609 }
610
611 $this->handle( $level, wpf_get_current_user_id(), '<strong>PHP error:</strong> ' . nl2br( $error['message'] ) . '<br /><br />' . $error['file'] . ':' . $error['line'], array( 'source' => $source ) );
612
613 }
614
615 }
616
617 }
618
619}