233 lines
10 KiB
PHP
233 lines
10 KiB
PHP
<?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' );
|