PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / DebugLogFileStore.php

DebugLogFileStore.php in 404 Solution 4.3.0, at includes/diagnostics/DebugLogFileStore.php

197 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 // allow-no-test-found: covered by tests/LoggingTest.php public Logging entry-point tests for debug writes and log rotation.
9
10 /**
11 * File-store for the plugin debug log.
12 *
13 * Owns debug-log path derivation, sanitized line writes, rotation/deletion,
14 * size accounting, and the dedupe-pointer reset that must happen whenever
15 * line numbers become invalid. It intentionally does not decide when to log
16 * or send feedback reports; ABJ_404_Solution_Logging remains that facade.
17 */
18 class ABJ_404_Solution_DebugLogFileStore {
19
20 /** @var callable */
21 private $sanitizeLogLine;
22
23 /** @var string */
24 private $debugFileKeyOptionName;
25
26 /** @var string */
27 private $lastSentLineOptionName;
28
29 /**
30 * @param callable $sanitizeLogLine Receives a raw line and returns the sanitized line to write.
31 * @param string $debugFileKeyOptionName Option key that stores the debug-file suffix.
32 * @param string $lastSentLineOptionName Option key that stores the last sent debug-log line.
33 */
34 public function __construct(callable $sanitizeLogLine, string $debugFileKeyOptionName, string $lastSentLineOptionName) {
35 $this->sanitizeLogLine = $sanitizeLogLine;
36 $this->debugFileKeyOptionName = $debugFileKeyOptionName;
37 $this->lastSentLineOptionName = $lastSentLineOptionName;
38 }
39
40 /**
41 * Write one sanitized line to the active debug file.
42 *
43 * @param string $line
44 * @param string $debugFilePath
45 * @return bool True on success, false on disk/permission failure.
46 */
47 public function writeLine(string $line, string $debugFilePath): bool {
48 $sanitizedLine = (string)call_user_func($this->sanitizeLogLine, $line);
49 $result = @file_put_contents($debugFilePath, $sanitizedLine . "\n", FILE_APPEND);
50
51 if ($result === false) {
52 abj404_logPhpFallback(
53 'logger-internal',
54 'Unable to write to debug log (possibly disk full): ' . $debugFilePath
55 );
56 return false;
57 }
58
59 return true;
60 }
61
62 /** @return string */
63 public function getDebugFilePath(): string {
64 return $this->getFilePathAndMoveOldFile(abj404_getUploadsDir(), $this->getDebugFilename());
65 }
66
67 /** @return string */
68 public function getDebugFilename(): string {
69 try {
70 $optionsRepo = abj_service('options_repository');
71 if (!is_object($optionsRepo) || !method_exists($optionsRepo, 'getOptions')) {
72 return 'abj404_debug.txt';
73 }
74 $options = $optionsRepo->getOptions(true);
75 $debugFileKey = null;
76 if (is_array($options) && array_key_exists($this->debugFileKeyOptionName, $options)) {
77 $debugFileKey = is_string($options[$this->debugFileKeyOptionName])
78 ? $options[$this->debugFileKeyOptionName] : null;
79 }
80 if ($debugFileKey === null || trim($debugFileKey) === '') {
81 $this->deleteDebugFile();
82
83 $syncUtils = abj_service('sync_utils');
84 if (!is_object($syncUtils) || !method_exists($syncUtils, 'uniqidReal')) {
85 return 'abj404_debug.txt';
86 }
87 $debugFileKey = $syncUtils->uniqidReal();
88 $options[$this->debugFileKeyOptionName] = $debugFileKey;
89 if (method_exists($optionsRepo, 'updateOptions')) {
90 $optionsRepo->updateOptions($options);
91 }
92 }
93
94 return 'abj404_debug_' . $debugFileKey . '.txt';
95 } catch (\Throwable $e) { // allow-silent-catch: debug filename derivation; fallback name keeps logging available during degraded boot
96 return 'abj404_debug.txt';
97 }
98 }
99
100 /** @return string */
101 public function getDebugFilePathOld(): string {
102 return $this->getDebugFilePath() . "_old.txt";
103 }
104
105 /** @return string */
106 public function getDebugFilePathSentFile(): string {
107 return $this->getFilePathAndMoveOldFile(abj404_getUploadsDir(), 'abj404_debug_sent_line.txt');
108 }
109
110 /** @return string */
111 public function getZipFilePath(): string {
112 return $this->getFilePathAndMoveOldFile(abj404_getUploadsDir(), 'abj404_debug.zip');
113 }
114
115 /**
116 * Create the storage directory and migrate a legacy root-level file if present.
117 *
118 * @param string $directory
119 * @param string $filename
120 * @return string
121 */
122 public function getFilePathAndMoveOldFile($directory, $filename): string {
123 if (!ABJ_404_Solution_FileSystemService::createDirectoryWithErrorMessages($directory)) {
124 return ABJ404_PATH . $filename;
125 }
126
127 if (file_exists(ABJ404_PATH . $filename)) {
128 rename(ABJ404_PATH . $filename, $directory . $filename);
129 }
130
131 return $directory . $filename;
132 }
133
134 /** @return void */
135 public function limitDebugFileSize(string $sentFilePath, string $oldDebugFilePath, string $debugFilePath): void {
136 if (file_exists($sentFilePath)) {
137 ABJ_404_Solution_FileSystemService::safeUnlink($sentFilePath);
138 }
139
140 $this->removeLastSentErrorLineFromDatabase();
141 ABJ_404_Solution_FileSystemService::safeUnlink($oldDebugFilePath);
142 rename($debugFilePath, $oldDebugFilePath);
143 }
144
145 /** @return void */
146 public function removeLastSentErrorLineFromDatabase(): void {
147 $optionsRepo = abj_service('options_repository');
148 $options = $optionsRepo->getOptions(true);
149 $options[$this->lastSentLineOptionName] = 0;
150 $optionsRepo->updateOptions($options);
151 }
152
153 /** @return bool true if every matching debug file was deleted. */
154 public function deleteDebugFile(): bool {
155 $allIsWell = true;
156
157 if (file_exists($this->getDebugFilePathSentFile())) {
158 ABJ_404_Solution_FileSystemService::safeUnlink($this->getDebugFilePathSentFile());
159 }
160 $this->removeLastSentErrorLineFromDatabase();
161
162 $uploadDir = abj404_getUploadsDir();
163 if (is_dir($uploadDir)) {
164 $files = glob($uploadDir . '/abj404_debug_*.txt');
165 if (!is_array($files)) {
166 $files = array();
167 }
168 foreach ($files as $file) {
169 if (is_file($file) && !ABJ_404_Solution_FileSystemService::safeUnlink($file)) {
170 $allIsWell = false;
171 }
172 }
173 }
174
175 $optionsRepo = abj_service('options_repository');
176 $options = $optionsRepo->getOptions(true);
177 $options[$this->debugFileKeyOptionName] = null;
178 $optionsRepo->updateOptions($options);
179
180 return $allIsWell;
181 }
182
183 /** @return int file size in bytes */
184 public function getDebugFileSize(string $debugFilePath, string $oldDebugFilePath): int {
185 $file1Size = 0;
186 $file2Size = 0;
187 if (file_exists($debugFilePath)) {
188 $file1Size = (int)filesize($debugFilePath);
189 }
190 if (file_exists($oldDebugFilePath)) {
191 $file2Size = (int)filesize($oldDebugFilePath);
192 }
193
194 return $file1Size + $file2Size;
195 }
196 }
197