PluginProbe
WP Discourse / trunk
WP Discourse vtrunk
2.6.4 2.6.3 1.2.1 1.2.2 1.2.5 1.2.6 1.2.7 1.2.8 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.85 1.4.0 1.4.15 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 All 150 releases
wp-discourse / admin / log-viewer.php

log-viewer.php in WP Discourse trunk, at admin/log-viewer.php

518 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Log Viewer.
4 *
5 * @package WPDiscourse
6 */
7
8 namespace WPDiscourse\Admin;
9
10 use WPDiscourse\Logs\FileManager;
11 use WPDiscourse\Logs\FileHandler;
12 use WPDiscourse\Shared\PluginUtilities;
13
14 /**
15 * Class LogViewer
16 */
17 class LogViewer {
18 use PluginUtilities;
19
20 /**
21 * Flag to determine whether LogViewer is enabled.
22 *
23 * @var bool
24 */
25 protected $enabled;
26
27 /**
28 * An instance of the FormHelper class.
29 *
30 * @access protected
31 * @var \WPDiscourse\Admin\FormHelper
32 */
33 protected $form_helper;
34
35 /**
36 * LogViewer's instance of FileHandler
37 *
38 * @var \WPDiscourse\Logs\FileHandler
39 */
40 protected $file_handler;
41
42 /**
43 * LogViewer's log list
44 *
45 * @var mixed
46 */
47 protected $logs;
48
49 /**
50 * Current log in LogViewer
51 *
52 * @var object
53 */
54 protected $selected_log;
55
56 /**
57 * Metafile name
58 *
59 * @var string
60 */
61 protected $metafile_name;
62
63 /**
64 * Gives access to the plugin options.
65 *
66 * @access protected
67 * @var mixed|void
68 */
69 protected $options;
70
71 /**
72 * LogViewer constructor.
73 *
74 * @param \WPDiscourse\Admin\FormHelper $form_helper An instance of the FormHelper class.
75 */
76 public function __construct( $form_helper ) {
77 $this->metafile_name = 'logs-metafile';
78 $this->form_helper = $form_helper;
79
80 add_action( 'admin_init', array( $this, 'setup_options' ) );
81 add_action( 'admin_init', array( $this, 'setup_log_viewer' ) );
82 }
83
84 /**
85 * Sets the plugin options.
86 *
87 * @param object $extra_options Extra options used for testing.
88 */
89 public function setup_options( $extra_options = null ) {
90 $this->options = $this->get_options();
91
92 if ( ! empty( $extra_options ) ) {
93 foreach ( $extra_options as $key => $value ) {
94 $this->options[ $key ] = $value;
95 }
96 }
97 }
98
99 /**
100 * Run LogViewer setup tasks.
101 *
102 * @param object $file_handler Instance of \WPDiscourse\Logs\FileHandler.
103 */
104 public function setup_log_viewer( $file_handler = null ) {
105 if ( $file_handler ) {
106 $this->file_handler = $file_handler;
107 } else {
108 $this->file_handler = new FileHandler( new FileManager() );
109 }
110
111 $handler_enabled = $this->file_handler->enabled();
112 $this->enabled = ! empty( $this->options['logs-enabled'] ) && $handler_enabled;
113
114 if ( $this->enabled ) {
115 $this->setup_logs();
116
117 add_action( 'wp_ajax_wpdc_view_log', array( $this, 'log_file_contents' ) );
118 add_action( 'wp_ajax_wpdc_view_logs_metafile', array( $this, 'meta_file_contents' ) );
119 add_action( 'wp_ajax_wpdc_download_logs', array( $this, 'download_logs' ) );
120 }
121
122 $this->register_log_viewer();
123 }
124
125 /**
126 * Add settings section and register the setting.
127 */
128 public function register_log_viewer() {
129 add_settings_section(
130 'discourse_log_viewer',
131 __( 'Logs', 'wp-discourse' ),
132 array(
133 $this,
134 'log_viewer_markup',
135 ),
136 'discourse_logs'
137 );
138
139 add_settings_field(
140 'discourse_logs_enabled',
141 __( 'Logging enabled', 'wp-discourse' ),
142 array(
143 $this,
144 'logs_enabled',
145 ),
146 'discourse_logs',
147 'discourse_log_viewer'
148 );
149
150 register_setting( 'discourse_logs', 'discourse_logs' );
151 }
152
153 /**
154 * Outputs markup for the discourse_logs checkbox.
155 */
156 public function logs_enabled() {
157 $this->form_helper->checkbox_input(
158 'logs-enabled',
159 'discourse_logs',
160 __(
161 'Enable WP Discourse logs.',
162 'wp-discourse'
163 )
164 );
165 }
166
167 /**
168 * Setup logs
169 */
170 public function setup_logs() {
171 $this->retrieve_logs();
172
173 if ( ! empty( $this->logs ) && empty( $this->selected_log ) ) {
174 $this->selected_log = reset( $this->logs );
175 }
176 }
177
178 /**
179 * Outputs the markup for the log viewer.
180 */
181 public function log_viewer_markup() {
182 $selected_log_key = null;
183
184 if ( ! empty( $this->selected_log ) ) {
185 $selected_log_key = $this->build_log_key( $this->selected_log );
186 }
187
188 ?>
189 <?php if ( $this->enabled ) : ?>
190 <?php /* translators: placeholder interpolates url to documentation on meta.discourse.org */ ?>
191 <p><?php printf( esc_html__( 'Please see %s for details.', 'wp-discourse' ), sprintf( '<a href="%s">%s</a>', esc_url( 'https://meta.discourse.org/t/190745' ), esc_html__( 'WP Discourse Logging', 'text-domain' ) ) ); ?></p>
192 <?php if ( ! empty( $this->logs ) ) : ?>
193 <div id="wpdc-log-viewer-controls">
194 <div class="name">
195 <h3>
196 <?php esc_html_e( 'Log for ', 'wp-discourse' ); ?>
197 <?php echo esc_html( $this->file_name( $this->selected_log ) ); ?>
198 </h3>
199 <a class="load-log">
200 <span class="refresh"><?php esc_html_e( 'Refresh', 'wp-discourse' ); ?></span>
201 <span class="return-to"><?php esc_html_e( 'Return to log', 'wp-discourse' ); ?></span>
202 </a>
203 </div>
204 <div class="select">
205 <select>
206 <?php foreach ( $this->logs as $log_key => $log_info ) : ?>
207 <option value="<?php echo esc_attr( $log_key ); ?>">
208 <?php echo esc_attr( $this->file_name( $log_info ) ); ?>
209 </option>
210 <?php endforeach; ?>
211 </select>
212 <div class="view-meta button"><?php esc_html_e( 'View Meta', 'wp-discourse' ); ?></div>
213 <div class="download-logs button"><?php esc_html_e( 'Download', 'wp-discourse' ); ?></div>
214 </div>
215 </div>
216 <div id="wpdc-log-viewer" data-log-key="<?php echo esc_attr( $selected_log_key ); ?>">
217 <span class="spinner"></span>
218 <pre><?php echo esc_html( file_get_contents( $this->selected_log['file'] ) ); ?></pre>
219 </div>
220 <?php else : ?>
221 <div class="inline"><p><?php esc_html_e( 'There are currently no logs to view.', 'wp-discourse' ); ?></p></div>
222 <?php endif; ?>
223 <?php else : ?>
224 <div class="inline"><p><?php esc_html_e( 'Logs are disabled.', 'wp-discourse' ); ?></p></div>
225 <?php endif; ?>
226 <?php
227 }
228
229 /**
230 * Retrieve log files.
231 */
232 public function retrieve_logs() {
233 $file_handler = $this->file_handler;
234 $log_files = $file_handler->list_files();
235
236 $this->logs = array_reduce(
237 $log_files,
238 function ( $result, $log_file ) use ( $file_handler ) {
239 $date = $file_handler->get_date_from_url( $log_file );
240 $number = $file_handler->get_number_from_url( $log_file );
241 $log = array(
242 'date' => $date,
243 'number' => $number,
244 'file' => $log_file,
245 );
246 $result[ $this->build_log_key( $log ) ] = $log;
247 return $result;
248 },
249 array()
250 );
251 }
252
253 /**
254 * Return log file contents for selected key.
255 */
256 public function log_file_contents() {
257 if ( ! current_user_can( 'manage_options' ) ||
258 ! isset( $_REQUEST['nonce'] ) ||
259 ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'admin-ajax-nonce' ) ) {
260 wp_send_json_error();
261 return;
262 }
263
264 if ( ! isset( $_POST['key'] ) ) {
265 wp_send_json_error();
266 return;
267 }
268
269 $log_key = sanitize_text_field( wp_unslash( $_POST ['key'] ) );
270 $log = $this->logs[ $log_key ];
271
272 if ( $log ) {
273 $this->selected_log = $log;
274
275 $response = array(
276 'contents' => file_get_contents( $this->selected_log['file'] ),
277 'name' => $this->file_name( $this->selected_log ),
278 );
279
280 wp_send_json_success( $response );
281 } else {
282 wp_send_json_error();
283 }
284 }
285
286 /**
287 * Return log meta file contents.
288 */
289 public function meta_file_contents() {
290 if ( ! current_user_can( 'manage_options' ) ||
291 ! isset( $_REQUEST['nonce'] ) ||
292 ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'admin-ajax-nonce' ) ) {
293 wp_send_json_error();
294 return;
295 }
296
297 $metafile_contents = $this->build_metafile_contents();
298
299 $response = array(
300 'contents' => $metafile_contents,
301 'name' => 'Log Meta File',
302 );
303 wp_send_json_success( $response );
304 }
305
306 /**
307 * Download bundled log files.
308 */
309 public function download_logs() {
310 if ( ! current_user_can( 'manage_options' ) ||
311 ! isset( $_REQUEST['nonce'] ) ||
312 ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'admin-ajax-nonce' ) ) {
313 wp_send_json_error();
314 return;
315 }
316
317 $log_files = $this->file_handler->list_files();
318 $date_range = $this->build_date_range( $log_files );
319
320 $plugin_data = get_plugin_data( WPDISCOURSE_PATH . 'wp-discourse.php' );
321 $plugin_name = $plugin_data['TextDomain'];
322 $site_title = str_replace( ' ', '-', strtolower( get_bloginfo( 'name' ) ) );
323
324 $filename = "{$site_title}-{$plugin_name}-logs-$date_range.zip";
325 $file = wp_tempnam( 'tmp', $filename );
326 $zip = new \ZipArchive();
327 $zip->open( $file, \ZipArchive::OVERWRITE );
328
329 foreach ( $log_files as $log_file ) {
330 $name = $this->file_handler->get_filename( $log_file );
331 $zip->addFile( $log_file, "$name.log" );
332 }
333
334 $metafile_name = $this->metafile_name;
335 $metafile_filename = "{$plugin_name}-{$metafile_name}-{$date_range}.txt";
336 $metafile_path = $this->get_metafile_path( $metafile_filename );
337
338 $this->update_meta_file( $metafile_path );
339
340 $zip->addFile( $metafile_path, $metafile_filename );
341 $zip->close();
342
343 header( 'Content-type: application/zip' );
344 header( 'Content-Length: ' . filesize( $file ) );
345 header( "Content-Disposition: attachment; filename=$filename" );
346 readfile( $file );
347 unlink( $file );
348
349 wp_die();
350 }
351
352 /**
353 * Update meta file.
354 *
355 * @param string $metafile_path Metafile path.
356 */
357 public function update_meta_file( $metafile_path ) {
358 $this->remove_meta_files();
359 $metafile_contents = $this->build_metafile_contents();
360 file_put_contents( $metafile_path, $metafile_contents );
361 }
362
363 /**
364 * Remove meta files.
365 */
366 public function remove_meta_files() {
367 $metafile_name = $this->metafile_name;
368 $metafiles = glob( $this->file_handler->file_manager->upload_dir . "/*{$metafile_name}*.txt" );
369
370 foreach ( $metafiles as $metafile ) {
371 if ( is_writable( $metafile ) ) {
372 // phpcs:disable WordPress.PHP.DevelopmentFunctions
373 set_error_handler(
374 function () {
375 return false;
376 }
377 );
378 unlink( $metafile );
379 restore_error_handler();
380 // phpcs:enabled WordPress.PHP.DevelopmentFunctions
381 }
382 }
383 }
384
385 /**
386 * Retrieve logs.
387 */
388 public function get_logs() {
389 return $this->logs;
390 }
391
392 /**
393 * Retrieve enabled state.
394 */
395 public function is_enabled() {
396 return $this->enabled;
397 }
398
399 /**
400 * Generate file name.
401 *
402 * @param array $log_info Log info array.
403 */
404 protected function file_name( $log_info ) {
405 $date = gmdate( get_option( 'date_format' ), strtotime( $log_info['date'] ) );
406 $number = $log_info['number'];
407 $name = esc_html( $date );
408 if ( $number > 1 ) {
409 $name .= ' (' . esc_html( $number ) . ')';
410 }
411 return $name;
412 }
413
414 /**
415 * Build log key from log in logs list.
416 *
417 * @param object $item Log object.
418 */
419 protected function build_log_key( $item ) {
420 $date = $item['date'];
421 $number = $item['number'];
422 return "$date-$number";
423 }
424
425 /**
426 * Generate server statistics file.
427 */
428 protected function build_metafile_contents() {
429 $contents = "### This file is included in log downloads ###\n\n";
430
431 global $wpdb;
432 global $wp_version;
433
434 if ( method_exists( $wpdb, 'db_version' ) ) {
435 $mysql = preg_replace( '/[^0-9.].*/', '', $wpdb->db_version() );
436 } else {
437 $mysql = 'N/A';
438 }
439 $wp = $wp_version;
440 $php = phpversion();
441 $multisite = is_multisite();
442
443 $contents .= "### Server ###\n\n";
444 $contents .= "WordPress - $wp\n";
445 $contents .= "PHP - $php\n";
446 $contents .= "MySQL - $mysql\n\n";
447
448 $active_plugins = get_option( 'active_plugins' );
449 $all_plugins = get_plugins();
450 $plugins = array();
451
452 $contents .= "### Active Plugins ###\n\n";
453
454 foreach ( $all_plugins as $plugin_folder => $plugin_data ) {
455 if ( in_array( $plugin_folder, $active_plugins, true ) ) {
456 $contents .= "{$plugin_data["Name"]} - {$plugin_data["Version"]}\n";
457 }
458 }
459
460 $contents .= "\n### WP Discourse Settings (Secrets Excluded) ###\n\n";
461 $excluded_keys = array(
462 'url',
463 'key',
464 'secret',
465 'text',
466 'publish-username',
467 'publish-category',
468 'publish-failure-email',
469 'login-path',
470 'existing-comments-heading',
471 'sso-client-login-form-redirect',
472 );
473
474 foreach ( $this->get_options() as $key => $value ) {
475 $exclude = false;
476
477 foreach ( $excluded_keys as $excluded_key ) {
478 if ( strpos( $key, $excluded_key ) !== false ) {
479 $exclude = true;
480 }
481 }
482
483 if ( ! $exclude ) {
484 if ( is_array( $value ) ) {
485 $value = implode( ',', $value );
486 }
487 $contents .= "$key - $value\n";
488 }
489 }
490
491 return $contents;
492 }
493
494 /**
495 * Get metafile name.
496 *
497 * @param string $filename Metafile filename.
498 */
499 protected function get_metafile_path( $filename ) {
500 $metafile_dir = $this->file_handler->file_manager->upload_dir;
501 return "$metafile_dir/{$filename}";
502 }
503
504 /**
505 * Build date range.
506 *
507 * @param array $log_files List of log files.
508 */
509 protected function build_date_range( $log_files ) {
510 $log_values = array_values( $log_files );
511 $newest_file = reset( $log_files );
512 $oldest_file = end( $log_values );
513 $date_end = $this->file_handler->get_date_from_url( $newest_file );
514 $date_start = $this->file_handler->get_date_from_url( $oldest_file );
515 return "$date_start-$date_end";
516 }
517 }
518