PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.84 51.1.85 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 All 39 releases
← All changes | includes/extensions/Activity_Log/Activity_Log.php +410 -4 51.1.4451.1.83 View file →
@@ -18,8 +18,38 @@
18 18 class Activity_Log
19 19 {
20 20 private const OPTION_NAME = 'king_addons_activity_log_settings';
21 21
22 + private const CORE_SETTINGS_OPTIONS_ALLOWLIST = [
23 + 'blogname',
24 + 'blogdescription',
25 + 'siteurl',
26 + 'home',
27 + 'admin_email',
28 + 'users_can_register',
29 + 'default_role',
30 + 'timezone_string',
31 + 'gmt_offset',
32 + 'date_format',
33 + 'time_format',
34 + 'start_of_week',
35 + 'permalink_structure',
36 + 'category_base',
37 + 'tag_base',
38 + 'posts_per_page',
39 + 'show_on_front',
40 + 'page_on_front',
41 + 'page_for_posts',
42 + 'thumbnail_size_w',
43 + 'thumbnail_size_h',
44 + 'thumbnail_crop',
45 + 'medium_size_w',
46 + 'medium_size_h',
47 + 'large_size_w',
48 + 'large_size_h',
49 + 'uploads_use_yearmonth_folders',
50 + ];
51 +
22 52 private static ?Activity_Log $instance = null;
23 53
24 54 /**
25 55 * Cached settings.
@@ -52,8 +82,16 @@
52 82 add_action('admin_post_kng_activity_log_export', [$this, 'handle_export']);
53 83 add_action('admin_post_kng_activity_log_purge', [$this, 'handle_manual_purge']);
54 84 add_action('admin_post_kng_activity_log_save_alerts', [$this, 'handle_save_alerts']);
55 85
86 + // Settings changes + King Addons option changes (Pro modules).
87 + add_action('updated_option', [$this, 'log_option_updated'], 10, 3);
88 + add_action('added_option', [$this, 'log_option_added'], 10, 2);
89 + add_action('deleted_option', [$this, 'log_option_deleted'], 10, 1);
90 +
91 + // WooCommerce events (Pro module).
92 + add_action('plugins_loaded', [$this, 'register_woocommerce_hooks'], 20);
93 +
56 94 add_action('wp_login', [$this, 'log_login'], 10, 2);
57 95 add_action('wp_login_failed', [$this, 'log_failed_login']);
58 96 add_action('wp_logout', [$this, 'log_logout']);
59 97 add_action('user_register', [$this, 'log_user_created']);
@@ -73,10 +111,304 @@
73 111
74 112 add_action('kng_activity_log/event', [$this, 'handle_custom_event']);
75 113 }
76 114
115 + public function register_woocommerce_hooks(): void
116 + {
117 + if (!function_exists('wc_get_order')) {
118 + return;
119 + }
120 +
121 + add_action('woocommerce_new_order', [$this, 'log_wc_order_created'], 10, 1);
122 + add_action('woocommerce_order_status_changed', [$this, 'log_wc_order_status_changed'], 10, 4);
123 + add_action('woocommerce_product_set_stock', [$this, 'log_wc_product_stock_changed'], 10, 1);
124 + }
125 +
126 + public function log_wc_order_created(int $order_id): void
127 + {
128 + if (!function_exists('wc_get_order')) {
129 + return;
130 + }
131 +
132 + $order = wc_get_order($order_id);
133 + if (!$order) {
134 + return;
135 + }
136 +
137 + $user_id = (int) $order->get_user_id();
138 + $user_login = '';
139 + if ($user_id > 0) {
140 + $user = get_userdata($user_id);
141 + $user_login = $user ? (string) $user->user_login : '';
142 + }
143 +
144 + $this->log_event([
145 + 'event_key' => 'woocommerce.order.created',
146 + 'severity' => 'notice',
147 + 'user_id' => $user_id ?: null,
148 + 'user_login' => $user_login,
149 + 'object_type' => 'shop_order',
150 + 'object_id' => (string) $order_id,
151 + 'object_title' => 'Order #' . $order->get_order_number(),
152 + 'source' => 'woocommerce',
153 + 'message' => __('Order created.', 'king-addons'),
154 + 'data' => [
155 + 'status' => $order->get_status(),
156 + 'total' => $order->get_total(),
157 + 'currency' => $order->get_currency(),
158 + 'payment_method' => $order->get_payment_method_title(),
159 + ],
160 + ]);
161 + }
162 +
163 + public function log_wc_order_status_changed(int $order_id, string $old_status, string $new_status, $order): void
164 + {
165 + if (!function_exists('wc_get_order')) {
166 + return;
167 + }
168 +
169 + if (!$order) {
170 + $order = wc_get_order($order_id);
171 + }
172 +
173 + if (!$order) {
174 + return;
175 + }
176 +
177 + $user_id = (int) $order->get_user_id();
178 + $user_login = '';
179 + if ($user_id > 0) {
180 + $user = get_userdata($user_id);
181 + $user_login = $user ? (string) $user->user_login : '';
182 + }
183 +
184 + $this->log_event([
185 + 'event_key' => 'woocommerce.order.status_changed',
186 + 'severity' => 'notice',
187 + 'user_id' => $user_id ?: null,
188 + 'user_login' => $user_login,
189 + 'object_type' => 'shop_order',
190 + 'object_id' => (string) $order_id,
191 + 'object_title' => 'Order #' . $order->get_order_number(),
192 + 'source' => 'woocommerce',
193 + 'message' => __('Order status changed.', 'king-addons'),
194 + 'data' => [
195 + 'from' => $old_status,
196 + 'to' => $new_status,
197 + ],
198 + ]);
199 + }
200 +
201 + public function log_wc_product_stock_changed($product): void
202 + {
203 + if (!is_object($product) || !method_exists($product, 'get_id')) {
204 + return;
205 + }
206 +
207 + $product_id = (int) $product->get_id();
208 + if ($product_id <= 0) {
209 + return;
210 + }
211 +
212 + $this->log_event([
213 + 'event_key' => 'woocommerce.product.stock_changed',
214 + 'severity' => 'notice',
215 + 'object_type' => 'product',
216 + 'object_id' => (string) $product_id,
217 + 'object_title' => method_exists($product, 'get_name') ? (string) $product->get_name() : ('#' . $product_id),
218 + 'source' => 'woocommerce',
219 + 'message' => __('Product stock updated.', 'king-addons'),
220 + 'data' => [
221 + 'stock_status' => method_exists($product, 'get_stock_status') ? (string) $product->get_stock_status() : '',
222 + 'stock_quantity' => method_exists($product, 'get_stock_quantity') ? $product->get_stock_quantity() : null,
223 + ],
224 + ]);
225 + }
226 +
227 + public function log_option_updated(string $option, $old_value, $value): void
228 + {
229 + if (!$this->should_log_option_change($option)) {
230 + return;
231 + }
232 +
233 + if ($old_value === $value) {
234 + return;
235 + }
236 +
237 + $event_key = $this->is_king_addons_option($option) ? 'kng.option.updated' : 'settings.option.updated';
238 +
239 + $data = [
240 + 'option' => $option,
241 + 'old' => $this->sanitize_option_value_for_log($option, $old_value),
242 + 'new' => $this->sanitize_option_value_for_log($option, $value),
243 + ];
244 +
245 + if (is_array($old_value) && is_array($value)) {
246 + $changed_keys = [];
247 + foreach (array_unique(array_merge(array_keys($old_value), array_keys($value))) as $key) {
248 + $old = $old_value[$key] ?? null;
249 + $new = $value[$key] ?? null;
250 + if ($old !== $new) {
251 + $changed_keys[] = (string) $key;
252 + }
253 + }
254 +
255 + $data['changed_count'] = count($changed_keys);
256 + $data['changed_keys'] = array_slice($changed_keys, 0, 20);
257 + }
258 +
259 + $this->log_event([
260 + 'event_key' => $event_key,
261 + 'severity' => 'notice',
262 + 'object_type' => 'option',
263 + 'object_id' => $option,
264 + 'object_title' => $option,
265 + 'source' => 'core',
266 + 'message' => __('Option updated.', 'king-addons'),
267 + 'data' => $data,
268 + ]);
269 + }
270 +
271 + public function log_option_added(string $option, $value): void
272 + {
273 + if (!$this->should_log_option_change($option)) {
274 + return;
275 + }
276 +
277 + $event_key = $this->is_king_addons_option($option) ? 'kng.option.added' : 'settings.option.added';
278 +
279 + $this->log_event([
280 + 'event_key' => $event_key,
281 + 'severity' => 'notice',
282 + 'object_type' => 'option',
283 + 'object_id' => $option,
284 + 'object_title' => $option,
285 + 'source' => 'core',
286 + 'message' => __('Option added.', 'king-addons'),
287 + 'data' => [
288 + 'option' => $option,
289 + 'new' => $this->sanitize_option_value_for_log($option, $value),
290 + ],
291 + ]);
292 + }
293 +
294 + public function log_option_deleted(string $option): void
295 + {
296 + if (!$this->should_log_option_change($option)) {
297 + return;
298 + }
299 +
300 + $event_key = $this->is_king_addons_option($option) ? 'kng.option.deleted' : 'settings.option.deleted';
301 +
302 + $this->log_event([
303 + 'event_key' => $event_key,
304 + 'severity' => 'warning',
305 + 'object_type' => 'option',
306 + 'object_id' => $option,
307 + 'object_title' => $option,
308 + 'source' => 'core',
309 + 'message' => __('Option deleted.', 'king-addons'),
310 + 'data' => [
311 + 'option' => $option,
312 + ],
313 + ]);
314 + }
315 +
316 + private function should_log_option_change(string $option): bool
317 + {
318 + if ($option === '' || $option === self::OPTION_NAME) {
319 + return false;
320 + }
321 +
322 + if (strpos($option, '_transient_') === 0 || strpos($option, '_site_transient_') === 0) {
323 + return false;
324 + }
325 +
326 + // Avoid recursion & internal runtime counters.
327 + if (strpos($option, 'king_addons_activity_log_') === 0) {
328 + return false;
329 + }
330 +
331 + if ($this->is_king_addons_option($option)) {
332 + if ($option === 'king_addons_options') {
333 + return true;
334 + }
335 +
336 + if (substr($option, -9) === '_settings') {
337 + return true;
338 + }
339 +
340 + return false;
341 + }
342 +
343 + return in_array($option, self::CORE_SETTINGS_OPTIONS_ALLOWLIST, true);
344 + }
345 +
346 + private function is_king_addons_option(string $option): bool
347 + {
348 + return strpos($option, 'king_addons_') === 0;
349 + }
350 +
351 + private function sanitize_option_value_for_log(string $option, $value): string
352 + {
353 + $lower = strtolower($option);
354 + if (preg_match('/(pass|password|secret|token|key|salt|nonce|license)/', $lower)) {
355 + return '[redacted]';
356 + }
357 +
358 + if (is_null($value)) {
359 + return 'null';
360 + }
361 +
362 + if (is_bool($value)) {
363 + return $value ? 'true' : 'false';
364 + }
365 +
366 + if (is_int($value) || is_float($value)) {
367 + return (string) $value;
368 + }
369 +
370 + if (is_string($value)) {
371 + $trimmed = trim($value);
372 + if ($trimmed === '') {
373 + return '';
374 + }
375 +
376 + if (strlen($trimmed) > 120) {
377 + return substr($trimmed, 0, 120) . '…';
378 + }
379 +
380 + return $trimmed;
381 + }
382 +
383 + if (is_array($value)) {
384 + return '[array:' . count($value) . ']';
385 + }
386 +
387 + if (is_object($value)) {
388 + return '[object:' . get_class($value) . ']';
389 + }
390 +
391 + return '[unknown]';
392 + }
393 +
77 394 public function register_admin_menu(): void
78 395 {
396 + $view_logs_cap = $this->get_view_logs_capability();
397 +
398 + // Optional logs-only entry for non-admin roles (Pro).
399 + // Admins can still access logs from the main Activity Log page.
400 + if ($view_logs_cap !== 'manage_options') {
401 + add_submenu_page(
402 + 'king-addons',
403 + __('Activity Logs', 'king-addons'),
404 + __('Activity Logs', 'king-addons'),
405 + $view_logs_cap,
406 + 'king-addons-activity-log-logs',
407 + [$this, 'render_admin_logs_page']
408 + );
409 + }
410 +
79 411 add_submenu_page(
80 412 'king-addons',
81 413 __('Activity Log', 'king-addons'),
82 414 __('Activity Log', 'king-addons'),
@@ -100,9 +432,9 @@
100 432 }
101 433
102 434 public function enqueue_admin_assets(string $hook): void
103 435 {
104 - if ($hook !== 'king-addons_page_king-addons-activity-log') {
436 + if (!in_array($hook, ['king-addons_page_king-addons-activity-log', 'king-addons_page_king-addons-activity-log-logs'], true)) {
105 437 return;
106 438 }
107 439
108 440 $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
@@ -138,8 +470,22 @@
138 470
139 471 include __DIR__ . '/templates/admin-page.php';
140 472 }
141 473
474 + public function render_admin_logs_page(): void
475 + {
476 + $capability = $this->get_view_logs_capability();
477 + if (!current_user_can($capability)) {
478 + return;
479 + }
480 +
481 + $view = 'logs';
482 + $is_pro = $this->is_pro();
483 + $settings = $this->get_settings();
484 +
485 + include __DIR__ . '/templates/admin-page.php';
486 + }
487 +
142 488 public function maybe_create_table(): void
143 489 {
144 490 Activity_Log_DB::maybe_create_table();
145 491 }
@@ -1079,12 +1425,16 @@
1079 1425 $allowed = ['info', 'notice', 'warning', 'critical'];
1080 1426 return in_array($severity, $allowed, true) ? $severity : 'info';
1081 1427 }
1082 1428
1083 - private function sanitize_settings(array $settings): array
1429 + public function sanitize_settings($settings): array
1084 1430 {
1085 1431 $defaults = $this->get_default_settings();
1086 1432
1433 + if (!is_array($settings)) {
1434 + $settings = [];
1435 + }
1436 +
1087 1437 $modules = $settings['modules'] ?? [];
1088 1438 $clean_modules = [];
1089 1439 foreach ($defaults['modules'] as $key => $value) {
1090 1440 $clean_modules[$key] = !empty($modules[$key]);
@@ -1103,8 +1453,9 @@
1103 1453 'retention_days' => max(1, absint($settings['retention_days'] ?? $defaults['retention_days'])),
1104 1454 'ip_storage' => in_array($settings['ip_storage'] ?? 'full', ['full', 'masked', 'hashed'], true) ? $settings['ip_storage'] : 'full',
1105 1455 'store_user_agent' => !empty($settings['store_user_agent']),
1106 1456 'trust_proxy_headers' => !empty($settings['trust_proxy_headers']),
1457 + 'view_logs_capability' => $this->sanitize_view_logs_capability($settings['view_logs_capability'] ?? $defaults['view_logs_capability']),
1107 1458 'modules' => $clean_modules,
1108 1459 'exclude_roles' => $this->sanitize_list($settings['exclude_roles'] ?? ''),
1109 1460 'exclude_user_ids' => $this->sanitize_id_list($settings['exclude_user_ids'] ?? ''),
1110 1461 'exclude_event_keys' => $this->sanitize_event_keys_list($settings['exclude_event_keys'] ?? ''),
@@ -1114,11 +1465,29 @@
1114 1465 if (!$this->is_pro() && $clean['retention_days'] > 14) {
1115 1466 $clean['retention_days'] = 14;
1116 1467 }
1117 1468
1469 + if (!$this->is_pro()) {
1470 + $clean['view_logs_capability'] = 'manage_options';
1471 + }
1472 +
1118 1473 return $clean;
1119 1474 }
1120 1475
1476 + private function sanitize_view_logs_capability($capability): string
1477 + {
1478 + $capability = sanitize_key((string) $capability);
1479 +
1480 + $allowed = [
1481 + 'manage_options',
1482 + 'edit_pages',
1483 + 'edit_posts',
1484 + 'read',
1485 + ];
1486 +
1487 + return in_array($capability, $allowed, true) ? $capability : 'manage_options';
1488 + }
1489 +
1121 1490 private function sanitize_alerts(array $alerts): array
1122 1491 {
1123 1492 return [
1124 1493 'failed_login_enabled' => !empty($alerts['failed_login_enabled']),
@@ -1179,9 +1548,9 @@
1179 1548
1180 1549 public function get_default_settings(): array
1181 1550 {
1182 1551 return [
1183 - 'enabled' => true,
1552 + 'enabled' => false,
1184 1553 'timezone' => 'site',
1185 1554 'rows_per_page' => 20,
1186 1555 'retention_days' => 14,
1187 1556 'ip_storage' => 'full',
@@ -1186,8 +1555,9 @@
1186 1555 'retention_days' => 14,
1187 1556 'ip_storage' => 'full',
1188 1557 'store_user_agent' => true,
1189 1558 'trust_proxy_headers' => false,
1559 + 'view_logs_capability' => 'manage_options',
1190 1560 'modules' => [
1191 1561 'auth' => true,
1192 1562 'content' => true,
1193 1563 'users' => true,
@@ -1207,8 +1577,14 @@
1207 1577 ],
1208 1578 ];
1209 1579 }
1210 1580
1581 + private function get_view_logs_capability(): string
1582 + {
1583 + $capability = $this->settings['view_logs_capability'] ?? 'manage_options';
1584 + return $this->sanitize_view_logs_capability($capability);
1585 + }
1586 +
1211 1587 public function get_settings(): array
1212 1588 {
1213 1589 $defaults = $this->get_default_settings();
1214 1590 $saved = get_option(self::OPTION_NAME, []);
@@ -1260,8 +1636,17 @@
1260 1636 'plugin.activated' => __('Plugin activated', 'king-addons'),
1261 1637 'plugin.deactivated' => __('Plugin deactivated', 'king-addons'),
1262 1638 'plugin.updated' => __('Plugin updated', 'king-addons'),
1263 1639 'theme.switched' => __('Theme switched', 'king-addons'),
1640 + 'settings.option.updated' => __('Setting updated', 'king-addons'),
1641 + 'settings.option.added' => __('Setting added', 'king-addons'),
1642 + 'settings.option.deleted' => __('Setting deleted', 'king-addons'),
1643 + 'woocommerce.order.created' => __('Order created', 'king-addons'),
1644 + 'woocommerce.order.status_changed' => __('Order status changed', 'king-addons'),
1645 + 'woocommerce.product.stock_changed' => __('Product stock updated', 'king-addons'),
1646 + 'kng.option.updated' => __('King Addons setting updated', 'king-addons'),
1647 + 'kng.option.added' => __('King Addons setting added', 'king-addons'),
1648 + 'kng.option.deleted' => __('King Addons setting deleted', 'king-addons'),
1264 1649 ];
1265 1650 }
1266 1651
1267 1652 private function get_plugin_name(string $plugin): string
@@ -1300,9 +1685,30 @@
1300 1685 }
1301 1686
1302 1687 private function is_pro(): bool
1303 1688 {
1304 - return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
1689 + if (function_exists('king_addons_can_use_pro')) {
1690 + return king_addons_can_use_pro();
1691 + }
1692 +
1693 + if (!function_exists('king_addons_freemius')) {
1694 + return false;
1695 + }
1696 +
1697 + $fs = king_addons_freemius();
1698 + if (!is_object($fs)) {
1699 + return false;
1700 + }
1701 +
1702 + if (method_exists($fs, 'can_use_premium_code__premium_only')) {
1703 + return (bool) $fs->can_use_premium_code__premium_only();
1704 + }
1705 +
1706 + if (method_exists($fs, 'can_use_premium_code')) {
1707 + return (bool) $fs->can_use_premium_code();
1708 + }
1709 +
1710 + return false;
1305 1711 }
1306 1712
1307 1713 private function redirect_with_message(string $view, string $message): void
1308 1714 {