PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / modules / logs / classes / class-log-manager.php

class-log-manager.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies trunk, at modules/logs/classes/class-log-manager.php

645 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Database Site Actions
4 *
5 * This file handles all interactions with the Site Actions DB.
6 *
7 * @package MainWP/Dashboard
8 */
9
10 namespace MainWP\Dashboard\Module\Log;
11
12 use MainWP\Dashboard\MainWP_DB;
13 use MainWP\Dashboard\MainWP_Utility;
14 use MainWP\Dashboard\MainWP_Logger;
15
16 // Exit if accessed directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Class Log_Manager
23 *
24 * @package MainWP\Dashboard
25 */
26 class Log_Manager {
27
28 /**
29 * Version
30 *
31 * @const string Plugin version number.
32 * */
33 const VERSION = '5.0.0';
34
35 /**
36 * Log_Admin
37 *
38 * @var \MainWP\Dashboard\Module\Log\Log_Admin Admin class.
39 * */
40 public $admin;
41
42 /**
43 * Holds Instance of settings object
44 *
45 * @var Log_Settings
46 */
47 public $settings;
48
49 /**
50 * Log_Connectors
51 *
52 * @var \MainWP\Dashboard\Module\Log\Log_Connectors Connectors class.
53 * */
54 public $connectors;
55
56 /**
57 * Log_DB
58 *
59 * @var \MainWP\Dashboard\Module\Log\Log_DB DB Class.
60 * */
61 public $db;
62
63 /**
64 * Log
65 *
66 * @var \MainWP\Dashboard\Module\Log\Log Log Class.
67 * */
68 public $log;
69
70 /**
71 * Log_Install class.
72 *
73 * @var \MainWP\Dashboard\Module\Log\Log_Install Install class.
74 * */
75 public $install;
76
77 /**
78 * Holds the last log created time.
79 *
80 * @var array
81 */
82 private static $last_log_created = array();
83
84
85 /**
86 * Locations.
87 *
88 * @var array URLs and Paths used by the plugin.
89 */
90 public $locations = array();
91
92 /**
93 * Protected static variable to hold the single instance of the class.
94 *
95 * @var mixed Default null
96 */
97 protected static $instance = null;
98
99 /**
100 * Return the single instance of the class.
101 *
102 * @return mixed $instance The single instance of the class.
103 */
104 public static function instance() {
105 if ( is_null( static::$instance ) ) {
106 static::$instance = new self();
107 }
108 return static::$instance;
109 }
110
111 /**
112 * Plugin constructor.
113 *
114 * Run each time the class is called.
115 */
116 public function __construct() {
117
118 $mod_log_dir = MAINWP_MODULES_DIR . 'logs/';
119 $this->locations = array(
120 'dir' => $mod_log_dir,
121 'url' => MAINWP_MODULES_URL . 'logs/',
122 'inc_dir' => $mod_log_dir . 'includes/',
123 'class_dir' => $mod_log_dir . 'classes/',
124 );
125
126 spl_autoload_register( array( $this, 'autoload' ) );
127
128 // Load helper functions.
129 require_once $this->locations['inc_dir'] . 'functions.php'; // NOSONAR - WP compatible.
130
131 $driver = new Log_DB_Driver_WPDB();
132 $this->db = new Log_DB( $driver );
133 $this->settings = new Log_Settings( $this );
134
135 // Load logger class.
136 $this->log = new Log( $this );
137
138 // Load settings and connectors after widgets_init and before the default init priority.
139 add_action( 'init', array( $this, 'init' ), 9 );
140
141 // Change DB driver after plugin loaded if any add-ons want to replace.
142 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ), 20 );
143
144 add_action( 'mainwp_delete_site', array( $this, 'hook_delete_site' ), 10, 3 );
145
146 // Load admin area classes.
147 if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
148 $this->admin = new Log_Admin( $this );
149 } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
150 $this->admin = new Log_Admin( $this, $driver );
151 }
152
153 add_filter( 'mainwp_module_log_enable_insert_log_type', array( $this, 'hook_enable_insert_log_type' ), 10, 2 );
154 add_filter( 'mainwp_get_cron_jobs_init', array( $this, 'hook_get_cron_jobs_init' ), 10, 2 ); // on/off by change status of use wp cron option.
155 add_filter( 'mainwp_module_logs_changes_logs_sync_params', array( $this, 'hook_changes_logs_sync_params' ), 10, 2 ); // on/off by change status of use wp cron option.
156 add_filter( 'mainwp_module_logs_get_log_records', array( $this, 'hook_get_log_records' ), 10, 2 ); // Retrieves MainWP log records based on provided query parameters.
157
158 if ( $this->is_enabled_auto_archive_logs() && ! empty( $this->settings->options['records_logs_ttl'] ) ) {
159 add_action( 'mainwp_module_log_cron_job_auto_archive', array( $this, 'cron_module_log_auto_archive' ) );
160 }
161
162 if ( $this->admin instanceof Log_Admin ) {
163 if ( ! empty( $this->settings->options['enabled'] ) ) {
164 add_action( 'mainwp_module_log_render_db_size_notice', array( $this->admin, 'render_logs_db_notice' ), 10, 2 );
165 }
166 add_action( 'mainwp_module_log_render_db_update_notice', array( $this->admin, 'render_update_db_notice' ), 10, 1 );
167 }
168 }
169
170
171 /**
172 * Autoloader for classes.
173 *
174 * @param string $class_name class name.
175 */
176 public function autoload( $class_name ) {
177
178 if ( ! preg_match( '/^(?P<namespace>.+)\\\\(?P<autoload>[^\\\\]+)$/', $class_name, $matches ) ) {
179 return;
180 }
181
182 static $reflection;
183
184 if ( empty( $reflection ) ) {
185 $reflection = new \ReflectionObject( $this );
186 }
187
188 if ( $reflection->getNamespaceName() !== $matches['namespace'] ) {
189 return;
190 }
191
192 $autoload_name = $matches['autoload'];
193 $autoload_dir = \trailingslashit( $this->locations['dir'] );
194 $load_dirs = array(
195 'classes' => 'class',
196 'pages' => 'page',
197 'widgets' => 'widget',
198 );
199 foreach ( $load_dirs as $dir => $prefix ) {
200 $dir = $dir . '/';
201 $autoload_path = sprintf( '%s%s%s-%s.php', $autoload_dir, $dir, $prefix, strtolower( str_replace( '_', '-', $autoload_name ) ) );
202 if ( is_readable( $autoload_path ) ) {
203 require_once $autoload_path; // NOSONAR - WP compatible.
204 return;
205 }
206 }
207 }
208
209
210 /**
211 * Get internal connectors.
212 */
213 public function get_internal_connectors() {
214 return array(
215 'compact',
216 );
217 }
218
219 /**
220 * Load Log_Connectors.
221 *
222 * @action init
223 *
224 * @uses \MainWP\Dashboard\Module\Log\Log_Connectors
225 */
226 public function init() {
227 if ( class_exists( '\MainWP\Dashboard\Module\Log\Log_Connectors' ) ) { // to fix fatal error in case class not found.
228 $this->connectors = new Log_Connectors( $this );
229 }
230 }
231
232
233 /**
234 * Getter for the version number.
235 *
236 * @return string
237 */
238 public function get_version() {
239 return static::VERSION;
240 }
241
242 /**
243 * Change plugin database driver in case driver plugin loaded after logs.
244 *
245 * @uses \MainWP\Dashboard\Module\Log\Log_DB
246 * @uses \MainWP\Dashboard\Module\Log\Log_DB_Driver_WPDB
247 */
248 public function plugins_loaded() {
249 // Load DB helper interface/class.
250 $driver_class = '\MainWP\Dashboard\Module\Log\Log_DB_Driver_WPDB';
251
252 if ( class_exists( $driver_class ) ) {
253 $driver = new $driver_class();
254 $this->db = new Log_DB( $driver );
255 }
256 }
257
258 /**
259 * Method get_sync_actions_last_created().
260 *
261 * Sync site changes logs data.
262 *
263 * @param int $site_id site id.
264 *
265 * @return bool
266 */
267 public function get_sync_actions_last_created( $site_id ) {
268
269 if ( ! isset( static::$last_log_created[ $site_id ] ) ) {
270 $site_opts = MainWP_DB::instance()->get_website_options_array( $site_id, array( 'non_mainwp_changes_sync_last_created' ) );
271
272 if ( ! is_array( $site_opts ) ) {
273 $site_opts = array();
274 }
275
276 static::$last_log_created[ $site_id ] = isset( $site_opts['non_mainwp_changes_sync_last_created'] ) ? $site_opts['non_mainwp_changes_sync_last_created'] : 0;
277
278 // to sure.
279 if ( empty( static::$last_log_created[ $site_id ] ) ) {
280 static::$last_log_created[ $site_id ] = time() - DAY_IN_SECONDS;
281 MainWP_DB::instance()->update_website_option( $site_id, 'non_mainwp_changes_sync_last_created', static::$last_log_created[ $site_id ] );
282 }
283 }
284
285 return static::$last_log_created[ $site_id ];
286 }
287
288
289 /**
290 * Method sync_log_site_actions().
291 *
292 * Sync site actions data.
293 *
294 * @param int $site_id site id.
295 * @param array $sync_actions action data.
296 * @param object $website website data.
297 *
298 * @return bool
299 */
300 public function sync_log_site_actions( $site_id, $sync_actions, $website ) { // phpcs:ignore -- NOSONAR - complex.
301
302 if ( empty( $sync_actions ) || ! is_array( $sync_actions ) ) {
303 return false;
304 }
305
306 MainWP_Utility::array_sort_existed_keys( $sync_actions, 'created', SORT_NUMERIC );
307
308 $sync_last_created = (float) $this->get_sync_actions_last_created( $site_id );
309
310 $new_last_created = 0;
311
312 foreach ( $sync_actions as $data ) {
313 if ( ! is_array( $data ) || empty( $data['action_user'] ) || empty( $data['created'] ) ) {
314 continue;
315 }
316
317 $item_created = round( (float) $data['created'], 4 ); // to fix float comparing issue.
318
319 if ( $item_created <= $sync_last_created ) {
320 continue;
321 }
322
323 if ( $new_last_created < $item_created ) {
324 $new_last_created = $item_created;
325 }
326
327 $user_meta = array();
328 $meta_data = array();
329 $extra_info = false;
330
331 if ( isset( $data['meta_data'] ) && is_array( $data['meta_data'] ) ) {
332 $meta_data = $data['meta_data'];
333 if ( isset( $meta_data['user_meta'] ) && is_array( $meta_data['user_meta'] ) ) {
334 $user_meta = $meta_data['user_meta']; // to compatible old user_meta site changes actions data.
335 unset( $meta_data['user_meta'] );
336 } elseif ( isset( $meta_data['meta_data'] ) && ! empty( $meta_data['meta_data'] ) && is_array( $meta_data['meta_data'] ) ) {
337 $user_meta = $meta_data['meta_data']; // new meta_data site changes actions.
338 unset( $meta_data['meta_data'] );
339 }
340 $meta_data['user_meta_json'] = wp_json_encode( $user_meta );
341 if ( isset( $meta_data['extra_info'] ) && is_array( $meta_data['extra_info'] ) ) {
342 $extra_info = $meta_data['extra_info'];
343 }
344 }
345
346 $user_login = sanitize_text_field( wp_unslash( $data['action_user'] ) );
347
348 $sum = '';
349 if ( false !== $extra_info ) {
350 $meta_data['extra_info'] = wp_json_encode( $extra_info );
351 $sum .= ! empty( $extra_info['name'] ) ? esc_html( $extra_info['name'] ) : '';
352 } else {
353 $sum .= ! empty( $meta_data['name'] ) ? esc_html( $meta_data['name'] ) : '';
354 }
355 $sum .= ' ';
356 $sum .= 'wordpress' !== $data['context'] ? esc_html( ucfirst( rtrim( $data['context'], 's' ) ) ) : 'WordPress'; //phpcs:ignore -- wordpress text.
357
358 if ( 'wordpress' === $data['context'] ) {
359 $sum = 'WordPress';
360 }
361
362 if ( isset( $user_meta['wp_user_id'] ) ) {
363 $user_id = ! empty( $user_meta['wp_user_id'] ) ? sanitize_text_field( $user_meta['wp_user_id'] ) : 0;
364 } elseif ( ! empty( $user_meta['user_id'] ) ) { // to compatible with old child actions data.
365 $user_id = sanitize_text_field( $user_meta['user_id'] );
366 } elseif ( isset( $data['user_id'] ) && ! empty( $data['user_id'] ) ) {
367 $user_id = $data['user_id']; // new sync changes logs data.
368 }
369
370 $actions_mapping = array(
371 'installed' => 'install',
372 'deleted' => 'delete',
373 'activated' => 'activate',
374 'deactivated' => 'deactivate',
375 );
376
377 $contexts_mapping = array(
378 'plugins' => 'plugin',
379 'themes' => 'theme',
380 'wordpress' => 'core',
381 );
382
383 $action = isset( $actions_mapping[ $data['action'] ] ) ? $actions_mapping[ $data['action'] ] : $data['action'];
384 $context = isset( $contexts_mapping[ $data['context'] ] ) ? $contexts_mapping[ $data['context'] ] : $data['context'];
385
386 $created = isset( $data['created'] ) ? (float) $data['created'] : microtime( true );
387
388 // to fix missing slug meta for theme context issue.
389 if ( ( 'theme' === $context ) && empty( $meta_data['slug'] ) && is_array( $extra_info ) ) {
390 if ( ! empty( $extra_info['slug'] ) ) {
391 $meta_data['slug'] = $extra_info['slug'];
392 } elseif ( empty( $meta_data['name'] ) && ! empty( $extra_info['name'] ) ) {
393 $meta_data['name'] = $extra_info['name'];
394 }
395 }
396
397 $record_mapping = array(
398 'site_id' => $site_id,
399 'user_id' => $user_id,
400 'user_login' => $user_login,
401 'created' => $created,
402 'item' => $sum,
403 'context' => $context,
404 'action' => $action,
405 'state' => 1,
406 'duration' => isset( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : 0, // sanitize_text_field for seconds.
407 'meta' => $meta_data,
408 );
409
410 do_action( 'mainwp_sync_site_log_install_actions', $website, $record_mapping );
411 }
412
413 if ( $new_last_created ) {
414 MainWP_DB::instance()->update_website_option( $site_id, 'non_mainwp_changes_sync_last_created', $new_last_created );
415 }
416
417 return true;
418 }
419
420 /**
421 * Method is_enabled_auto_archive_logs().
422 *
423 * @return bool True|False Enabled auto archive log or not.
424 */
425 public function is_enabled_auto_archive_logs() {
426 return is_array( $this->settings->options ) && ! empty( $this->settings->options['enabled'] ) && ! empty( $this->settings->options['auto_archive'] ) ? true : false;
427 }
428
429 /**
430 * Method hook_delete_site()
431 *
432 * @param mixed $site site object.
433 *
434 * @return bool result.
435 */
436 public function hook_delete_site( $site ) {
437 if ( empty( $site ) ) {
438 return false;
439 }
440 return Log_DB_Helper::instance()->remove_logs_by( $site->id );
441 }
442
443 /**
444 * Method hook_enable_insert_log_type()
445 *
446 * @param bool $enabled Enable input value.
447 * @param array $data Log data.
448 *
449 * @return bool True: Enable log.
450 */
451 public function hook_enable_insert_log_type( $enabled, $data ) {
452 if ( is_array( $data ) && ! empty( $data['log_type_id'] ) ) {
453 return Log_Settings::is_action_log_enabled( $data['log_type_id'], 'changeslogs' );
454 } elseif ( is_array( $data ) && isset( $data['connector'] ) && isset( $data['context'] ) && isset( $data['action'] ) ) {
455 if ( 'non-mainwp-changes' === $data['connector'] ) {
456 return Log_Settings::is_action_log_enabled( $data['context'] . '_' . $data['action'], 'nonmainwpchanges' );
457 } elseif ( 'compact' !== $data['connector'] ) {
458 return Log_Settings::is_action_log_enabled( $data['context'] . '_' . $data['action'], 'dashboard' );
459 }
460 }
461 return $enabled;
462 }
463
464 /**
465 * Method hook_get_cron_jobs_init()
466 *
467 * @param array $init_jobs Jobs to init.
468 *
469 * @return array Init Jobs.
470 */
471 public function hook_get_cron_jobs_init( $init_jobs ) {
472 if ( $this->is_enabled_auto_archive_logs() ) {
473 $init_jobs['mainwp_module_log_cron_job_auto_archive'] = 'daily';
474 }
475 return $init_jobs;
476 }
477
478 /**
479 * Method cron_module_log_auto_archive()
480 */
481 public function cron_module_log_auto_archive() {
482 $ttl = 3 * YEAR_IN_SECONDS;
483 if ( is_array( $this->settings->options ) && isset( $this->settings->options['records_logs_ttl'] ) ) {
484 $ttl = intval( $this->settings->options['records_logs_ttl'] );
485 }
486 if ( $ttl ) {
487 do_action( 'mainwp_log_action', 'Module Log :: Archive logs schedule start.', MainWP_Logger::LOGS_AUTO_PURGE_LOG_PRIORITY );
488 $time = time();
489 $before = $time - $ttl;
490 Log_DB_Archive::instance()->archive_sites_changes( $before );
491 update_option( 'mainwp_module_log_last_time_auto_archive_logs', $time );
492 update_option( 'mainwp_module_log_next_time_auto_archive_logs', $time + $ttl );
493 }
494 }
495
496 /**
497 * Method hook_changes_logs_sync_params()
498 *
499 * @param string $params Input value.
500 * @param int $site_id Site id.
501 * @param array $postdata Post data.
502 *
503 * @return string Empty or json encoded value.
504 *
505 * @since 5.5
506 */
507 public function hook_changes_logs_sync_params( $params, $site_id, $postdata = array() ) {
508
509 // if it is not a manual sync data.
510 $sync_logs = isset( $_POST['action'] ) && 'mainwp_syncsites' === $_POST['action'] ? true : false; //phpcs:ignore --ok.
511 $sync_logs = apply_filters( 'mainwp_module_logs_sync_changes_log', $sync_logs );
512
513 if ( ! $sync_logs ) {
514 return $params;
515 }
516
517 $last_created = Log_Changes_Logs_Helper::instance()->get_sync_changes_logs_last_created( $site_id );
518 $events_count = apply_filters( 'mainwp_module_log_changes_logs_sync_count', 100, $site_id, $postdata );
519
520 $disabled_changeslogs = Log_Settings::get_disabled_logs_type( 'changeslogs' );
521 $disabled_nonmainwp_actions = Log_Settings::get_disabled_logs_type( 'nonmainwpchanges' );
522
523 $log_settings = get_option( 'mainwp_module_log_settings', array() );
524 if ( ! is_array( $log_settings ) ) {
525 $log_settings = array();
526 }
527
528 return array(
529 'newer_than' => $last_created,
530 'events_count' => $events_count,
531 'ignore_sync_changes_logs' => ! empty( $disabled_changeslogs ) ? $disabled_changeslogs : -1, // -1 to prevent it removed nested empty array from http query builder.
532 'ignore_sync_nonmainwp_actions' => ! empty( $disabled_nonmainwp_actions ) ? $disabled_nonmainwp_actions : -1,
533 'child_logs_ttl' => ! empty( $log_settings['child_logs_ttl'] ) ? absint( $log_settings['child_logs_ttl'] ) : 7,
534 'child_logs_enabled' => ! empty( $log_settings['enabled'] ) ? 1 : 0,
535 );
536 }
537
538 /**
539 * Method hook_get_log_records()
540 *
541 * Retrieves MainWP log records based on provided query parameters.
542 *
543 * Available $params arguments (all optional):
544 *
545 * Default values:
546 * <code>
547 * array(
548 * // LOG TARGETING
549 * 'log_id' => 0, // Specific log ID.
550 * 'wpid' => 0, // Site ID or array of site IDs.
551 *
552 * // ACCESS / VIEW
553 * 'check_access' => true, // Apply access restrictions.
554 * 'view' => '', // View mode: '', 'events_list', 'api-view'.
555 *
556 * // OPTIMIZATION
557 * 'optimize' => false, // Enable optimized query.
558 * 'optimize_with_meta' => false, // Fetch meta separately when optimized.
559 * 'with_all_logs_meta' => false, // Load full logs meta.
560 *
561 * // RESULT MODE
562 * 'count_only' => false, // Return count only.
563 * 'not_count' => false, // Skip count query.
564 *
565 * // SEARCH / FILTERS
566 * 'search' => '', // Search keyword.
567 * 'dismiss' => null, // true|false|null.
568 *
569 * // GROUP / CLIENT FILTERS
570 * 'groups_ids' => array(), // Filter by group IDs.
571 * 'client_ids' => array(), // Filter by client IDs.
572 *
573 * // USER FILTERS
574 * 'usersfilter_sites_ids' => array(), // Format: userId-siteId-isDashboardUser.
575 * 'user_ids' => array(), // Legacy user filter.
576 *
577 * // TIME RANGE
578 * 'timestart' => 0, // Start timestamp (seconds).
579 * 'timestop' => 0, // End timestamp (seconds).
580 *
581 * // SOURCE FILTER
582 * 'sources_conds' => '', // 'wp-admin-only'|'dashboard-only'.
583 *
584 * // CONTEXT / EVENTS
585 * 'contexts' => '', // CSV contexts list.
586 * 'sites_ids' => array(), // Explicit site IDs.
587 * 'events' => array(), // Actions/events filter.
588 *
589 * // PAGINATION
590 * 'start' => 0, // Offset.
591 * 'records_per_page' => 0, // LIMIT size (0 = unlimited).
592 *
593 * // SORTING
594 * 'order' => 'DESC', // ASC|DESC.
595 * 'orderby' => 'created', // Sort column.
596 *
597 * // RECENT EVENTS
598 * 'recent_number' => 0, // Limit to recent logs.
599 * )
600 * </code>
601 *
602 * @param mixed $input_val Default value.
603 * @param array $params {
604 * Optional. Log query parameters.
605 * }
606 *
607 * @return array Results.
608 *
609 * @since 6.0.1
610 */
611 public function hook_get_log_records( $input_val, $params = array() ) {
612
613 unset( $input_val ); // not used, just for hook compatibility.
614
615 $defaults = array(
616 // Search param.
617 'search' => null,
618 'search_field' => 'item',
619 'records_per_page' => get_option( 'posts_per_page', 20 ),
620 'paged' => 1,
621 // Order.
622 'order' => 'desc',
623 'orderby' => 'date',
624 );
625
626 $args = wp_parse_args( $params, $defaults );
627
628 return (array) $this->db->get_records( $args );
629 }
630
631
632 /**
633 * Method normalize_to_microseconds()
634 *
635 * @param mixed $time float time value or microsecords to check.
636 *
637 * @return int Normalize microseconds value.
638 *
639 * @since 5.5
640 */
641 public static function normalize_to_microseconds( $time ) {
642 return ( $time < 1e12 ) ? (int) ( $time * 1000000 ) : (int) $time;
643 }
644 }
645