PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / site-error-logs / class-metasync-error-logs.php

class-metasync-error-logs.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at site-error-logs/class-metasync-error-logs.php

411 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The site error logs for the plugin.
10 *
11 * @since 1.0.0
12 * @package Metasync
13 * @subpackage Metasync/site-error-logs
14 * @author Engineering Team <[email protected]>
15 */
16 class Metasync_Error_Logs
17 {
18 /**
19 * The log file path
20 * @var string
21 */
22 private $log_file_path = null;
23
24 /**
25 * The error message if log cannot be displayed
26 * @var string
27 */
28 private $error_message = '';
29
30 /**
31 * Maximum log file size in bytes (10MB)
32 * @var int
33 */
34 private $max_log_size = 10485760; // 10MB
35
36 /**
37 * Show copy button - integrated into preview header
38 */
39 public function show_copy_button()
40 {
41 // Button is now shown in the preview header
42 // Keeping this method for backward compatibility
43 }
44
45 /**
46 * Show information about the error log file.
47 * Info is now integrated into the preview header
48 */
49 public function show_info()
50 {
51 // Info is now shown in the preview header
52 // Keeping this method for backward compatibility
53 }
54
55 /**
56 * Show strong message.
57 */
58 public function show_strong_message($message)
59 {
60 ?>
61 <strong class="error-log-cannot-display"><?php echo esc_html($message) ?></strong><br>
62 <?php
63 }
64
65 /**
66 * Show strong message.
67 */
68 public function show_code_html($code)
69 {
70 ?>
71 <code><?php echo esc_html($code); ?></code>
72 <?php
73 }
74
75 /**
76 * Show error log preview with sitemap-style layout
77 */
78 public function show_logs()
79 {
80 $log_file = $this->get_actual_log_path();
81 $log_content = $this->get_error_logs(-1); // Get all content to count lines
82 $lines = explode("\n", $log_content);
83 $total_lines = count(array_filter($lines, 'strlen')); // Count non-empty lines
84
85 // Get last 100 lines for preview
86 $preview_lines = array_slice($lines, -100);
87 $preview_content = implode("\n", $preview_lines);
88 $hidden_lines = max(0, $total_lines - 100);
89 ?>
90 <div class="metasync-sitemap-preview-container" style="margin-top: 20px;">
91 <div class="metasync-sitemap-preview-header" style="display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--dashboard-card-bg); border: 1px solid var(--dashboard-border); border-bottom: none; border-radius: 8px 8px 0 0;">
92 <span class="metasync-preview-label" style="font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; font-size: 13px; color: var(--dashboard-text-primary); font-weight: 500;">
93 📄 plugin_errors.log
94 <span style="color: var(--dashboard-text-secondary); font-weight: normal; margin-left: 8px;">
95 (<?php echo esc_html($this->get_human_number($this->wp_filesystem()->size($log_file))); ?> • <?php echo esc_html(number_format($total_lines)); ?> lines)
96 </span>
97 </span>
98 <div style="display: flex; gap: 8px;">
99 <button type="button" class="button button-small" id="copy-log-btn" style="display: flex; align-items: center; gap: 4px;">
100 <span class="dashicons dashicons-clipboard" style="font-size: 16px; width: 16px; height: 16px;"></span>
101 Copy to Clipboard
102 </button>
103 <a href="<?php echo esc_url(content_url('metasync_data/plugin_errors.log')); ?>" target="_blank" class="button button-small" style="display: flex; align-items: center; gap: 4px;">
104 <span class="dashicons dashicons-external" style="font-size: 16px; width: 16px; height: 16px;"></span>
105 View Full Log
106 </a>
107 </div>
108 </div>
109 <div class="metasync-sitemap-code-block" style="background: var(--dashboard-card-bg); border: 1px solid var(--dashboard-border); border-radius: 0 0 8px 8px; padding: 20px; overflow: auto; max-height: 500px;">
110 <pre id="error-log-content" style="margin: 0; padding: 0; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; font-size: 12px; line-height: 1.6; color: var(--dashboard-text-primary); white-space: pre-wrap; word-wrap: break-word;"><?php
111 if ($hidden_lines > 0) {
112 echo esc_html("... [" . number_format($hidden_lines) . " earlier lines hidden]\n\n");
113 }
114 echo esc_html($preview_content);
115 ?></pre>
116 </div>
117 </div>
118
119 <?php
120 }
121
122 /**
123 * Show message if the log cannot be established.
124 */
125 public function can_show_error_logs()
126 {
127 // Reset error message
128 $this->error_message = '';
129
130 // Create the error log file first
131 $file_path = $this->createErrorLog();
132
133 // Store the log file path for use in other methods
134 $this->log_file_path = $file_path;
135
136 // Use the created log file path instead of ini_get('error_log')
137 $log_file = $file_path;
138
139 if (empty($file_path)) {
140 $this->error_message = 'Unable to create error log file.';
141 return false;
142 }
143
144 $wp_filesystem = $this->wp_filesystem();
145
146 if (is_null($wp_filesystem)) {
147 $this->error_message = 'WordPress filesystem not available.';
148 return false;
149 }
150
151 if (!$wp_filesystem->exists($log_file)) {
152 $this->error_message = 'Error log file does not exist: ' . $log_file;
153 return false;
154 }
155
156 if (!$wp_filesystem->is_readable($log_file)) {
157 $this->error_message = 'Error log file is not readable.';
158 return false;
159 }
160
161 // Error log must be smaller than 100 MB.
162 $size = $wp_filesystem->size($log_file);
163 if ($size > 100000000) {
164 $wp_filesystem->delete($log_file);
165 $this->error_message = 'The error log cannot be retrieved: Error log file is too large.';
166 return false;
167 }
168
169 return true;
170 }
171
172 /**
173 * Get the error message if log cannot be displayed.
174 * @return string
175 */
176 public function get_error_message()
177 {
178 return $this->error_message;
179 }
180
181 /**
182 * WordPress filesystem for use.
183 *
184 * @return string
185 */
186 public function get_error_logs($limit = -1)
187 {
188 $wp_filesystem = $this->wp_filesystem();
189
190 // Use the actual log file path
191 $log_file = $this->get_actual_log_path();
192
193 if (empty($log_file) || !$wp_filesystem->exists($log_file)) {
194 return '';
195 }
196
197 $contents = $wp_filesystem->get_contents_array($log_file);
198
199 if (-1 === $limit) {
200 return join('', $contents);
201 }
202
203 return join('', array_slice($contents, -$limit));
204 }
205
206 /**
207 * WordPress filesystem for use.
208 *
209 * @return object|null
210 */
211 private function wp_filesystem()
212 {
213 global $wp_filesystem;
214
215 if (empty($wp_filesystem)) {
216 require_once ABSPATH . '/wp-admin/includes/file.php';
217 WP_Filesystem();
218 }
219
220 return $wp_filesystem;
221 }
222
223 /**
224 * WordPress filesystem, only when it writes to local disk directly.
225 * An FTP/SSH transport that could not connect fatals on first use.
226 *
227 * @return object|null
228 */
229 private function direct_filesystem()
230 {
231 $fs = $this->wp_filesystem();
232
233 return ($fs && isset($fs->method) && 'direct' === $fs->method) ? $fs : null;
234 }
235
236 /**
237 * Get error log file location.
238 *
239 * @return string
240 */
241 private function get_log_path()
242 {
243 return ini_get('error_log') != '' ? ini_get('error_log') : '';
244 }
245
246 /**
247 * Get the actual log file path (either stored or create a new one).
248 *
249 * @return string
250 */
251 private function get_actual_log_path()
252 {
253 // If we have a stored log file path, use it
254 if (!empty($this->log_file_path)) {
255 return $this->log_file_path;
256 }
257
258 // Otherwise, try to get the default log path
259 $log_file = $this->get_log_path();
260
261 // If default path is empty or doesn't exist, create one
262 if (empty($log_file)) {
263 $log_file = $this->createErrorLog();
264 $this->log_file_path = $log_file;
265 }
266
267 return $log_file;
268 }
269
270 /**
271 * Get human read number of units.
272 *
273 * @param int $bytes
274 * @return string
275 */
276 public function get_human_number(int $bytes)
277 {
278 if ($bytes >= 1073741824) {
279 $bytes = number_format($bytes / 1073741824, 2) . ' GB';
280 } elseif ($bytes >= 1048576) {
281 $bytes = number_format($bytes / 1048576, 2) . ' MB';
282 } elseif ($bytes >= 1024) {
283 $bytes = number_format($bytes / 1024, 2) . ' KB';
284 } elseif ($bytes > 1) {
285 $bytes = $bytes . ' bytes';
286 } elseif ($bytes == 1) {
287 $bytes = $bytes . ' byte';
288 } else {
289 $bytes = '0 bytes';
290 }
291
292 return $bytes;
293 }
294
295 /**
296 * Get or create the error log file path
297 * Points to /metasync_data/plugin_errors.log which is actively written by Log_Manager
298 *
299 * @return string|false Log file path or false on failure
300 */
301 public function createErrorLog()
302 {
303 // Directory creation, web-server protection, and writability
304 // guarantees live in Metasync_Data_Store so every writer agrees on
305 // the location; a false return means "no writable data directory".
306 if (false === Metasync_Data_Store::base_dir()) {
307 return false;
308 }
309
310 // Point to the actual log file being written by Log_Manager
311 $log_file = Metasync_Data_Store::file_path('plugin_errors.log');
312 if (false === $log_file) {
313 return false;
314 }
315
316 // Create file if it doesn't exist
317 if (!file_exists($log_file)) {
318 $fs = $this->direct_filesystem();
319 if (!$fs || !$fs->put_contents($log_file, '', defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644)) {
320 error_log('Metasync_Error_Logs: Failed to create log file: ' . $log_file); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
321 return false;
322 }
323 }
324
325 // Rotate log file if it's too large
326 $this->rotate_log_if_needed($log_file);
327
328 return $log_file;
329 }
330
331 /**
332 * Rotate log file if it exceeds maximum size
333 * Keeps the most recent entries and archives old ones
334 *
335 * @param string $log_file Path to the log file
336 * @return bool True if rotation occurred, false otherwise
337 */
338 private function rotate_log_if_needed($log_file)
339 {
340 if (!file_exists($log_file)) {
341 return false;
342 }
343
344 $file_size = filesize($log_file);
345
346 // If file is under the limit, no rotation needed
347 if ($file_size < $this->max_log_size) {
348 return false;
349 }
350
351 // Create backup of current log with timestamp
352 $backup_file = $log_file . '.old.' . gmdate('Y-m-d-His');
353
354 // Read last 50% of the file to keep recent entries
355 $wp_filesystem = $this->direct_filesystem();
356 if (!$wp_filesystem) {
357 return false;
358 }
359
360 $content = $wp_filesystem->get_contents($log_file);
361 if ($content === false) {
362 return false;
363 }
364
365 // Split into lines and keep the most recent half
366 $lines = explode("\n", $content);
367 $total_lines = count($lines);
368 $keep_lines = (int) ($total_lines / 2);
369
370 // Keep the most recent lines
371 $recent_content = implode("\n", array_slice($lines, -$keep_lines));
372
373 // Archive old content
374 $wp_filesystem->put_contents($backup_file, $content, FS_CHMOD_FILE);
375
376 // Write recent content back to main log
377 $wp_filesystem->put_contents($log_file, $recent_content, FS_CHMOD_FILE);
378
379 // Clean up old backup files (keep only last 3 backups)
380 $this->cleanup_old_backups(dirname($log_file));
381
382 return true;
383 }
384
385 /**
386 * Clean up old backup log files
387 * Keeps only the 3 most recent backup files
388 *
389 * @param string $directory Directory containing backup files
390 */
391 private function cleanup_old_backups($directory)
392 {
393 $backup_files = glob($directory . '/plugin_errors.log.old.*');
394
395 if (!$backup_files || count($backup_files) <= 3) {
396 return;
397 }
398
399 // Sort by modification time (newest first)
400 usort($backup_files, function($a, $b) {
401 return filemtime($b) - filemtime($a);
402 });
403
404 // Delete all but the 3 newest
405 $files_to_delete = array_slice($backup_files, 3);
406 foreach ($files_to_delete as $file) {
407 @wp_delete_file($file);
408 }
409 }
410 }
411