Initial commit

This commit is contained in:
2026-07-29 22:52:47 +05:00
commit cb5a058984
13 changed files with 2064 additions and 0 deletions
+543
View File
@@ -0,0 +1,543 @@
<?php
/**
* Админка: меню, рендер страницы настроек, сохранение.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс администрирования плагина.
*
* @since 1.0.0
*/
class AIPMSP_Admin {
/**
* Слаг страницы настроек.
*
* @var string
*/
const PAGE_SLUG = 'aipmsp-settings';
/**
* URL страницы плагина на сайте автора.
*
* @var string
*/
const PLUGIN_PAGE_URL = 'https://быстросайты.рф/modules/ai-proxy-manager/';
/**
* URL поддержки.
*
* @var string
*/
const SUPPORT_URL = 'https://t.me/speedsite';
/**
* Партнёрская ссылка на рекомендованные прокси.
*
* @var string
*/
const PROXY_AFFILIATE_URL = 'https://proxys.io/?refid=2512';
/**
* Конструктор: регистрирует хуки админки.
*
* @since 1.0.0
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_post_aipmsp_save_settings', array( $this, 'handle_save' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
$basename = plugin_basename( AIPMSP_PLUGIN_FILE );
add_filter( 'plugin_action_links_' . $basename, array( $this, 'action_links' ) );
add_filter( 'plugin_row_meta', array( $this, 'row_meta' ), 10, 2 );
}
/**
* Возвращает data-URI монохромной иконки-молнии для меню админки.
*
* @since 1.0.5
*
* @return string
*/
private function menu_icon() {
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iI2E3YWFhZCIgZD0iTTEyIDEgTDQgMTEgSDguNSBMNyAxOSBMMTYgOCBIMTEuNSBMMTMgMSBaIi8+PC9zdmc+';
}
/**
* Регистрирует пункт меню в админке.
*
* @since 1.0.0
*
* @return void
*/
public function register_menu() {
add_menu_page(
__( 'AI Proxy Manager', 'ai-proxy-manager' ),
__( 'AI Proxy Manager', 'ai-proxy-manager' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' ),
$this->menu_icon(),
81
);
}
/**
* Добавляет ссылку «Настройки» рядом с кнопкой деактивации плагина.
*
* @since 1.0.5
*
* @param array $links Существующие ссылки действий.
* @return array
*/
public function action_links( $links ) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ),
esc_html__( 'Настройки', 'ai-proxy-manager' )
);
array_unshift( $links, $settings_link );
return $links;
}
/**
* Добавляет ссылку «Детали» в строку плагина в списке плагинов.
*
* @since 1.0.5
*
* @param array $links Метаданные строки плагина.
* @param string $file Путь к файлу плагина.
* @return array
*/
public function row_meta( $links, $file ) {
if ( plugin_basename( AIPMSP_PLUGIN_FILE ) === $file ) {
$links[] = sprintf(
'<a href="%s" target="_blank" rel="noopener">%s</a>',
esc_url( self::PLUGIN_PAGE_URL ),
esc_html__( 'Детали', 'ai-proxy-manager' )
);
}
return $links;
}
/**
* Подключает CSS/JS только на странице плагина.
*
* @since 1.0.0
*
* @param string $hook Текущий хук админ-страницы.
* @return void
*/
public function enqueue_assets( $hook ) {
if ( 'toplevel_page_' . self::PAGE_SLUG !== $hook ) {
return;
}
wp_enqueue_style(
'aipmsp-admin',
AIPMSP_PLUGIN_URL . 'assets/css/admin.css',
array(),
AIPMSP_VERSION
);
wp_enqueue_script(
'aipmsp-admin',
AIPMSP_PLUGIN_URL . 'assets/js/admin.js',
array( 'jquery' ),
AIPMSP_VERSION,
true
);
wp_localize_script(
'aipmsp-admin',
'aipmspData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aipmsp_ajax_nonce' ),
'checking' => __( 'Проверка…', 'ai-proxy-manager' ),
'checkLabel' => __( 'Проверить', 'ai-proxy-manager' ),
)
);
}
/**
* Обрабатывает сохранение формы настроек.
*
* @since 1.0.0
*
* @return void
*/
public function handle_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Недостаточно прав.', 'ai-proxy-manager' ) );
}
check_admin_referer( 'aipmsp_save_settings', 'aipmsp_nonce' );
$settings = AIPMSP_Proxy::get_settings();
// Тип прокси.
$type = isset( $_POST['aipmsp_proxy_type'] )
? AIPMSP_Proxy::sanitize_type( wp_unslash( $_POST['aipmsp_proxy_type'] ) )
: 'http';
// Прокси (с сохранением состояния enabled из прежних настроек).
$raw_proxies = isset( $_POST['aipmsp_proxies'] )
? sanitize_textarea_field( wp_unslash( $_POST['aipmsp_proxies'] ) )
: '';
$new_proxies = AIPMSP_Proxy::parse_proxies( $raw_proxies, $type, $settings['proxies'] );
// Применяем индивидуальные переключатели enabled из чекбоксов (по индексу ip:port).
$enabled_keys = array();
if ( isset( $_POST['aipmsp_proxy_enabled_keys'] ) && is_array( $_POST['aipmsp_proxy_enabled_keys'] ) ) {
foreach ( wp_unslash( $_POST['aipmsp_proxy_enabled_keys'] ) as $k ) {
$enabled_keys[ sanitize_text_field( $k ) ] = true;
}
}
// Только если форма прислала набор ключей-переключателей — применяем их.
if ( isset( $_POST['aipmsp_toggles_present'] ) ) {
foreach ( $new_proxies as &$proxy ) {
$key = $proxy['ip'] . ':' . $proxy['port'];
$proxy['enabled'] = isset( $enabled_keys[ $key ] ) ? 1 : 0;
}
unset( $proxy );
}
$settings['proxies'] = $new_proxies;
$settings['proxy_type'] = $type;
$settings['proxy_enabled'] = isset( $_POST['aipmsp_proxy_master'] ) ? 1 : 0;
$settings['debug_enabled'] = isset( $_POST['aipmsp_debug'] ) ? 1 : 0;
// Домены.
$raw_domains = isset( $_POST['aipmsp_domains'] )
? sanitize_textarea_field( wp_unslash( $_POST['aipmsp_domains'] ) )
: '';
$settings['domains'] = AIPMSP_Proxy::parse_domains( $raw_domains );
if ( empty( $settings['domains'] ) ) {
$defaults = AIPMSP_Plugin::get_default_settings();
$settings['domains'] = $defaults['domains'];
}
// Список провайдеров, отмеченных переключателем «Использовать для проверки».
$use_keys = array();
if ( isset( $_POST['aipmsp_key_use'] ) && is_array( $_POST['aipmsp_key_use'] ) ) {
foreach ( wp_unslash( $_POST['aipmsp_key_use'] ) as $slug ) {
$use_keys[ sanitize_text_field( $slug ) ] = true;
}
}
// API-ключи: шифруем только если поле непустое, иначе сохраняем прежний.
$providers = array( 'openai', 'anthropic', 'google' );
foreach ( $providers as $provider ) {
$field = 'aipmsp_key_' . $provider;
if ( isset( $_POST[ $field ] ) ) {
$raw_key = trim( sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) );
if ( '' !== $raw_key ) {
$settings['keys'][ $provider ] = AIPMSP_Crypto::encrypt( $raw_key );
}
}
// Очистка ключа, если установлен флаг.
if ( isset( $_POST[ 'aipmsp_clear_' . $provider ] ) ) {
$settings['keys'][ $provider ] = '';
}
// Состояние переключателя «использовать для проверки».
$settings['key_enabled'][ $provider ] = isset( $use_keys[ $provider ] ) ? 1 : 0;
}
AIPMSP_Proxy::save_settings( $settings );
$redirect = add_query_arg(
array(
'page' => self::PAGE_SLUG,
'aipmsp_saved' => '1',
),
admin_url( 'admin.php' )
);
wp_safe_redirect( $redirect );
exit;
}
/**
* Рендерит страницу настроек.
*
* @since 1.0.0
*
* @return void
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = AIPMSP_Proxy::get_settings();
// Восстанавливаем textarea прокси БЕЗ пароля (пароль не выводится в HTML).
// Формат строки: ЛОГИН@IP:ПОРТ либо IP:ПОРТ. При сохранении пустого пароля
// ранее сохранённый пароль остаётся без изменений.
$proxy_lines = array();
foreach ( $settings['proxies'] as $proxy ) {
if ( ! empty( $proxy['user'] ) ) {
$line = $proxy['user'] . '@' . $proxy['ip'] . ':' . $proxy['port'];
} else {
$line = $proxy['ip'] . ':' . $proxy['port'];
}
$proxy_lines[] = $line;
}
$proxies_text = implode( "\n", $proxy_lines );
$domains_text = implode( "\n", $settings['domains'] );
$curl_available = function_exists( 'curl_init' );
$ssl_available = function_exists( 'openssl_encrypt' );
// Сообщение об успешном сохранении.
if ( isset( $_GET['aipmsp_saved'] ) && '1' === $_GET['aipmsp_saved'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Настройки сохранены.', 'ai-proxy-manager' )
. '</p></div>';
}
?>
<div class="wrap aipmsp-wrap">
<h1><?php esc_html_e( 'AI Proxy Manager', 'ai-proxy-manager' ); ?></h1>
<?php if ( ! $curl_available ) : ?>
<div class="notice notice-error">
<p><strong><?php esc_html_e( 'Внимание:', 'ai-proxy-manager' ); ?></strong>
<?php esc_html_e( 'Расширение cURL недоступно на сервере. Прокси с авторизацией работают только через cURL — обратитесь к хостингу.', 'ai-proxy-manager' ); ?></p>
</div>
<?php endif; ?>
<?php if ( ! $ssl_available ) : ?>
<div class="notice notice-warning">
<p><strong><?php esc_html_e( 'Внимание:', 'ai-proxy-manager' ); ?></strong>
<?php esc_html_e( 'Расширение OpenSSL недоступно — шифрование секретов невозможно. Пароли прокси и API-ключи не будут сохранены.', 'ai-proxy-manager' ); ?></p>
</div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aipmsp-form">
<input type="hidden" name="action" value="aipmsp_save_settings" />
<input type="hidden" name="aipmsp_toggles_present" value="1" />
<?php wp_nonce_field( 'aipmsp_save_settings', 'aipmsp_nonce' ); ?>
<h2 class="title"><?php esc_html_e( 'Основные настройки', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Использовать прокси на сайте', 'ai-proxy-manager' ); ?></th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_proxy_master" value="1" <?php checked( ! empty( $settings['proxy_enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Главный переключатель прокси', 'ai-proxy-manager' ); ?></span>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Режим отладки (Debug)', 'ai-proxy-manager' ); ?></th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_debug" value="1" <?php checked( ! empty( $settings['debug_enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Показывать результат проверки подключения к AI-провайдерам', 'ai-proxy-manager' ); ?></span>
</td>
</tr>
<tr>
<th scope="row">
<label for="aipmsp_proxy_type"><?php esc_html_e( 'Тип прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<select name="aipmsp_proxy_type" id="aipmsp_proxy_type">
<?php
$types = array(
'http' => 'HTTP',
'https' => 'HTTPS',
'socks5' => 'SOCKS5',
);
foreach ( $types as $value => $label ) {
printf(
'<option value="%1$s" %2$s>%3$s</option>',
esc_attr( $value ),
selected( $settings['proxy_type'], $value, false ),
esc_html( $label )
);
}
?>
</select>
<p class="description"><?php esc_html_e( 'Применяется ко всем добавленным прокси.', 'ai-proxy-manager' ); ?></p>
</td>
</tr>
</table>
<h2 class="title"><?php esc_html_e( 'Прокси-серверы', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="aipmsp_proxies"><?php esc_html_e( 'Список прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<textarea name="aipmsp_proxies" id="aipmsp_proxies" rows="6" class="large-text code" placeholder="логин:пароль@ip:порт"><?php echo esc_textarea( $proxies_text ); ?></textarea>
<p class="description">
<?php esc_html_e( 'По одному прокси на строку в формате ЛОГИН:ПАРОЛЬ@IP:ПОРТ. Поддерживается формат без авторизации: IP:ПОРТ.', 'ai-proxy-manager' ); ?>
<br />
<?php esc_html_e( 'В целях безопасности сохранённые пароли здесь не отображаются — строки показаны как ЛОГИН@IP:ПОРТ. Если оставить строку без пароля, прежний пароль сохранится. Чтобы изменить пароль — укажите его: ЛОГИН:НОВЫЙПАРОЛЬ@IP:ПОРТ.', 'ai-proxy-manager' ); ?>
</p>
<p class="description aipmsp-recommend">
<?php
printf(
wp_kses(
/* translators: %s: ссылка на сервис прокси */
__( '💡 Нет своих прокси? Рекомендуем проверенный сервис: <a href="%s" target="_blank" rel="noopener nofollow sponsored">proxys.io</a>.', 'ai-proxy-manager' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
),
esc_url( self::PROXY_AFFILIATE_URL )
);
?>
</p>
</td>
</tr>
</table>
<?php if ( ! empty( $settings['proxies'] ) ) : ?>
<h3><?php esc_html_e( 'Добавленные прокси', 'ai-proxy-manager' ); ?></h3>
<table class="widefat striped aipmsp-proxy-table">
<thead>
<tr>
<th><?php esc_html_e( 'Вкл', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'IP', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Порт', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Тип', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Логин', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Пароль', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Проверка', 'ai-proxy-manager' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $settings['proxies'] as $proxy ) : ?>
<?php
$key = $proxy['ip'] . ':' . $proxy['port'];
$pass_masked = empty( $proxy['pass'] ) ? '' : '••••••••';
?>
<tr>
<td>
<label class="aipmsp-switch aipmsp-switch-sm">
<input type="checkbox" name="aipmsp_proxy_enabled_keys[]" value="<?php echo esc_attr( $key ); ?>" <?php checked( ! empty( $proxy['enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
</td>
<td><?php echo esc_html( $proxy['ip'] ); ?></td>
<td><?php echo esc_html( $proxy['port'] ); ?></td>
<td><?php echo esc_html( strtoupper( $proxy['type'] ) ); ?></td>
<td><?php echo esc_html( $proxy['user'] ); ?></td>
<td><code><?php echo esc_html( $pass_masked ); ?></code></td>
<td>
<button type="button"
class="button aipmsp-check-btn"
data-ip="<?php echo esc_attr( $proxy['ip'] ); ?>"
data-port="<?php echo esc_attr( $proxy['port'] ); ?>"
data-type="<?php echo esc_attr( $proxy['type'] ); ?>"
data-user="<?php echo esc_attr( $proxy['user'] ); ?>">
<?php esc_html_e( 'Проверить', 'ai-proxy-manager' ); ?>
</button>
<span class="aipmsp-check-result"></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php esc_html_e( 'Чтобы удалить прокси — уберите его строку из поля «Список прокси» выше и сохраните.', 'ai-proxy-manager' ); ?>
</p>
<?php endif; ?>
<h2 class="title"><?php esc_html_e( 'Маршрутизация доменов', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="aipmsp_domains"><?php esc_html_e( 'Домены через прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<textarea name="aipmsp_domains" id="aipmsp_domains" rows="4" class="large-text code"><?php echo esc_textarea( $domains_text ); ?></textarea>
<p class="description">
<?php esc_html_e( 'По одному домену на строку. Прокси применяется только к запросам на эти домены и их поддомены. Остальные запросы идут напрямую.', 'ai-proxy-manager' ); ?>
</p>
</td>
</tr>
</table>
<h2 class="title"><?php esc_html_e( 'API-ключи провайдеров', 'ai-proxy-manager' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Ключи хранятся в зашифрованном виде (AES-256-GCM) и используются только для проверки подключения в режиме отладки. Оставьте поле пустым, чтобы сохранить уже введённый ключ.', 'ai-proxy-manager' ); ?>
</p>
<table class="form-table" role="presentation">
<?php
$providers = array(
'openai' => 'OpenAI API Key',
'anthropic' => 'Anthropic API Key',
'google' => 'Google (Gemini) API Key',
);
foreach ( $providers as $slug => $label ) :
$has_key = ! empty( $settings['keys'][ $slug ] );
$use_key = ! empty( $settings['key_enabled'][ $slug ] );
?>
<tr>
<th scope="row">
<label for="aipmsp_key_<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $label ); ?></label>
</th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_key_use[]" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $use_key ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Использовать для проверки', 'ai-proxy-manager' ); ?></span>
<br /><br />
<input type="password"
name="aipmsp_key_<?php echo esc_attr( $slug ); ?>"
id="aipmsp_key_<?php echo esc_attr( $slug ); ?>"
class="regular-text"
autocomplete="new-password"
placeholder="<?php echo $has_key ? esc_attr__( '•••••••• (ключ сохранён)', 'ai-proxy-manager' ) : ''; ?>" />
<?php if ( $has_key ) : ?>
<label class="aipmsp-clear-key">
<input type="checkbox" name="aipmsp_clear_<?php echo esc_attr( $slug ); ?>" value="1" />
<?php esc_html_e( 'Удалить ключ', 'ai-proxy-manager' ); ?>
</label>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php submit_button( __( 'Сохранить параметры', 'ai-proxy-manager' ) ); ?>
</form>
<hr />
<p class="aipmsp-footer">
<a href="<?php echo esc_url( self::PLUGIN_PAGE_URL ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Страница плагина', 'ai-proxy-manager' ); ?></a>
&nbsp;·&nbsp;
<a href="<?php echo esc_url( self::SUPPORT_URL ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Получить поддержку', 'ai-proxy-manager' ); ?></a>
</p>
</div>
<?php
}
}