PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Activity_Log / Activity_Log.php

Activity_Log.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.49, at includes/extensions/Activity_Log/Activity_Log.php

1,319 lines 42.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Activity Log extension.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Activity_Log;
9
10 use WP_Post;
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 require_once __DIR__ . '/Activity_Log_DB.php';
17
18 class Activity_Log
19 {
20 private const OPTION_NAME = 'king_addons_activity_log_settings';
21
22 private static ?Activity_Log $instance = null;
23
24 /**
25 * Cached settings.
26 *
27 * @var array<string, mixed>
28 */
29 private array $settings = [];
30
31 public static function instance(): Activity_Log
32 {
33 if (self::$instance === null) {
34 self::$instance = new self();
35 }
36
37 return self::$instance;
38 }
39
40 private function __construct()
41 {
42 $this->settings = $this->get_settings();
43
44 add_action('init', [$this, 'maybe_create_table']);
45 add_action('init', [$this, 'schedule_purge']);
46 add_action('kng_activity_log_purge', [$this, 'purge_old_logs']);
47
48 add_action('admin_menu', [$this, 'register_admin_menu']);
49 add_action('admin_init', [$this, 'register_settings']);
50 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
51
52 add_action('admin_post_kng_activity_log_export', [$this, 'handle_export']);
53 add_action('admin_post_kng_activity_log_purge', [$this, 'handle_manual_purge']);
54 add_action('admin_post_kng_activity_log_save_alerts', [$this, 'handle_save_alerts']);
55
56 add_action('wp_login', [$this, 'log_login'], 10, 2);
57 add_action('wp_login_failed', [$this, 'log_failed_login']);
58 add_action('wp_logout', [$this, 'log_logout']);
59 add_action('user_register', [$this, 'log_user_created']);
60 add_action('profile_update', [$this, 'log_user_updated'], 10, 2);
61 add_action('delete_user', [$this, 'log_user_deleted']);
62 add_action('set_user_role', [$this, 'log_user_role_changed'], 10, 3);
63
64 add_action('save_post', [$this, 'log_post_saved'], 10, 3);
65 add_action('wp_trash_post', [$this, 'log_post_trashed']);
66 add_action('untrash_post', [$this, 'log_post_restored']);
67 add_action('before_delete_post', [$this, 'log_post_deleted']);
68
69 add_action('activated_plugin', [$this, 'log_plugin_activated'], 10, 2);
70 add_action('deactivated_plugin', [$this, 'log_plugin_deactivated'], 10, 2);
71 add_action('upgrader_process_complete', [$this, 'log_plugin_updated'], 10, 2);
72 add_action('switch_theme', [$this, 'log_theme_switched'], 10, 3);
73
74 add_action('kng_activity_log/event', [$this, 'handle_custom_event']);
75 }
76
77 public function register_admin_menu(): void
78 {
79 add_submenu_page(
80 'king-addons',
81 __('Activity Log', 'king-addons'),
82 __('Activity Log', 'king-addons'),
83 'manage_options',
84 'king-addons-activity-log',
85 [$this, 'render_admin_page']
86 );
87 }
88
89 public function register_settings(): void
90 {
91 register_setting(
92 'king_addons_activity_log',
93 self::OPTION_NAME,
94 [
95 'type' => 'array',
96 'sanitize_callback' => [$this, 'sanitize_settings'],
97 'default' => $this->get_default_settings(),
98 ]
99 );
100 }
101
102 public function enqueue_admin_assets(string $hook): void
103 {
104 if ($hook !== 'king-addons_page_king-addons-activity-log') {
105 return;
106 }
107
108 $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
109 $shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css';
110 $shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION;
111 wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version);
112
113 $admin_css = KING_ADDONS_URL . 'includes/extensions/Activity_Log/assets/admin.css';
114 $admin_path = KING_ADDONS_PATH . 'includes/extensions/Activity_Log/assets/admin.css';
115 $admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION;
116 wp_enqueue_style('king-addons-activity-log-admin', $admin_css, ['king-addons-admin-v3'], $admin_version);
117
118 $admin_js = KING_ADDONS_URL . 'includes/extensions/Activity_Log/assets/admin.js';
119 $admin_path_js = KING_ADDONS_PATH . 'includes/extensions/Activity_Log/assets/admin.js';
120 $admin_js_version = file_exists($admin_path_js) ? filemtime($admin_path_js) : KING_ADDONS_VERSION;
121 wp_enqueue_script('king-addons-activity-log-admin', $admin_js, ['jquery'], $admin_js_version, true);
122
123 wp_localize_script('king-addons-activity-log-admin', 'KNGActivityLog', [
124 'ajaxUrl' => admin_url('admin-ajax.php'),
125 'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'),
126 ]);
127 }
128
129 public function render_admin_page(): void
130 {
131 if (!current_user_can('manage_options')) {
132 return;
133 }
134
135 $view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard';
136 $is_pro = $this->is_pro();
137 $settings = $this->get_settings();
138
139 include __DIR__ . '/templates/admin-page.php';
140 }
141
142 public function maybe_create_table(): void
143 {
144 Activity_Log_DB::maybe_create_table();
145 }
146
147 public function schedule_purge(): void
148 {
149 if (!wp_next_scheduled('kng_activity_log_purge')) {
150 wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'kng_activity_log_purge');
151 }
152 }
153
154 public function purge_old_logs(): void
155 {
156 global $wpdb;
157
158 $days = $this->get_retention_days();
159 $cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
160
161 $table = Activity_Log_DB::get_table();
162 $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE created_at < %s", $cutoff));
163 }
164
165 public function handle_manual_purge(): void
166 {
167 if (!current_user_can('manage_options')) {
168 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
169 }
170
171 check_admin_referer('kng_activity_log_purge');
172 $this->purge_old_logs();
173
174 $this->redirect_with_message('tools', 'purged');
175 }
176
177 public function handle_export(): void
178 {
179 if (!current_user_can('manage_options')) {
180 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
181 }
182
183 check_admin_referer('kng_activity_log_export');
184
185 $filters = $this->get_filters_from_request();
186 $filters = $this->apply_retention_limit($filters);
187 $logs = $this->get_logs($filters, 1, 0);
188
189 header('Content-Type: text/csv; charset=utf-8');
190 header('Content-Disposition: attachment; filename=activity-log-' . gmdate('Y-m-d') . '.csv');
191
192 $output = fopen('php://output', 'w');
193 fputcsv($output, [
194 'Time',
195 'Event',
196 'Severity',
197 'User',
198 'Role',
199 'IP',
200 'Object',
201 'Source',
202 'Message',
203 ]);
204
205 foreach ($logs as $log) {
206 $object = trim($log->object_type . ' ' . $log->object_title);
207 $row = [
208 $this->format_time($log->created_at),
209 $log->event_key,
210 $log->severity,
211 $log->user_login ?: 'Guest',
212 $log->user_role ?: '',
213 $log->ip ?: '',
214 trim($object),
215 $log->source,
216 $log->message,
217 ];
218 $row = array_map([$this, 'escape_csv_value'], $row);
219 fputcsv($output, $row);
220 }
221
222 fclose($output);
223 exit;
224 }
225
226 public function handle_save_alerts(): void
227 {
228 if (!current_user_can('manage_options')) {
229 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
230 }
231
232 check_admin_referer('kng_activity_log_alerts');
233
234 $settings = $this->get_settings();
235 $alerts = [
236 'failed_login_enabled' => !empty($_POST['failed_login_enabled']),
237 'failed_login_threshold' => absint($_POST['failed_login_threshold'] ?? 5),
238 'failed_login_window' => absint($_POST['failed_login_window'] ?? 10),
239 'failed_login_emails' => sanitize_text_field(wp_unslash($_POST['failed_login_emails'] ?? '')),
240 ];
241
242 $settings['alerts'] = $this->sanitize_alerts($alerts);
243 update_option(self::OPTION_NAME, $settings);
244
245 $this->redirect_with_message('alerts', 'alerts_saved');
246 }
247
248 public function log_login(string $user_login, \WP_User $user): void
249 {
250 $this->log_event([
251 'event_key' => 'auth.login.success',
252 'severity' => 'info',
253 'user_id' => $user->ID,
254 'user_login' => $user_login,
255 'user_role' => $this->get_user_role($user),
256 'object_type' => 'user',
257 'object_id' => (string) $user->ID,
258 'object_title' => $user_login,
259 'source' => 'core',
260 'message' => __('User logged in.', 'king-addons'),
261 ]);
262 }
263
264 public function log_failed_login(string $username): void
265 {
266 $user = get_user_by('login', $username);
267 $user_id = $user ? $user->ID : 0;
268 $user_role = $user ? $this->get_user_role($user) : '';
269
270 $this->log_event([
271 'event_key' => 'auth.login.failed',
272 'severity' => 'warning',
273 'user_id' => $user_id,
274 'user_login' => $username,
275 'user_role' => $user_role,
276 'object_type' => 'user',
277 'object_id' => $user_id ? (string) $user_id : '',
278 'object_title' => $username,
279 'source' => 'core',
280 'message' => __('Failed login attempt.', 'king-addons'),
281 'data' => [
282 'login' => $username,
283 ],
284 ]);
285
286 $this->maybe_send_failed_login_alert();
287 }
288
289 public function log_logout(): void
290 {
291 $user = wp_get_current_user();
292 if (!$user || !$user->ID) {
293 return;
294 }
295
296 $this->log_event([
297 'event_key' => 'auth.logout',
298 'severity' => 'info',
299 'user_id' => $user->ID,
300 'user_login' => $user->user_login,
301 'user_role' => $this->get_user_role($user),
302 'object_type' => 'user',
303 'object_id' => (string) $user->ID,
304 'object_title' => $user->user_login,
305 'source' => 'core',
306 'message' => __('User logged out.', 'king-addons'),
307 ]);
308 }
309
310 public function log_user_created(int $user_id): void
311 {
312 $user = get_userdata($user_id);
313 if (!$user) {
314 return;
315 }
316
317 $this->log_event([
318 'event_key' => 'user.created',
319 'severity' => 'notice',
320 'user_id' => $user_id,
321 'user_login' => $user->user_login,
322 'user_role' => $this->get_user_role($user),
323 'object_type' => 'user',
324 'object_id' => (string) $user_id,
325 'object_title' => $user->user_login,
326 'source' => 'core',
327 'message' => __('User account created.', 'king-addons'),
328 ]);
329 }
330
331 public function log_user_updated(int $user_id, $old_user_data): void
332 {
333 $user = get_userdata($user_id);
334 if (!$user) {
335 return;
336 }
337
338 $this->log_event([
339 'event_key' => 'user.updated',
340 'severity' => 'notice',
341 'user_id' => $user_id,
342 'user_login' => $user->user_login,
343 'user_role' => $this->get_user_role($user),
344 'object_type' => 'user',
345 'object_id' => (string) $user_id,
346 'object_title' => $user->user_login,
347 'source' => 'core',
348 'message' => __('User profile updated.', 'king-addons'),
349 ]);
350 }
351
352 public function log_user_deleted(int $user_id): void
353 {
354 $user = get_userdata($user_id);
355 $user_login = $user ? $user->user_login : '';
356
357 $this->log_event([
358 'event_key' => 'user.deleted',
359 'severity' => 'warning',
360 'user_id' => $user_id,
361 'user_login' => $user_login,
362 'user_role' => $user ? $this->get_user_role($user) : '',
363 'object_type' => 'user',
364 'object_id' => (string) $user_id,
365 'object_title' => $user_login,
366 'source' => 'core',
367 'message' => __('User account deleted.', 'king-addons'),
368 ]);
369 }
370
371 public function log_user_role_changed(int $user_id, string $role, array $old_roles): void
372 {
373 $user = get_userdata($user_id);
374 if (!$user) {
375 return;
376 }
377
378 $severity = 'notice';
379 if ($role === 'administrator' && !in_array('administrator', $old_roles, true)) {
380 $severity = 'critical';
381 }
382
383 $this->log_event([
384 'event_key' => 'user.role_changed',
385 'severity' => $severity,
386 'user_id' => $user_id,
387 'user_login' => $user->user_login,
388 'user_role' => $this->get_user_role($user),
389 'object_type' => 'user',
390 'object_id' => (string) $user_id,
391 'object_title' => $user->user_login,
392 'source' => 'core',
393 'message' => __('User role changed.', 'king-addons'),
394 'data' => [
395 'old_roles' => $old_roles,
396 'new_role' => $role,
397 ],
398 ]);
399 }
400
401 public function log_post_saved(int $post_id, WP_Post $post, bool $update): void
402 {
403 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
404 return;
405 }
406
407 if ($post->post_status === 'auto-draft') {
408 return;
409 }
410
411 $event_key = $update ? 'content.updated' : 'content.created';
412 $severity = $update ? 'notice' : 'info';
413
414 $this->log_event([
415 'event_key' => $event_key,
416 'severity' => $severity,
417 'object_type' => $post->post_type,
418 'object_id' => (string) $post_id,
419 'object_title' => $post->post_title ?: ('#' . $post_id),
420 'source' => 'core',
421 'message' => $update ? __('Content updated.', 'king-addons') : __('Content created.', 'king-addons'),
422 'data' => [
423 'status' => $post->post_status,
424 ],
425 ]);
426 }
427
428 public function log_post_trashed(int $post_id): void
429 {
430 $post = get_post($post_id);
431 if (!$post) {
432 return;
433 }
434
435 $this->log_event([
436 'event_key' => 'content.trashed',
437 'severity' => 'notice',
438 'object_type' => $post->post_type,
439 'object_id' => (string) $post_id,
440 'object_title' => $post->post_title ?: ('#' . $post_id),
441 'source' => 'core',
442 'message' => __('Content moved to trash.', 'king-addons'),
443 ]);
444 }
445
446 public function log_post_restored(int $post_id): void
447 {
448 $post = get_post($post_id);
449 if (!$post) {
450 return;
451 }
452
453 $this->log_event([
454 'event_key' => 'content.restored',
455 'severity' => 'notice',
456 'object_type' => $post->post_type,
457 'object_id' => (string) $post_id,
458 'object_title' => $post->post_title ?: ('#' . $post_id),
459 'source' => 'core',
460 'message' => __('Content restored from trash.', 'king-addons'),
461 ]);
462 }
463
464 public function log_post_deleted(int $post_id): void
465 {
466 $post = get_post($post_id);
467 if (!$post) {
468 return;
469 }
470
471 $this->log_event([
472 'event_key' => 'content.deleted',
473 'severity' => 'warning',
474 'object_type' => $post->post_type,
475 'object_id' => (string) $post_id,
476 'object_title' => $post->post_title ?: ('#' . $post_id),
477 'source' => 'core',
478 'message' => __('Content deleted permanently.', 'king-addons'),
479 ]);
480 }
481
482 public function log_plugin_activated(string $plugin, bool $network_wide): void
483 {
484 $plugin_name = $this->get_plugin_name($plugin);
485
486 $this->log_event([
487 'event_key' => 'plugin.activated',
488 'severity' => 'notice',
489 'object_type' => 'plugin',
490 'object_id' => $plugin,
491 'object_title' => $plugin_name,
492 'source' => 'core',
493 'message' => __('Plugin activated.', 'king-addons'),
494 ]);
495 }
496
497 public function log_plugin_deactivated(string $plugin, bool $network_wide): void
498 {
499 $plugin_name = $this->get_plugin_name($plugin);
500
501 $this->log_event([
502 'event_key' => 'plugin.deactivated',
503 'severity' => 'notice',
504 'object_type' => 'plugin',
505 'object_id' => $plugin,
506 'object_title' => $plugin_name,
507 'source' => 'core',
508 'message' => __('Plugin deactivated.', 'king-addons'),
509 ]);
510 }
511
512 public function log_plugin_updated($upgrader, array $hook_extra): void
513 {
514 if (($hook_extra['action'] ?? '') !== 'update' || ($hook_extra['type'] ?? '') !== 'plugin') {
515 return;
516 }
517
518 $plugins = $hook_extra['plugins'] ?? [];
519 if (!is_array($plugins)) {
520 return;
521 }
522
523 foreach ($plugins as $plugin) {
524 $plugin_name = $this->get_plugin_name($plugin);
525 $this->log_event([
526 'event_key' => 'plugin.updated',
527 'severity' => 'notice',
528 'object_type' => 'plugin',
529 'object_id' => (string) $plugin,
530 'object_title' => $plugin_name,
531 'source' => 'core',
532 'message' => __('Plugin updated.', 'king-addons'),
533 ]);
534 }
535 }
536
537 public function log_theme_switched(string $new_name, \WP_Theme $new_theme, \WP_Theme $old_theme): void
538 {
539 $this->log_event([
540 'event_key' => 'theme.switched',
541 'severity' => 'notice',
542 'object_type' => 'theme',
543 'object_id' => (string) $new_theme->get_stylesheet(),
544 'object_title' => $new_name,
545 'source' => 'core',
546 'message' => __('Theme switched.', 'king-addons'),
547 'data' => [
548 'previous' => $old_theme->get_stylesheet(),
549 ],
550 ]);
551 }
552
553 public function handle_custom_event(array $event): void
554 {
555 if (empty($event['event_key'])) {
556 return;
557 }
558
559 $this->log_event($event);
560 }
561
562 private function log_event(array $event): void
563 {
564 if (empty($this->settings['enabled'])) {
565 return;
566 }
567
568 $event_key = $event['event_key'] ?? '';
569 if ($event_key === '' || !$this->is_event_allowed($event_key, (int) ($event['user_id'] ?? 0))) {
570 return;
571 }
572
573 $user = null;
574 if (!empty($event['user_id'])) {
575 $user = get_userdata((int) $event['user_id']);
576 }
577
578 if (!$user) {
579 $user = wp_get_current_user();
580 }
581
582 $user_id = $event['user_id'] ?? ($user && $user->ID ? $user->ID : null);
583 $user_login = $event['user_login'] ?? ($user && $user->ID ? $user->user_login : '');
584 $user_role = $event['user_role'] ?? ($user && $user->ID ? $this->get_user_role($user) : '');
585
586 $data = $event['data'] ?? [];
587 if (!is_array($data)) {
588 $data = [];
589 }
590
591 $context = $event['context'] ?? $this->get_context();
592 $source = $event['source'] ?? 'core';
593
594 $log = [
595 'created_at' => current_time('mysql', true),
596 'event_key' => sanitize_text_field($event_key),
597 'severity' => $this->sanitize_severity($event['severity'] ?? 'info'),
598 'user_id' => $user_id ? (int) $user_id : null,
599 'user_login' => $user_login ? sanitize_text_field($user_login) : null,
600 'user_role' => $user_role ? sanitize_text_field($user_role) : null,
601 'ip' => $this->get_ip_for_storage(),
602 'user_agent' => $this->settings['store_user_agent'] ? $this->get_user_agent() : null,
603 'object_type' => sanitize_key($event['object_type'] ?? ''),
604 'object_id' => isset($event['object_id']) ? sanitize_text_field((string) $event['object_id']) : null,
605 'object_title' => isset($event['object_title']) ? sanitize_text_field((string) $event['object_title']) : null,
606 'source' => sanitize_text_field($source),
607 'context' => sanitize_key($context),
608 'message' => isset($event['message']) ? sanitize_text_field((string) $event['message']) : '',
609 'data' => wp_json_encode($data),
610 'checksum' => null,
611 'chain_prev_checksum' => null,
612 ];
613
614 global $wpdb;
615
616 $table = Activity_Log_DB::get_table();
617 $wpdb->insert($table, $log, [
618 '%s',
619 '%s',
620 '%s',
621 '%d',
622 '%s',
623 '%s',
624 '%s',
625 '%s',
626 '%s',
627 '%s',
628 '%s',
629 '%s',
630 '%s',
631 '%s',
632 '%s',
633 '%s',
634 '%s',
635 ]);
636 }
637
638 private function is_event_allowed(string $event_key, int $user_id): bool
639 {
640 $settings = $this->settings;
641
642 if ($user_id > 0 && !empty($settings['exclude_user_ids']) && in_array($user_id, $settings['exclude_user_ids'], true)) {
643 return false;
644 }
645
646 if (!empty($settings['exclude_roles']) && $user_id > 0) {
647 $user = get_userdata($user_id);
648 $role = $user ? $this->get_user_role($user) : '';
649 if ($role !== '' && in_array($role, $settings['exclude_roles'], true)) {
650 return false;
651 }
652 }
653
654 if (!empty($settings['exclude_event_keys']) && in_array($event_key, $settings['exclude_event_keys'], true)) {
655 return false;
656 }
657
658 $modules = $settings['modules'] ?? [];
659 $prefix = strstr($event_key, '.', true);
660
661 $module_map = [
662 'auth' => 'auth',
663 'content' => 'content',
664 'user' => 'users',
665 'plugin' => 'plugins_themes',
666 'theme' => 'plugins_themes',
667 'settings' => 'settings',
668 'woocommerce' => 'woocommerce',
669 'kng' => 'king_addons',
670 ];
671
672 if ($prefix && isset($module_map[$prefix])) {
673 $module_key = $module_map[$prefix];
674 if (isset($modules[$module_key]) && !$modules[$module_key]) {
675 return false;
676 }
677 }
678
679 return true;
680 }
681
682 private function maybe_send_failed_login_alert(): void
683 {
684 $alerts = $this->settings['alerts'] ?? [];
685 if (empty($alerts['failed_login_enabled'])) {
686 return;
687 }
688
689 $threshold = max(1, (int) ($alerts['failed_login_threshold'] ?? 5));
690 $window = max(1, (int) ($alerts['failed_login_window'] ?? 10));
691 $emails = trim((string) ($alerts['failed_login_emails'] ?? ''));
692 if ($emails === '') {
693 $emails = get_option('admin_email');
694 }
695
696 $last_sent = (int) get_option('king_addons_activity_log_failed_login_last', 0);
697 if ($last_sent > 0 && (time() - $last_sent) < ($window * 60)) {
698 return;
699 }
700
701 global $wpdb;
702 $table = Activity_Log_DB::get_table();
703 $cutoff = gmdate('Y-m-d H:i:s', time() - ($window * 60));
704 $count = (int) $wpdb->get_var($wpdb->prepare(
705 "SELECT COUNT(*) FROM {$table} WHERE event_key = %s AND created_at >= %s",
706 'auth.login.failed',
707 $cutoff
708 ));
709
710 if ($count < $threshold) {
711 return;
712 }
713
714 $subject = __('Failed login alert', 'king-addons');
715 $message = sprintf(
716 __('There have been %d failed login attempts in the last %d minutes.', 'king-addons'),
717 $count,
718 $window
719 );
720
721 wp_mail($emails, $subject, $message);
722 update_option('king_addons_activity_log_failed_login_last', time());
723 }
724
725 public function get_logs(array $filters, int $page, int $per_page): array
726 {
727 global $wpdb;
728
729 $table = Activity_Log_DB::get_table();
730 [$where_sql, $params] = $this->build_where_clause($filters);
731
732 $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC";
733
734 if ($per_page > 0) {
735 $offset = max(0, ($page - 1) * $per_page);
736 $sql .= $wpdb->prepare(' LIMIT %d OFFSET %d', $per_page, $offset);
737 }
738
739 if (!empty($params)) {
740 $sql = $wpdb->prepare($sql, $params);
741 }
742
743 return $wpdb->get_results($sql);
744 }
745
746 public function get_logs_count(array $filters): int
747 {
748 global $wpdb;
749
750 $table = Activity_Log_DB::get_table();
751 [$where_sql, $params] = $this->build_where_clause($filters);
752
753 $sql = "SELECT COUNT(*) FROM {$table} {$where_sql}";
754 if (!empty($params)) {
755 $sql = $wpdb->prepare($sql, $params);
756 }
757
758 return (int) $wpdb->get_var($sql);
759 }
760
761 public function get_dashboard_stats(): array
762 {
763 global $wpdb;
764
765 $table = Activity_Log_DB::get_table();
766 $now = time();
767
768 $last_24 = gmdate('Y-m-d H:i:s', $now - DAY_IN_SECONDS);
769 $last_7 = gmdate('Y-m-d H:i:s', $now - (7 * DAY_IN_SECONDS));
770
771 $total_24h = (int) $wpdb->get_var($wpdb->prepare(
772 "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s",
773 $last_24
774 ));
775
776 $failed_24h = (int) $wpdb->get_var($wpdb->prepare(
777 "SELECT COUNT(*) FROM {$table} WHERE event_key = %s AND created_at >= %s",
778 'auth.login.failed',
779 $last_24
780 ));
781
782 $critical_7d = (int) $wpdb->get_var($wpdb->prepare(
783 "SELECT COUNT(*) FROM {$table} WHERE severity = %s AND created_at >= %s",
784 'critical',
785 $last_7
786 ));
787
788 $unique_users_7d = (int) $wpdb->get_var($wpdb->prepare(
789 "SELECT COUNT(DISTINCT user_id) FROM {$table} WHERE created_at >= %s AND user_id IS NOT NULL",
790 $last_7
791 ));
792
793 return [
794 'total_24h' => $total_24h,
795 'failed_24h' => $failed_24h,
796 'critical_7d' => $critical_7d,
797 'unique_users_7d' => $unique_users_7d,
798 ];
799 }
800
801 public function get_events_over_time(int $days = 14): array
802 {
803 global $wpdb;
804
805 $table = Activity_Log_DB::get_table();
806 $end = strtotime('today', time());
807 $start = $end - (($days - 1) * DAY_IN_SECONDS);
808
809 $start_sql = gmdate('Y-m-d H:i:s', $start);
810 $rows = $wpdb->get_results($wpdb->prepare(
811 "SELECT DATE(created_at) AS day, COUNT(*) AS total
812 FROM {$table}
813 WHERE created_at >= %s
814 GROUP BY day
815 ORDER BY day ASC",
816 $start_sql
817 ));
818
819 $map = [];
820 foreach ($rows as $row) {
821 $map[$row->day] = (int) $row->total;
822 }
823
824 $series = [];
825 for ($i = 0; $i < $days; $i++) {
826 $day = gmdate('Y-m-d', $start + ($i * DAY_IN_SECONDS));
827 $series[] = [
828 'date' => $day,
829 'count' => $map[$day] ?? 0,
830 ];
831 }
832
833 return $series;
834 }
835
836 public function get_top_events(int $limit = 6): array
837 {
838 global $wpdb;
839
840 $table = Activity_Log_DB::get_table();
841 $last_7 = gmdate('Y-m-d H:i:s', time() - (7 * DAY_IN_SECONDS));
842
843 return $wpdb->get_results($wpdb->prepare(
844 "SELECT event_key, COUNT(*) AS total
845 FROM {$table}
846 WHERE created_at >= %s
847 GROUP BY event_key
848 ORDER BY total DESC
849 LIMIT %d",
850 $last_7,
851 $limit
852 ));
853 }
854
855 public function get_top_users(int $limit = 6): array
856 {
857 global $wpdb;
858
859 $table = Activity_Log_DB::get_table();
860 $last_7 = gmdate('Y-m-d H:i:s', time() - (7 * DAY_IN_SECONDS));
861
862 return $wpdb->get_results($wpdb->prepare(
863 "SELECT user_login, COUNT(*) AS total
864 FROM {$table}
865 WHERE created_at >= %s AND user_login IS NOT NULL AND user_login != ''
866 GROUP BY user_login
867 ORDER BY total DESC
868 LIMIT %d",
869 $last_7,
870 $limit
871 ));
872 }
873
874 public function get_recent_users_for_filter(): array
875 {
876 global $wpdb;
877
878 $table = Activity_Log_DB::get_table();
879 $rows = $wpdb->get_results(
880 "SELECT DISTINCT user_id, user_login
881 FROM {$table}
882 WHERE user_id IS NOT NULL AND user_login IS NOT NULL
883 ORDER BY created_at DESC
884 LIMIT 50"
885 );
886
887 $users = [];
888 foreach ($rows as $row) {
889 $users[$row->user_id] = $row->user_login;
890 }
891
892 return $users;
893 }
894
895 private function build_where_clause(array $filters): array
896 {
897 global $wpdb;
898
899 $where = 'WHERE 1=1';
900 $params = [];
901
902 if (!empty($filters['search'])) {
903 $like = '%' . $wpdb->esc_like($filters['search']) . '%';
904 $where .= " AND (event_key LIKE %s OR message LIKE %s OR object_title LIKE %s OR user_login LIKE %s OR ip LIKE %s)";
905 $params[] = $like;
906 $params[] = $like;
907 $params[] = $like;
908 $params[] = $like;
909 $params[] = $like;
910 }
911
912 if (!empty($filters['event_key'])) {
913 $where .= ' AND event_key = %s';
914 $params[] = $filters['event_key'];
915 }
916
917 if (!empty($filters['severity'])) {
918 $where .= ' AND severity = %s';
919 $params[] = $filters['severity'];
920 }
921
922 if (!empty($filters['user_id'])) {
923 $where .= ' AND user_id = %d';
924 $params[] = (int) $filters['user_id'];
925 }
926
927 if (!empty($filters['date_from'])) {
928 $where .= ' AND created_at >= %s';
929 $params[] = $filters['date_from'];
930 }
931
932 if (!empty($filters['date_to'])) {
933 $where .= ' AND created_at <= %s';
934 $params[] = $filters['date_to'];
935 }
936
937 if (!empty($filters['ip']) && $this->is_pro()) {
938 $where .= ' AND ip = %s';
939 $params[] = $filters['ip'];
940 }
941
942 return [$where, $params];
943 }
944
945 private function get_filters_from_request(): array
946 {
947 $filters = [
948 'search' => isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '',
949 'event_key' => isset($_GET['event_key']) ? sanitize_text_field(wp_unslash($_GET['event_key'])) : '',
950 'severity' => isset($_GET['severity']) ? sanitize_text_field(wp_unslash($_GET['severity'])) : '',
951 'user_id' => isset($_GET['user_id']) ? absint($_GET['user_id']) : 0,
952 'date_from' => isset($_GET['date_from']) ? sanitize_text_field(wp_unslash($_GET['date_from'])) : '',
953 'date_to' => isset($_GET['date_to']) ? sanitize_text_field(wp_unslash($_GET['date_to'])) : '',
954 'ip' => isset($_GET['ip']) ? sanitize_text_field(wp_unslash($_GET['ip'])) : '',
955 ];
956
957 if ($filters['date_from'] !== '') {
958 $filters['date_from'] = $this->normalize_date($filters['date_from'], false);
959 }
960
961 if ($filters['date_to'] !== '') {
962 $filters['date_to'] = $this->normalize_date($filters['date_to'], true);
963 }
964
965 return $filters;
966 }
967
968 private function apply_retention_limit(array $filters): array
969 {
970 $days = $this->get_retention_days();
971 $cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
972
973 if (empty($filters['date_from']) || strtotime($filters['date_from']) < strtotime($cutoff)) {
974 $filters['date_from'] = $cutoff;
975 }
976
977 return $filters;
978 }
979
980 private function normalize_date(string $date, bool $end_of_day): string
981 {
982 $date = preg_replace('/[^0-9\\-]/', '', $date);
983 $time = $end_of_day ? '23:59:59' : '00:00:00';
984 $local = $date . ' ' . $time;
985 return get_gmt_from_date($local, 'Y-m-d H:i:s');
986 }
987
988 private function get_context(): string
989 {
990 if (defined('WP_CLI') && WP_CLI) {
991 return 'wp_cli';
992 }
993
994 if (defined('REST_REQUEST') && REST_REQUEST) {
995 return 'rest';
996 }
997
998 if (wp_doing_cron()) {
999 return 'cron';
1000 }
1001
1002 return is_admin() ? 'admin' : 'frontend';
1003 }
1004
1005 private function get_user_role($user): string
1006 {
1007 $roles = $user->roles ?? [];
1008 return $roles ? (string) $roles[0] : '';
1009 }
1010
1011 private function get_user_agent(): ?string
1012 {
1013 $agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
1014 return $agent !== '' ? $agent : null;
1015 }
1016
1017 private function get_ip_for_storage(): ?string
1018 {
1019 $ip = $this->get_client_ip();
1020 if ($ip === '') {
1021 return null;
1022 }
1023
1024 $storage = $this->settings['ip_storage'] ?? 'full';
1025 if ($storage === 'masked') {
1026 return $this->mask_ip($ip);
1027 }
1028
1029 if ($storage === 'hashed') {
1030 return hash('sha256', $ip);
1031 }
1032
1033 return $ip;
1034 }
1035
1036 private function get_client_ip(): string
1037 {
1038 $ip = '';
1039
1040 if (!empty($this->settings['trust_proxy_headers']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1041 $forwarded = explode(',', (string) wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
1042 foreach ($forwarded as $candidate) {
1043 $candidate = trim($candidate);
1044 if (filter_var($candidate, FILTER_VALIDATE_IP)) {
1045 $ip = $candidate;
1046 break;
1047 }
1048 }
1049 }
1050
1051 if ($ip === '' && !empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
1052 $ip = (string) $_SERVER['REMOTE_ADDR'];
1053 }
1054
1055 return $ip;
1056 }
1057
1058 private function mask_ip(string $ip): string
1059 {
1060 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
1061 $parts = explode('.', $ip);
1062 $parts[3] = '0';
1063 return implode('.', $parts);
1064 }
1065
1066 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
1067 $parts = explode(':', $ip);
1068 $parts = array_pad($parts, 8, '0000');
1069 $parts[7] = '0000';
1070 $parts[6] = '0000';
1071 return implode(':', $parts);
1072 }
1073
1074 return $ip;
1075 }
1076
1077 private function sanitize_severity(string $severity): string
1078 {
1079 $allowed = ['info', 'notice', 'warning', 'critical'];
1080 return in_array($severity, $allowed, true) ? $severity : 'info';
1081 }
1082
1083 private function sanitize_settings(array $settings): array
1084 {
1085 $defaults = $this->get_default_settings();
1086
1087 $modules = $settings['modules'] ?? [];
1088 $clean_modules = [];
1089 foreach ($defaults['modules'] as $key => $value) {
1090 $clean_modules[$key] = !empty($modules[$key]);
1091 }
1092
1093 if (!$this->is_pro()) {
1094 $clean_modules['settings'] = false;
1095 $clean_modules['woocommerce'] = false;
1096 $clean_modules['king_addons'] = false;
1097 }
1098
1099 $clean = [
1100 'enabled' => !empty($settings['enabled']),
1101 'timezone' => in_array($settings['timezone'] ?? 'site', ['site', 'utc'], true) ? $settings['timezone'] : 'site',
1102 'rows_per_page' => max(10, min(200, absint($settings['rows_per_page'] ?? $defaults['rows_per_page']))),
1103 'retention_days' => max(1, absint($settings['retention_days'] ?? $defaults['retention_days'])),
1104 'ip_storage' => in_array($settings['ip_storage'] ?? 'full', ['full', 'masked', 'hashed'], true) ? $settings['ip_storage'] : 'full',
1105 'store_user_agent' => !empty($settings['store_user_agent']),
1106 'trust_proxy_headers' => !empty($settings['trust_proxy_headers']),
1107 'modules' => $clean_modules,
1108 'exclude_roles' => $this->sanitize_list($settings['exclude_roles'] ?? ''),
1109 'exclude_user_ids' => $this->sanitize_id_list($settings['exclude_user_ids'] ?? ''),
1110 'exclude_event_keys' => $this->sanitize_event_keys_list($settings['exclude_event_keys'] ?? ''),
1111 'alerts' => $this->sanitize_alerts($settings['alerts'] ?? []),
1112 ];
1113
1114 if (!$this->is_pro() && $clean['retention_days'] > 14) {
1115 $clean['retention_days'] = 14;
1116 }
1117
1118 return $clean;
1119 }
1120
1121 private function sanitize_alerts(array $alerts): array
1122 {
1123 return [
1124 'failed_login_enabled' => !empty($alerts['failed_login_enabled']),
1125 'failed_login_threshold' => max(1, min(50, absint($alerts['failed_login_threshold'] ?? 5))),
1126 'failed_login_window' => max(1, min(60, absint($alerts['failed_login_window'] ?? 10))),
1127 'failed_login_emails' => sanitize_text_field((string) ($alerts['failed_login_emails'] ?? '')),
1128 ];
1129 }
1130
1131 private function sanitize_list($value): array
1132 {
1133 if (is_array($value)) {
1134 $items = $value;
1135 } else {
1136 $items = explode(',', (string) $value);
1137 }
1138
1139 $items = array_map('trim', $items);
1140 $items = array_filter($items);
1141 $items = array_map('sanitize_key', $items);
1142 $items = array_filter($items);
1143
1144 return array_values(array_unique($items));
1145 }
1146
1147 private function sanitize_id_list($value): array
1148 {
1149 if (is_array($value)) {
1150 $items = $value;
1151 } else {
1152 $items = explode(',', (string) $value);
1153 }
1154
1155 $ids = array_map('absint', $items);
1156 $ids = array_filter($ids);
1157
1158 return array_values(array_unique($ids));
1159 }
1160
1161 private function sanitize_event_keys_list($value): array
1162 {
1163 if (is_array($value)) {
1164 $items = $value;
1165 } else {
1166 $items = explode(',', (string) $value);
1167 }
1168
1169 $items = array_map('trim', $items);
1170 $items = array_filter($items);
1171 $items = array_map(function ($item) {
1172 $item = strtolower($item);
1173 return preg_replace('/[^a-z0-9._-]/', '', $item);
1174 }, $items);
1175 $items = array_filter($items);
1176
1177 return array_values(array_unique($items));
1178 }
1179
1180 public function get_default_settings(): array
1181 {
1182 return [
1183 'enabled' => true,
1184 'timezone' => 'site',
1185 'rows_per_page' => 20,
1186 'retention_days' => 14,
1187 'ip_storage' => 'full',
1188 'store_user_agent' => true,
1189 'trust_proxy_headers' => false,
1190 'modules' => [
1191 'auth' => true,
1192 'content' => true,
1193 'users' => true,
1194 'plugins_themes' => true,
1195 'settings' => false,
1196 'woocommerce' => false,
1197 'king_addons' => false,
1198 ],
1199 'exclude_roles' => [],
1200 'exclude_user_ids' => [],
1201 'exclude_event_keys' => [],
1202 'alerts' => [
1203 'failed_login_enabled' => false,
1204 'failed_login_threshold' => 5,
1205 'failed_login_window' => 10,
1206 'failed_login_emails' => '',
1207 ],
1208 ];
1209 }
1210
1211 public function get_settings(): array
1212 {
1213 $defaults = $this->get_default_settings();
1214 $saved = get_option(self::OPTION_NAME, []);
1215
1216 $settings = wp_parse_args($saved, $defaults);
1217 $settings['modules'] = wp_parse_args($settings['modules'] ?? [], $defaults['modules']);
1218
1219 if (!isset($settings['exclude_roles']) || !is_array($settings['exclude_roles'])) {
1220 $settings['exclude_roles'] = $defaults['exclude_roles'];
1221 }
1222 if (!isset($settings['exclude_user_ids']) || !is_array($settings['exclude_user_ids'])) {
1223 $settings['exclude_user_ids'] = $defaults['exclude_user_ids'];
1224 }
1225 if (!isset($settings['exclude_event_keys']) || !is_array($settings['exclude_event_keys'])) {
1226 $settings['exclude_event_keys'] = $defaults['exclude_event_keys'];
1227 }
1228 if (!isset($settings['alerts']) || !is_array($settings['alerts'])) {
1229 $settings['alerts'] = $defaults['alerts'];
1230 }
1231
1232 return $settings;
1233 }
1234
1235 public function format_time(string $gmt): string
1236 {
1237 $timestamp = strtotime($gmt . ' UTC');
1238 if (($this->settings['timezone'] ?? 'site') === 'utc') {
1239 return gmdate('Y-m-d H:i:s', $timestamp);
1240 }
1241
1242 return wp_date('Y-m-d H:i:s', $timestamp);
1243 }
1244
1245 public function get_event_labels(): array
1246 {
1247 return [
1248 'auth.login.success' => __('Login success', 'king-addons'),
1249 'auth.login.failed' => __('Login failed', 'king-addons'),
1250 'auth.logout' => __('Logout', 'king-addons'),
1251 'user.created' => __('User created', 'king-addons'),
1252 'user.updated' => __('User updated', 'king-addons'),
1253 'user.deleted' => __('User deleted', 'king-addons'),
1254 'user.role_changed' => __('Role changed', 'king-addons'),
1255 'content.created' => __('Content created', 'king-addons'),
1256 'content.updated' => __('Content updated', 'king-addons'),
1257 'content.trashed' => __('Content trashed', 'king-addons'),
1258 'content.restored' => __('Content restored', 'king-addons'),
1259 'content.deleted' => __('Content deleted', 'king-addons'),
1260 'plugin.activated' => __('Plugin activated', 'king-addons'),
1261 'plugin.deactivated' => __('Plugin deactivated', 'king-addons'),
1262 'plugin.updated' => __('Plugin updated', 'king-addons'),
1263 'theme.switched' => __('Theme switched', 'king-addons'),
1264 ];
1265 }
1266
1267 private function get_plugin_name(string $plugin): string
1268 {
1269 if (!function_exists('get_plugin_data')) {
1270 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1271 }
1272
1273 $file = WP_PLUGIN_DIR . '/' . $plugin;
1274 if (file_exists($file)) {
1275 $data = get_plugin_data($file, false, false);
1276 if (!empty($data['Name'])) {
1277 return $data['Name'];
1278 }
1279 }
1280
1281 return $plugin;
1282 }
1283
1284 private function get_retention_days(): int
1285 {
1286 $days = (int) ($this->settings['retention_days'] ?? 14);
1287 if (!$this->is_pro()) {
1288 $days = min($days, 14);
1289 }
1290
1291 return max(1, $days);
1292 }
1293
1294 private function escape_csv_value(string $value): string
1295 {
1296 if (preg_match('/^[=+\\-@]/', $value)) {
1297 return "'" . $value;
1298 }
1299 return $value;
1300 }
1301
1302 private function is_pro(): bool
1303 {
1304 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
1305 }
1306
1307 private function redirect_with_message(string $view, string $message): void
1308 {
1309 $args = [
1310 'page' => 'king-addons-activity-log',
1311 'view' => $view,
1312 'message' => $message,
1313 ];
1314
1315 wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
1316 exit;
1317 }
1318 }
1319