plug/s1-hide-admin/s1-hide-admin.php

233 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Plugin Name: S1 Hide Admin
* Plugin URI: https://1studi.ru
* Description: Скрывает стандартные адреса входа (wp-login.php, wp-admin) за собственным адресом-слагом, который задаётся в настройках. Прямые обращения к wp-login.php и wp-admin для неавторизованных пользователей отдают обычную 404, вместо того чтобы показывать форму входа или редиректить на неё.
* Version: 1.0.0
* Author: S1
* Author URI: https://1studi.ru
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: s1-hide-admin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Прямой доступ запрещён.
}
class S1_Hide_Admin {
const OPTION_KEY = 's1_hide_admin_slug';
const DEFAULT_SLUG = 'login';
const NONCE_ACTION = 's1_hide_admin_save';
/** Флаг, чтобы отличить наш внутренний require wp-login.php от прямого хита по этому файлу. */
private static $serving_login = false;
public static function init() {
add_action( 'init', array( __CLASS__, 'maybe_intercept' ), 1 );
add_filter( 'site_url', array( __CLASS__, 'filter_login_url' ), 10, 3 );
add_filter( 'network_site_url', array( __CLASS__, 'filter_login_url' ), 10, 3 );
add_action( 'admin_menu', array( __CLASS__, 'add_settings_page' ) );
add_action( 'admin_init', array( __CLASS__, 'maybe_save_settings' ) );
}
/* ------------------------------------------------------------------ */
/* Перехват запросов на фронтенде */
/* ------------------------------------------------------------------ */
public static function maybe_intercept() {
// Никогда не трогаем AJAX, cron, REST и WP-CLI — это внутренние/служебные запросы.
if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX )
|| ( defined( 'DOING_CRON' ) && DOING_CRON )
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
|| ( defined( 'WP_CLI' ) && WP_CLI )
) {
return;
}
$request_path = self::get_request_path();
// admin-ajax.php и admin-post.php должны продолжать работать как обычно
// (в том числе для неавторизованных посетителей — формы, счётчики и т.п.).
$basename = basename( $request_path );
if ( in_array( $basename, array( 'admin-ajax.php', 'admin-post.php' ), true ) ) {
return;
}
$slug = self::get_slug();
$is_custom_slug = ( $request_path === $slug );
$is_real_wp_login = ( 'wp-login.php' === $basename );
// Запрос на наш кастомный адрес — показываем настоящую страницу логина.
if ( $is_custom_slug && ! self::$serving_login ) {
self::serve_login();
return; // serve_login() сама завершает работу через exit.
}
// Прямое обращение к wp-login.php, минуя наш внутренний require, — прячем.
if ( $is_real_wp_login && ! self::$serving_login ) {
self::force_404();
}
// Прямое обращение к /wp-admin/ без авторизации — тоже прячем,
// вместо того чтобы штатно редиректить на (уже скрытую) форму входа.
if ( self::is_wp_admin_path( $request_path ) && ! is_user_logged_in() ) {
self::force_404();
}
}
private static function serve_login() {
self::$serving_login = true;
require ABSPATH . 'wp-login.php';
exit;
}
private static function force_404() {
status_header( 404 );
nocache_headers();
wp_die(
'<h1>404</h1><p>Страница не найдена.</p>',
'404',
array( 'response' => 404 )
);
}
/** Путь текущего запроса относительно корня сайта, без query-строки, в нижнем регистре, без слэшей по краям. */
private static function get_request_path() {
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
$path = is_string( $path ) ? $path : '';
$home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
$home_path = is_string( $home_path ) ? trim( $home_path, '/' ) : '';
$path = trim( $path, '/' );
// Если сайт установлен в подпапке — отрезаем этот префикс от пути запроса.
if ( $home_path && 0 === strpos( $path, $home_path ) ) {
$path = trim( substr( $path, strlen( $home_path ) ), '/' );
}
return strtolower( $path );
}
private static function is_wp_admin_path( $path ) {
return 'wp-admin' === $path || 0 === strpos( $path, 'wp-admin/' );
}
/* ------------------------------------------------------------------ */
/* Подмена ссылок, которые генерирует само ядро WordPress */
/* ------------------------------------------------------------------ */
/**
* wp_login_url(), wp_logout_url(), wp_registration_url(), wp_lostpassword_url()
* в ядре всегда вызывают site_url('wp-login.php', 'login'|'login_post') —
* подменяем результат на наш слаг, чтобы нигде на сайте не светился
* настоящий адрес wp-login.php.
*/
public static function filter_login_url( $url, $path, $scheme = null ) {
if ( in_array( $scheme, array( 'login', 'login_post' ), true ) ) {
$url = str_replace( 'wp-login.php', self::get_slug(), $url );
}
return $url;
}
/* ------------------------------------------------------------------ */
/* Настройки */
/* ------------------------------------------------------------------ */
public static function get_slug() {
$slug = get_option( self::OPTION_KEY, self::DEFAULT_SLUG );
return $slug ? $slug : self::DEFAULT_SLUG;
}
public static function add_settings_page() {
add_options_page(
'S1 Hide Admin',
'S1 Hide Admin',
'manage_options',
's1-hide-admin',
array( __CLASS__, 'render_settings_page' )
);
}
public static function maybe_save_settings() {
if ( empty( $_POST['s1_hide_admin_nonce'] ) || ! wp_verify_nonce( $_POST['s1_hide_admin_nonce'], self::NONCE_ACTION ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$slug = isset( $_POST['s1_hide_admin_slug'] ) ? sanitize_title( wp_unslash( $_POST['s1_hide_admin_slug'] ) ) : '';
// Запрещаем очевидно ломающие значения: пустое, wp-admin, wp-login(.php).
$forbidden = array( '', 'wp-admin', 'wp-login', 'wp-login.php' );
if ( in_array( $slug, $forbidden, true ) ) {
add_action( 'admin_notices', function () {
echo '<div class="notice notice-error"><p><strong>S1 Hide Admin:</strong> этот слаг использовать нельзя (пусто, wp-admin или wp-login). Настройка не сохранена.</p></div>';
} );
return;
}
update_option( self::OPTION_KEY, $slug );
add_action( 'admin_notices', function () {
echo '<div class="notice notice-success is-dismissible"><p>Адрес входа обновлён.</p></div>';
} );
}
public static function render_settings_page() {
$slug = self::get_slug();
$login_url = home_url( '/' . $slug );
?>
<div class="wrap">
<h1>S1 Hide Admin</h1>
<p>Стандартные адреса <code>wp-login.php</code> и <code>/wp-admin/</code> для неавторизованных посетителей скрыты — вместо них показывается обычная страница 404. Вход в панель управления возможен только по адресу ниже.</p>
<div class="notice notice-warning inline">
<p><strong>Важно:</strong> сохраните этот адрес, прежде чем закрывать страницу. Если вы забудете новый слаг, попасть в админку через обычный <code>wp-login.php</code> не получится — восстановить доступ можно будет только через переименование/удаление файла плагина по FTP или через файловый менеджер хостинга.</p>
</div>
<table class="form-table" role="presentation">
<tr>
<th scope="row">Текущий адрес входа</th>
<td><code><?php echo esc_html( $login_url ); ?></code></td>
</tr>
</table>
<form method="post" action="">
<?php wp_nonce_field( self::NONCE_ACTION, 's1_hide_admin_nonce' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="s1_hide_admin_slug">Новый адрес (слаг)</label></th>
<td>
<code><?php echo esc_html( home_url( '/' ) ); ?></code>
<input type="text" id="s1_hide_admin_slug" name="s1_hide_admin_slug" value="<?php echo esc_attr( $slug ); ?>" class="regular-text" pattern="[a-z0-9\-_]+" required>
<p class="description">Только латинские буквы, цифры, дефис и подчёркивание. Нельзя использовать <code>wp-admin</code> или <code>wp-login</code>.</p>
</td>
</tr>
</table>
<?php submit_button( 'Сохранить' ); ?>
</form>
</div>
<?php
}
}
add_action( 'plugins_loaded', array( 'S1_Hide_Admin', 'init' ) );
/**
* При активации задаём слаг по умолчанию, если он ещё не задан, —
* чтобы сайт сразу оказался под защитой, а не оставался открытым
* до первого захода в настройки.
*/
function s1_hide_admin_activate() {
if ( false === get_option( S1_Hide_Admin::OPTION_KEY, false ) ) {
add_option( S1_Hide_Admin::OPTION_KEY, S1_Hide_Admin::DEFAULT_SLUG );
}
}
register_activation_hook( __FILE__, 's1_hide_admin_activate' );