' . 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(); return array( 'monitor_404s' => empty( $input['monitor_404s'] ) ? 0 : 1, 'retention_days' => in_array( absint( $input['retention_days'] ?? 30 ), array( 7, 30, 90 ), true ) ? absint( $input['retention_days'] ) : 30, 'delete_data' => empty( $input['delete_data'] ) ? 0 : 1, ); } /** * 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 ) { 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() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination state. $paged = isset( $_GET['paged'] ) ? sanitize_text_field( wp_unslash( $_GET['paged'] ) ) : 1; return max( 1, absint( $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 AdminWatch 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 ) { ?> 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( __( 'AdminWatch Central', 'adminwatch-central' ), __( 'Know who can change your WordPress website.', 'adminwatch-central' ), true ); ?>


$count ) : ?>

$roles Eligible role labels. * @param string $search Optional user search. */ private function active_session_users( $roles, $search ) { $session_counts = $this->active_session_counts( $this->access_user_ids( $roles ) ); $users = $session_counts ? get_users( array( 'include' => array_keys( $session_counts ) ) ) : array(); if ( $search ) { $users = array_filter( $users, function( $user ) use ( $search ) { return false !== stripos( $user->display_name, $search ) || false !== stripos( $user->user_login, $search ) || false !== stripos( $user->user_email, $search ); } ); } usort( $users, function( $first, $second ) { return (int) get_user_meta( $second->ID, WebStudio_Admin_Watch_Login_Monitor::META_KEY, true ) <=> (int) get_user_meta( $first->ID, WebStudio_Admin_Watch_Login_Monitor::META_KEY, true ); } ); $total_matching = count( $users ); $users = array_slice( $users, 0, 3 ); ?>
page_header( __( 'Users & Access', 'adminwatch-central' ), __( 'See which users can make meaningful changes to this website.', 'adminwatch-central' ) ); ?> users_filter_navigation( $roles, '', true ); ?>

display_name ); ?>role_label( $user ) ); ?>time_label( get_user_meta( $user->ID, WebStudio_Admin_Watch_Login_Monitor::META_KEY, true ) ) ); ?>ID ], 'adminwatch-central' ), $session_counts[ $user->ID ] ) ); ?>
3 ) : ?>

require_access(); $roles = $this->access_roles(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter state. $session_filter = isset( $_GET['session'] ) && 'active' === sanitize_key( wp_unslash( $_GET['session'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter state. $selected = isset( $_GET['role'] ) ? sanitize_key( wp_unslash( $_GET['role'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only search state. $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; if ( $session_filter ) { $this->active_session_users( $roles, $search ); return; } $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 ( $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 ); ?>
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 ) ) ); ?>
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 = wp_parse_args( get_option( 'admin_watch_settings', array() ), array( 'monitor_404s' => 1, 'retention_days' => 30, 'delete_data' => 0 ) ); ?>
page_header( __( 'Settings', 'adminwatch-central' ), __( 'Configure AdminWatch Central Free.', 'adminwatch-central' ) ); ?>

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( __( 'AdminWatch Central Bug Report', 'adminwatch-central' ) ); $support_url = 'mailto:hello@webstudiowa.com.au?subject=' . rawurlencode( __( 'AdminWatch Central Support', 'adminwatch-central' ) ); ?>
page_header( __( 'About & Support', 'adminwatch-central' ), __( 'Product information, support and privacy details for AdminWatch Central Free.', 'adminwatch-central' ) ); ?>