' . esc_html__( 'Settings', 'adminwatch-central' ) . ''; $about = '' . esc_html__( 'About & Support', 'adminwatch-central' ) . ''; array_unshift( $links, $about, $settings ); return $links; } /** Load assets only on AdminWatch Central screens. */ public function assets( $hook ) { if ( false === strpos( $hook, 'admin-watch' ) ) { return; } wp_enqueue_style( 'admin-watch-admin', ADMIN_WATCH_URL . 'assets/css/admin.css', array(), ADMIN_WATCH_VERSION ); } /** Register Settings API fields. */ public function register_settings() { register_setting( 'admin_watch_settings_group', 'admin_watch_settings', array( 'sanitize_callback' => array( $this, 'sanitize_settings' ) ) ); } /** * Validate the complete settings option. * * @param array $input Submitted settings. * @return array */ public function sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); $retention = absint( $input['retention_days'] ?? 30 ); $activity_retention = absint( $input['activity_retention_days'] ?? 30 ); return array( 'monitor_404s' => empty( $input['monitor_404s'] ) ? 0 : 1, 'retention_days' => in_array( $retention, array( 7, 30, 90 ), true ) ? $retention : 30, 'delete_data' => empty( $input['delete_data'] ) ? 0 : 1, 'monitor_activity' => empty( $input['monitor_activity'] ) ? 0 : 1, 'record_failed_logins' => empty( $input['record_failed_logins'] ) ? 0 : 1, 'activity_retention_days' => in_array( $activity_retention, array( 7, 30, 90 ), true ) ? $activity_retention : 30, ); } /** * Return roles able to make material content or administrative changes. * * A role qualifies with manage_options, publish_posts, edit_others_posts, or * equivalent publish/edit-others capabilities for a custom post type. * * @return array */ private function access_roles() { $wp_roles = wp_roles(); $roles = array(); foreach ( $wp_roles->roles as $slug => $role ) { $capabilities = isset( $role['capabilities'] ) ? (array) $role['capabilities'] : array(); if ( $this->role_can_change_site( $capabilities ) ) { $roles[ $slug ] = $role['name']; } } return $roles; } /** * Return IDs for users in AdminWatch Central's documented access scope. * * This is called only from AdminWatch Central screens. Session state is not queried * on unrelated wp-admin requests. * * @param array $roles Eligible role slugs. * @return array */ private function access_user_ids( $roles ) { if ( ! $roles ) { return array(); } return array_map( 'absint', get_users( array( 'fields' => 'ID', 'role__in' => array_keys( $roles ), 'number' => -1, ) ) ); } /** * Count current valid core sessions for eligible users. * * @param array $user_ids Eligible user IDs. * @return array */ private function active_session_counts( $user_ids ) { return ( new WebStudio_Admin_Watch_Session_Visibility() )->counts_for_users( $user_ids ); } /** * Determine whether role capabilities satisfy the documented V1 rule. * * @param array $capabilities Role capabilities. * @return bool */ private function role_can_change_site( $capabilities ) { if ( ! empty( $capabilities['manage_options'] ) || ! empty( $capabilities['publish_posts'] ) || ! empty( $capabilities['edit_others_posts'] ) ) { return true; } foreach ( $capabilities as $capability => $granted ) { if ( $granted && ( 0 === strpos( $capability, 'publish_' ) || 0 === strpos( $capability, 'edit_others_' ) ) ) { return true; } } return false; } /** @return string */ private function role_label( $user ) { $role_names = wp_roles()->get_names(); $labels = array(); foreach ( (array) $user->roles as $role ) { if ( isset( $role_names[ $role ] ) ) { $labels[] = $role_names[ $role ]; } } return $labels ? implode( ', ', $labels ) : __( 'No role', 'adminwatch-central' ); } /** @return string */ private function time_label( $timestamp ) { if ( ! $timestamp ) { return __( 'Not recorded yet', 'adminwatch-central' ); } return sprintf( /* translators: Human-readable time difference, such as “3 days”. */ __( '%s ago', 'adminwatch-central' ), human_time_diff( (int) $timestamp, current_time( 'timestamp', true ) ) ); } /** @return string */ private function password_strength_label( $strength ) { $labels = array( 'very_weak' => __( 'Very weak', 'adminwatch-central' ), 'weak' => __( 'Weak', 'adminwatch-central' ), 'medium' => __( 'Medium', 'adminwatch-central' ), 'strong' => __( 'Strong', 'adminwatch-central' ), ); return isset( $labels[ $strength ] ) ? $labels[ $strength ] : __( 'Not recorded yet', 'adminwatch-central' ); } /** @return string */ private function mfa_label( $status ) { $labels = array( WebStudio_Admin_Watch_MFA_Detector::ENABLED => __( 'Enabled', 'adminwatch-central' ), WebStudio_Admin_Watch_MFA_Detector::NOT_ENABLED => __( 'Not enabled', 'adminwatch-central' ), WebStudio_Admin_Watch_MFA_Detector::NOT_DETECTED => __( 'Not detected', 'adminwatch-central' ), ); return isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'Not detected', 'adminwatch-central' ); } /** * Render a text-based status badge. * * @param string $label Badge text. * @param string $state Sanitised presentation state. */ private function status_badge( $label, $state ) { ?> modify( '-' . absint( $days_ago ) . ' days' ); } return $today->format( 'Y-m-d' ); } /** * Get a conservative summary of the latest Site Health check stored by core. * * @return array|null */ private function site_health_summary() { $health = get_transient( 'health-check-site-status-result' ); $health = is_string( $health ) ? json_decode( $health, true ) : $health; if ( ! is_array( $health ) || ! isset( $health['critical'], $health['recommended'] ) || ! is_numeric( $health['critical'] ) || ! is_numeric( $health['recommended'] ) ) { return null; } return array( 'critical' => absint( $health['critical'] ), 'recommended' => absint( $health['recommended'] ), ); } /** Stop direct access to page callbacks. */ private function require_access() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'adminwatch-central' ) ); } } /** @return int */ private function requested_page() { return max( 1, absint( $this->query_value( 'paged' ) ) ); } /** * Render pagination preserving explicit AdminWatch Central query state. * * @param int $page Current page. * @param int $total Total records. * @param array $args Query arguments to preserve. */ private function pagination( $page, $total, $args = array() ) { $pages = (int) ceil( $total / self::PER_PAGE ); if ( $pages < 2 ) { return; } $base = add_query_arg( array_merge( $args, array( 'paged' => '%#%' ) ), admin_url( 'admin.php' ) ); echo wp_kses_post( paginate_links( array( 'base' => $base, 'format' => '', 'current' => $page, 'total' => $pages, ) ) ); } /** * Render the shared Admin Watch Central page heading. * * @param string $title Current page title. * @param string $description Short page description. * @param bool $is_dashboard Whether this is the dashboard. */ private function page_header( $title, $description, $is_dashboard = false ) { ?>

$roles Eligible roles. * @param string $selected Selected role slug. * @param bool $sessions Whether the session filter is active. */ private function users_filter_navigation( $roles, $selected = '', $sessions = false ) { $route = $sessions ? 'admin-watch-sessions' : 'admin-watch-users'; ?> require_access(); $repository = new WebStudio_Admin_Watch_404_Repository(); $access_roles = $this->access_roles(); $available_roles = count_users()['avail_roles']; $role_counts = array(); $health = $this->site_health_summary(); $access_user_ids = $this->access_user_ids( $access_roles ); $session_counts = $this->active_session_counts( $access_user_ids ); $session_total = array_sum( $session_counts ); $active_users = count( $session_counts ); $today_hits = $repository->daily_hit_count( $this->site_date(), $this->site_date() ); $seven_day_hits = $repository->daily_hit_count( $this->site_date( 6 ), $this->site_date() ); $mfa_detector = new WebStudio_Admin_Watch_MFA_Detector(); $security = array( 'password_recorded' => 0, 'strong_medium' => 0, 'mfa_enabled' => 0, 'mfa_unknown' => 0 ); foreach ( $access_roles as $slug => $name ) { $count = isset( $available_roles[ $slug ] ) ? (int) $available_roles[ $slug ] : 0; if ( $count ) { $role_counts[ $name ] = $count; } } $users = get_users( array( 'meta_key' => WebStudio_Admin_Watch_Login_Monitor::META_KEY, 'number' => 5, 'orderby' => 'meta_value_num', 'order' => 'DESC', 'role__in' => array_keys( $access_roles ), ) ); update_meta_cache( 'user', $access_user_ids ); foreach ( $access_user_ids as $user_id ) { $password_changed = get_user_meta( $user_id, WebStudio_Admin_Watch_Password_Monitor::CHANGED_META_KEY, true ); $password_strength = get_user_meta( $user_id, WebStudio_Admin_Watch_Password_Monitor::STRENGTH_META_KEY, true ); $mfa_status = $mfa_detector->status_for_user( $user_id ); if ( $password_changed ) { ++$security['password_recorded']; } if ( in_array( $password_strength, array( 'medium', 'strong' ), true ) ) { ++$security['strong_medium']; } if ( WebStudio_Admin_Watch_MFA_Detector::ENABLED === $mfa_status ) { ++$security['mfa_enabled']; } if ( WebStudio_Admin_Watch_MFA_Detector::NOT_DETECTED === $mfa_status ) { ++$security['mfa_unknown']; } } ?>
page_header( __( 'Admin Watch Central', 'adminwatch-central' ), __( 'Know who can change your WordPress website.', 'adminwatch-central' ), true ); ?>


$count ) : ?>

activity_list( ( new WebStudio_Admin_Watch_Activity_Repository() )->items( array(), 1, 5 ), true ); ?>

query_value( 'page' ) && 'active' === $this->query_value( 'session' ) ) { $this->require_access(); $url = add_query_arg( array_filter( array( 'page' => 'admin-watch-sessions', 's' => $this->query_value( 's' ), 'role' => sanitize_key( $this->query_value( 'role' ) ) ) ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } } /** Render current state through a dedicated, paginated route. */ public function sessions() { $this->require_access(); $roles = $this->access_roles(); $selected = sanitize_key( $this->query_value( 'role' ) ); $selected = isset( $roles[ $selected ] ) ? $selected : ''; $search = $this->query_value( 's' ); $result = ( new WebStudio_Admin_Watch_Session_Visibility() )->page( $selected ? array( $selected ) : array_keys( $roles ), $search, $this->requested_page(), self::PER_PAGE ); $mfa = new WebStudio_Admin_Watch_MFA_Detector(); ?>
page_header( __( 'Active Sessions', 'adminwatch-central' ), __( 'Review currently valid WordPress authentication sessions for users who can make meaningful changes to this website.', 'adminwatch-central' ) ); ?>

__( 'Active Sessions', 'adminwatch-central' ), 'total' => __( 'Users with Sessions', 'adminwatch-central' ), 'eligible' => __( 'Matching Privileged Users', 'adminwatch-central' ) ) as $key => $label ) : ?>
users_filter_navigation( $roles, $selected, true ); ?>
display_name ); ?>
user_login ); ?>
role_label( $user ) ); ?> ID ] ) ); ?> time_label( get_user_meta( $user->ID, WebStudio_Admin_Watch_Login_Monitor::META_KEY, true ) ) ); ?> status_badge( $this->password_strength_label( get_user_meta( $user->ID, WebStudio_Admin_Watch_Password_Monitor::STRENGTH_META_KEY, true ) ), 'not-recorded' ); ?>
mfa_label( $mfa->status_for_user( $user->ID ) ) ) ); ?>
user_activity_link( $user->ID ); ?>
pagination( $result['page'], $result['total'], array_filter( array( 'page' => 'admin-watch-sessions', 'role' => $selected, 's' => $search ) ) ); ?>
require_access(); $roles = $this->access_roles(); $selected = sanitize_key( $this->query_value( 'role' ) ); $search = $this->query_value( 's' ); $page = $this->requested_page(); $args = array( 'number' => self::PER_PAGE, 'paged' => $page, 'orderby' => 'display_name', 'order' => 'ASC', 'role__in' => array_keys( $roles ), 'search' => $search ? '*' . $search . '*' : '', 'search_columns' => array( 'display_name', 'user_login', 'user_email' ), ); if ( ! $roles ) { $args['include'] = array( 0 ); } if ( $selected && isset( $roles[ $selected ] ) ) { unset( $args['role__in'] ); $args['role'] = $selected; } else { $selected = ''; } $query = new WP_User_Query( $args ); $total = (int) $query->get_total(); $total_pages = max( 1, (int) ceil( $total / self::PER_PAGE ) ); if ( $page > $total_pages ) { $page = $total_pages; $args['paged'] = $page; $query = new WP_User_Query( $args ); } $mfa_detector = new WebStudio_Admin_Watch_MFA_Detector(); $user_results = $query->get_results(); update_meta_cache( 'user', wp_list_pluck( $user_results, 'ID' ) ); ?>
page_header( __( 'Users & Access', 'adminwatch-central' ), __( 'See which users can make meaningful changes to this website.', 'adminwatch-central' ) ); ?>

