wpal_admin = new WPAL_ADMIN(); add_action( 'admin_notices', [$this, 'wpal_handle_log_limit_notice'], 10, 3 ); add_action('updated_option', [$this, 'wpal_database_connect'], 10, 3); $this->wpal_database_connect('wpal-settings', null, get_option('wpal-settings')); add_action('init', [$this, 'wpal_init']); register_activation_hook( __FILE__, function() { update_option( 'wpal_log_limit_notice', 0 ); }); } /** * Retrieves the singleton instance of the WPAL_MAIN class. * * Ensures that only one instance of the class exists, providing a global point of access for * plugin functionality. Creates a new instance if none exists. * * @return WPAL_MAIN The singleton instance of the class. * @since 1.0.0 */ public static function get_instance() { if (self::$wpal === null) { self::$wpal = new self(); } return self::$wpal; } /** * Prevents cloning of the singleton instance. * * Ensures the singleton pattern is maintained by disallowing object cloning. * * @since 1.0.0 */ public function __clone() { // Cloning is not allowed. } /** * Prevents unserializing of the singleton instance. * * Maintains the singleton pattern by preventing unserialization of the class instance. * * @since 1.0.0 */ public function __wakeup() { // Unserializing is not allowed. } /** * Initializes WordPress action hooks for activity monitoring. * * Registers hooks to track user events (login, logout, registration, deletion) and post events * (publish, edit, delete). Attached to the 'init' action to ensure hooks are set up early in * the WordPress lifecycle. * * @since 1.0.0 */ public function wpal_init() { add_action('wp_login', [$this, 'wpal_on_user_login'], 10, 2); add_action('wp_logout', [$this, 'wpal_on_user_logout']); add_action('user_register', [$this, 'wpal_on_user_registered']); add_action('delete_user', [$this, 'wpal_on_user_deleted']); add_action('transition_post_status', [$this, 'wpal_on_post_publish'], 10, 3); add_action('post_updated', [$this, 'wpal_on_post_edit'], 10, 3); add_action('wp_trash_post', [$this, 'wpal_on_post_delete']); } /** * Connects to the external database based on updated settings. * * Initializes the database connection using the provided configuration settings and sets up * the necessary database tables. If the connection fails, the database handler is set to null. * * @param string $option_name The name of the updated option. * @param mixed $old_value The old option value. * @param mixed $new_value The new option value. * @since 1.0.0 */ public function wpal_database_connect($option_name, $old_value, $new_value) { if ($option_name !== 'wpal-settings') { return; } if (!$this->is_valid_db_config($new_value)) { return; } $this->wpal_db = new WPAL_DB( $new_value['user'], $new_value['pass'], $new_value['database'], $new_value['host'] ); if (!empty($this->wpal_db->connect_error)) { $this->wpal_db = null; } else { $this->wpal_db->wpal_tables(); $this->wpal_log = new WPAL_LOG($this->wpal_db); $this->wpal_features = new WPAL_FEATURES($this->wpal_db, $this->wpal_log); $this->wpal_admin->wpal_set_features($this->wpal_features); $this->wpal_admin->wpal_users_init(); } } /** * Validates the database configuration array. * * Checks if the provided configuration array contains all required fields (host, user, pass, database) * and ensures they are not empty. * * @param array $config Database configuration array. * @return bool True if the configuration is valid, false otherwise. * @since 1.0.0 */ private function is_valid_db_config($config) { return is_array($config) && !empty($config['host']) && !empty($config['user']) && array_key_exists('pass', $config) && !empty($config['database']); } /** * Retrieves user information for logging purposes. * * Attempts to gather user data from a provided WP_User object, the currently logged-in user, * or a fallback cookie-based detection using WordPress's logged-in cookie. Returns null if * no valid user is identified. * * @param WP_User|null $user Optional WP_User object to extract information from. * @return array|null Associative array containing user_id, user_email, user_login, and user_role, or null if no user is found. * @since 1.0.0 */ public function wpal_user_info($user = null) { $wp_user = null; // If a WP_User object is provided if ($user instanceof WP_User) { $wp_user = $user; } elseif (is_user_logged_in()) { $wp_user = wp_get_current_user(); } elseif (isset($_COOKIE[LOGGED_IN_COOKIE])) { $cookie = sanitize_text_field(wp_unslash($_COOKIE[LOGGED_IN_COOKIE])); $user_login = explode('|', $cookie)[0] ?? null; $wp_user = get_user_by('login', $user_login); } if (!$wp_user || empty($wp_user->ID)) { return null; } $user_id = $wp_user->ID; $user_email = $wp_user->user_email; $user_login = $wp_user->user_login; $user_role = implode(', ', $wp_user->roles); return [ 'user_id' => $user_id, 'user_email' => $user_email, 'user_login' => $user_login, 'user_role' => $user_role, ]; } /** * Logs user login activity and initializes a session. * * Generates a unique session key using a timestamp and random password, logs the login event * via WPAL_LOG, records session details (IP address, user agent), and sets a secure session * cookie for tracking subsequent activities. Attached to the 'wp_login' action. * * @param string $user_login Username of the logging-in user. * @param WP_User $user WP_User object of the logging-in user. * @since 1.0.0 */ public function wpal_on_user_login($user_login, $user) { if (!($user instanceof WP_User)) { return; } $user_info = $this->wpal_user_info($user); $user_id = $user_info['user_id']; $user_name = $user_info['user_login']; $key = hash('sha256', time() . '-' . wp_generate_password(64, false)); $user_ip = '0.0.0.0'; if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $user_ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR'])); } elseif (!empty($_SERVER['REMOTE_ADDR'])) { $user_ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])); } $user_agent = !empty($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown'; $type = "user"; $detail = "{$user_name} logged into the dashboard"; $this->wpal_log->wpal_activity_log($user_id, $type, $detail, $key); $this->wpal_log->wpal_sessions_log($key, $user_ip, $user_agent, $user_id); setcookie(WPAL_SESSION_KEY, $key, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); } /** * Logs user logout activity. * * Retrieves the current user's information and session key from the cookie, then logs the * logout event via WPAL_LOG. Attached to the 'wp_logout' action. * * @since 1.0.0 */ public function wpal_on_user_logout() { $user_info = $this->wpal_user_info(); if (!$user_info) { return; } $user_id = $user_info['user_id']; $user_name = $user_info['user_login']; $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $type = "user"; $detail = "{$user_name} logged out of the dashboard"; $this->wpal_log->wpal_activity_log($user_id, $type, $detail, $key); } /** * Logs user registration activity. * * Records the registration of a new user by logging the event and adding the new user's details * to the user log database via WPAL_LOG. Attached to the 'user_register' action. * * @param int $reg_user ID of the newly registered user. * @since 1.0.0 */ public function wpal_on_user_registered($reg_user) { $user_info = $this->wpal_user_info(); if (!$user_info) { return; } $user_id = $user_info['user_id']; $user_name = $user_info['user_login']; $user_email = $user_info['user_email']; $user_role = $user_info['user_role']; $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $type = "user"; $reg_user_data = get_userdata($reg_user); $reg_user_name = $reg_user_data->user_login; $reg_user_email = $reg_user_data->user_email; $reg_user_role = implode(', ', $reg_user_data->roles); $detail = "{$reg_user_name} registered as {$reg_user_role} by {$user_name}"; $this->wpal_log->wpal_activity_log($user_id, $type, $detail, $key); $this->wpal_log->wpal_user_log($reg_user, $reg_user_name, $reg_user_email, $reg_user_role); } /** * Logs user deletion activity. * * Records the deletion of a user and updates their status in the user log database to mark them * as deleted. Attached to the 'delete_user' action. * * @param int $del_user ID of the deleted user. * @since 1.0.0 */ public function wpal_on_user_deleted($del_user) { $user_info = $this->wpal_user_info(); if (!$user_info) { return; } $user_id = $user_info['user_id']; $user_name = $user_info['user_login']; $user_email = $user_info['user_email']; $user_role = $user_info['user_role']; $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $type = "user"; $del_user_data = get_userdata($del_user); $del_user_name = $del_user_data->user_login; $del_user_role = implode(', ', $del_user_data->roles); $detail = "{$del_user_name} deleted as {$del_user_role} by {$user_name}"; $this->wpal_log->wpal_activity_log($user_id, $type, $detail, $key); $this->wpal_log->wpal_user_update($del_user, 0); } /** * Logs post publishing activity. * * Records when a post transitions from a non-published state to a published state. Uses a meta * flag (wpal_published_flag) to prevent duplicate logging. Attached to the 'transition_post_status' action. * * @param string $new_status New post status (e.g., 'publish'). * @param string $old_status Previous post status (e.g., 'draft'). * @param WP_Post $post Post object being published. * @since 1.0.0 */ public function wpal_on_post_publish($new_status, $old_status, $post) { if ($new_status !== 'publish' || $old_status === 'publish') { return; } if (get_post_meta($post->ID, 'wpal_published_flag', true)) { return; } $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $post = get_post($post->ID); $user_info = $this->wpal_user_info(); if (!$user_info) { return; } update_post_meta($post->ID, 'wpal_published_flag', 1); update_post_meta($post->ID, 'wpal_edited_flag', 1); $this->wpal_log->wpal_activity_log( $user_info['user_id'], 'post', "'{$post->post_title}' (ID: {$post->ID}) was published by {$user_info['user_login']}", $key ); update_post_meta($post->ID, 'wpal_before_title', 'Auto Draft'); update_post_meta($post->ID, 'wpal_after_title', $post->post_title); } /** * Logs post editing activity. * * Records edits to published posts, skipping autosaves and using a meta flag (wpal_edited_flag) * to prevent duplicate logs. Tracks title changes for accurate logging. Attached to the 'post_updated' action. * * @param int $post_id ID of the post being edited. * @param WP_Post $post_after Post object after the update. * @param WP_Post $post_before Post object before the update. * @since 1.0.0 */ public function wpal_on_post_edit($post_id, $post_after, $post_before) { if ($post_after->post_status !== 'publish') { return; } if (wp_is_post_autosave($post_id)) { return; } if (get_post_meta($post_id, 'wpal_edited_flag', true)) { update_post_meta($post_id, 'wpal_edited_flag', 0); return; } $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $user_info = $this->wpal_user_info(); if (!$user_info) { return; } if (get_post_meta($post_id, 'wpal_before_title', true) === "Auto Draft") { update_post_meta($post_id, 'wpal_before_title', $post_before->post_title); update_post_meta($post_id, 'wpal_after_title', $post_after->post_title); return; } $this->wpal_log->wpal_activity_log( $user_info['user_id'], 'post', "'{$post_after->post_title}' (ID: {$post_after->ID}) was edited by {$user_info['user_login']}", $key ); update_post_meta($post_id, 'wpal_before_title', $post_before->post_title); update_post_meta($post_id, 'wpal_after_title', $post_after->post_title); update_post_meta($post_id, 'wpal_edited_flag', 1); } /** * Logs post deletion activity. * * Records when a post is moved to the trash, logging the post title and ID. Attached to the * 'wp_trash_post' action. * * @param int $post_id ID of the post being trashed. * @since 1.0.0 */ public function wpal_on_post_delete($post_id) { $post = get_post($post_id); $user_info = $this->wpal_user_info(); if (!$user_info) { return; } $key = !empty($_COOKIE[WPAL_SESSION_KEY]) ? sanitize_text_field(wp_unslash($_COOKIE[WPAL_SESSION_KEY])) : ''; $this->wpal_log->wpal_activity_log( $user_info['user_id'], 'post', "'{$post->post_title}' (ID: {$post->ID}) was deleted by {$user_info['user_login']}", $key ); } /** * Display admin notice when log limit is reached. * * @param string $option_name The name of the updated option. * @param mixed $old_value The old value of the option. * @param mixed $value The new value of the option. */ public function wpal_handle_log_limit_notice() { if ( get_option( 'wpal_log_limit_notice' ) ) { $allowed_screens = [ 'toplevel_page_wpal-settings', 'wpal_page_wpal-db-settings', 'wpal_page_wpal-export-data', ]; // Safe way to get current screen $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && isset( $screen->id ) && in_array( $screen->id, $allowed_screens, true ) ) { ?>