PluginProbe
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity / 3.3.10
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity v3.3.10
3.3.10 3.3.9 3.3.8 trunk 1.0 1.1.0 1.10.0 1.11.0 1.11.1 1.12.0 1.13.0 1.14.0 1.15.0 1.16.0 1.17.0 1.17.1 1.18.0 1.19.0 1.2.0 1.20.0 1.20.1 1.3.0 1.3.1 1.4.0 1.5.0 All 68 releases
logtivity / Base / Logtivity_Debug_Log.php

Logtivity_Debug_Log.php in Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity 3.3.10, at Base/Logtivity_Debug_Log.php

259 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @package Logtivity
5 * @contact logtivity.io, [email protected]
6 * @copyright 2024-2026 Logtivity. All rights reserved
7 * @license https://www.gnu.org/licenses/gpl.html GNU/GPL
8 *
9 * This file is part of Logtivity.
10 *
11 * Logtivity is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation, either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * Logtivity is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with Logtivity. If not, see <https://www.gnu.org/licenses/>.
23 */
24
25 /**
26 * Writes an append-only, size-capped history of Logtivity API calls (request and
27 * response data) to a protected file in the uploads directory when debug mode is
28 * enabled. This is separate from, and does not affect, the `logtivity_latest_response`
29 * option used to track connection status.
30 */
31 class Logtivity_Debug_Log
32 {
33 public const DIRECTORY_NAME = 'logtivity-logs';
34
35 public const FILE_NAME = 'debug.log';
36
37 public const MAX_BYTES = 5 * MB_IN_BYTES;
38
39 public const MAX_ENTRIES_RETURNED = 50;
40
41 /**
42 * @var ?WP_Filesystem_Base
43 */
44 protected $filesystem = null;
45
46 /**
47 * Append a log entry to the debug log file.
48 *
49 * @param array $entry
50 *
51 * @return void
52 */
53 public function write(array $entry): void
54 {
55 if (!$this->initFilesystem() || !$this->ensureDirectory()) {
56 return;
57 }
58
59 $path = $this->filePath();
60 $existing = $this->filesystem->exists($path) ? $this->filesystem->get_contents($path) : '';
61
62 $this->filesystem->put_contents(
63 $path,
64 $existing . wp_json_encode($entry) . "\n",
65 FS_CHMOD_FILE
66 );
67
68 $this->maybeTrim($path);
69 }
70
71 /**
72 * Get the most recent log entries, newest first.
73 *
74 * @param int $limit
75 *
76 * @return array
77 */
78 public function recent(int $limit = self::MAX_ENTRIES_RETURNED): array
79 {
80 if (!$this->initFilesystem() || !$this->filesystem->exists($this->filePath())) {
81 return [];
82 }
83
84 $lines = $this->readLines($this->filePath());
85 $entries = [];
86
87 foreach (array_slice($lines, -$limit) as $line) {
88 $entry = json_decode($line, true);
89 if (is_array($entry)) {
90 $entries[] = $entry;
91 }
92 }
93
94 return array_reverse($entries);
95 }
96
97 /**
98 * Get the raw contents of the debug log file, for downloading.
99 *
100 * @return string
101 */
102 public function contents(): string
103 {
104 if (!$this->initFilesystem() || !$this->filesystem->exists($this->filePath())) {
105 return '';
106 }
107
108 return (string)$this->filesystem->get_contents($this->filePath());
109 }
110
111 /**
112 * Empty the debug log file.
113 *
114 * @return bool
115 */
116 public function clear(): bool
117 {
118 if (!$this->initFilesystem() || !$this->ensureDirectory()) {
119 return false;
120 }
121
122 return (bool)$this->filesystem->put_contents($this->filePath(), '', FS_CHMOD_FILE);
123 }
124
125 /**
126 * Remove the debug log file and its containing directory (including the
127 * protective .htaccess/index.php, and anything else that happens to be in
128 * there) entirely.
129 *
130 * Lists whatever files are actually present with PHP's own scandir()
131 * (guaranteed to include dotfiles like .htaccess, unlike some
132 * WP_Filesystem::dirlist() implementations) and deletes each one before
133 * removing the directory itself, so the final directory delete never fails
134 * because something was left behind.
135 *
136 * @return bool
137 */
138 public function delete(): bool
139 {
140 if (!$this->initFilesystem()) {
141 return false;
142 }
143
144 $dir = $this->directoryPath();
145
146 if (!$this->filesystem->is_dir($dir)) {
147 return true;
148 }
149
150 foreach (scandir($dir) ?: [] as $file) {
151 if ($file === '.' || $file === '..') {
152 continue;
153 }
154
155 $this->filesystem->delete($dir . '/' . $file, true);
156 }
157
158 return (bool)$this->filesystem->delete($dir);
159 }
160
161 /**
162 * Trim the oldest entries if the file has grown past the size cap.
163 *
164 * @param string $path
165 *
166 * @return void
167 */
168 protected function maybeTrim(string $path): void
169 {
170 if ((int)$this->filesystem->size($path) <= static::MAX_BYTES) {
171 return;
172 }
173
174 $lines = $this->readLines($path);
175 $lines = array_slice($lines, (int)(count($lines) * 0.25));
176
177 $this->filesystem->put_contents(
178 $path,
179 implode("\n", $lines) . (empty($lines) ? '' : "\n"),
180 FS_CHMOD_FILE
181 );
182 }
183
184 /**
185 * @param string $path
186 *
187 * @return array
188 */
189 protected function readLines(string $path): array
190 {
191 $contents = $this->filesystem->get_contents($path);
192
193 return array_values(array_filter(explode("\n", trim((string)$contents))));
194 }
195
196 /**
197 * @return string
198 */
199 protected function filePath(): string
200 {
201 return $this->directoryPath() . '/' . static::FILE_NAME;
202 }
203
204 /**
205 * @return string
206 */
207 protected function directoryPath(): string
208 {
209 $uploads = wp_upload_dir();
210
211 return rtrim($uploads['basedir'], '/') . '/' . static::DIRECTORY_NAME;
212 }
213
214 /**
215 * Create the log directory, protected from direct web access, if it doesn't
216 * already exist.
217 *
218 * @return bool
219 */
220 protected function ensureDirectory(): bool
221 {
222 $dir = $this->directoryPath();
223
224 if ($this->filesystem->is_dir($dir)) {
225 return true;
226 }
227
228 if (!wp_mkdir_p($dir)) {
229 return false;
230 }
231
232 $this->filesystem->put_contents($dir . '/.htaccess', "deny from all\n", FS_CHMOD_FILE);
233 $this->filesystem->put_contents($dir . '/index.php', "<?php\n// Silence is golden.\n", FS_CHMOD_FILE);
234
235 return true;
236 }
237
238 /**
239 * @return bool
240 */
241 protected function initFilesystem(): bool
242 {
243 if ($this->filesystem) {
244 return true;
245 }
246
247 global $wp_filesystem;
248
249 if (!$wp_filesystem) {
250 require_once ABSPATH . 'wp-admin/includes/file.php';
251 WP_Filesystem();
252 }
253
254 $this->filesystem = $wp_filesystem ?: null;
255
256 return (bool)$this->filesystem;
257 }
258 }
259