' . esc_html__( 'Settings', '3task-doorman' ) . '' ); return $links; } /** * URL of a tab. * * @param string $tab Tab. * @param array $args Extra query args. * @return string */ public static function url( $tab = 'overview', $args = array() ) { return add_query_arg( array_merge( array( 'page' => self::SLUG, 'tab' => $tab ), $args ), admin_url( 'admin.php' ) ); } /** * The doors with label, explanation and whether an open door is a risk. * * @return array */ public static function doors() { return array( 'users' => array( __( 'User list for guests', '3task-doorman' ), __( '/wp/v2/users lists the login names of all authors. Attackers use them to guess passwords. The block editor keeps working, it is used by logged-in users only.', '3task-doorman' ), true ), 'index' => array( __( 'Route directory for guests', '3task-doorman' ), __( '/wp-json/ and the root of every namespace list all routes, and with them most of your plugins.', '3task-doorman' ), true ), 'batch' => array( __( 'Batch endpoint for guests', '3task-doorman' ), __( '/batch/v1 runs several requests in one. In July 2026 it was the entry point of the core vulnerability wp2shell. Guests do not need it. Updates protect you, this switch is a second lock.', '3task-doorman' ), true ), 'ai_guest' => array( __( 'Hide AI access from guests', '3task-doorman' ), __( 'MCP servers and the Abilities API let AI agents work on your site. Guests never need them, logged-in agents keep their access.', '3task-doorman' ), true ), 'ai_off' => array( __( 'Switch off all AI access', '3task-doorman' ), __( 'Blocks MCP and Abilities for every machine access (application passwords and other logins). You in the browser keep access.', '3task-doorman' ), false ), 'app_readonly' => array( __( 'Application passwords: read only', '3task-doorman' ), __( 'Requests with an application password may read but not write, delete or publish. This also stops MCP tools, because they always send POST.', '3task-doorman' ), false ), 'app_passwords_off' => array( __( 'Switch off application passwords', '3task-doorman' ), __( 'Nobody can log in with an application password any more. Apps, backups or AI agents that use one stop working.', '3task-doorman' ), false ), 'xmlrpc' => array( __( 'Switch off XML-RPC', '3task-doorman' ), __( 'xmlrpc.php is an old interface, still used by some mobile apps and by Jetpack. Without them you do not need it.', '3task-doorman' ), false ), 'hide_discovery' => array( __( 'Remove REST hints from the page head', '3task-doorman' ), __( 'Removes the link to /wp-json/ from the HTML head and the Link header. The REST API itself stays available.', '3task-doorman' ), false ), ); } /** * Main page. * * @return void */ public static function page() { if ( ! current_user_can( self::CAP ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- tab selection only. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'overview'; $tabs = array( 'overview' => array( __( 'Overview', '3task-doorman' ), 'shield-check' ), 'namespaces' => array( __( 'Namespaces', '3task-doorman' ), 'stack-2' ), 'rules' => array( __( 'Rules', '3task-doorman' ), 'list-details' ), 'learn' => array( __( 'Learning mode', '3task-doorman' ), 'route' ), 'ai' => array( __( 'AI access', '3task-doorman' ), 'robot' ), ); if ( ! isset( $tabs[ $tab ] ) ) { $tab = 'overview'; } $settings = ThreeDoor_Settings::get(); ?>

define( \'THREEDOOR_DISABLE\', true );' ); ?>

ThreeDoor_Cache::state()['rest_cached'] ? __( 'Saved. The REST cache was emptied, the rules apply now.', '3task-doorman' ) : __( 'Saved. The rules apply now.', '3task-doorman' ), 'imported' => __( 'Rules of Disable REST API taken over. You can deactivate Disable REST API now.', '3task-doorman' ), 'applied' => __( 'Suggestion applied: guests now only reach the routes from the list.', '3task-doorman' ), 'reset' => __( 'Learning data deleted.', '3task-doorman' ), 'confirm' => __( 'Please confirm first that you have read the cache warning.', '3task-doorman' ), ); if ( isset( $text[ $done ] ) ) { $type = 'confirm' === $done ? 'warning' : 'success'; echo '

' . esc_html( $text[ $done ] ) . '

'; } } /** * Hidden fields of a form. * * @param string $action admin-post action. * @param string $section Settings section. * @param string $tab Tab to return to. * @return void */ private static function form_fields( $action, $section, $tab ) { wp_nonce_field( 'threedoor_' . $action ); echo ''; echo ''; echo ''; } /** * Totals of the learning data. * * @param array $rows Summary rows. * @return array guest, denied, machine. */ private static function totals( array $rows ) { $t = array( 'guest' => 0, 'denied' => 0, 'machine' => 0 ); foreach ( $rows as $row ) { if ( 'guest' === $row['who'] ) { $t['guest'] += $row['hits']; } if ( $row['denied'] ) { $t['denied'] += $row['hits']; } if ( in_array( $row['who'], array( 'app_password', 'other' ), true ) ) { $t['machine'] += $row['hits']; } } return $t; } /** * Tab: overview with all doors. * * @param array $settings Settings. * @return void */ private static function tab_overview( array $settings ) { $rows = ThreeDoor_Learn::summary(); $totals = self::totals( $rows ); $ns = ThreeDoor_Origin::namespaces(); $ai = array_filter( $ns, function ( $n ) { return $n['ai']; } ); $open = 0; foreach ( self::doors() as $key => $door ) { if ( $door[2] && empty( $settings['doors'][ $key ] ) ) { ++$open; } } $cache = ThreeDoor_Cache::state(); ?>

__( 'WordPress', '3task-doorman' ), 'plugin' => __( 'Plugin', '3task-doorman' ), 'mu-plugin' => __( 'Must-use plugin', '3task-doorman' ), 'theme' => __( 'Theme', '3task-doorman' ), 'unknown' => __( 'unknown', '3task-doorman' ), ); ?>