users_filter_navigation( $roles, $selected ); ?>
ID, WebStudio_Admin_Watch_Login_Monitor::META_KEY, true ); $password_changed = get_user_meta( $user->ID, WebStudio_Admin_Watch_Password_Monitor::CHANGED_META_KEY, true ); $password_strength = get_user_meta( $user->ID, WebStudio_Admin_Watch_Password_Monitor::STRENGTH_META_KEY, true ); $mfa_status = $mfa_detector->status_for_user( $user->ID ); ?>
display_name ); ?>
user_login ); ?>
user_email ); ?>

user_activity_link( $user->ID ); ?>
role_label( $user ) ); ?>time_label( $last_login ) ); ?>time_label( $password_changed ) ); ?>status_badge( $this->password_strength_label( $password_strength ), $password_strength ? 'strength-' . $password_strength : 'not-recorded' ); ?>status_badge( $this->mfa_label( $mfa_status ), $mfa_status ); ?>
pagination( $page, $total, array_filter( array( 'page' => 'admin-watch-users', 'role' => $selected, 's' => $search ) ) ); ?>
'admin-watch-activity', 'user_id' => absint( $id ) ), admin_url( 'admin.php' ) ); ?> 'admin-users', 'content' => 'admin-page', 'plugins' => 'admin-plugins', 'themes' => 'admin-appearance', 'wordpress' => 'wordpress', 'adminwatch' => 'visibility' ); $previous_day = ''; if ( ! $rows ) { echo '

' . esc_html__( 'No matching activity has been recorded.', 'adminwatch-central' ) . '

'; return; } ?>
    last_occurred_at . ' UTC' ); $day = wp_date( 'Y-m-d', $timestamp ); $category = $categories[ $row->category ] ?? __( 'Activity', 'adminwatch-central' ); ?>
  1. site_date() ? __( 'Today', 'adminwatch-central' ) : ( $day === $this->site_date( 1 ) ? __( 'Yesterday', 'adminwatch-central' ) : wp_date( get_option( 'date_format' ), $timestamp ) ) ); ?>

    object_id && ! in_array( $row->object_type, array( 'user', 'plugin', 'theme' ), true ) && get_post( $row->object_id ) && current_user_can( 'edit_post', $row->object_id ) ) : ?>
