PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
← All changes | logger/logger.php +135 -334 5.6.05.5.0 View file →
@@ -13,363 +13,164 @@
13 13 if (!defined('F12_DEBUG_LOG_LEVEL')) {
14 14 define('F12_DEBUG_LOG_LEVEL', 200);
15 15 }
16 16
17 -if (!class_exists('Forge12\Shared\Logger')) {
18 - class Logger implements LoggerInterface
19 - {
20 - private static $instance;
21 - private $log_file;
22 - private $log_level;
23 - private $log_dir;
17 +if ( ! class_exists( 'Forge12\Shared\Logger' ) ) {
18 + class Logger implements LoggerInterface {
19 + private static $instance;
20 + private $log_file;
21 + private $log_level;
22 + private $log_dir;
24 23
25 - const DEBUG = 100;
26 - const INFO = 200;
27 - const NOTICE = 250;
28 - const WARNING = 300;
29 - const ERROR = 400;
30 - const CRITICAL = 500;
24 + const DEBUG = 100;
25 + const INFO = 200;
26 + const NOTICE = 250;
27 + const WARNING = 300;
28 + const ERROR = 400;
29 + const CRITICAL = 500;
31 30
32 - private function __construct()
33 - {
34 - // Debug aus → Logger ist komplett deaktiviert
35 - if (!defined('F12_DEBUG') || !F12_DEBUG) {
36 - return;
37 - }
31 + private function __construct() {
32 + if ( ! defined( 'F12_DEBUG' ) || ! F12_DEBUG ) {
33 + // Wenn F12_DEBUG nicht definiert ist oder false, beenden wir.
34 + return;
35 + }
38 36
39 - // Sicher WordPress Upload-DIR holen
40 - $upload_dir = wp_upload_dir();
37 + $upload_dir = wp_upload_dir();
38 + $this->log_dir = $upload_dir['basedir'] . '/f12-logs';
41 39
42 - if (empty($upload_dir['basedir']) || !is_string($upload_dir['basedir'])) {
43 - $base = WP_CONTENT_DIR . '/uploads';
44 - } else {
45 - $base = $upload_dir['basedir'];
46 - }
40 + if ( ! file_exists( $this->log_dir ) ) {
41 + wp_mkdir_p( $this->log_dir );
42 + }
47 43
48 - // Pfad normalisieren
49 - $base = $this->normalizePath($base);
44 + // Tagesbasiert loggen
45 + $this->log_file = $this->log_dir . '/plugins-' . date('Y-m-d') . '.log';
50 46
51 - // Log-Ordner festlegen
52 - $this->log_dir = $this->normalizePath($base . '/f12-logs');
47 + // Setze Level je nach WP_DEBUG
48 + $this->log_level = F12_DEBUG_LOG_LEVEL;
49 + }
53 50
54 - // Falls Pfad relativ ist → absolut machen
55 - $this->log_dir = $this->ensureAbsolutePath($this->log_dir);
51 + public static function getInstance() {
52 + if ( ! self::$instance ) {
53 + self::$instance = new self();
54 + }
55 + return self::$instance;
56 + }
56 57
57 - // Sicherstellen, dass Verzeichnis existiert
58 - if (!is_dir($this->log_dir)) {
59 - wp_mkdir_p($this->log_dir);
60 - }
58 + private function sanitizeContext(array $context): array {
59 + foreach ($context as $key => $value) {
60 + if (in_array(strtolower($key), ['ip', 'user_ip'])) {
61 + $context[$key] = $this->mask_ip($value);
62 + }
63 + if (in_array(strtolower($key), ['email', 'user_email'])) {
64 + $context[$key] = $this->mask_email($value);
65 + }
66 + if (in_array(strtolower($key), ['password', 'pwd'])) {
67 + $context[$key] = $this->mask_password($value);
68 + }
69 + }
70 + return $context;
71 + }
61 72
62 - // The directory is inside uploads/ and therefore web-reachable.
63 - // A guessable name (plugins-<date>.log) was downloadable by
64 - // anyone on a live site. Deny rules first, unguessable file
65 - // names second — the latter also holds on servers that ignore
66 - // .htaccess (nginx).
67 - $secret = self::fileSecret();
68 - self::protectDirectory($this->log_dir);
69 - self::migrateGuessableFiles($this->log_dir, $secret);
73 + private function mask_password(string $password): string {
74 + return '********';
75 + }
70 76
71 - // Logdatei definieren
72 - $this->log_file = $this->normalizePath(
73 - self::logFilePath($this->log_dir, date('Y-m-d'), $secret)
74 - );
77 + private function mask_email(string $email): string {
78 + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
79 + return 'invalid';
80 + }
81 + [$user, $domain] = explode('@', $email, 2);
82 + $len = strlen($user);
83 + if ($len <= 2) {
84 + $maskedUser = substr($user, 0, 1) . '*';
85 + } else {
86 + $maskedUser = substr($user, 0, 1) . str_repeat('*', $len - 2) . substr($user, -1);
87 + }
88 + return $maskedUser . '@' . $domain;
89 + }
75 90
76 - // Logdatei 100% absolut sicher machen
77 - $this->log_file = $this->ensureAbsolutePath($this->log_file);
91 + private function mask_ip(string $ip): string {
92 + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
93 + $parts = explode('.', $ip);
94 + $parts[3] = '0';
95 + return implode('.', $parts);
96 + }
97 + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
98 + return substr($ip, 0, 20) . '::';
99 + }
100 + return 'unknown';
101 + }
78 102
79 - // Log-Level
80 - $this->log_level = defined('F12_DEBUG_LOG_LEVEL')
81 - ? F12_DEBUG_LOG_LEVEL
82 - : self::INFO;
83 - }
103 + private function rotateLogs(int $maxSize = 52428800, int $maxFiles = 5): void {
104 + if (file_exists($this->log_file) && filesize($this->log_file) > $maxSize) {
105 + for ($i = $maxFiles - 1; $i >= 1; $i--) {
106 + $old = $this->log_file . '.' . $i;
107 + $new = $this->log_file . '.' . ($i + 1);
108 + if (file_exists($old)) {
109 + rename($old, $new);
110 + }
111 + }
112 + rename($this->log_file, $this->log_file . '.1');
113 + }
114 + }
84 115
85 - /**
86 - * Daily log file: plugins-<date>-<secret>.log. The glob in
87 - * cleanupOldLogs() (plugins-*.log*) matches both shapes.
88 - */
89 - public static function logFilePath(string $dir, string $date, string $secret): string
90 - {
91 - return rtrim($dir, '/\\') . '/plugins-' . $date . ($secret !== '' ? '-' . $secret : '') . '.log';
92 - }
116 + private function cleanupOldLogs(int $days = 7): void {
117 + foreach (glob($this->log_dir . '/plugins-*.log*') as $file) {
118 + if (filemtime($file) < strtotime("-{$days} days")) {
119 + @unlink($file);
120 + }
121 + }
122 + }
93 123
94 - /**
95 - * A per-site value nobody outside the server knows. Derived from
96 - * the wp-config keys when they are set (no database access — the
97 - * logger is constructed while plugins load); otherwise a random
98 - * value kept in an option.
99 - */
100 - public static function fileSecret(): string
101 - {
102 - $material = '';
103 - foreach (array('AUTH_KEY', 'AUTH_SALT', 'SECURE_AUTH_KEY') as $constant) {
104 - if (defined($constant)) {
105 - $value = (string) constant($constant);
106 - if ($value !== '' && $value !== 'put your unique phrase here') {
107 - $material .= $value;
108 - }
109 - }
110 - }
124 + private function writeLog( $level, $levelName, $message, array $context = [] ) {
125 + if ( ! defined( 'F12_DEBUG' ) || ! F12_DEBUG ) {
126 + // Wenn F12_DEBUG nicht definiert ist oder false, beenden wir.
127 + return;
128 + }
111 129
112 - if ($material === '' && function_exists('get_option')) {
113 - $stored = get_option('f12_logs_file_secret');
114 - if (!is_string($stored) || strlen($stored) < 32) {
115 - $stored = bin2hex(random_bytes(16));
116 - if (function_exists('update_option')) {
117 - update_option('f12_logs_file_secret', $stored, false);
118 - }
119 - }
120 - $material = $stored;
121 - }
130 + if ( $level < $this->log_level ) {
131 + return;
132 + }
122 133
123 - return $material === '' ? '' : substr(hash_hmac('sha256', 'f12-logs', $material), 0, 20);
124 - }
134 + // Cleanup & Rotation bei jedem Schreiben
135 + $this->cleanupOldLogs();
136 + $this->rotateLogs();
125 137
126 - /**
127 - * Deny web access to the log directory: Apache 2.2 and 2.4
128 - * (.htaccess), IIS (web.config), and no directory listing
129 - * (index.php). Existing files are left alone, so an admin's own
130 - * rules are never overwritten.
131 - */
132 - public static function protectDirectory(string $dir): void
133 - {
134 - if (!is_dir($dir) || !is_writable($dir)) {
135 - return;
136 - }
138 + $context = $this->sanitizeContext($context);
137 139
138 - $files = array(
139 - '.htaccess' => "# Written by the Forge12 logger. Log files must not be downloadable.\n"
140 - . "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n"
141 - . "<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n",
142 - 'web.config' => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n"
143 - . "\t<system.webServer>\n\t\t<authorization>\n\t\t\t<deny users=\"*\" />\n\t\t</authorization>\n"
144 - . "\t</system.webServer>\n</configuration>\n",
145 - 'index.php' => "<?php\n// Silence is golden.\n",
146 - );
140 + $time = date( 'Y-m-d H:i:s' );
141 + $plugin = $context['plugin'] ?? 'unknown';
142 + $msg = sprintf( "[%s] [%s] [%s] %s %s\n",
143 + $time,
144 + strtoupper( $levelName ),
145 + $plugin,
146 + $message,
147 + $context ? json_encode( $context ) : ''
148 + );
149 + error_log( $msg, 3, $this->log_file );
150 + }
147 151
148 - foreach ($files as $name => $content) {
149 - $path = $dir . '/' . $name;
150 - if (!file_exists($path)) {
151 - @file_put_contents($path, $content);
152 - }
153 - }
154 - }
152 + public function debug( $message, array $context = [] ) :void {
153 + $this->writeLog( self::DEBUG, 'DEBUG', $message, $context );
154 + }
155 155
156 - /**
157 - * Rename log files from the guessable scheme (plugins-<date>.log,
158 - * plugins-<date>.log.<n>) so their known URLs stop working.
159 - */
160 - public static function migrateGuessableFiles(string $dir, string $secret): void
161 - {
162 - if ($secret === '' || !is_dir($dir)) {
163 - return;
164 - }
156 + public function info( $message, array $context = [] ) : void {
157 + $this->writeLog( self::INFO, 'INFO', $message, $context );
158 + }
165 159
166 - foreach ((array) glob($dir . '/plugins-*.log*') as $file) {
167 - if (!is_string($file)) {
168 - continue;
169 - }
170 - if (!preg_match('/^plugins-(\d{4}-\d{2}-\d{2})\.log(\.\d+)?$/', basename($file), $m)) {
171 - continue;
172 - }
173 - $target = self::logFilePath($dir, $m[1], $secret) . ($m[2] ?? '');
174 - if (file_exists($target)) {
175 - // Both exist (concurrent request): keep the content.
176 - @file_put_contents($target, (string) @file_get_contents($file), FILE_APPEND);
177 - @unlink($file);
178 - } else {
179 - @rename($file, $target);
180 - }
181 - }
182 - }
160 + public function error( $message, array $context = [] ) : void {
161 + $this->writeLog( self::ERROR, 'ERROR', $message, $context );
162 + }
183 163
184 - public static function getInstance()
185 - {
186 - if (!self::$instance) {
187 - self::$instance = new self();
188 - }
189 - return self::$instance;
190 - }
164 + public function warning( $message, array $context = [] ) : void {
165 + $this->writeLog( self::WARNING, 'WARNING', $message, $context );
166 + }
191 167
192 - /**
193 - * Pfade bereinigen (Windows + Linux)
194 - */
195 - private function normalizePath(string $path): string
196 - {
197 - // Backslashes → Slashes
198 - $path = str_replace('\\', '/', $path);
168 + public function notice( $message, array $context = [] ) : void {
169 + $this->writeLog( self::NOTICE, 'NOTICE', $message, $context );
170 + }
199 171
200 - // Doppelte Slashes entfernen, außer nach C:
201 - $path = preg_replace('#(?<!:)/{2,}#', '/', $path);
202 -
203 - return rtrim($path, '/');
204 - }
205 -
206 - /**
207 - * ABSOLUTEN Pfad erzwingen
208 - */
209 - private function ensureAbsolutePath(string $path): string
210 - {
211 - $path = $this->normalizePath($path);
212 -
213 - // Linux absolute Pfade: /var/www/...
214 - if (substr($path, 0, 1) === '/') {
215 - return $path;
216 - }
217 -
218 - // Windows absolute Pfade: C:/xampp/...
219 - if (preg_match('#^[A-Za-z]:/#', $path)) {
220 - return $path;
221 - }
222 -
223 - // → Pfad ist relativ → ABSPATH davor hängen
224 - $absolute = $this->normalizePath(ABSPATH . '/' . $path);
225 -
226 - return $absolute;
227 - }
228 -
229 -
230 - private function sanitizeContext(array $context): array
231 - {
232 - foreach ($context as $key => $value) {
233 - if (in_array(strtolower($key), ['ip', 'user_ip'])) {
234 - $context[$key] = $this->mask_ip($value);
235 - }
236 - if (in_array(strtolower($key), ['email', 'user_email'])) {
237 - $context[$key] = $this->mask_email($value);
238 - }
239 - if (in_array(strtolower($key), ['password', 'pwd'])) {
240 - $context[$key] = $this->mask_password($value);
241 - }
242 - }
243 - return $context;
244 - }
245 -
246 - private function mask_password(string $password): string
247 - {
248 - return '********';
249 - }
250 -
251 - private function mask_email(string $email): string
252 - {
253 - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
254 - return 'invalid';
255 - }
256 - [$user, $domain] = explode('@', $email, 2);
257 - $len = strlen($user);
258 - if ($len <= 2) {
259 - $maskedUser = substr($user, 0, 1) . '*';
260 - } else {
261 - $maskedUser = substr($user, 0, 1) . str_repeat('*', $len - 2) . substr($user, -1);
262 - }
263 - return $maskedUser . '@' . $domain;
264 - }
265 -
266 - private function mask_ip(string $ip): string
267 - {
268 - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
269 - $parts = explode('.', $ip);
270 - $parts[3] = '0';
271 - return implode('.', $parts);
272 - }
273 - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
274 - return substr($ip, 0, 20) . '::';
275 - }
276 - return 'unknown';
277 - }
278 -
279 - private function rotateLogs(int $maxSize = 52428800, int $maxFiles = 5): void
280 - {
281 - if (file_exists($this->log_file) && filesize($this->log_file) > $maxSize) {
282 -
283 - for ($i = $maxFiles - 1; $i >= 1; $i--) {
284 - $old = $this->log_file . '.' . $i;
285 - $new = $this->log_file . '.' . ($i + 1);
286 - if (file_exists($old)) {
287 - rename($old, $new);
288 - }
289 - }
290 -
291 - rename($this->log_file, $this->log_file . '.1');
292 - }
293 - }
294 -
295 - private function cleanupOldLogs(int $days = 7): void
296 - {
297 - foreach (glob($this->log_dir . '/plugins-*.log*') as $file) {
298 - if (filemtime($file) < strtotime("-{$days} days")) {
299 - @unlink($file);
300 - }
301 - }
302 - }
303 -
304 - private function writeLog($level, $levelName, $message, array $context = [])
305 - {
306 - if (!defined('F12_DEBUG') || !F12_DEBUG) {
307 - return;
308 - }
309 -
310 - if ($level < $this->log_level) {
311 - return;
312 - }
313 -
314 - // Vor jedem Schreiben absolut sicherstellen
315 - $this->log_file = $this->ensureAbsolutePath($this->log_file);
316 -
317 - // Cleanup & Rotation
318 - $this->cleanupOldLogs();
319 - $this->rotateLogs();
320 -
321 - $context = $this->sanitizeContext($context);
322 -
323 - $time = date('Y-m-d H:i:s');
324 - $plugin = $context['plugin'] ?? 'unknown';
325 -
326 - $msg = sprintf(
327 - "[%s] [%s] [%s] %s %s\n",
328 - $time,
329 - strtoupper($levelName),
330 - $plugin,
331 - $message,
332 - $context ? json_encode($context) : ''
333 - );
334 -
335 - // Schreiben in absolut sicheren Pfad
336 - error_log($msg, 3, $this->log_file);
337 - }
338 -
339 - public function debug($message, array $context = []): void
340 - {
341 - if ( ! F12_DEBUG ) { return; }
342 - $this->writeLog(self::DEBUG, 'DEBUG', $message, $context);
343 - }
344 -
345 - public function info($message, array $context = []): void
346 - {
347 - if ( ! F12_DEBUG ) { return; }
348 - $this->writeLog(self::INFO, 'INFO', $message, $context);
349 - }
350 -
351 - public function error($message, array $context = []): void
352 - {
353 - if ( ! F12_DEBUG ) { return; }
354 - $this->writeLog(self::ERROR, 'ERROR', $message, $context);
355 - }
356 -
357 - public function warning($message, array $context = []): void
358 - {
359 - if ( ! F12_DEBUG ) { return; }
360 - $this->writeLog(self::WARNING, 'WARNING', $message, $context);
361 - }
362 -
363 - public function notice($message, array $context = []): void
364 - {
365 - if ( ! F12_DEBUG ) { return; }
366 - $this->writeLog(self::NOTICE, 'NOTICE', $message, $context);
367 - }
368 -
369 - public function critical($message, array $context = []): void
370 - {
371 - if ( ! F12_DEBUG ) { return; }
372 - $this->writeLog(self::CRITICAL, 'CRITICAL', $message, $context);
373 - }
374 - }
172 + public function critical( $message, array $context = [] ) : void {
173 + $this->writeLog( self::CRITICAL, 'CRITICAL', $message, $context );
174 + }
175 + }
375 176 }