PluginProbe
Debug Log Viewer / 2.2.3
Debug Log Viewer v2.2.3
2.5 2.5.1 2.5.2 2.4 trunk 1.0.2 1.0.3 1.1 1.1.1 1.2 1.2.1 1.3 1.4 1.4.1 1.4.2 1.4.3 2.0.1 2.0.3 2.0.4 2.0.5 2.1 2.2.3
debug-log-viewer / admin / controllers / LogController.php

LogController.php in Debug Log Viewer 2.2.3, at admin/controllers/LogController.php

578 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace DebugLogViewer\Admin\Controllers;
4
5 if (!defined('ABSPATH')) {
6 exit; // Exit if accessed directly
7 }
8
9 use DebugLogViewer\Admin\Traits\ScheduleTrait;
10 use DebugLogViewer\Admin\Models\LogModel;
11 use DebugLogViewer\Admin\Helpers\Utils;
12 use DebugLogViewer\Admin\Services\LogCleanupService;
13
14 require_once realpath(__DIR__) . '/../../vendor/autoload.php';
15 require_once realpath(__DIR__) . '/BaseController.php';
16 require_once realpath(__DIR__) . '/LiveUpdatesController.php';
17 require_once realpath(__DIR__) . '/../../admin/models/LogModel.php';
18 require_once realpath(__DIR__) . '/../../front/views/pages/index.tpl.php';
19 require_once realpath(__DIR__) . '/../../admin/helpers/utils.php';
20 require_once realpath(__DIR__) . '/../../admin/traits/ScheduleTrait.php';
21 require_once realpath(__DIR__) . '/../../admin/services/Email.php';
22
23 class LogController extends BaseController {
24
25 use ScheduleTrait;
26
27 const SCHEDULE_MAIL_SEND = 'DBG_LV_NOTIFY_LOG_CONTROLLER';
28
29 private $configEditor;
30 private $logModel;
31 private $notificationController;
32
33 public function __construct( ?LogModel $logModel = null, ?AlertController $notificationController = null ) {
34
35 $this->logModel = $logModel ?? new LogModel();
36 $this->notificationController = $notificationController ?? new AlertController();
37 try {
38 $this->configEditor = new \WPConfigTransformer($this->logModel->getWpConfigPath());
39 } catch (\Exception $error) {
40 // Please, make sure permissions and path is correct.
41 // The plugin need an access to wp-config.php to manage debugging constants
42 }
43 }
44
45 public static function isCustomLoggingPath() {
46 return LogModel::isCustomLoggingPath();
47 }
48
49 public function firstRunEnableLogging() {
50 // phpcs:ignore WordPress.Security.NonceVerification.Missing
51 $this->verifyAjaxRequest();
52
53 try {
54
55 $path = $this->logModel->getLogFilePath();
56 $this->configEditor->update('constant', 'WP_DEBUG', '1');
57
58 if (!self::isCustomLoggingPath()) {
59 $this->configEditor->update('constant', 'WP_DEBUG_LOG', '1');
60 }
61
62 if (filesize($path) == 0) {
63 $message = __('This is a demo entry. Debugging is enabled. Any notices, warnings, or errors that occur on your site will appear here.', 'debug-log-viewer');
64 $demo_string = '[' . gmdate('d-M-Y H:i:s T') . '] PHP Notice: <b>' . $message . '</b> in ' . Utils::getDocumentRoot() . "/example.php on line 0\n";
65 file_put_contents($path, $demo_string);
66 }
67
68 wp_send_json_success();
69 } catch (\Exception $e) {
70 wp_send_json_error($e->getMessage());
71 }
72 }
73
74 public function toggleDebugMode() {
75 // phpcs:ignore WordPress.Security.NonceVerification.Missing
76 $this->verifyAjaxRequest();
77
78 $state = $this->prepareState();
79
80 try {
81 $this->configEditor->update('constant', 'WP_DEBUG', $state);
82
83 wp_send_json_success([
84 'state' => $this->stateToText($state),
85 ]);
86 } catch (\Exception $e) {
87 wp_send_json_error($e->getMessage());
88 }
89 }
90
91 public function toggleDebugScripts() {
92 // phpcs:ignore WordPress.Security.NonceVerification.Missing
93 $this->verifyAjaxRequest();
94
95 $state = $this->prepareState();
96
97 try {
98 $this->configEditor->update('constant', 'SCRIPT_DEBUG', $state);
99
100 wp_send_json_success([
101 'state' => $this->stateToText($state),
102 ]);
103 } catch (\Exception $e) {
104 wp_send_json_error($e->getMessage());
105 }
106 }
107
108 public function toggleLogInFile() {
109 // phpcs:ignore WordPress.Security.NonceVerification.Missing
110 $this->verifyAjaxRequest();
111
112 $state = $this->prepareState();
113
114 try {
115 if ($state == '1') {
116 $this->configEditor->update('constant', 'WP_DEBUG', $state);
117 }
118
119 $this->configEditor->update('constant', 'WP_DEBUG_LOG', $state);
120
121 wp_send_json_success([
122 'state' => $this->stateToText($state),
123 ]);
124 } catch (\Exception $e) {
125 wp_send_json_error($e->getMessage());
126 }
127 }
128
129 public function toggleDisplayErrors() {
130 // phpcs:ignore WordPress.Security.NonceVerification.Missing
131 $this->verifyAjaxRequest();
132
133 $state = $this->prepareState();
134
135 try {
136 if ($state == '1') {
137 $this->configEditor->update('constant', 'WP_DEBUG', $state);
138 }
139
140 $this->configEditor->update('constant', 'WP_DEBUG_DISPLAY', $state);
141
142 wp_send_json_success([
143 'state' => $this->stateToText($state),
144 ]);
145 } catch (\Exception $e) {
146 wp_send_json_error($e->getMessage());
147 }
148 }
149
150 public function clear() {
151 // phpcs:ignore WordPress.Security.NonceVerification.Missing
152 $this->verifyAjaxRequest();
153
154 try {
155 $path = $this->logModel->getLogFilePath();
156
157 if (is_file($path) && file_exists($path)) {
158
159 if (is_writable($path)) {
160 file_put_contents($path, '');
161
162 update_option(LogModel::LAST_POSITION_OPTION_NAME, 0); // reset the stored position
163
164 wp_send_json_success();
165 }
166
167 throw new \Exception(__('The log file was found but cannot be cleared due to missing write permissions', 'debug-log-viewer'));
168 }
169 throw new \Exception(__('The log file was not found and cannot be removed', 'debug-log-viewer'));
170 } catch (\Exception $e) {
171 wp_send_json_error($e->getMessage());
172 }
173 }
174
175 public function download() {
176
177 // phpcs:ignore WordPress.Security.NonceVerification.Missing
178 $this->verifyAjaxRequest();
179
180 try {
181 $path = $this->logModel->getLogFilePath();
182
183 if (is_file($path) && file_exists($path)) {
184
185 $basename = basename($path);
186 $filesize = filesize($path);
187
188 header('Content-Description: File Transfer');
189 header('Content-Type: text/plain');
190 header('Cache-Control: no-cache, must-revalidate');
191 header('Expires: 0');
192 header("Content-Disposition: attachment; filename=$basename");
193 header("Content-Length: $filesize");
194 header('Pragma: public');
195
196 flush();
197
198 readfile($path);
199 wp_die();
200 }
201
202 throw new \Exception(__('The log file was not found and cannot be removed', 'debug-log-viewer'));
203 } catch (\Exception $e) {
204 wp_send_json_error($e->getMessage());
205 }
206 }
207
208 public function getCurrentUserEmail() {
209 // phpcs:ignore WordPress.Security.NonceVerification.Missing
210 $this->verifyAjaxRequest();
211
212 try {
213 global $current_user;
214
215 wp_send_json_success($current_user->user_email);
216 } catch (\Exception $e) {
217 wp_send_json_error($e->getMessage());
218 }
219 }
220
221 public function prepareState() {
222 if (!isset($_POST['state'])) {
223 throw new \Exception('Empty state passed');
224 }
225
226 $state = sanitize_text_field(wp_unslash($_POST['state']));
227 switch ( (int) $state) {
228 case 0:
229 case 1:
230 return (string) $state;
231 default:
232 throw new \Exception(esc_html__('An incorrect state value was passed', 'debug-log-viewer'));
233 }
234 }
235
236 public function getEntriesByLevel( $recurrence ) {
237 $rows = $this->logModel->getParsedContent();
238
239 if (!$rows) {
240 return;
241 }
242
243 $errors = [];
244 foreach ($this->notificationController->getLevelsToReport() as $level) {
245 $errors[ $level ] = [];
246 }
247
248 $new_lines = 0;
249 foreach ($rows as $row) {
250 if (empty($row)) {
251 continue;
252 }
253
254 $datetime = LogModel::getDatetime($row);
255 $datetimeOffset = new \DateTime();
256 $datetimeOffset->sub($this->getScheduleInterval($recurrence));
257 $datetimeError = new \DateTime();
258 $strToTime = strtotime($datetime);
259 $datetimeError->setTimestamp($strToTime);
260
261 if ($datetimeError < $datetimeOffset) {
262 continue;
263 }
264
265 $type = LogModel::getType($row);
266
267 if (!in_array($type, $this->notificationController->getLevelsToReport())) {
268 continue;
269 }
270
271 $line = LogModel::getLine($row);
272 $file = LogModel::getFile($row);
273 $text = LogModel::getDescription($row);
274 $hash = LogModel::buildEntryHash($type, $text, $file, $line);
275
276 if (array_key_exists($hash, $errors[ $type ])) {
277 $errors[ $type ][ $hash ]['hits'] += 1;
278 } else {
279 $errors[ $type ][ $hash ] = [
280 'timestamp' => $strToTime * 1000,
281 'datetime' => $datetime,
282 'line' => $line,
283 'file' => $file,
284 'type' => $type,
285 'description' => [
286 'text' => $text,
287 'stack_trace' => LogModel::getStackTrace($row),
288 ],
289 'hits' => 1,
290 ];
291 }
292 $new_lines += 1;
293 }
294
295 return $new_lines ? $errors : null;
296 }
297
298 public function changeLogsUpdateMode() {
299 // phpcs:ignore WordPress.Security.NonceVerification.Missing
300 $this->verifyAjaxRequest();
301
302 $mode = isset($_POST['mode']) ? sanitize_text_field(wp_unslash($_POST['mode'])) : null;
303
304 if (!$mode) {
305 wp_send_json_error(__('The "mode" parameter is missing from the request', 'debug-log-viewer'), 400);
306 }
307
308 $allowed_modes = [ 'AUTO', 'MANUAL' ];
309 if (!in_array($mode, $allowed_modes, true)) {
310 wp_send_json_error(__('The specified mode is invalid. Please select either "AUTO" or "MANUAL"', 'debug-log-viewer'), 400);
311 }
312
313 // Retrieve the current mode setting
314 $current_mode = get_option(LogModel::LOG_UPDATES_MODE_OPTION_NAME);
315 if ($current_mode === $mode) {
316 // If the current value matches the new value, consider it a success
317 wp_send_json_success(__('The updates mode has been successfully updated', 'debug-log-viewer'));
318 }
319
320 // Attempt to update the mode setting
321 if (update_option(LogModel::LOG_UPDATES_MODE_OPTION_NAME, $mode)) {
322 wp_send_json_success(__('The update mode has been successfully updated', 'debug-log-viewer'));
323 } else {
324 wp_send_json_error(__('An error occurred while updating the update mode. Please try again', 'debug-log-viewer'), 500);
325 }
326 }
327
328 public function changeDatetimeFormat() {
329 // phpcs:ignore WordPress.Security.NonceVerification.Missing
330 $this->verifyAjaxRequest();
331
332 $format = isset($_POST['format']) ? sanitize_text_field(wp_unslash($_POST['format'])) : null;
333
334 if (!$format) {
335 wp_send_json_error(__('The "format" parameter is missing from the request', 'debug-log-viewer'), 400);
336 }
337
338 $allowed_formats = [ 'ABSOLUTE', 'RELATIVE' ];
339 if (!in_array($format, $allowed_formats, true)) {
340 wp_send_json_error(__('The specified format is invalid. Please select either "ABSOLUTE" or "RELATIVE"', 'debug-log-viewer'), 400);
341 }
342
343 // Retrieve the current format setting
344 $current_format = get_option(LogModel::DATETIME_FORMAT_OPTION_NAME);
345 if ($current_format === $format) {
346 // If the current value matches the new value, consider it a success
347 wp_send_json_success(__('The datetime format has been successfully updated', 'debug-log-viewer'));
348 }
349
350 // Attempt to update the format setting
351 if (update_option(LogModel::DATETIME_FORMAT_OPTION_NAME, $format)) {
352 wp_send_json_success(__('The datetime format has been successfully updated', 'debug-log-viewer'));
353 } else {
354 wp_send_json_error(__('An error occurred while updating the datetime format. Please try again', 'debug-log-viewer'), 500);
355 }
356 }
357
358 /**
359 * Handle timezone change request.
360 *
361 * @return void
362 */
363 public function changeTimezone() {
364 // phpcs:ignore WordPress.Security.NonceVerification.Missing
365 $this->verifyAjaxRequest();
366
367 $timezone = isset($_POST['timezone']) ? sanitize_text_field(wp_unslash($_POST['timezone'])) : null;
368
369 if ($timezone === null || (!empty($timezone) && false === LogModel::getDateTimeZone($timezone))) {
370 wp_send_json_error('Invalid timezone', 400);
371 }
372
373 update_user_meta(get_current_user_id(), LogModel::TIMEZONE_OPTION_NAME, $timezone);
374 wp_send_json_success(__('Timezone updated', 'debug-log-viewer'));
375 }
376
377 public function changeGroupEntries() {
378 // phpcs:ignore WordPress.Security.NonceVerification.Missing
379 $this->verifyAjaxRequest();
380
381 $value = isset($_POST['value']) ? sanitize_text_field(wp_unslash($_POST['value'])) : null;
382
383 if (!in_array($value, [ '0', '1' ], true)) {
384 wp_send_json_error(__('Invalid value', 'debug-log-viewer'), 400);
385 }
386
387 update_user_meta(get_current_user_id(), LogModel::GROUP_ENTRIES_OPTION_NAME, $value);
388 wp_send_json_success(__('Setting updated', 'debug-log-viewer'));
389 }
390
391 public function isDebugLogPubliclyAccessible() {
392 // phpcs:ignore WordPress.Security.NonceVerification.Missing
393 $this->verifyAjaxRequest();
394
395 try {
396 $path = $this->logModel->getLogFilePath();
397
398 // If file doesn't exist, it's not accessible
399 if (!file_exists($path)) {
400 wp_send_json_success(['is_public' => false]);
401 }
402 // Convert file path to URL
403 $site_url = site_url();
404 $site_path = wp_normalize_path(ABSPATH);
405 $log_path = wp_normalize_path($path);
406
407 // Only proceed if the log is within the WordPress directory structure
408 if (strpos($log_path, $site_path) === 0) {
409 // Get the relative path and convert to URL
410 $relative_path = str_replace($site_path, '', $log_path);
411 $log_url = trailingslashit($site_url) . ltrim($relative_path, '/');
412
413 // Use HEAD request instead of GET (much more efficient)
414 $response = wp_remote_head($log_url, [
415 'timeout' => 3, // Short timeout is sufficient for HEAD request
416 'sslverify' => false,
417 'redirection' => 0,
418 ]);
419
420 if (!is_wp_error($response)) {
421 $is_public = wp_remote_retrieve_response_code($response) === 200;
422 // If the response is 200, the file is publicly accessible
423 if ($is_public) {
424 wp_send_json_success([
425 'is_public' => true,
426 'info' => [
427 'icon' => 'fa-globe',
428 'type' => 'danger',
429 'title' => __('Public Access', 'debug-log-viewer'),
430 'message' => __('Log file is publicly accessible via URL', 'debug-log-viewer'),
431 'key' => 'dbg_lv_public_log_alert_dismissed',
432 ]
433 ]);
434 } else {
435 // If the response is not 200, the file is not publicly accessible
436 wp_send_json_success(['is_public' => false]);
437 }
438 }
439 }
440
441 wp_send_json_success(['is_public' => false]);
442 } catch (\Exception $e) {
443 wp_send_json_error($e->getMessage());
444 }
445 }
446
447 private function stateToText( $state ): string {
448 return (int) $state ? __('ON', 'debug-log-viewer') : __('OFF', 'debug-log-viewer');
449 }
450
451 public function getLogFileWarnings() {
452 $path = $this->logModel->getLogFilePath();
453
454 // Get file information and check for warnings
455 $file_exists = file_exists($path);
456 if(!$file_exists) {
457 return [];
458 }
459
460 $warnings = [];
461
462 // Check file size
463 $file_size = filesize($path);
464 if ($file_size > LogModel::LOG_FILE_LIMIT) {
465 $limit = $this->logModel->getLogLimit(LogModel::UNIT_MEGABYTES, true);
466
467 $warnings[] = [
468 'icon' => 'fa-database',
469 'type' => 'warning',
470 'title' => __('Large File Size', 'debug-log-viewer'),
471 /* translators: %1$s: file size limit, %2$s: amount of content displayed */
472 'message' => sprintf(__('The file exceeds the %1$s limit, only the last %2$s will be displayed.', 'debug-log-viewer'), $limit, $limit)
473 ];
474 }
475
476 return $warnings;
477 }
478
479 public function getWarningsSeverity() {
480 $warnings = $this->getLogFileWarnings();
481
482 if (empty($warnings)) {
483 return 'none';
484 }
485
486 // Check for danger level warnings (public access)
487 foreach ($warnings as $warning) {
488 if ($warning['type'] === 'danger') {
489 return 'danger';
490 }
491 }
492
493 // Default level is warning
494 return 'warning';
495 }
496
497 /**
498 * Get popover data for JavaScript rendering
499 * @return array Data for wp_localize_script
500 */
501 public function getPopoverData() {
502 $path = $this->logModel->getLogFilePath();
503 $file_exists = file_exists($path);
504
505 $fileInfo = [];
506
507 if ($file_exists) {
508 $last_modified = filemtime($path);
509 $file_size = $this->logModel->getLogSize(['with_measure_units' => true]);
510 $permissions = substr(sprintf('%o', fileperms($path)), -4);
511 $is_readable = is_readable($path);
512 $is_writable = is_writable($path);
513
514 $fileInfo = [
515 'fileSize' => $file_size,
516 'lastModified' => $last_modified ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $last_modified) : __('Unknown', 'debug-log-viewer'),
517 'permissions' => $permissions . ' (' . ($is_readable ? 'R' : '-') . ($is_writable ? 'W' : '-') . ')',
518 ];
519 }
520
521 $warnings = $this->getLogFileWarnings();
522
523 return [
524 'fileInfo' => $fileInfo,
525 'warnings' => $warnings
526 ];
527 }
528
529 /**
530 * Save cleanup settings
531 */
532 public function saveCleanupSettings() {
533 // Verify nonce
534 $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
535 if (!wp_verify_nonce($nonce, 'dbg_lv_save_cleanup_settings')) {
536 wp_send_json_error(array('message' => __('Security check failed', 'debug-log-viewer')), 403);
537 }
538
539 // Check user capabilities
540 if (!current_user_can('manage_options')) {
541 wp_send_json_error(array('message' => __('You do not have permission to perform this action', 'debug-log-viewer')), 403);
542 }
543
544 // Collect settings
545 $settings = array(
546 'cleanup_enabled' => isset($_POST['cleanup_enabled']) && $_POST['cleanup_enabled'] == '1',
547 'max_log_size' => isset($_POST['max_log_size']) ? absint($_POST['max_log_size']) : 50,
548 'cleanup_method' => isset($_POST['cleanup_method']) ? sanitize_text_field(wp_unslash($_POST['cleanup_method'])) : 'keep_last',
549 'retention_percentage' => isset($_POST['retention_percentage']) ? absint($_POST['retention_percentage']) : 10,
550 'email_notifications' => isset($_POST['email_notifications']) && $_POST['email_notifications'] == '1',
551 );
552
553 $validated = LogCleanupService::validate_settings($settings);
554
555 // Save to database
556 update_option(LogCleanupService::OPTION_CLEANUP_ENABLED, $validated['cleanup_enabled']);
557 update_option(LogCleanupService::OPTION_MAX_LOG_SIZE, $validated['max_log_size']);
558 update_option(LogCleanupService::OPTION_CLEANUP_METHOD, $validated['cleanup_method']);
559 update_option('dlv_cleanup_notifications_enabled', $validated['email_notifications'] ?? false);
560 update_option(LogCleanupService::OPTION_RETENTION_PERCENT, $validated['retention_percentage']);
561
562 // Handle cron scheduling
563 $cleanup_controller = new CleanupController();
564
565 if ($validated['cleanup_enabled']) {
566 $schedule = isset($_POST['cleanup_schedule']) ? sanitize_text_field(wp_unslash($_POST['cleanup_schedule'])) : 'daily';
567 $cleanup_controller->schedule_cleanup($schedule);
568 } else {
569 $cleanup_controller->unschedule_cleanup();
570 }
571
572 wp_send_json_success(array(
573 'message' => __('Auto-Cleanup settings saved successfully.', 'debug-log-viewer'),
574 'settings' => $validated,
575 ));
576 }
577 }
578