require_access(); $repository = new WebStudio_Admin_Watch_Activity_Repository(); $categories = WebStudio_Admin_Watch_Activity_Events::categories(); $category = sanitize_key( $this->query_value( 'category' ) ); $category = isset( $categories[ $category ] ) ? $category : ''; $user = $this->query_value( 'user_id' ); $user = in_array( $user, array( 'system', 'anonymous' ), true ) ? $user : ( ctype_digit( $user ) ? (string) absint( $user ) : '' ); $date = $this->query_value( 'date' ); $date = in_array( $date, array( 'today', '7', '30' ), true ) ? $date : ''; $search = WebStudio_Admin_Watch_Activity_Repository::text( $this->query_value( 's' ), 100 ); $filters = array( 'category' => $category, 'user_id' => $user, 'search' => $search ); if ( $date ) { $since = new DateTimeImmutable( 'today', wp_timezone() ); if ( 'today' !== $date ) { $since = $since->modify( '-' . ( (int) $date - 1 ) . ' days' ); } $filters['since'] = $since->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ); } $total = $repository->count( $filters ); $page = min( $this->requested_page(), max( 1, (int) ceil( $total / self::PER_PAGE ) ) ); $users = $repository->users(); if ( ctype_digit( $user ) && (int) $user > 0 ) { $users[] = (int) $user; } $settings = WebStudio_Admin_Watch_Activity_Repository::settings(); ?>
page_header( __( 'Site Activity', 'adminwatch-central' ), __( 'Review important login, user, content, plugin, theme and WordPress activity recorded by Admin Watch Central.', 'adminwatch-central' ) ); ?>

