Linux webm002.cluster126.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64
/
home
/
a
/
r
/
i
/
ariannadhf
/
www
/
wp-content
/
plugins
/
simple-history
/
loggers
/
/home/a/r/i/ariannadhf/www/wp-content/plugins/simple-history/loggers/class-user-logger.php
<?php namespace Simple_History\Loggers; use Error; use Simple_History\Event_Details\Event_Details_Group; use Simple_History\Event_Details\Event_Details_Item; use Simple_History\Existing_Data_Importer; use Simple_History\Helpers; use Simple_History\Log_Initiators; /** * Logs changes to user logins (and logouts). */ class User_Logger extends Logger { /** @var string Logger slug */ public $slug = 'SimpleUserLogger'; /** @var array<int,array> Context for modified user. */ private $user_profile_update_modified_context = []; /** @var array Detected WP Cli Changes */ private $wp_cli_changes = [ 'user_roles_added' => [], 'user_roles_removed' => [], ]; /** * Username captured from the `authenticate` filter chain. * * Needed for XML-RPC application-password failures, where the username travels * inside the XML body and never lands in `$_SERVER['PHP_AUTH_USER']`. The * `application_password_failed_authentication` action passes only the WP_Error, * so we stash the value seen during `authenticate` and read it back here. * * @var string|null */ private $last_attempted_username = null; /** * Dedupe guard for `application_password_failed_authentication`. * * WP fires the action twice per request (during `authenticate` and again during * `determine_current_user`), and the resolve-current-user call inside the log * write can re-enter via `wp_validate_application_password`. We only want one * log row per request. * * @var bool */ private $app_password_failure_logged = false; /** @inheritDoc */ public function get_info() { return array( 'name' => __( 'User Logger', 'simple-history' ), 'description' => __( 'Logs user logins, logouts, and failed logins', 'simple-history' ), 'capability' => 'edit_users', 'messages' => array( 'user_login_failed' => __( 'Failed to login with username "{login}" (incorrect password entered)', 'simple-history' ), 'user_unknown_login_failed' => __( 'Failed to login with username "{failed_username}" (username does not exist)', 'simple-history' ), 'user_logged_in' => __( 'Logged in', 'simple-history' ), 'user_unknown_logged_in' => __( 'Unknown user logged in', 'simple-history' ), 'user_logged_out' => __( 'Logged out', 'simple-history' ), 'user_updated_profile' => __( 'Edited the profile for user "{edited_user_login}" ({edited_user_email})', 'simple-history' ), 'user_created' => __( 'Created user {created_user_login} ({created_user_email}) with role {created_user_role}', 'simple-history' ), 'user_deleted' => __( 'Deleted user {deleted_user_login} ({deleted_user_email})', 'simple-history' ), 'user_password_reseted' => __( 'Reset their password', 'simple-history' ), 'user_requested_password_reset_link' => __( "Requested a password reset link for user with login '{user_login}' and email '{user_email}'", 'simple-history' ), 'user_session_destroy_others' => _x( 'Logged out from all other sessions', 'User destroys other login sessions for themself', 'simple-history' ), 'user_session_destroy_everywhere' => _x( 'Logged out "{user_display_name}" from all sessions', 'User destroys all login sessions for a user', 'simple-history' ), 'user_role_updated' => _x( 'Changed role for user "{edited_user_login}" to "{new_role}" from "{old_role}"', 'User updates the role for a user', 'simple-history' ), 'user_role_added' => _x( 'Added role(s) "{roles}" to user "{edited_user_login}"', 'A role is added to a user', 'simple-history' ), 'user_role_removed' => _x( 'Removed role(s) "{roles}" from user "{edited_user_login}"', 'A role is removed from a user', 'simple-history' ), 'user_admin_email_confirm_correct_clicked' => _x( 'Verified that administration email for website is correct', 'User clicks confirm admin email on admin email confirm screen', 'simple-history' ), 'user_application_password_created' => _x( 'Added application password "{application_password_name}" for user "{edited_user_login}"', 'User add new application password', 'simple-history' ), 'user_application_password_revoked' => _x( 'Revoked application password "{application_password_name}" for user "{edited_user_login}"', 'User revoke application password', 'simple-history' ), 'user_application_password_login_failed' => _x( 'Failed to authenticate with application password for user "{login}"', 'Failed application password authentication on REST/XML-RPC', 'simple-history' ), 'user_application_password_unknown_login_failed' => _x( 'Failed to authenticate with application password for user "{failed_username}" (user does not exist)', 'Failed application password authentication for unknown user', 'simple-history' ), 'user_admin_page_access_denied' => _x( 'Attempted to access restricted admin page "{admin_page}"', 'User was denied access to an admin page', 'simple-history' ), ), 'labels' => array( 'search' => array( 'label' => _x( 'Users', 'User logger: search', 'simple-history' ), 'label_all' => _x( 'All user activity', 'User logger: search', 'simple-history' ), 'options' => array( _x( 'Successful user logins', 'User logger: search', 'simple-history' ) => array( 'user_logged_in', 'user_unknown_logged_in', ), _x( 'Failed user logins', 'User logger: search', 'simple-history' ) => array( 'user_login_failed', 'user_unknown_login_failed', ), _x( 'Failed login (known user)', 'User logger: search', 'simple-history' ) => array( 'user_login_failed', ), _x( 'Failed login (unknown user)', 'User logger: search', 'simple-history' ) => array( 'user_unknown_login_failed', ), _x( 'User logouts', 'User logger: search', 'simple-history' ) => array( 'user_logged_out', ), _x( 'Created users', 'User logger: search', 'simple-history' ) => array( 'user_created', ), _x( 'User profile updates', 'User logger: search', 'simple-history' ) => array( 'user_updated_profile', ), _x( 'User role changes', 'User logger: search', 'simple-history' ) => array( 'user_role_updated', ), _x( 'User deletions', 'User logger: search', 'simple-history' ) => array( 'user_deleted', ), _x( 'User application password created', 'User logger: search', 'simple-history' ) => array( 'user_application_password_created', ), _x( 'User application password deletion', 'User logger: search', 'simple-history' ) => array( 'user_application_password_revoked', ), _x( 'Failed application password authentication', 'User logger: search', 'simple-history' ) => array( 'user_application_password_login_failed', 'user_application_password_unknown_login_failed', ), _x( 'Admin page access denied', 'User logger: search', 'simple-history' ) => array( 'user_admin_page_access_denied', ), ), ), ), ); } /** * Add actions and filters when logger is loaded by Simple History */ public function loaded() { // Plain logins and logouts. add_action( 'wp_login', array( $this, 'on_wp_login' ), 1, 2 ); add_action( 'wp_logout', array( $this, 'onWpLogout' ), 10, 1 ); // Failed login attempt to username that exists. add_action( 'wp_authenticate_user', array( $this, 'onWpAuthenticateUser' ), 10, 2 ); // Failed to login to user that did not exist (perhaps brute force) // run this later than 10 because WordPress own email login check is done with priority 20 // so if we run at 10 we just get null. add_filter( 'authenticate', array( $this, 'onAuthenticate' ), 30, 3 ); // User is created. add_action( 'user_register', array( $this, 'on_user_register' ), 10, 2 ); // User is deleted. add_action( 'delete_user', array( $this, 'onDeleteUser' ), 10, 2 ); // User sessions is destroyed. AJAX call that we hook onto early. add_action( 'wp_ajax_destroy-sessions', array( $this, 'onDestroyUserSession' ), 0 ); // User reaches reset password (from link or only from user created link). add_action( 'validate_password_reset', array( $this, 'onValidatePasswordReset' ), 10, 2 ); add_action( 'retrieve_password_message', array( $this, 'onRetrievePasswordMessage' ), 10, 4 ); // New way, fired before update so we can get old user data. Does not commit, that's done in another hook. add_filter( 'wp_pre_insert_user_data', array( $this, 'on_pre_insert_user_data_collect' ), 10, 4 ); // Commit changes to user profile. Run on hook with higher prio, so other plugins, // for example the "Members" plugin for roles, can modify user data before we commit. add_action( 'profile_update', [ $this, 'on_profile_update_commit' ], 50, 1 ); add_action( 'set_user_role', array( $this, 'on_set_user_role_on_admin_overview_screen' ), 10, 3 ); // Administration email verification-screen. add_action( 'login_form_confirm_admin_email', array( $this, 'on_action_login_form_confirm_admin_email' ) ); add_action( 'wp_create_application_password', array( $this, 'on_action_wp_create_application_password' ), 10, 4 ); add_action( 'wp_delete_application_password', array( $this, 'on_action_wp_delete_application_password' ), 10, 2 ); // TODO: there is also an action "wp_update_application_password". Used by rest api and fired when a user updates app password there. // Failed application password authentication (REST/XML-RPC). // Gated behind experimental features while we validate noise levels in the wild. // Sites can additionally opt out via the `simple_history/log_failed_app_password_auth` filter. /** * Filter whether failed application password authentication attempts should be logged. * * Defaults to true when experimental features are enabled. Set to false to disable * logging without turning off experimental features entirely (useful on sites with * heavy API traffic where the log would fill with brute-force noise). * * @since 5.x * * @param bool $should_log Whether to log failed application password authentication. */ $log_failed_app_password_auth = apply_filters( 'simple_history/log_failed_app_password_auth', Helpers::experimental_features_is_enabled() ); if ( $log_failed_app_password_auth ) { // Capture the attempted username just before WP's wp_authenticate_application_password // (priority 20) runs, so the XML-RPC code path — where PHP_AUTH_USER is empty — still // has a username to log. add_filter( 'authenticate', array( $this, 'capture_attempted_username' ), 19, 3 ); add_action( 'application_password_failed_authentication', array( $this, 'on_application_password_failed_authentication' ), 10, 1 ); } // Admin page access denied. add_action( 'admin_page_access_denied', array( $this, 'on_admin_page_access_denied' ), 10 ); $this->add_wp_cli_hooks(); } /** * Adds hooks for WP CLI. */ private function add_wp_cli_hooks() { if ( ! Helpers::is_wp_cli() ) { return; } // Add hooks to collect and commit add-role changes. \WP_CLI::add_hook( 'before_invoke:user add-role', [ $this, 'on_wp_cli_before_invoke_user_add_role_collect' ] ); \WP_CLI::add_hook( 'after_invoke:user add-role', [ $this, 'on_wp_cli_after_invoke_user_add_role_commit_changes' ] ); // Add hooks to collect and commit remove-role changes. \WP_CLI::add_hook( 'before_invoke:user remove-role', [ $this, 'on_wp_cli_before_invoke_user_remove_role_collect' ] ); \WP_CLI::add_hook( 'after_invoke:user remove-role', [ $this, 'on_wp_cli_after_invoke_user_remove_role_commit_changes' ] ); } /** * Collect user add role changes. */ public function on_wp_cli_before_invoke_user_add_role_collect() { add_action( 'add_user_role', [ $this, 'on_wp_cli_add_user_role_collect' ], 10, 2 ); } /** * Commit changes when functions has run. */ public function on_wp_cli_after_invoke_user_add_role_commit_changes() { // Keyed by user id. Roles as array. $user_roles_added = $this->wp_cli_changes['user_roles_added']; foreach ( $user_roles_added as $user_id => $roles ) { $user = get_user_by( 'ID', $user_id ); $context = [ 'roles' => implode( ', ', $roles ), 'edited_user_id' => $user_id, 'edited_user_email' => $user->user_email, 'edited_user_login' => $user->user_login, ]; $this->info_message( 'user_role_added', $context ); } } /** * Collect user add role changes. */ public function on_wp_cli_before_invoke_user_remove_role_collect() { add_action( 'remove_user_role', [ $this, 'on_wp_cli_remove_user_role_collect' ], 10, 2 ); } /** * Commit changes when functions has run. */ public function on_wp_cli_after_invoke_user_remove_role_commit_changes() { // Keyed by user id. Roles as array. $user_roles_removed = $this->wp_cli_changes['user_roles_removed']; foreach ( $user_roles_removed as $user_id => $roles ) { $user = get_user_by( 'ID', $user_id ); $context = [ 'roles' => implode( ', ', $roles ), 'edited_user_id' => $user_id, 'edited_user_email' => $user->user_email, 'edited_user_login' => $user->user_login, ]; $this->info_message( 'user_role_removed', $context ); } } /** * Collect changes when user role is removed via WP CLI. * * @param mixed $user_id User id. * @param mixed $role Role that was removed. */ public function on_wp_cli_remove_user_role_collect( $user_id, $role ) { if ( ! isset( $this->wp_cli_changes['user_roles_removed'][ $user_id ] ) ) { $this->wp_cli_changes['user_roles_removed'][ $user_id ] = []; } $this->wp_cli_changes['user_roles_removed'][ $user_id ][] = $role; } /** * From WP_CLI this filter is only called when a user role is actually added, * it is not fired if the user already has the role. * * The filter will be called multiple times if multiple roles are added at the same time. * * @param mixed $user_id The user ID. * @param mixed $role The new role. */ public function on_wp_cli_add_user_role_collect( $user_id, $role ) { if ( ! isset( $this->wp_cli_changes['user_roles_added'][ $user_id ] ) ) { $this->wp_cli_changes['user_roles_added'][ $user_id ] = []; } $this->wp_cli_changes['user_roles_added'][ $user_id ][] = $role; } /** * Log when an Application Password is created for a user. * * Fired from action `wp_create_application_password`. * * @param int $user_id The user ID. * @param array $item { * The details about the created password. * * @type string $uuid The unique identifier for the application password. * @type string $app_id A UUID provided by the application to uniquely identify it. * @type string $name The name of the application password. * @type string $password A one-way hash of the password. * @type int $created Unix timestamp of when the password was created. * @type null $last_used Null. * @type null $last_ip Null. * } * @param string $new_password The unhashed generated application password. * @param array $args { * Arguments used to create the application password. * * @type string $name The name of the application password. * @type string $app_id A UUID provided by the application to uniquely identify it. * } */ public function on_action_wp_create_application_password( $user_id, $item, $new_password, $args ) { $user = get_user_by( 'ID', $user_id ); $this->info_message( 'user_application_password_created', array( 'edited_user_id' => $user_id, 'edited_user_email' => $user->user_email, 'edited_user_login' => $user->user_login, 'application_password_name' => $item['name'], ) ); } /** * Log when an Application password is deleted (revoked). * Fired from action `wp_delete_application_password`. * * @since 5.6.0 * * @param int $user_id The user ID. * @param array $item The data about the application password. */ public function on_action_wp_delete_application_password( $user_id, $item ) { $user = get_user_by( 'ID', $user_id ); $this->info_message( 'user_application_password_revoked', array( 'edited_user_id' => $user_id, 'edited_user_email' => $user->user_email, 'edited_user_login' => $user->user_login, 'application_password_name' => $item['name'], ) ); } /** * Capture the attempted username from the `authenticate` filter chain. * * The `application_password_failed_authentication` action only receives the WP_Error, * not the username. For XML-RPC requests the username never reaches `$_SERVER['PHP_AUTH_USER']`, * so without this we'd log an empty value. * * This fires only for paths that go through `wp_authenticate()` — primarily XML-RPC. * The REST API code path (`determine_current_user` -> `wp_validate_application_password()` * -> `wp_authenticate_application_password()`) bypasses the `authenticate` filter chain * entirely, which is why the failed-auth handler also falls back to `$_SERVER['PHP_AUTH_USER']`. * * @param null|\WP_User|\WP_Error $user Authenticated user, error, or null. * @param string $username The username or email being authenticated. * @param string $password The password being authenticated. * @return null|\WP_User|\WP_Error Unchanged $user — we only observe. */ public function capture_attempted_username( $user, $username, $password ) { $this->last_attempted_username = (string) $username; return $user; } /** * Log failed application password authentication. * * Fired from action `application_password_failed_authentication` (since WP 5.6.0) * when an application password fails to authenticate a REST or XML-RPC request. * * Possible error codes from WordPress core: * - `invalid_username` / `invalid_email` — username does not exist. * - `incorrect_password` — wrong application password. * - `application_passwords_disabled` — disabled site-wide. * - `application_passwords_disabled_for_user` — disabled for this user. * * @param \WP_Error $error The authentication error. */ public function on_application_password_failed_authentication( $error ) { // Guard against recursion: resolving the current user during the log write can // re-trigger `determine_current_user` -> `wp_validate_application_password` and // fire this action again. Also dedupes the action firing twice per request // (once during `authenticate`, once during `determine_current_user`). if ( $this->app_password_failure_logged ) { return; } $this->app_password_failure_logged = true; // Prefer the username captured during the `authenticate` filter chain — this is // the only source that works for XML-RPC, where the username is inside the XML // body and never lands in PHP_AUTH_USER. // // Fall back to PHP_AUTH_USER for the REST API code path: WP fires the action // from `wp_validate_application_password()` (hooked on `determine_current_user`), // which calls `wp_authenticate_application_password()` *directly* — bypassing the // `authenticate` filter chain, so our capture at priority 19 never runs there. $captured_username = (string) ( $this->last_attempted_username ?? '' ); if ( $captured_username !== '' ) { $attempted_username = sanitize_text_field( $captured_username ); } else { // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.BasicAuthentication -- Reading the attempted username for security logging only; authentication is handled by WP core. $attempted_username = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ?? '' ) ); } $error_code = is_a( $error, 'WP_Error' ) ? $error->get_error_code() : ''; $error_message = is_a( $error, 'WP_Error' ) ? wp_strip_all_tags( $error->get_error_message() ) : ''; $context = array( '_initiator' => Log_Initiators::WEB_USER, 'error_code' => (string) $error_code, 'error_message' => $error_message, 'request_uri' => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ), 'request_method' => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) ), // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__HTTP_USER_AGENT__ -- User agent logging important for security (brute force detection). Accept VIP caching limitation. 'server_http_user_agent' => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), '_occasionsID' => self::class . '/failed_application_password_login', ); $is_unknown_user = in_array( $error_code, array( 'invalid_username', 'invalid_email' ), true ); if ( $is_unknown_user ) { $context['failed_username'] = $attempted_username; $this->warning_message( 'user_application_password_unknown_login_failed', $context ); return; } $context['login'] = $attempted_username; $user = $attempted_username !== '' ? get_user_by( 'login', $attempted_username ) : false; if ( ! $user && is_email( $attempted_username ) ) { $user = get_user_by( 'email', $attempted_username ); } if ( $user instanceof \WP_User ) { $context['login_id'] = $user->ID; $context['login_email'] = $user->user_email; } $this->warning_message( 'user_application_password_login_failed', $context ); } /** * Fires after the user's role has changed. * * Captures role changes from the users admin overview screen (quick edit), * REST API, and WP-CLI. Profile-screen role changes flow through the * profile_update / pre_user_data path instead. * * @param int $user_id The user ID. * @param string $role The new role. * @param string[] $old_roles An array of the user's previous roles. */ public function on_set_user_role_on_admin_overview_screen( $user_id, $role, $old_roles ) { $is_users_screen = false; if ( is_admin() ) { $current_screen = helpers::get_current_screen(); $is_users_screen = isset( $current_screen->id ) && $current_screen->id === 'users'; } if ( ! $is_users_screen && ! Helpers::is_rest_request() && ! Helpers::is_wp_cli() ) { return; } // Suppress the implicit role assignment that wp_insert_user() does during // user creation — the user_created event already records the role. New // users have no previous roles, so $old_roles is empty. Edge case: an // admin who explicitly cleared a user's roles first (set_role('')) and // then assigns a new one would also hit this branch on the second call // and not be logged. Considered acceptable — the clearing call itself // is logged. if ( empty( $old_roles ) ) { return; } $changed_user = get_user_by( 'ID', $user_id ); if ( ! is_array( $old_roles ) ) { $old_roles = array(); } $old_role = (string) reset( $old_roles ); $this->notice_message( 'user_role_updated', array( 'edited_user_id' => $user_id, 'edited_user_email' => $changed_user->user_email, 'edited_user_login' => $changed_user->user_login, 'new_role' => $role, 'old_role' => $old_role, ) ); } /** * Log when user confirms that admin email is correct. * Fired from filter 'login_form_confirm_admin_email'. */ public function on_action_login_form_confirm_admin_email() { // Bail if button with name "correct-admin-email" was not clicked or if no nonce field exists. if ( empty( $_POST['confirm_admin_email_nonce'] ) || empty( $_POST['correct-admin-email'] ) ) { return; } // Bail if nonce not valid. $nonce_valid = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['confirm_admin_email_nonce'] ) ), 'confirm_admin_email' ); if ( $nonce_valid === false ) { return; } $this->info_message( 'user_admin_email_confirm_correct_clicked' ); } /** * Commits the context previously collected. * In this hook the changes are written, so we can get any new data from the user object. * * @param int $user_id ID of the user that was created or updated. */ public function on_profile_update_commit( $user_id ) { $context = $this->user_profile_update_modified_context[ $user_id ] ?? null; // Bail if we don't have any context. if ( ! $context ) { return; } $user = get_user_by( 'ID', $user_id ); // Get new roles, to detect changes. $context['user_new_roles'] = (array) $user->roles; $prev_roles = $context['user_prev_roles'] ?? []; $added_roles = array_values( array_diff( $context['user_new_roles'], $prev_roles ) ); if ( $added_roles ) { // Comma separated list of added roles. $context['user_added_roles'] = implode( ', ', $added_roles ); } $removed_roles = array_values( array_diff( $prev_roles, $context['user_new_roles'] ) ); if ( $removed_roles ) { // Comma separated list of removed roles. $context['user_removed_roles'] = implode( ', ', $removed_roles ); } // Remove keys used for diff. unset( $context['user_prev_roles'], $context['user_new_roles'] ); $this->info_message( 'user_updated_profile', $context ); } /** * Filters user data before the record is created or updated. * Used to gather user profile updates. * * It only includes data in the users table, not any user metadata. * * @param array $data { * Values and keys for the user. * * @type string $user_login The user's login. Only included if $update == false * @type string $user_pass The user's password. * @type string $user_email The user's email. * @type string $user_url The user's url. * @type string $user_nicename The user's nice name. Defaults to a URL-safe version of user's login * @type string $display_name The user's display name. * @type string $user_registered MySQL timestamp describing the moment when the user registered. Defaults to * the current UTC timestamp. * } * @param bool $update Whether the user is being updated rather than created. * @param int|null $user_id ID of the user to be updated, or NULL if the user is being created. * @param array $userdata The raw array of data passed to wp_insert_user(). */ public function on_pre_insert_user_data_collect( $data, $update, $user_id, $userdata = array() ) { // Bail if this is not a user update, i.e. only collect for user edits. if ( ! $update ) { return $data; } // Bail if we don't have all needed data. if ( ! $data || ! $user_id ) { return $data; } // Only capture in contexts where changes are intentional: admin user screens, // REST API, or WP-CLI. Arbitrary wp_update_user() calls from cron or plugins // are excluded to avoid noise. $is_admin_user_screen = false; if ( is_admin() ) { $current_screen = helpers::get_current_screen(); $is_admin_user_screen = in_array( $current_screen->id, array( 'user-edit', 'profile' ), true ); } if ( ! $is_admin_user_screen && ! Helpers::is_rest_request() && ! Helpers::is_wp_cli() ) { return $data; } // Array with differences between old and new values. $user_data_diff = array(); // Get user object that contains old/existing values. $user_before_update = get_user_by( 'ID', $user_id ); $password_changed = false; foreach ( $userdata as $option_key => $one_maybe_updated_option_value ) { $prev_option_value = $user_before_update->$option_key; $add_diff = true; // Some options need special treatment. if ( $option_key === 'user_pass' ) { $password_changed = $one_maybe_updated_option_value !== $prev_option_value; $add_diff = false; } elseif ( $option_key === 'comment_shortcuts' ) { if ( empty( $one_maybe_updated_option_value ) ) { $one_maybe_updated_option_value = 'false'; } } elseif ( $option_key === 'locale' ) { if ( $one_maybe_updated_option_value === '' ) { $one_maybe_updated_option_value = 'SITE_DEFAULT'; } if ( $prev_option_value === '' ) { $prev_option_value = 'SITE_DEFAULT'; } } if ( ! $add_diff ) { continue; } $user_data_diff = $this->addDiff( $user_data_diff, $option_key, $prev_option_value, $one_maybe_updated_option_value ); } // Setup basic context. $context = array( 'edited_user_id' => $user_id, 'edited_user_email' => $user_before_update->user_email, 'edited_user_login' => $user_before_update->user_login, ); if ( $password_changed ) { $context['edited_user_password_changed'] = '1'; } // Add diff to context. foreach ( $user_data_diff as $one_diff_key => $one_diff_vals ) { $context[ "user_prev_{$one_diff_key}" ] = $one_diff_vals['old']; $context[ "user_new_{$one_diff_key}" ] = $one_diff_vals['new']; } $context['user_prev_roles'] = (array) $user_before_update->roles; // Store in private var to retrieve in commit function. $this->user_profile_update_modified_context[ $user_id ] = $context; return $data; } /** * Fired from hook "retrieve_password_message" in "wp-login.php". * Hook filters the message body of the password reset mail. * * If this hook is fired then WP has checked for valid username etc already. * * This hook is not fired when using for example WooCommerce because it has it's own reset password system. * Maybe get_password_reset_key() can be used instead? * * @param string $message Default mail message. * @param string $key The activation key. * @param string $user_login The username for the user. * @param object $user_data WP_User object. */ public function onRetrievePasswordMessage( $message, $key, $user_login, $user_data = null ) { // Do not log $message: it contains the password reset URL (with the activation key), // which is credential-equivalent until the key expires or is consumed. $context = array( 'user_login' => $user_login, 'user_email' => $user_data->user_email, ); // Request to send reset password link // can be initiated from login screen or from users-listing-page in admin. // Detect where from the request is coming. $request_origin = 'unknown'; if ( function_exists( 'get_current_screen' ) ) { $screen = get_current_screen(); if ( $screen && $screen->base === 'users' ) { $request_origin = 'wp_admin_users_admin'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing } elseif ( ! empty( $_POST['user_login'] ) ) { $request_origin = 'login_screen'; } if ( $request_origin === 'login_screen' ) { $context['_initiator'] = Log_Initiators::WEB_USER; } $this->notice_message( 'user_requested_password_reset_link', $context ); return $message; } /** * Fired before the password reset procedure is validated. * * @param \WP_Error $errors WP Error object. * @param \WP_User|\WP_Error $user WP_User object if the login and reset key match. WP_Error object otherwise. */ public function onValidatePasswordReset( $errors, $user ) { $context = array(); if ( is_a( $user, 'WP_User' ) ) { $context['_initiator'] = Log_Initiators::WP_USER; $context['_user_id'] = $user->ID; $context['_user_login'] = $user->user_login; $context['_user_email'] = $user->user_email; } // PHPCS:ignore WordPress.Security.NonceVerification.Missing if ( $errors->get_error_code() || ! isset( $_POST['pass1'] ) || empty( $_POST['pass1'] ) ) { return; } $this->info_message( 'user_password_reseted', $context ); } /** * Called when user sessions are destroyed from admin. * Can be called for current logged in user = destroy all other sessions. * or for another user = destroy all sessions for that user * Fires from AJAX call. * * @since 2.0.6 */ public function onDestroyUserSession() { // PHPCS:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotValidated $user = get_userdata( (int) $_POST['user_id'] ); if ( $user ) { if ( ! current_user_can( 'edit_user', $user->ID ) ) { $user = false; } elseif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'update-user_' . $user->ID ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $user = false; } } if ( ! $user ) { // Could not log out user sessions. Please try again. return; } $context = array(); if ( $user->ID === get_current_user_id() ) { $this->info_message( 'user_session_destroy_others' ); } else { $context['user_id'] = $user->ID; $context['user_login'] = $user->user_login; $context['user_display_name'] = $user->display_name; $this->info_message( 'user_session_destroy_everywhere', $context ); } } /** * Fires before a user is deleted from the database. * * @param int $user_id ID of the deleted user. * @param int|null $reassign ID of the user to reassign posts and links to. * Default null, for no reassignment. */ public function onDeleteUser( $user_id, $reassign ) { $wp_user_to_delete = get_userdata( $user_id ); $context = array( 'deleted_user_id' => $wp_user_to_delete->ID, 'deleted_user_email' => $wp_user_to_delete->user_email, 'deleted_user_login' => $wp_user_to_delete->user_login, 'deleted_user_role' => implode( ', ', $wp_user_to_delete->roles ), 'reassign_user_id' => $reassign, ); $this->notice_message( 'user_deleted', $context ); } /** * Modify plain text row output * - adds link to user profile * - change to "your profile" if you're looking at your own edit * * @param object $row Log row. * @return string */ public function get_log_row_plain_text_output( $row ) { $context = $row->context; $output = parent::get_log_row_plain_text_output( $row ); $current_user_id = get_current_user_id(); if ( $context['_message_key'] === 'user_updated_profile' ) { $wp_user = get_user_by( 'id', $context['edited_user_id'] ); // If edited_user_id and _user_id is the same then a user edited their own profile // Note: it's not the same thing as the currently logged in user (but.. it can be!). if ( ! empty( $context['_user_id'] ) && $context['edited_user_id'] === $context['_user_id'] ) { if ( $wp_user ) { $context['edit_profile_link'] = get_edit_user_link( $wp_user->ID ); $use_you = apply_filters( 'simple_history/user_logger/plain_text_output_use_you', true ); // User still exist, so link to their profile. if ( $current_user_id === (int) $context['_user_id'] && $use_you ) { // User that is viewing the log is the same as the edited user. $msg = __( 'Edited <a href="{edit_profile_link}">your profile</a>', 'simple-history' ); } else { $msg = __( 'Edited <a href="{edit_profile_link}">their profile</a>', 'simple-history' ); } $output = helpers::interpolate( $msg, $context, $row ); } else { // User does not exist any longer. $output = __( 'Edited your profile', 'simple-history' ); } } elseif ( $wp_user ) { // User edited another users profile // Edited user still exist, so link to their profile. $context['edit_profile_link'] = get_edit_user_link( $wp_user->ID ); $msg = __( 'Edited the profile for user <a href="{edit_profile_link}">{edited_user_login} ({edited_user_email})</a>', 'simple-history' ); $output = helpers::interpolate( $msg, $context, $row ); } } elseif ( $context['_message_key'] === 'user_created' ) { // A user was created. Create link of username that goes to user profile. $wp_user = get_user_by( 'id', $context['created_user_id'] ); if ( $wp_user ) { $context['edit_profile_link'] = get_edit_user_link( $wp_user->ID ); // Use simplified message for imported users (no email/role placeholders). // Imported users don't have email/role stored since those can change over time. if ( isset( $context[ Existing_Data_Importer::BACKFILLED_CONTEXT_KEY ] ) ) { $msg = __( 'Created user <a href="{edit_profile_link}">{created_user_login}</a>', 'simple-history' ); } else { $msg = __( 'Created user <a href="{edit_profile_link}">{created_user_login} ({created_user_email})</a> with role {created_user_role}', 'simple-history' ); } $output = helpers::interpolate( $msg, $context, $row ); } } return $output; } /** * User logs in * * @param string $user_login Username. * @param object $user WP_User object. */ public function on_wp_login( $user_login = null, $user = null ) { $user_obj = null; $context = array( 'user_login' => $user_login, ); if ( isset( $user_login ) ) { $user_obj = get_user_by( 'login', $user_login ); } elseif ( isset( $user ) && isset( $user->ID ) ) { $user_obj = get_user_by( 'id', $user->ID ); } if ( is_a( $user_obj, 'WP_User' ) ) { $context = array( 'user_id' => $user_obj->ID, 'user_email' => $user_obj->user_email, 'user_login' => $user_obj->user_login, ); // Override some data that is usually set automagically by Simple History // Because wp_get_current_user() does not return any data yet at this point. $context['_initiator'] = Log_Initiators::WP_USER; $context['_user_id'] = $user_obj->ID; $context['_user_login'] = $user_obj->user_login; $context['_user_email'] = $user_obj->user_email; // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__HTTP_USER_AGENT__ -- User agent logging important for security (brute force detection). Accept VIP caching limitation. $context['server_http_user_agent'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); $this->info_message( 'user_logged_in', $context ); } else { // Could not get any info about the user logging in. $this->warning_message( 'user_unknown_logged_in', $context ); } } /** * User logs out * http://codex.wordpress.org/Plugin_API/Action_Reference/wp_logout * * @param int $user_id ID of the user that was logged out. Added in WP 5.5. */ public function onWpLogout( $user_id = null ) { $context = array(); $user = get_userdata( $user_id ); if ( is_a( $user, 'WP_User' ) ) { $context['_initiator'] = Log_Initiators::WP_USER; $context['_user_id'] = $user->ID; $context['_user_login'] = $user->user_login; $context['_user_email'] = $user->user_email; } $this->info_message( 'user_logged_out', $context ); } /** * User is created. Fired from action user_register. * Fires immediately after a new user is registered. * * @param int $user_id User ID. * @param array $userdata The raw array of data passed to wp_insert_user(). */ public function on_user_register( $user_id, $userdata = array() ) { if ( ! $user_id || ! is_numeric( $user_id ) ) { return; } $wp_user_added = get_userdata( $user_id ); $role = ''; $roles = []; // On a subsite of a multisite network, // newly created users have no roles or caps until they are added to a blog. // So at this time we can't get the role of the user, if on a subsite. // Use value from $_POST instead. if ( is_multisite() ) { // PHPCS:ignore WordPress.Security.NonceVerification.Missing $role = sanitize_title( wp_unslash( $_POST['role'] ?? '' ) ); $roles = array( $role ); } elseif ( is_array( $wp_user_added->roles ) && ! empty( $wp_user_added->roles[0] ) ) { // Single site, get role from user object. $roles = $wp_user_added->roles; } // PHPCS:ignore WordPress.Security.NonceVerification.Missing $send_user_notification = (int) ( isset( $_POST['send_user_notification'] ) && sanitize_text_field( wp_unslash( $_POST['send_user_notification'] ) ) ); $context = array( 'created_user_id' => $wp_user_added->ID, 'created_user_email' => $wp_user_added->user_email, 'created_user_login' => $wp_user_added->user_login, 'created_user_first_name' => $wp_user_added->first_name, 'created_user_last_name' => $wp_user_added->last_name, 'created_user_url' => $wp_user_added->user_url, 'created_user_role' => implode( ', ', $roles ), 'send_user_notification' => $send_user_notification, ); $this->info_message( 'user_created', $context ); } /** * Log failed login attempt to username that exists * * @param \WP_User|\WP_Error $userOrError The WP_User object of the user being edited, * or a WP_Error object if validation has already failed. * @param string $password The user's password. */ public function onWpAuthenticateUser( $userOrError, $password ) { if ( ! is_a( $userOrError, 'WP_User' ) ) { return $userOrError; } // Only log failed attempts. if ( ! wp_check_password( $password, $userOrError->user_pass, $userOrError->ID ) ) { // Overwrite some vars that Simple History set automagically. $context = array( '_initiator' => Log_Initiators::WEB_USER, 'login_id' => $userOrError->ID, 'login_email' => $userOrError->user_email, 'login' => $userOrError->user_login, // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__HTTP_USER_AGENT__ -- User agent logging important for security (brute force detection). Accept VIP caching limitation. 'server_http_user_agent' => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), '_occasionsID' => self::class . '/failed_user_login', ); $this->warning_message( 'user_login_failed', $context ); } return $userOrError; } /** * Attempt to login to user that does not exist * * @param \WP_User|\WP_Error|null $user (required) * null indicates no process has authenticated the user yet. * A WP_Error object indicates another process has failed the authentication. * A WP_User object indicates another process has authenticated the user. * @param string $username The user's username. since 4.5.0 `$username` now accepts an email address. * @param string $password The user's password (encrypted). */ public function onAuthenticate( $user, $username, $password ) { // Don't log empty usernames. if ( trim( $username ) === '' ) { return $user; } // If null then no auth done yet. Weird. But what can we do. if ( is_null( $user ) ) { return $user; } // If auth ok then $user is a wp_user object. if ( is_a( $user, 'WP_User' ) ) { return $user; } // If user is a WP_Error object then auth failed // Error codes can be: // "incorrect_password" | "empty_password" | "invalid_email" | "invalid_username" // We only act on invalid emails and invalid usernames. if ( is_a( $user, 'WP_Error' ) && ( $user->get_error_code() === 'invalid_username' || $user->get_error_code() === 'invalid_email' ) ) { $context = array( '_initiator' => Log_Initiators::WEB_USER, 'failed_username' => $username, // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__HTTP_USER_AGENT__ -- User agent logging important for security (brute force detection). Accept VIP caching limitation. 'server_http_user_agent' => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), // Count all failed logins to unknown users as the same occasions, // to prevent log being flooded with login/hack attempts. '_occasionsID' => self::class . '/failed_user_login', ); $this->warning_message( 'user_unknown_login_failed', $context ); } return $user; } /** * Add diff to diff array if old and new values are different. * * Since 2.0.29 * * @param array $post_data_diff Array with diffs. * @param string $key Key in $post_data_diff array. * @param string $old_value Old value. * @param string $new_value New value. * @return array $post_data_diff */ public function addDiff( $post_data_diff, $key, $old_value, $new_value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Loose comparison intentional to avoid false diffs when types differ. if ( $old_value != $new_value ) { $post_data_diff[ $key ] = array( 'old' => $old_value, 'new' => $new_value, ); } return $post_data_diff; } /** * Return more info about an logged event. * * Get action links for a log row. * * @param object $row Log row object. * @return array Array of action link arrays. */ public function get_action_links( $row ) { $context = $row->context; $message_key = $context['_message_key'] ?? ''; if ( $message_key === 'user_updated_profile' ) { $user_id = isset( $context['edited_user_id'] ) ? (int) $context['edited_user_id'] : 0; } elseif ( $message_key === 'user_created' ) { $user_id = isset( $context['created_user_id'] ) ? (int) $context['created_user_id'] : 0; } else { return []; } if ( ! $user_id || ! get_userdata( $user_id ) ) { return []; } if ( ! current_user_can( 'edit_user', $user_id ) ) { return []; } return [ [ 'url' => get_edit_user_link( $user_id ), 'label' => __( 'Edit user', 'simple-history' ), 'action' => 'edit', ], ]; } /** * @param object $row Log row. * @return Event_Details_Group|string */ public function get_log_row_details_output( $row ) { $context = $row->context; $message_key = $context['_message_key']; $group = new Event_Details_Group(); if ( $message_key === 'user_updated_profile' ) { $arr_user_keys_to_show_diff_for = array( 'rich_editing' => array( 'title' => _x( 'Visual editor', 'User logger', 'simple-history' ), 'type' => 'checkbox', 'value_true' => _x( 'Enable', 'User logger', 'simple-history' ), 'value_false' => _x( 'Disable', 'User logger', 'simple-history' ), ), 'admin_color' => array( 'title' => _x( 'Colour scheme', 'User logger', 'simple-history' ), ), 'comment_shortcuts' => array( 'title' => _x( 'Keyboard shortcuts', 'User logger', 'simple-history' ), 'type' => 'checkbox', 'value_true' => _x( 'Enable', 'User logger', 'simple-history' ), 'value_false' => _x( 'Disable', 'User logger', 'simple-history' ), ), 'show_admin_bar_front' => array( 'title' => _x( 'Toolbar', 'User logger', 'simple-history' ), 'type' => 'checkbox', 'value_true' => _x( 'Show', 'User logger', 'simple-history' ), 'value_false' => _x( "Don't show", 'User logger', 'simple-history' ), ), 'locale' => array( 'title' => _x( 'Language', 'User logger', 'simple-history' ), ), 'first_name' => array( 'title' => _x( 'First name', 'User logger', 'simple-history' ), ), 'last_name' => array( 'title' => _x( 'Last name', 'User logger', 'simple-history' ), ), 'nickname' => array( 'title' => _x( 'Nickname', 'User logger', 'simple-history' ), ), 'display_name' => array( 'title' => _x( 'Display name', 'User logger', 'simple-history' ), ), 'user_email' => array( 'title' => _x( 'Email', 'User logger', 'simple-history' ), ), 'user_url' => array( 'title' => _x( 'Website', 'User logger', 'simple-history' ), ), 'description' => array( 'title' => _x( 'Description', 'User logger', 'simple-history' ), ), 'aim' => array( 'title' => _x( 'AIM', 'User logger', 'simple-history' ), ), 'yim' => array( 'title' => _x( 'Yahoo IM', 'User logger', 'simple-history' ), ), 'jabber' => array( 'title' => _x( 'Jabber / Google Talk ', 'User logger', 'simple-history' ), ), ); require_once ABSPATH . 'wp-admin/includes/translation-install.php'; $translations = wp_get_available_translations(); if ( ! isset( $translations['en_US'] ) ) { $translations['en_US'] = array( 'language' => 'en_US', 'english_name' => 'English', ); } foreach ( $arr_user_keys_to_show_diff_for as $key => $val ) { if ( ! isset( $context[ "user_prev_{$key}" ] ) || ! isset( $context[ "user_new_{$key}" ] ) ) { continue; } $user_old_value = $context[ "user_prev_{$key}" ]; $user_new_value = $context[ "user_new_{$key}" ]; // Transform locale codes to display names. if ( $key === 'locale' ) { if ( isset( $translations[ $user_old_value ] ) ) { $user_old_value = "{$translations[ $user_old_value ]['english_name']} ({$user_old_value})"; } elseif ( $user_old_value === 'SITE_DEFAULT' ) { $user_old_value = __( 'Site Default', 'simple-history' ); } if ( isset( $translations[ $user_new_value ] ) ) { $user_new_value = "{$translations[ $user_new_value ]['english_name']} ({$user_new_value})"; } elseif ( $user_new_value === 'SITE_DEFAULT' ) { $user_new_value = __( 'Site Default', 'simple-history' ); } } // Transform checkbox values to user-friendly labels. if ( isset( $val['type'] ) && $val['type'] === 'checkbox' ) { $user_old_value = $user_old_value === 'true' ? $val['value_true'] : $val['value_false']; $user_new_value = $user_new_value === 'true' ? $val['value_true'] : $val['value_false']; } $group->add_item( ( new Event_Details_Item( null, $val['title'] ) ) ->set_values( $user_new_value, $user_old_value ) ); } // Password changed. if ( isset( $context['edited_user_password_changed'] ) ) { $group->add_item( ( new Event_Details_Item( null, _x( 'Password', 'User logger', 'simple-history' ) ) ) ->set_new_value( _x( 'Changed', 'User logger', 'simple-history' ) ) ); } } elseif ( $message_key === 'user_created' ) { $arr_user_keys_to_show = array( 'created_user_role' => _x( 'Role', 'User logger', 'simple-history' ), 'created_user_first_name' => _x( 'First name', 'User logger', 'simple-history' ), 'created_user_last_name' => _x( 'Last name', 'User logger', 'simple-history' ), 'created_user_url' => _x( 'Website', 'User logger', 'simple-history' ), 'send_user_notification' => _x( 'Send notification', 'User logger', 'simple-history' ), ); foreach ( $arr_user_keys_to_show as $key => $title ) { if ( ! isset( $context[ $key ] ) || ! trim( $context[ $key ] ) ) { continue; } $value = $context[ $key ]; if ( $key === 'send_user_notification' ) { if ( (int) $value !== 1 ) { continue; } $value = _x( 'Checked', 'User logger', 'simple-history' ); } $group->add_item( ( new Event_Details_Item( null, $title ) ) ->set_new_value( $value ) ); } } // Common for both modified and added users. if ( isset( $context['user_added_roles'] ) ) { $group->add_item( ( new Event_Details_Item( 'user_added_roles', __( 'Role added', 'simple-history' ) ) ) ); } if ( isset( $context['user_removed_roles'] ) ) { $group->add_item( ( new Event_Details_Item( 'user_removed_roles', __( 'Role removed', 'simple-history' ) ) ) ); } if ( empty( $group->items ) ) { return ''; } return $group; } /** * Fires when a user is denied access to an admin page due to insufficient capabilities. */ public function on_admin_page_access_denied() { $admin_page = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $admin_page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // Get the current admin page file. $pagenow = $GLOBALS['pagenow'] ?? ''; // Construct full page path. $full_page = $pagenow; if ( $admin_page ) { $full_page .= '?page=' . $admin_page; } $this->info_message( 'user_admin_page_access_denied', [ 'admin_page' => $full_page, ] ); } }