/`, * the REST API lists every author at `/wp/v2/users`, the users sitemap indexes them, and * oEmbed responses name them. A username is half of a login. Sites that never publish author * pages give it away for nothing, and the ones with a single administrator give away the one * account that matters. * * One switch, no settings. Every surface is closed for visitors who are not logged in and left * exactly as it was for everyone who is, because the block editor and the admin need the users * routes. Nothing here writes to the database; switching it off reopens everything at once. * * @package Adminkeep */ namespace Adminkeep\Feature; use Adminkeep\Feature; defined( 'ABSPATH' ) || exit; /** * Keeps usernames out of public view. */ class UsernamePrivacy implements Feature { /** * Feature id. * * @return string */ public function id() { return 'username_privacy'; } /** * Label. * * @return string */ public function label() { return __( 'Username Privacy', 'adminkeep' ); } /** * Description. * * @return string */ public function description() { return __( 'Keeps your usernames out of public view. WordPress hands them to anyone who asks, through author pages, the REST API, sitemaps and embeds; that is half of a login, given away. Logged-in users see everything as before.', 'adminkeep' ); } /** * A word for what this feature is doing right now. * * @return string */ public function state_label() { return __( 'Private', 'adminkeep' ); } /** * Settings schema. Nothing to decide. * * @return array */ public function settings_schema() { return array(); } /** * No fields, so no labels. * * @param string $key Field key. * @return string */ public function field_label( $key ) { return ''; } /** * What switching this on actually does. * * @return string[] */ public function how_it_works() { return array( __( 'Author pages answer "not found" to visitors, including the ?author=1 probe that would otherwise redirect to the username.', 'adminkeep' ), __( 'The public users list in the REST API is closed to visitors. Posts, pages and everything else stay readable.', 'adminkeep' ), __( 'Authors are left out of the sitemap, WordPress\'s own and Yoast SEO\'s, and out of embed previews of your posts.', 'adminkeep' ), __( 'Author names in your theme stop linking to author pages, so the username never appears in a link.', 'adminkeep' ), __( 'Anyone logged in, from subscriber to administrator, sees all of it as before. The block editor keeps working.', 'adminkeep' ), __( 'Nothing is written to the database. Switch it off and every page and route is back immediately.', 'adminkeep' ), __( 'Note: on a multi-author blog whose readers use the author pages, leave this off.', 'adminkeep' ), ); } /** * Register hooks. * * @param array $settings Resolved settings. Unused — this feature has none. * @return void */ public function boot( array $settings ) { // Priority 1: before redirect_canonical (10) turns ?author=N into /author//. add_action( 'template_redirect', array( $this, 'refuse_author_archive' ), 1 ); add_filter( 'rest_endpoints', array( $this, 'remove_users_routes' ), 100 ); add_filter( 'wp_sitemaps_add_provider', array( $this, 'drop_users_sitemap' ), 100, 2 ); add_filter( 'wpseo_sitemap_exclude_author', array( $this, 'empty_yoast_author_sitemap' ), 100 ); add_filter( 'oembed_response_data', array( $this, 'strip_oembed_author' ), 100 ); add_filter( 'author_link', array( $this, 'author_link_home' ), 100 ); } /** * Whether the current request should see usernames. * * @return bool */ private function is_visitor() { return ! is_user_logged_in(); } /** * Turn an author query into a 404 for visitors. * * @return void */ public function refuse_author_archive() { global $wp_query; if ( ! $this->is_visitor() || ! is_author() ) { return; } $wp_query->set_404(); status_header( 404 ); nocache_headers(); } /** * Drop the users routes from the REST route table for visitors. * * Removing the routes (rather than answering 401) means `?rest_route=` and pretty URLs * behave the same, and `_embed` requests simply carry no author object. * * @param array $endpoints Route => handlers. * @return array */ public function remove_users_routes( $endpoints ) { if ( ! $this->is_visitor() || ! is_array( $endpoints ) ) { return $endpoints; } foreach ( array_keys( $endpoints ) as $route ) { if ( 0 === strpos( $route, '/wp/v2/users' ) ) { unset( $endpoints[ $route ] ); } } return $endpoints; } /** * Leave core's users provider out of the sitemap index. * * The sitemap is public by definition, so no visitor check: an index that lists authors for * a crawler lists them for everyone. * * @param \WP_Sitemaps_Provider|false $provider Provider. * @param string $name Provider name. * @return \WP_Sitemaps_Provider|false */ public function drop_users_sitemap( $provider, $name ) { return 'users' === $name ? false : $provider; } /** * Empty Yoast SEO's author sitemap. * * @param array $users Users Yoast would list. * @return array */ public function empty_yoast_author_sitemap( $users ) { return array(); } /** * Drop the author fields from oEmbed responses. Embeds are fetched server to server, never * as a logged-in user, so no visitor check. * * @param array $data Response data. * @return array */ public function strip_oembed_author( $data ) { if ( is_array( $data ) ) { unset( $data['author_name'], $data['author_url'] ); } return $data; } /** * Point author links at the homepage for visitors, so the login never appears in markup. * * @param string $link Author archive URL. * @return string */ public function author_link_home( $link ) { return $this->is_visitor() ? home_url( '/' ) : $link; } }