activity_list( $repository->items( $filters, $page, self::PER_PAGE ) ); ?> pagination( $page, $total, array_filter( array( 'page' => 'admin-watch-activity', 'category' => $category, 'user_id' => $user, 'date' => $date, 's' => $search ) ) ); ?>
require_access(); check_admin_referer( 'admin_watch_clear_activity' ); if ( ! isset( $_POST['confirm_clear'] ) || ! is_string( $_POST['confirm_clear'] ) || '1' !== sanitize_text_field( wp_unslash( $_POST['confirm_clear'] ) ) ) { wp_die( esc_html__( 'Confirm that you want to clear the activity log.', 'adminwatch-central' ) ); } $result = ( new WebStudio_Admin_Watch_Activity_Repository() )->clear(); wp_safe_redirect( add_query_arg( array( 'page' => 'admin-watch-settings', 'activity_cleared' => false === $result ? 'error' : 'yes' ), admin_url( 'admin.php' ) ) ); exit; } /** Render the 404 Monitor screen. */ public function monitor_404s() { $this->require_access(); $repository = new WebStudio_Admin_Watch_404_Repository(); $page = $this->requested_page(); $total = $repository->count_items(); $total_pages = max( 1, (int) ceil( $total / self::PER_PAGE ) ); $page = min( $page, $total_pages ); $items = $repository->get_items( $page, self::PER_PAGE ); ?>
page_header( __( '404 Monitor', 'adminwatch-central' ), __( 'See broken URLs visitors are reaching.', 'adminwatch-central' ) ); ?>

total_hits() ) ); ?>
url ); ?>hit_count ) ); ?>last_seen . ' UTC' ) ) ); ?>
pagination( $page, $total, array( 'page' => 'admin-watch-404s' ) ); ?>
require_access(); $settings = WebStudio_Admin_Watch_Activity_Repository::settings(); ?>
page_header( __( 'Settings', 'adminwatch-central' ), __( 'Configure Admin Watch Central Free.', 'adminwatch-central' ) ); ?>

query_value( 'activity_cleared' ) ) : ?>

query_value( 'activity_cleared' ) ) : ?>

require_access(); $website_url = 'https://webstudiowa.com.au/'; $github_url = 'https://github.com/asankalk/admin-watch-free'; $bug_url = 'mailto:hello@webstudiowa.com.au?subject=' . rawurlencode( __( 'Admin Watch Central Bug Report', 'adminwatch-central' ) ); $support_url = 'mailto:hello@webstudiowa.com.au?subject=' . rawurlencode( __( 'Admin Watch Central Support', 'adminwatch-central' ) ); ?>
page_header( __( 'About & Support', 'adminwatch-central' ), __( 'Product information, support and privacy details for Admin Watch Central Free.', 'adminwatch-central' ) ); ?>