$n ) : $probe = ThreeDoor_Rules::decide( array( 'route' => '/' . $name . '/x', 'method' => 'GET', 'roles' => array(), 'auth' => 'none' ), $settings ); $hits = 0; foreach ( $seen as $route => $count ) { if ( 0 === strpos( $route, '/' . $name . '/' ) || '/' . $name === $route ) { $hits += $count; } } ?>
' . esc_html__( 'AI', '3task-doorman' ) . '' : ''; ?> ()
'allow', 'allow' => array(), 'deny' => array() ); $roles = wp_roles()->get_names(); unset( $roles['administrator'] ); ?>

$label ) : $rule = isset( $settings['roles'][ $role ] ) ? $settings['roles'][ $role ] : null; $mode = $rule ? $rule['default'] : 'none'; ?>
>
__( 'Guest', '3task-doorman' ), 'cookie' => __( 'Logged in (browser)', '3task-doorman' ), 'app_password' => __( 'Application password', '3task-doorman' ), 'other' => __( 'Other login', '3task-doorman' ), ); ?>

array( 'ID', 'display_name' ), 'number' => 500 ) ) as $user ) { foreach ( WP_Application_Passwords::get_user_application_passwords( $user->ID ) as $item ) { $list[] = array( $user, $item ); } } } if ( empty( $list ) ) : ?>

display_name ); ?>

403 ) ); } check_admin_referer( 'threedoor_' . $action ); return isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'overview'; } /** * Save a settings section. * * @return void */ public static function handle_save() { $tab = self::verify( 'save' ); $input = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- nonce checked in verify(), every field sanitized in ThreeDoor_Settings::from_form(). $was = ThreeDoor_Settings::get(); $new = ThreeDoor_Settings::from_form( (array) $input, $was ); if ( empty( $was['learn'] ) && ! empty( $new['learn'] ) ) { update_option( 'threedoor_learn_since', time(), false ); } ThreeDoor_Settings::save( $new ); wp_safe_redirect( self::url( $tab, array( 'threedoor' => 'saved' ) ) ); exit; } /** * Take over Disable REST API. * * @return void */ public static function handle_import() { self::verify( 'import' ); ThreeDoor_Settings::save( ThreeDoor_Import::from_disable_rest_api( ThreeDoor_Settings::get() ) ); wp_safe_redirect( self::url( 'rules', array( 'threedoor' => 'imported' ) ) ); exit; } /** * Apply the learning suggestion as guest rule. * * @return void */ public static function handle_suggestion() { self::verify( 'suggestion' ); $suggest = ThreeDoor_Learn::suggest(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked in verify(). $confirmed = ! empty( $_POST['confirm'] ); if ( $suggest['uncertain'] && ! $confirmed ) { wp_safe_redirect( self::url( 'learn', array( 'threedoor' => 'confirm' ) ) ); exit; } $settings = ThreeDoor_Settings::get(); $deny = isset( $settings['guest']['deny'] ) ? (array) $settings['guest']['deny'] : array(); $settings['guest'] = array( 'default' => 'deny', 'allow' => $suggest['allow'], 'deny' => $deny, ); ThreeDoor_Settings::save( $settings ); wp_safe_redirect( self::url( 'rules', array( 'threedoor' => 'applied' ) ) ); exit; } /** * Delete learning data. * * @return void */ public static function handle_reset_log() { self::verify( 'reset_log' ); ThreeDoor_Learn::reset(); update_option( 'threedoor_learn_since', time(), false ); wp_safe_redirect( self::url( 'learn', array( 'threedoor' => 'reset' ) ) ); exit; } }