settings = ITSEC_Modules::get_settings( 'hide-backend' );
add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
add_filter( 'itsec_hide-backend_notification_strings', array( $this, 'notification_strings' ) );
if ( ! $this->settings['enabled'] ) {
return;
}
add_action( 'init', array( $this, 'handle_specific_page_requests' ), 1000 );
add_action( 'signup_hidden_fields', array( $this, 'add_token_to_registration_form' ) );
add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue' ) );
add_filter( 'site_url', array( $this, 'filter_generated_url' ), 100, 2 );
add_filter( 'network_site_url', array( $this, 'filter_generated_url' ), 100, 2 );
add_filter( 'admin_url', array( $this, 'filter_admin_url' ), 100, 2 );
add_filter( 'wp_redirect', array( $this, 'filter_redirect' ) );
add_filter( 'comment_moderation_text', array( $this, 'filter_comment_moderation_text' ) );
add_filter( 'itsec_notify_admin_page_url', array( $this, 'filter_notify_admin_page_urls' ) );
remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
}
/**
* Filters emailed comment moderation links to use modified login links with redirection.
*
* Comment moderation links link directly to wp-admin pages. Since direct requests to wp-admin are blocked by Hide
* Backend, these links are updated to link to the login page with a redirect to the wp-admin page.
*
* @since 4.5
*
* @param string $text Comment moderation email text.
*
* @return string Comment moderation email text.
*/
public function filter_comment_moderation_text( $text ) {
if ( $this->disable_filters ) {
return $text;
}
// The email is plain text and the links are at the end of lines, so a lazy match can be used.
if ( preg_match_all( '|(https?:\/\/((.*)wp-admin(.*)))|', $text, $urls ) ) {
foreach ( $urls[0] as $url ) {
$url = trim( $url );
$text = str_replace( $url, wp_login_url( $url ), $text );
}
}
return $text;
}
/**
* Ensure that login and registration pages and their aliases are handled properly.
*
* This function is responsible for identifying if the current page request is for wp-login.php, wp-signup.php, a
* canonical alias for one of those pages, a wp-admin request, or one of Hide Backend's replacements pages. If a
* matching page page is found, the appropriate function is called to handle the rest of the processing.
*
* @since 4.0
*
* @return void
*/
public function handle_specific_page_requests() {
if ( ITSEC_Core::is_api_request() ) {
return;
}
$request_path = ITSEC_Lib::get_request_path();
if ( $request_path === $this->settings['slug'] ) {
$this->handle_login_alias();
} else if ( in_array( $request_path, array( 'wp-login', 'wp-login.php' ) ) ) {
$this->handle_canonical_login_page();
} else if ( 'wp-admin' === $request_path || 'wp-admin/' === substr( $request_path, 0, 9 ) ) {
$this->handle_wp_admin_page();
} else if ( $request_path === $this->settings['register'] && $this->allow_access_to_wp_signup() ) {
$this->handle_registration_alias();
} else if ( 'wp-signup.php' === $request_path ) {
$this->handle_canonical_signup_page();
}
}
/**
* Handle a request for the Hide Backend replacement login page slug.
*
* @return void
*/
private function handle_login_alias() {
if ( isset( $_GET['action'] ) && $_GET['action'] === trim( $this->settings['post_logout_slug'] ) ) {
// I'm not sure if this feature is still needed or if anyone still uses it. - Chris
do_action( 'itsec_custom_login_slug' );
}
$this->do_redirect_with_token( 'login', 'wp-login.php' );
}
/**
* Handle a request for wp-login.php or a canonical alias for it.
*
* @return void
*/
private function handle_canonical_login_page() {
$action = isset( $_GET['action'] ) ? $_GET['action'] : '';
if ( 'postpass' === $action ) {
return;
} else if ( 'register' === $action ) {
$this->block_access( 'register' );
return;
} else if ( 'jetpack_json_api_authorization' === $action && has_filter( 'login_form_jetpack_json_api_authorization' ) ) {
// Jetpack handles authentication for this action. Processing is left to it.
return;
} else if ( 'jetpack-sso' === $action && has_filter( 'login_form_jetpack-sso' ) ) {
// Jetpack's SSO redirects from wordpress.com to wp-login.php on the site. Only allow this process to
// continue if they successfully log in, which should happen by login_init in Jetpack which happens just
// before this action fires.
add_action( 'login_form_jetpack-sso', array( $this, 'block_access' ) );
return;
}
$this->block_access( 'login' );
}
/**
* Handle a request for the Hide Backend replacement register page slug.
*
* @return void
*/
private function handle_registration_alias() {
if ( 'wp-signup.php' === $this->settings['register'] ) {
return;
}
if ( get_option( 'users_can_register' ) ) {
if ( is_multisite() ) {
$this->do_redirect_with_token( 'register', 'wp-signup.php' );
} else {
$this->do_redirect_with_token( 'register', 'wp-login.php?action=register' );
}
}
}
/**
* Handle a request for wp-signup.php.
*
* @return void
*/
private function handle_canonical_signup_page() {
$this->block_access( 'register' );
}
/**
* Handle a request for any wp-admin directory request.
*
* @return void
*/
private function handle_wp_admin_page() {
$request_path = ITSEC_Lib::get_request_path();
if ( 'wp-admin/maint/repair.php' === $request_path && defined( 'WP_ALLOW_REPAIR' ) ) {
// Make sure to only allow access if the page would function.
return;
}
$this->block_access( 'login' );
}
/**
* Block access to the page if the visitor is not a logged in user and the request fails validation.
*
* @param string $type The type of request to be validated.
*
* @return void
*/
public function block_access( $type = 'login' ) {
if ( is_user_logged_in() || $this->is_validated( $type ) ) {
return;
}
if ( $this->settings['theme_compat'] ) {
// The "Enable Redirection" setting is enabled. Redirect to the "Redirection Slug" setting.
wp_redirect( ITSEC_Lib::get_home_root() . $this->settings['theme_compat_slug'], 302 );
exit;
} else {
// The "Enable Redirection" setting is disabled. Return a 403 error.
wp_die( __( 'This has been disabled.', 'better-wp-security' ), 403 );
}
}
/**
* Redirect to requested path with the token query arg added to ensure that the redirected request is validated.
*
* This function will also set an appropriate cookie when doing the redirect. The presence of the cookie and query
* arg should ensure that the redirect request validates properly.
*
* @param string $type The type of request to add an access token for.
* @param string $path The path to redirect to.
*
* @return void
*/
private function do_redirect_with_token( $type, $path ) {
// Set the cookie so that access via unknown integrations works more smoothly.
$this->set_cookie( $type );
// Preserve existing query vars and add access token query arg.
$query_vars = $_GET;
$query_vars[$this->token_var] = $this->get_access_token( $type );
$query = http_build_query( $query_vars, null, '&' );
// Disable the Hide Backend URL filters to prevent infinite loops when calling site_url().
$this->disable_filters = true;
if ( false === strpos( $path, '?' ) ) {
$url = site_url( "$path?$query" );
} else {
$url = site_url( "$path&$query" );
}
wp_redirect( $url );
exit;
}
/**
* Filter generated login and signup URLs to include the access token query arg.
*
* @param string $url The complete URL to be filtered.
* @param string $path The path submitted by the originating function call.
*
* @return string The complete URL with conditionally added access token query arg.
*/
public function filter_generated_url( $url, $path ) {
if ( $this->disable_filters ) {
return $url;
}
list( $clean_path ) = explode( '?', $path );
if ( 'wp-login.php' === $clean_path && 'wp-login.php' !== $this->settings['slug'] ) {
$request_path = ITSEC_Lib::get_request_path();
if ( false !== strpos( $path, 'action=postpass' ) ) {
// No special handling is needed for a password-protected post.
return $url;
} else if ( false !== strpos( $path, 'action=register' ) ) {
$url = $this->add_token_to_url( $url, 'register' );
} elseif ( 'wp-login.php' !== $request_path || empty( $_GET['action'] ) || 'register' !== $_GET['action'] ) {
$url = $this->add_token_to_url( $url, 'login' );
}
} else if ( 'wp-signup.php' === $clean_path && 'wp-signup.php' !== $this->settings['register'] ) {
$url = $this->add_token_to_url( $url, 'register' );
}
return $url;
}
/**
* Filter the admin URL to include hide backend tokens when necessary.
*
* @param string $url Complete admin URL.
* @param string $path Path passed to the admin_url function.
*
* @return string
*/
public function filter_admin_url( $url, $path ) {
if ( 0 === strpos( $path, 'profile.php?newuseremail=' ) ) {
$url = $this->add_token_to_url( $url, 'login' );
}
return $url;
}
/**
* Filter redirection URLs to login and signup pages to include the access token query arg.
*
* @param string $location The relative path to redirect to.
*
* @return string The location with conditionally added access token query arg.
*/
public function filter_redirect( $location ) {
return $this->filter_generated_url( $location, $location );
}
/**
* Filter URLs to admin pages in emails to include the access token query arg.
*
* This ensures that users are redirected to the correct login page if they are logged-out.
*
* @param string $location
*
* @return string
*/
public function filter_notify_admin_page_urls( $location ) {
return $this->add_token_to_url( $location, 'login' );
}
/**
* Add the access token query arg to the URL.
*
* @param string $url The URL to modify.
* @param string $type The type of request to add an access token for.
*
* @return string The URL with the added access token query arg.
*/
private function add_token_to_url( $url, $type ) {
$token = $this->get_access_token( $type );
$url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
$url .= $this->token_var . '=' . urlencode( $token );
return $url;
}
/**
* Add a hidden input containing the appropriate access token name and value.
*
* This function is only used on multisite user signup pages. It is needed since the code that generates the form on
* that page does not use site_url() or network_site_url() to generate a full URL for form's action URL.
*
* @param string $context The type of signup form being rendered.
*
* @return null
*/
public function add_token_to_registration_form( $context ) {
if ( 'validate-user' === $context ) {
echo '' . "\n";
}
}
/**
* Hide the navigation links on the registration page.
*
* These links have their security tokens removed in PHP. We only hide them for UX purposes as they would
* lead to a 404 page.
*/
public function login_enqueue() {
if ( ! empty( $_GET['action'] ) && 'register' === $_GET['action'] ) {
wp_enqueue_style( 'itsec-hide-backend-login-page', plugins_url( 'css/login-page.css', __FILE__ ) );
}
}
/**
* Register the New Login URL notification.
*
* @param array $notifications
*
* @return array
*/
public function register_notification( $notifications ) {
if ( ITSEC_Modules::get_setting( 'hide-backend', 'enabled' ) ) {
$notifications['hide-backend'] = array(
'subject_editable' => true,
'message_editable' => true,
'schedule' => ITSEC_Notification_Center::S_NONE,
'recipient' => ITSEC_Notification_Center::R_USER_LIST,
'tags' => array( 'login_url', 'site_title', 'site_url' ),
'module' => 'hide-backend',
);
}
return $notifications;
}
/**
* Register the strings for the Hide Backend change notification.
*
* @return array
*/
public function notification_strings() {
return array(
'label' => esc_html__( 'Hide Backend – New Login URL', 'better-wp-security' ),
'description' => sprintf( esc_html__( '%1$sHide Backend%2$s will notify the chosen recipients whenever the login URL is changed.', 'better-wp-security' ), '', '' ),
'subject' => esc_html__( 'WordPress Login Address Changed', 'better-wp-security' ),
'message' => esc_html__( 'The login address for {{ $site_title }} has changed. The new login address is {{ $login_url }}. You will be unable to use the old login address.', 'better-wp-security' ),
'tags' => array(
'login_url' => esc_html__( 'The new login link.', 'better-wp-security' ),
'site_title' => esc_html__( 'The WordPress Site Title. Can be changed under Settings -> General -> Site Title', 'better-wp-security' ),
'site_url' => esc_html__( 'The URL to your website.', 'better-wp-security' ),
),
);
}
/**
* Creates a cookie to validate future requests.
*
* @param string $type The type of request to add an access token for.
* @param int $duration Number of seconds that the key will be valid.
*
* @return null
*/
private function set_cookie( $type, $duration = 3600 /* 1 hour */ ) {
$expires = time() + $duration;
setcookie( "itsec-hb-$type-" . COOKIEHASH, $this->get_access_token( $type ), $expires, ITSEC_Lib::get_home_root(), COOKIE_DOMAIN, is_ssl(), true );
}
/**
* Checks to see if a cookie or query arg value validates the current request for the type being checked.
*
* @param string $type The type of request to add an access token to validate.
*
* @return bool true if the request is validated, false otherwise.
*/
private function is_validated( $type ) {
$token = $this->get_access_token( $type );
if ( isset( $_REQUEST[$this->token_var] ) && $_REQUEST[$this->token_var] === $token ) {
$this->set_cookie( $type );
return true;
} else if ( isset( $_COOKIE["itsec-hb-$type-" . COOKIEHASH] ) && $_COOKIE["itsec-hb-$type-" . COOKIEHASH] === $token ) {
return true;
}
return false;
}
/**
* The access token to use for the specific request.
*
* @param string $type The type of request to create an access token for.
*
* @return string The access token.
*/
private function get_access_token( $type ) {
if ( isset( $this->settings[$type] ) ) {
return $this->settings[$type];
}
return $this->settings['slug'];
}
private function allow_access_to_wp_signup() {
if ( is_multisite() ) {
// Multisite will show its own error message and without links if signups are disabled.
return true;
}
if ( get_option( 'users_can_register' ) ) {
return true;
}
return false;
}
}