PluginProbe
DecaLog / 4.4.0
DecaLog v4.4.0
3.0.2 3.1.0 3.10.0 3.2.0 3.3.0 3.4.0 3.4.1 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.6.3 3.7.0 3.7.1 3.8.0 3.9.0 3.9.1 4.0.0 4.1.0 4.2.0 4.3.0 4.3.1 4.4.0 4.5.0 All 75 releases
decalog / includes / features / class-traces.php

class-traces.php in DecaLog 4.4.0, at includes/features/class-traces.php

683 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Events list
4 *
5 * Lists all events.
6 *
7 * @package Features
8 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
9 * @since 3.0.0
10 */
11
12 namespace Decalog\Plugin\Feature;
13
14 use Decalog\Logger;
15 use Decalog\Storage\AbstractStorage;
16 use Decalog\System\Date;
17 use Decalog\System\Option;
18 use Decalog\System\Role;
19 use Decalog\System\Timezone;
20 use Feather\Icons;
21 use Decalog\System\GeoIP;
22 use Decalog\System\Hash;
23 use Decalog\Storage\DBTraceStorage;
24 use Decalog\Storage\APCuStorage;
25
26
27 if ( ! class_exists( 'WP_List_Table' ) ) {
28 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
29 }
30
31 /**
32 * Define the events list functionality.
33 *
34 * Lists all events.
35 *
36 * @package Features
37 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
38 * @since 3.0.0
39 */
40 class Traces extends \WP_List_Table {
41
42 /**
43 * The available events logs.
44 *
45 * @since 3.0.0
46 * @var array $logs The loggers list.
47 */
48 private static $logs = [];
49
50 /**
51 * The storage engine.
52 *
53 * @since 3.0.0
54 * @var \Decalog\Storage\AbstractStorage $storage The storage engine.
55 */
56 private $storage = null;
57
58 /**
59 * The columns always shown.
60 *
61 * @since 3.0.0
62 * @var array $standard_columns The columns always shown.
63 */
64 private static $standard_columns = [];
65
66 /**
67 * The columns which may be shown.
68 *
69 * @since 3.0.0
70 * @var array $extra_columns The columns which may be shown.
71 */
72 private static $extra_columns = [];
73
74 /**
75 * The columns which must be shown to the current user.
76 *
77 * @since 3.0.0
78 * @var array $extra_columns The columns which must be shown to the current user.
79 */
80 private static $user_columns = [];
81
82 /**
83 * The order of the columns.
84 *
85 * @since 3.0.0
86 * @var array $columns_order The order of the columns.
87 */
88 private static $columns_order = [];
89
90 /**
91 * The events types icons.
92 *
93 * @since 3.0.0
94 * @var array $icons The icons list.
95 */
96 private $icons = [];
97
98 /**
99 * The number of lines to display.
100 *
101 * @since 3.0.0
102 * @var integer $limit The number of lines to display.
103 */
104 private $limit = 25;
105
106 /**
107 * The logger ID.
108 *
109 * @since 3.0.0
110 * @var string $logger The logger ID.
111 */
112 private $logger = null;
113
114 /**
115 * The main filter.
116 *
117 * @since 3.0.0
118 * @var array $filters The main filter.
119 */
120 private $filters = [];
121
122 /**
123 * Forces the site_id filter if set.
124 *
125 * @since 3.0.0
126 * @var integer $force_siteid Forces the site_id filter if set.
127 */
128 private $force_siteid = null;
129
130 /**
131 * The token of the current session.
132 *
133 * @since 2.4.0
134 * @var string $selftoken The token of the current session.
135 */
136 private $selftoken = '';
137
138 /**
139 * Initialize the class and set its properties.
140 *
141 * @since 3.0.0
142 */
143 public function __construct() {
144 parent::__construct(
145 [
146 'singular' => 'trace',
147 'plural' => 'traces',
148 'ajax' => true,
149 ]
150 );
151 $this->selftoken = Hash::simple_hash( wp_get_session_token(), false );
152 }
153
154 /**
155 * Default column formatter.
156 *
157 * @param object $item The current item to render.
158 * @param string $column_name The name of the current rendered column.
159 * @return string The cell formatted, ready to print.
160 * @since 3.0.0
161 */
162 protected function column_default( $item, $column_name ) {
163 return $item[ $column_name ];
164 }
165
166 /**
167 * "event" column formatter.
168 *
169 * @param object $item The current item to render.
170 * @return string The cell formatted, ready to print.
171 * @since 3.0.0
172 */
173 protected function column_trace( $item ) {
174 $args = [];
175 $args['page'] = 'decalog-tviewer';
176 $args['logid'] = $this->logger;
177 $args['traceid'] = $item['id'];
178 $url = add_query_arg( $args, admin_url( 'admin.php' ) );
179 $icon = '<img style="width:18px;float:left;padding-right:6px;" src="' . Icons::get_base64( 'clock', '#ABCFF9', '#192783' ) . '" />';
180 $name = '<a href="' . $url . '">' . ChannelTypes::$channel_names[ strtoupper( $item['channel'] ) ] . '</a>' . $this->get_filter( 'channel', $item['channel'] ) . $this->get_actions( 'trace', $item ) . '&nbsp;<span style="color:silver">#' . $item['id'] . '</span>';
181 /* translators: as in the sentence "TraceID xxxx" */
182 $code = '<br /><span style="color:silver">' . sprintf( decalog_esc_html__( 'TraceID %s', 'decalog' ), $item['trace_id'] ) . '</span>';
183 $result = $icon . $name . $code;
184 return $result;
185 }
186
187 /**
188 * "component" column formatter.
189 *
190 * @param object $item The current item to render.
191 * @return string The cell formatted, ready to print.
192 * @since 3.0.0
193 */
194 protected function column_duration( $item ) {
195 $name = $item['duration'] . ' ms';
196 $result = $name . $this->get_actions( 'duration', $item ) . '<br /><span style="color:silver">' . sprintf( decalog_esc_html__( '%d spans', 'decalog' ), $item['scount'] ) . '</span>';
197 return $result;
198 }
199
200 /**
201 * "time" column formatter.
202 *
203 * @param object $item The current item to render.
204 * @return string The cell formatted, ready to print.
205 * @since 3.0.0
206 */
207 protected function column_time( $item ) {
208 $result = Date::get_date_from_mysql_utc( $item['timestamp'], Timezone::network_get()->getName(), 'Y-m-d H:i:s' ) . $this->get_actions( 'time', $item );
209 $result .= '<br /><span style="color:silver">' . Date::get_positive_time_diff_from_mysql_utc( $item['timestamp'] ) . '</span>';
210 return $result;
211 }
212
213 /**
214 * "site" column formatter.
215 *
216 * @param object $item The current item to render.
217 * @return string The cell formatted, ready to print.
218 * @since 3.0.0
219 */
220 protected function column_site( $item ) {
221 $name = $item['site_name'] . $this->get_filter( 'site_id', $item['site_id'] ) . $this->get_actions( 'site', $item );
222 // phpcs:ignore
223 $result = $name . '<br /><span style="color:silver">' . sprintf(decalog_esc_html__('Site ID %s', 'decalog'), $item['site_id'] ) . '</span>';
224 return $result;
225 }
226
227 /**
228 * "user" column formatter.
229 *
230 * @param object $item The current item to render.
231 * @return string The cell formatted, ready to print.
232 * @since 3.0.0
233 */
234 protected function column_user( $item ) {
235 $user = $item['user_name'];
236 if ( 'anonymous' === $user ) {
237 $user = '<em>' . decalog_esc_html__( 'Anonymous user', 'decalog' ) . '</em>';
238 }
239 $id = '';
240 $se = '';
241 if ( 0 === strpos( $item['user_name'], '{' ) ) {
242 $user = '<em>' . decalog_esc_html__( 'Pseudonymized user', 'decalog' ) . '</em>';
243 } elseif ( 0 !== (int) $item['user_id'] ) {
244 // phpcs:ignore
245 $id = sprintf( decalog_esc_html__( ' (UID %s)', 'decalog' ), $item[ 'user_id' ] );
246 $se = '<br /><span style="color:silver">' . sprintf( decalog_esc_html__( 'Session #%s…%s', 'decalog' ), substr( $item[ 'user_session' ], 0, 2 ), substr( $item[ 'user_session' ], -2 ) ) . '</span>';
247 }
248 $result = $user . $id . $this->get_filter( 'user_id', $item['user_id'] ) . $this->get_actions( 'user', $item ) . $se . ( '' !== $se ? $this->get_pose_shortcut( (int) $item['user_id'] ) : '' ) . ( '' !== $se ? $this->get_filter( 'user_session', $item['user_session'] ) : '' );
249 return '<span' . ( ( $item['user_session'] ?? '') === $this->selftoken ? ' class="decalog-selftoken"' : '' ) . '>' . $result . '</span>';
250 }
251
252 /**
253 * Initialize the list view.
254 *
255 * @return array The columns to render.
256 * @since 3.0.0
257 */
258 public function get_columns() {
259 $columns = [];
260 foreach ( self::$columns_order as $column ) {
261 if ( array_key_exists( $column, self::$standard_columns ) ) {
262 $columns[ $column ] = self::$standard_columns[ $column ];
263 // phpcs:ignore
264 } elseif ( array_key_exists( $column, self::$extra_columns ) && in_array( $column, self::$user_columns, true ) ) {
265 $columns[ $column ] = self::$extra_columns[ $column ];
266 }
267 }
268 return $columns;
269 }
270
271 /**
272 * Initialize storage.
273 *
274 * @since 3.0.0
275 */
276 protected function init_storage() {
277 $this->storage = null;
278 if ( $this->logger ) {
279 $loggers = Option::network_get( 'loggers' );
280 if ( array_key_exists( $this->logger, $loggers ) ) {
281 $bucket_name = 'decalog_' . str_replace( '-', '', $this->logger );
282 switch ( $loggers[ $this->logger ]['configuration']['constant-storage'] ) {
283 case 'apcu':
284 $this->storage = new APCuStorage( $bucket_name );
285 break;
286 default:
287 $this->storage = new DBTraceStorage( $bucket_name );
288 }
289 }
290 }
291 }
292
293 /**
294 * Initialize values and filter.
295 *
296 * @since 3.0.0
297 */
298 protected function init_values() {
299 $this->limit = filter_input( INPUT_GET, 'limit', FILTER_SANITIZE_NUMBER_INT );
300 if ( ! $this->limit ) {
301 $this->limit = 25;
302 }
303 $this->force_siteid = null;
304 $this->logger = filter_input( INPUT_GET, 'logger_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
305 if ( $this->logger ) {
306 $this->set_level_access();
307 } else {
308 $this->set_first_available();
309 }
310 $this->init_storage();
311 $this->filters = [];
312 $level = filter_input( INPUT_GET, 'level', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
313 if ( $level && array_key_exists( strtolower( $level ), EventTypes::$levels ) && 'debug' !== strtolower( $level ) ) {
314 $this->filters['level'] = strtolower( $level );
315 }
316 foreach ( [ 'channel', 'site_id', 'user_id', 'user_session' ] as $f ) {
317 $v = filter_input( INPUT_GET, $f, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
318 if ( $v ) {
319 $this->filters[ $f ] = strtolower( $v );
320 }
321 }
322 if ( $this->force_siteid ) {
323 $this->filters['site_id'] = $this->force_siteid;
324 }
325 }
326
327 /**
328 * Get the filter image.
329 *
330 * @param string $filter The filter name.
331 * @param string $value The filter value.
332 * @param boolean $soft Optional. The image must be softened.
333 * @return string The filter image, ready to print.
334 * @since 3.0.0
335 */
336 protected function get_filter( $filter, $value, $soft = false ) {
337 $filters = $this->filters;
338 if ( array_key_exists( $filter, $this->filters ) ) {
339 unset( $this->filters[ $filter ] );
340 $url = $this->get_page_url();
341 $alt = decalog_esc_html__( 'Remove this filter', 'decalog' );
342 $fill = '#9999FF';
343 $stroke = '#0000AA';
344 } else {
345 $this->filters[ $filter ] = $value;
346 $url = $this->get_page_url();
347 $alt = decalog_esc_html__( 'Add as filter', 'decalog' );
348 $fill = 'none';
349 if ( $soft ) {
350 $stroke = '#C0C0FF';
351 } else {
352 $stroke = '#3333AA';
353 }
354 }
355 $this->filters = $filters;
356 return '&nbsp;<a href="' . $url . '"><img title="' . $alt . '" style="width:11px;vertical-align:baseline;" src="' . Icons::get_base64( 'filter', $fill, $stroke ) . '" /></a>';
357 }
358
359 /**
360 * Get a shortcut to Sessions (if running).
361 *
362 * @param string $uid The user id.
363 * @return string The shortcut image, ready to print.
364 * @since 2.4.0
365 */
366 protected function get_pose_shortcut( $uid ) {
367 if ( 0 === $uid || ! class_exists( 'POSessions\Plugin\Core' ) ) {
368 return '';
369 }
370 $url = esc_url( admin_url( 'admin.php?page=pose-manager&id=' ) . $uid );
371 $alt = decalog_esc_html__( 'See all sessions', 'decalog' );
372 $fill = '#C0C0FF';
373 $stroke = '#3333AA';
374 return '&nbsp;<a target="_blank" href="' . $url . '"><img title="' . $alt . '" style="width:11px;vertical-align:baseline;" src="' . Icons::get_base64( 'users', $fill, $stroke ) . '" /></a>';
375 }
376
377 /**
378 * Get the action image.
379 *
380 * @param string $url The url to call.
381 * @param string $hint The hint to display.
382 * @param string $icon The icon to display.
383 * @param boolean $soft Optional. The image must be softened.
384 * @return string The action image, ready to print.
385 * @since 3.3.0
386 */
387 protected function get_action( $url, $hint, $icon, $soft = false ) {
388 return '&nbsp;<a href="' . $url . '" target="_blank"><img title="' . esc_html( $hint ) . '" style="width:11px;vertical-align:baseline;" src="' . Icons::get_base64( $icon, 'none', $soft ? '#C0C0FF' : '#3333AA' ) . '" /></a>';
389 }
390
391 /**
392 * Get the action image.
393 *
394 * @param string $column The column where to display actions.
395 * @param object $item The row item, an event.
396 * @return string The actions images, ready to print.
397 * @since 3.3.0
398 */
399 protected function get_actions( $column, $item ) {
400 $item = (array) $item;
401 $item['logger_id'] = $this->logger;
402
403 /**
404 * Filters the available actions for the current item and column.
405 *
406 * @See https://github.com/Pierre-Lannoy/wp-decalog/blob/master/HOOKS.md
407 * @since 3.3.0
408 * @param array $item The full trace with metadata.
409 */
410 $actions = apply_filters( 'decalog_traces_list_actions_for_' . $column, [], $item );
411
412 $result = '';
413 foreach ( $actions as $action ) {
414 if ( isset( $action['url'] ) ) {
415 $result .= $this->get_action( $action['url'], isset( $action['hint'] ) ? $action['hint'] :decalog__( 'Unknown action', 'decalog' ), isset( $action['icon'] ) ? $action['icon'] : '' );
416 }
417 }
418 return $result;
419 }
420
421 /**
422 * Initialize the list view.
423 *
424 * @since 3.0.0
425 */
426 public function prepare_items() {
427 $this->init_values();
428 $columns = $this->get_columns();
429 $hidden = [];
430 $sortable = $this->get_sortable_columns();
431 $this->_column_headers = [ $columns, $hidden, $sortable ];
432 $current_page = $this->get_pagenum();
433 $total_items = $this->get_count();
434 $this->items = $this->get_list( ( $current_page - 1 ) * $this->limit, $this->limit );
435 $this->set_pagination_args(
436 [
437 'total_items' => $total_items,
438 'per_page' => $this->limit,
439 'total_pages' => ceil( $total_items / $this->limit ),
440 ]
441 );
442 }
443
444 /**
445 * Generate the table navigation above or below the table
446 *
447 * @param string $which Position of extra control.
448 * @since 3.0.0
449 */
450 protected function display_tablenav( $which ) {
451 echo '<div class="tablenav ' . esc_attr( $which ) . '">';
452 $this->extra_tablenav( $which );
453 $this->pagination( $which );
454 echo '<br class="clear" />';
455 echo '</div>';
456 }
457
458 /**
459 * Extra controls to be displayed between bulk actions and pagination.
460 *
461 * @param string $which Position of extra control.
462 * @since 3.0.0
463 */
464 public function extra_tablenav( $which ) {
465 $list = $this;
466 $args = compact( 'list' );
467 foreach ( $args as $key => $val ) {
468 $$key = $val;
469 }
470 if ( 'top' === $which ) {
471 include DECALOG_ADMIN_DIR . 'partials/decalog-admin-view-traces-top.php';
472 }
473 if ( 'bottom' === $which ) {
474 include DECALOG_ADMIN_DIR . 'partials/decalog-admin-view-traces-bottom.php';
475 }
476 }
477
478 /**
479 * Get the page url with args.
480 *
481 * @return string The url.
482 * @since 3.0.0
483 */
484 public function get_page_url() {
485 $args = [];
486 $args['page'] = 'decalog-tviewer';
487 $args['logger_id'] = $this->logger;
488 if ( count( $this->filters ) > 0 ) {
489 foreach ( $this->filters as $key => $filter ) {
490 if ( '' !== $filter ) {
491 $args[ $key ] = $filter;
492 }
493 }
494 }
495 if ( 25 !== $this->limit ) {
496 $args['limit'] = $this->limit;
497 }
498 $url = add_query_arg( $args, admin_url( 'admin.php' ) );
499 return $url;
500 }
501
502 /**
503 * Get the available events logs.
504 *
505 * @return array The list of available events logs.
506 * @since 3.0.0
507 */
508 public function get_loggers() {
509 return self::$logs;
510 }
511
512 /**
513 * Get the available events logs.
514 *
515 * @return array The list of available events logs.
516 * @since 3.0.0
517 */
518 public static function get() {
519 return self::$logs;
520 }
521
522 /**
523 * Get the current events log id.
524 *
525 * @return string The current events log id.
526 * @since 3.0.0
527 */
528 public function get_current_Log_id() {
529 return $this->logger;
530 }
531
532 /**
533 * Get available lines breakdowns.
534 *
535 * @since 3.0.0
536 */
537 public function get_line_number_select() {
538 $_disp = [ 25, 50, 100, 250, 500 ];
539 $result = [];
540 foreach ( $_disp as $d ) {
541 $l = [];
542 $l['value'] = $d;
543 // phpcs:ignore
544 $l['text'] = sprintf( decalog_esc_html__( 'Show %d lines per page', 'decalog' ), $d );
545 $l['selected'] = ( $d === (int) $this->limit ? 'selected="selected" ' : '' );
546 $result[] = $l;
547 }
548 return $result;
549 }
550
551 /**
552 * Set the level access to an events log.
553 *
554 * @since 3.0.0
555 */
556 private function set_level_access() {
557 $this->force_siteid = null;
558 $id = $this->logger;
559 $this->logger = null;
560 foreach ( self::$logs as $log ) {
561 if ( $id === $log['id'] ) {
562 $this->logger = $id;
563 if ( array_key_exists( 'limit', $log ) ) {
564 $this->force_siteid = $log['limit'];
565 }
566 }
567 }
568 }
569
570 /**
571 * Set the level access to an events log.
572 *
573 * @since 3.0.0
574 */
575 private function set_first_available() {
576 $this->force_siteid = null;
577 $this->logger = null;
578 foreach ( self::$logs as $log ) {
579 if ( array_key_exists( 'limit', $log ) ) {
580 $this->force_siteid = $log['limit'];
581 }
582 $this->logger = $log['id'];
583 break;
584 }
585 }
586
587 /**
588 * Get list of logged errors.
589 *
590 * @param integer $offset The offset to record.
591 * @param integer $rowcount Optional. The number of rows to return.
592 * @return array An array containing the filtered logged errors.
593 * @since 3.0.0
594 */
595 protected function get_list( $offset = null, $rowcount = null ) {
596 return ( $this->storage ? $this->storage->get_list( $this->filters, $offset, $rowcount ) : [] );
597 }
598
599 /**
600 * Count logged errors.
601 *
602 * @return integer The count of the filtered logged errors.
603 * @since 3.0.0
604 */
605 protected function get_count() {
606 return ( $this->storage ? $this->storage->get_count( $this->filters ) : 0 );
607 }
608
609 /**
610 * Initialize the meta class and set its columns properties.
611 *
612 * @since 3.0.0
613 */
614 private static function load_columns() {
615 self::$standard_columns = [];
616 self::$standard_columns['trace'] = decalog_esc_html__( 'Trace', 'decalog' );
617 self::$standard_columns['duration'] = decalog_esc_html__( 'Duration', 'decalog' );
618 self::$standard_columns['time'] = decalog_esc_html__( 'Time', 'decalog' );
619 self::$extra_columns = [];
620 self::$extra_columns['site'] = decalog_esc_html__( 'Site', 'decalog' );
621 self::$extra_columns['user'] = decalog_esc_html__( 'User', 'decalog' );
622 self::$columns_order = [ 'trace', 'duration', 'time', 'site', 'user' ];
623 self::$user_columns = [];
624 foreach ( self::$extra_columns as $key => $extra_column ) {
625 if ( 'site' !== $key || ( 'site' === $key && is_multisite() ) ) {
626 self::$user_columns[] = $key;
627 }
628 }
629 }
630
631 /**
632 * Initialize the meta class and set its properties.
633 *
634 * @since 3.0.0
635 */
636 public static function init() {
637 self::$logs = [];
638 foreach ( Option::network_get( 'loggers' ) as $key => $logger ) {
639 if ( 'WordpressTracingHandler' === $logger['handler'] ) {
640 if ( array_key_exists( 'configuration', $logger ) ) {
641 if ( array_key_exists( 'local', $logger['configuration'] ) ) {
642 $local = $logger['configuration']['local'];
643 } else {
644 $local = false;
645 }
646 if ( Role::SUPER_ADMIN === Role::admin_type() || Role::SINGLE_ADMIN === Role::admin_type() || ( Role::LOCAL_ADMIN === Role::admin_type() && $local ) || Role::override_privileges() ) {
647 $log = [
648 'name' => $logger['name'],
649 'running' => $logger['running'],
650 'id' => $key,
651 ];
652 if ( Role::LOCAL_ADMIN === Role::admin_type() ) {
653 $log['limit'] = get_current_blog_id();
654 }
655 self::$logs[] = $log;
656 }
657 }
658 }
659 }
660 uasort(
661 self::$logs,
662 function ( $a, $b ) {
663 if ( $a['running'] === $b['running'] ) {
664 return strcasecmp( str_replace( ' ', '', $a['name'] ), str_replace( ' ', '', $b['name'] ) );
665 } return $a['running'] ? -1 : 1;
666 }
667 );
668 self::load_columns();
669 }
670
671 /**
672 * Get the number of available logs.
673 *
674 * @return integer The number of logs.
675 * @since 3.0.0
676 */
677 public static function loggers_count() {
678 return count( self::$logs );
679 }
680 }
681
682 Traces::init();
683