wp-domain-mapping/includes/class-admin.php
2025-05-26 22:47:52 +08:00

1455 lines
60 KiB
PHP
Raw 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
/**
* Admin functionality for WP Domain Mapping plugin
*
* @package WP Domain Mapping
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WP Domain Mapping Admin Class
*/
class WP_Domain_Mapping_Admin {
/**
* Class instance
*
* @var WP_Domain_Mapping_Admin
*/
private static $instance = null;
/**
* Database table names
*
* @var array
*/
private $tables = array();
/**
* Get class instance
*
* @return WP_Domain_Mapping_Admin
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->tables = dm_get_table_names();
$this->init();
}
/**
* Initialize admin functionality
*/
private function init() {
// Setup admin-related hooks
$this->setup_admin_hooks();
// Load admin pages
require_once WP_DOMAIN_MAPPING_DIR_PATH . 'admin/pages.php';
}
/**
* Setup admin hooks
*/
private function setup_admin_hooks() {
// Add menu pages
add_action( 'admin_menu', array( $this, 'add_admin_pages' ) );
add_action( 'network_admin_menu', array( $this, 'add_network_admin_pages' ) );
// Add AJAX handlers
add_action( 'wp_ajax_dm_handle_actions', array( $this, 'ajax_handle_actions' ) );
// Add domain link to sites list
add_filter( 'manage_sites_action_links', array( $this, 'add_domain_link_to_sites' ), 10, 2 );
// Add custom columns
add_filter( 'wpmu_blogs_columns', array( $this, 'add_domain_mapping_columns' ) );
add_action( 'manage_blogs_custom_column', array( $this, 'display_domain_mapping_column' ), 1, 3 );
add_action( 'manage_sites_custom_column', array( $this, 'display_domain_mapping_column' ), 1, 3 );
// 添加仪表盘小工具
add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
// 添加仪表盘小工具的AJAX处理
add_action( 'wp_ajax_dm_refresh_widget_status', array( $this, 'ajax_refresh_widget_status' ) );
add_action( 'wp_ajax_dm_quick_health_check', array( $this, 'ajax_quick_health_check' ) );
// Handle user domain mapping actions
if ( isset( $_GET['page'] ) && 'domainmapping' === $_GET['page'] ) {
add_action( 'admin_init', array( $this, 'handle_user_domain_actions' ) );
}
// Enqueue admin scripts and styles
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
}
/**
* Add admin menu pages
*/
public function add_admin_pages() {
global $current_site, $wpdb, $wp_db_version;
if ( ! isset( $current_site ) && $wp_db_version >= 15260 ) {
add_action( 'admin_notices', array( $this, 'domain_mapping_warning' ) );
return false;
}
if ( isset($current_site->path) && $current_site->path != "/" ) {
wp_die( esc_html__( "The domain mapping plugin only works if WordPress is installed in the root directory of your webserver. It is currently installed in '%s'.", 'wp-domain-mapping' ) );
}
if ( get_site_option( 'dm_user_settings' ) && $current_site->blog_id != $wpdb->blogid && ! $this->sunrise_warning( false ) ) {
add_management_page(
__( 'Domain Mapping', 'wp-domain-mapping' ),
__( 'Domain Mapping', 'wp-domain-mapping' ),
'manage_options',
'domainmapping',
array( $this, 'render_manage_page' )
);
}
}
/**
* Add network admin menu pages
*/
public function add_network_admin_pages() {
add_submenu_page(
'settings.php',
__( 'Domain Mapping', 'wp-domain-mapping' ),
__( 'Domain Mapping', 'wp-domain-mapping' ),
'manage_network',
'domain-mapping',
array( $this, 'render_admin_page' )
);
add_submenu_page(
'sites.php',
__( 'Domains', 'wp-domain-mapping' ),
__( 'Domains', 'wp-domain-mapping' ),
'manage_network',
'domains',
array( $this, 'render_domains_admin' )
);
}
/**
* 添加仪表盘小工具
*/
public function add_dashboard_widget() {
// 只在子站显示,不在主站显示
if ( is_main_site() ) {
return;
}
// 检查用户权限和设置
if ( ! current_user_can( 'manage_options' ) || ! get_site_option( 'dm_user_settings' ) ) {
return;
}
wp_add_dashboard_widget(
'dm_domain_status_widget',
__( 'Domain Mapping Status', 'wp-domain-mapping' ),
array( $this, 'render_dashboard_widget' ),
array( $this, 'render_dashboard_widget_config' )
);
}
/**
* 渲染仪表盘小工具
*/
public function render_dashboard_widget() {
global $wpdb;
$blog_id = get_current_blog_id();
$domains = dm_get_domains_by_blog_id( $blog_id );
$show_health_status = get_user_meta( get_current_user_id(), 'dm_widget_show_health', true );
$show_health_status = ( $show_health_status === '' ) ? '1' : $show_health_status;
?>
<style>
.dm-dashboard-widget{padding:0}
.dm-dashboard-widget>div{margin-bottom:15px;padding-bottom:15px;border-bottom:1px solid #eee}
.dm-dashboard-widget>div:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}
.dm-dashboard-widget h4{margin:0 0 8px 0;font-size:13px;font-weight:600;color:#1d2327;display:flex;align-items:center;justify-content:space-between}
.dm-domain-primary{font-size:14px;display:flex;align-items:center;gap:8px}
.dm-domain-primary a{text-decoration:none;color:#2271b1}
.dm-domain-primary a:hover{color:#135e96;text-decoration:underline}
.dm-no-primary{color:#666;font-style:italic;display:flex;align-items:center;gap:8px}
.dm-stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;text-align:center}
.dm-stat{background:#f0f0f1;padding:10px;border-radius:4px}
.dm-stat-value{display:block;font-size:24px;font-weight:600;color:#2271b1;line-height:1}
.dm-stat-label{display:block;font-size:11px;color:#666;margin-top:5px}
.dm-domains-list ul{margin:0;padding:0;list-style:none}
.dm-domains-list li{padding:5px 0;display:flex;align-items:center;gap:5px}
.dm-domains-list a{text-decoration:none;color:#50575e}
.dm-domains-list a:hover{color:#2271b1}
.dm-badge-primary{background:#2271b1;color:#fff;font-size:11px;padding:2px 6px;border-radius:3px;margin-left:auto}
.dm-health-status{position:relative}
.dm-refresh-health{padding:2px 4px!important;min-height:24px!important;line-height:1!important}
.dm-refresh-health .dashicons{font-size:16px;width:16px;height:16px}
.dm-health-good{color:#46b450;display:flex;align-items:center;gap:5px;margin:0}
.dm-health-issues{background:#fcf0f1;border:1px solid #f0c4c6;border-radius:4px;padding:10px}
.dm-issue-item{margin-bottom:5px}
.dm-issue-item:last-child{margin-bottom:0}
.dm-issue-list{color:#d63638;font-size:12px}
.dm-last-check{margin:8px 0 0;font-size:12px;color:#666;font-style:italic}
.dm-dns-info code{background:#f0f0f1;padding:3px 6px;border-radius:3px;font-family:Consolas,Monaco,monospace;font-size:12px}
.dm-activity-list{margin:0;padding:0;list-style:none}
.dm-activity-list li{padding:6px 0;border-bottom:1px solid #f0f0f1;font-size:12px}
.dm-activity-list li:last-child{border-bottom:none;padding-bottom:0}
.dm-activity-action{display:inline-block;padding:2px 6px;border-radius:3px;font-size:11px;font-weight:600;margin-right:5px}
.dm-action-add{background-color:#dff0d8;color:#3c763d}
.dm-action-edit{background-color:#d9edf7;color:#31708f}
.dm-action-delete{background-color:#f2dede;color:#a94442}
.dm-activity-domain{font-weight:500;color:#1d2327}
.dm-activity-meta{display:block;color:#666;margin-top:2px}
.dm-quick-actions{display:flex;gap:10px}
.dm-quick-actions .button{flex:1;display:flex;align-items:center;justify-content:center;gap:5px}
.dm-quick-actions .dashicons{font-size:16px;width:16px;height:16px;line-height:1}
.dm-loading{opacity:.6;pointer-events:none}
.dm-loading::after{content:'';position:absolute;top:50%;left:50%;width:20px;height:20px;margin:-10px 0 0 -10px;border:2px solid #f0f0f1;border-top-color:#2271b1;border-radius:50%;animation:dm-spin 1s linear infinite}
@keyframes dm-spin{to{transform:rotate(360deg)}
}
@media screen and (max-width:782px){.dm-stats-grid{grid-template-columns:repeat(3,1fr);gap:5px}
.dm-stat{padding:8px 5px}
.dm-stat-value{font-size:20px}
.dm-quick-actions{flex-direction:column}
.dm-quick-actions .button{width:100%}
}
#dm_domain_status_widget .postbox-header{cursor:move}
#dm_domain_status_widget.closed .inside{display:none}
</style>
<div class="dm-dashboard-widget">
<!-- 1. 主域名状态 -->
<div class="dm-primary-domain">
<h4><?php _e( 'Primary Domain', 'wp-domain-mapping' ); ?></h4>
<?php
$primary = wp_list_filter( $domains, array( 'active' => 1 ) );
if ( ! empty( $primary ) ) {
$primary_domain = reset( $primary );
$ssl_status = $this->check_domain_ssl( $primary_domain->domain );
?>
<p class="dm-domain-primary">
<span class="dashicons dashicons-admin-site-alt3"></span>
<strong>
<a href="<?php echo esc_url( ( $ssl_status ? 'https://' : 'http://' ) . $primary_domain->domain ); ?>" target="_blank">
<?php echo esc_html( $primary_domain->domain ); ?>
</a>
</strong>
<?php if ( $ssl_status ): ?>
<span class="dashicons dashicons-lock" style="color: #46b450;" title="<?php esc_attr_e( 'SSL Active', 'wp-domain-mapping' ); ?>"></span>
<?php else: ?>
<span class="dashicons dashicons-unlock" style="color: #d63638;" title="<?php esc_attr_e( 'No SSL', 'wp-domain-mapping' ); ?>"></span>
<?php endif; ?>
</p>
<?php
} else {
$orig_url = WP_Domain_Mapping_Core::get_instance()->get_original_url( 'siteurl' );
$orig_domain = parse_url( $orig_url, PHP_URL_HOST );
?>
<p class="dm-no-primary">
<span class="dashicons dashicons-admin-site"></span>
<?php _e( 'Using original domain:', 'wp-domain-mapping' ); ?>
<strong><?php echo esc_html( $orig_domain ); ?></strong>
</p>
<?php
}
?>
</div>
<!-- 2. 映射域名统计 -->
<div class="dm-domains-stats">
<h4><?php _e( 'Domain Statistics', 'wp-domain-mapping' ); ?></h4>
<div class="dm-stats-grid">
<div class="dm-stat">
<span class="dm-stat-value"><?php echo count( $domains ); ?></span>
<span class="dm-stat-label"><?php _e( 'Total Domains', 'wp-domain-mapping' ); ?></span>
</div>
<div class="dm-stat">
<span class="dm-stat-value"><?php echo count( wp_list_filter( $domains, array( 'active' => 1 ) ) ); ?></span>
<span class="dm-stat-label"><?php _e( 'Primary', 'wp-domain-mapping' ); ?></span>
</div>
<div class="dm-stat">
<span class="dm-stat-value"><?php echo count( wp_list_filter( $domains, array( 'active' => 0 ) ) ); ?></span>
<span class="dm-stat-label"><?php _e( 'Secondary', 'wp-domain-mapping' ); ?></span>
</div>
</div>
</div>
<!-- 3. 映射域名列表 -->
<?php if ( ! empty( $domains ) && count( $domains ) > 1 ): ?>
<div class="dm-domains-list">
<h4><?php _e( 'All Mapped Domains', 'wp-domain-mapping' ); ?></h4>
<ul>
<?php foreach ( $domains as $domain ): ?>
<li>
<span class="dashicons dashicons-admin-links"></span>
<a href="<?php echo esc_url( 'http://' . $domain->domain ); ?>" target="_blank">
<?php echo esc_html( $domain->domain ); ?>
</a>
<?php if ( $domain->active ): ?>
<span class="dm-badge-primary"><?php _e( 'Primary', 'wp-domain-mapping' ); ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- 4. 健康状态(可选) -->
<?php if ( $show_health_status == '1' ): ?>
<div class="dm-health-status" id="dm-widget-health-status">
<h4>
<?php _e( 'Health Status', 'wp-domain-mapping' ); ?>
<button type="button" class="button-link dm-refresh-health" data-blog-id="<?php echo $blog_id; ?>">
<span class="dashicons dashicons-update"></span>
<span class="screen-reader-text"><?php _e( 'Refresh', 'wp-domain-mapping' ); ?></span>
</button>
</h4>
<div class="dm-health-content">
<?php $this->render_widget_health_status( $domains ); ?>
</div>
</div>
<?php endif; ?>
<!-- 5. DNS 配置提示 -->
<?php
$ipaddress = get_site_option( 'dm_ipaddress' );
$cname = get_site_option( 'dm_cname' );
if ( ( $ipaddress || $cname ) && empty( $domains ) ):
?>
<div class="dm-dns-info">
<h4><?php _e( 'DNS Configuration', 'wp-domain-mapping' ); ?></h4>
<p class="description">
<?php if ( $cname ): ?>
<?php printf( __( 'Point your domain CNAME to: %s', 'wp-domain-mapping' ), '<code>' . esc_html( $cname ) . '</code>' ); ?>
<?php elseif ( $ipaddress ): ?>
<?php printf( __( 'Point your domain A record to: %s', 'wp-domain-mapping' ), '<code>' . esc_html( $ipaddress ) . '</code>' ); ?>
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<!-- 6. 最近活动 -->
<?php
$recent_logs = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$this->tables['logs']} WHERE blog_id = %d ORDER BY timestamp DESC LIMIT 3",
$blog_id
));
if ( ! empty( $recent_logs ) ):
?>
<div class="dm-recent-activity">
<h4><?php _e( 'Recent Activity', 'wp-domain-mapping' ); ?></h4>
<ul class="dm-activity-list">
<?php foreach ( $recent_logs as $log ):
$user = get_userdata( $log->user_id );
$username = $user ? $user->display_name : __( 'Unknown', 'wp-domain-mapping' );
?>
<li>
<span class="dm-activity-action dm-action-<?php echo esc_attr( $log->action ); ?>">
<?php echo esc_html( dm_format_action_name( $log->action ) ); ?>
</span>
<span class="dm-activity-domain"><?php echo esc_html( $log->domain ); ?></span>
<span class="dm-activity-meta">
<?php
printf(
__( 'by %s %s ago', 'wp-domain-mapping' ),
esc_html( $username ),
human_time_diff( strtotime( $log->timestamp ), current_time( 'timestamp' ) )
);
?>
</span>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- 7. 快速操作 -->
<div class="dm-quick-actions">
<a href="<?php echo admin_url( 'tools.php?page=domainmapping' ); ?>" class="button button-primary">
<?php _e( 'Manage Domains', 'wp-domain-mapping' ); ?>
</a>
<?php if ( ! empty( $domains ) ): ?>
<button type="button" class="button dm-check-all-health" data-blog-id="<?php echo $blog_id; ?>">
<?php _e( 'Check Health', 'wp-domain-mapping' ); ?>
</button>
<?php endif; ?>
</div>
</div>
<script type="text/javascript">
(function($) {
'use strict';
// 等待小工具完全渲染
$(document).ready(function() {
console.log('DM Widget: Document ready');
// 初始化变量
var ajaxUrl = '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>';
var nonce = '<?php echo esc_js( wp_create_nonce( 'domain_mapping' ) ); ?>';
// 检查元素是否存在
console.log('Refresh button count:', $('.dm-refresh-health').length);
console.log('Check all button count:', $('.dm-check-all-health').length);
// 刷新健康状态
$('#dm_domain_status_widget').on('click', '.dm-refresh-health', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Refresh health clicked');
var $button = $(this);
var $healthContent = $('.dm-health-content');
var blogId = $button.data('blog-id');
// 禁用按钮
$button.prop('disabled', true).blur();
$button.find('.dashicons').addClass('dashicons-update-spin');
$healthContent.addClass('dm-loading');
$.ajax({
url: ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
action: 'dm_quick_health_check',
blog_id: blogId,
nonce: nonce
},
success: function(response) {
console.log('Health check response:', response);
if (response.success) {
$healthContent.html(response.data.html);
// 显示成功消息
var $widget = $('#dm_domain_status_widget');
var $notice = $('<div class="notice notice-success is-dismissible"><p>' +
response.data.message + '</p></div>');
$widget.find('.inside').prepend($notice);
setTimeout(function() {
$notice.fadeOut(function() {
$(this).remove();
});
}, 3000);
} else {
alert(response.data || 'An error occurred');
}
},
error: function(xhr, status, error) {
console.error('AJAX error:', status, error);
alert('An error occurred during the health check');
},
complete: function() {
$button.prop('disabled', false);
$button.find('.dashicons').removeClass('dashicons-update-spin');
$healthContent.removeClass('dm-loading');
}
});
});
// 检查所有域名健康状态
$('#dm_domain_status_widget').on('click', '.dm-check-all-health', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Check all health clicked');
var $button = $(this);
var blogId = $button.data('blog-id');
var originalHtml = $button.html();
// 禁用按钮
$button.prop('disabled', true).blur();
$button.html('<span class="dashicons dashicons-update dashicons-update-spin"></span> Processing...');
$.ajax({
url: ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
action: 'dm_quick_health_check',
blog_id: blogId,
nonce: nonce
},
success: function(response) {
console.log('Check all response:', response);
if (response.success) {
// 更新健康状态区域
var $healthSection = $('#dm-widget-health-status');
if ($healthSection.length) {
$healthSection.find('.dm-health-content').html(response.data.html);
if (!$healthSection.is(':visible')) {
$healthSection.slideDown();
}
}
// 显示成功消息
var $widget = $('#dm_domain_status_widget');
var $notice = $('<div class="notice notice-success is-dismissible"><p>' +
response.data.message + '</p></div>');
$widget.find('.inside').prepend($notice);
setTimeout(function() {
$notice.fadeOut(function() {
$(this).remove();
});
}, 3000);
} else {
alert(response.data || wpDomainMapping.messages.error);
}
},
error: function(xhr, status, error) {
console.error('AJAX error:', status, error);
alert(wpDomainMapping.messages.error);
},
complete: function() {
$button.prop('disabled', false);
$button.html(originalHtml);
}
});
});
});
})(jQuery);
</script>
<?php
}
/**
* 渲染小工具配置
*/
public function render_dashboard_widget_config() {
// 处理表单提交 - WordPress 会在保存前调用这个方法
if ( 'POST' == $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget_id'] ) && $_POST['widget_id'] == 'dm_domain_status_widget' ) {
// 重要checkbox 未勾选时不会在 $_POST 中出现
$show_health = isset( $_POST['dm_widget_show_health'] ) ? '1' : '0';
update_user_meta( get_current_user_id(), 'dm_widget_show_health', $show_health );
}
// 获取当前设置
$show_health = get_user_meta( get_current_user_id(), 'dm_widget_show_health', true );
$show_health = ( $show_health === '' ) ? '1' : $show_health; // 默认显示
?>
<p>
<label for="dm_widget_show_health">
<input type="checkbox" id="dm_widget_show_health" name="dm_widget_show_health" value="1" <?php checked( $show_health, '1' ); ?> />
<?php _e( 'Show health status', 'wp-domain-mapping' ); ?>
</label>
</p>
<script type="text/javascript">
jQuery(document).ready(function($) {
// 提供实时预览
$('#dm_widget_show_health').on('change', function() {
var $healthSection = $('#dm-widget-health-status');
if ($(this).is(':checked')) {
$healthSection.slideDown();
} else {
$healthSection.slideUp();
}
});
});
</script>
<?php
}
/**
* 渲染健康状态内容
*/
private function render_widget_health_status( $domains ) {
if ( empty( $domains ) ) {
echo '<p class="dm-no-domains">' . __( 'No domains to check.', 'wp-domain-mapping' ) . '</p>';
return;
}
$all_healthy = true;
$issues = array();
foreach ( $domains as $domain ) {
$health = dm_get_health_result( $domain->domain );
if ( $health ) {
$domain_issues = array();
if ( isset( $health['accessible'] ) && ! $health['accessible'] ) {
$domain_issues[] = __( 'Not accessible', 'wp-domain-mapping' );
$all_healthy = false;
}
if ( isset( $health['ssl_valid'] ) && ! $health['ssl_valid'] ) {
$domain_issues[] = __( 'SSL issue', 'wp-domain-mapping' );
$all_healthy = false;
}
if ( isset( $health['dns_status'] ) && $health['dns_status'] !== 'success' ) {
$domain_issues[] = __( 'DNS issue', 'wp-domain-mapping' );
$all_healthy = false;
}
if ( ! empty( $domain_issues ) ) {
$issues[$domain->domain] = $domain_issues;
}
}
}
if ( $all_healthy ) {
echo '<p class="dm-health-good"><span class="dashicons dashicons-yes-alt"></span> ' .
__( 'All domains are healthy!', 'wp-domain-mapping' ) . '</p>';
} else {
echo '<div class="dm-health-issues">';
foreach ( $issues as $domain => $domain_issues ) {
echo '<div class="dm-issue-item">';
echo '<strong>' . esc_html( $domain ) . ':</strong> ';
echo '<span class="dm-issue-list">' . implode( ', ', $domain_issues ) . '</span>';
echo '</div>';
}
echo '</div>';
}
// 显示最后检查时间
$last_check = get_site_option( 'dm_widget_last_health_check_' . get_current_blog_id() );
if ( $last_check ) {
echo '<p class="dm-last-check">' .
sprintf(
__( 'Last checked: %s ago', 'wp-domain-mapping' ),
human_time_diff( $last_check, current_time( 'timestamp' ) )
) . '</p>';
}
}
/**
* AJAX刷新小工具状态
*/
public function ajax_refresh_widget_status() {
check_ajax_referer( 'domain_mapping', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'wp-domain-mapping' ) );
}
ob_start();
$this->render_dashboard_widget();
$html = ob_get_clean();
wp_send_json_success( $html );
}
/**
* AJAX快速健康检查
*/
public function ajax_quick_health_check() {
check_ajax_referer( 'domain_mapping', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'wp-domain-mapping' ) );
}
$blog_id = isset( $_POST['blog_id'] ) ? intval( $_POST['blog_id'] ) : get_current_blog_id();
$domains = dm_get_domains_by_blog_id( $blog_id );
if ( empty( $domains ) ) {
wp_send_json_error( __( 'No domains found.', 'wp-domain-mapping' ) );
}
// 执行健康检查
$tools = WP_Domain_Mapping_Tools::get_instance();
foreach ( $domains as $domain ) {
$result = $tools->check_domain_health( $domain->domain );
dm_save_health_result( $domain->domain, $result );
}
// 更新最后检查时间
update_site_option( 'dm_widget_last_health_check_' . $blog_id, current_time( 'timestamp' ) );
// 返回更新后的健康状态HTML
ob_start();
$this->render_widget_health_status( $domains );
$html = ob_get_clean();
wp_send_json_success( array(
'html' => $html,
'message' => __( 'Health check completed.', 'wp-domain-mapping' )
));
}
/**
* 检查域名SSL状态
*/
private function check_domain_ssl( $domain ) {
$health_result = dm_get_health_result( $domain );
return $health_result && isset( $health_result['ssl_valid'] ) && $health_result['ssl_valid'];
}
/**
* 获取域名健康问题
*/
private function get_domain_health_issues( $blog_id ) {
$domains = dm_get_domains_by_blog_id( $blog_id );
$issues = array();
foreach ( $domains as $domain ) {
$health_result = dm_get_health_result( $domain->domain );
if ( $health_result ) {
if ( ! $health_result['accessible'] || ! $health_result['ssl_valid'] || $health_result['dns_status'] !== 'success' ) {
$issues[] = $domain->domain;
}
}
}
return $issues;
}
/**
* Enqueue admin scripts and styles
*/
public function enqueue_admin_assets( $hook ) {
// Only load on our admin pages
if ( ! in_array( $hook, array( 'sites_page_domains', 'settings_page_domain-mapping', 'tools_page_domainmapping' ) ) ) {
return;
}
wp_enqueue_style(
'wp-domain-mapping-admin',
WP_DOMAIN_MAPPING_DIR_URL . 'assets/css/admin.css',
array(),
WP_DOMAIN_MAPPING_VERSION
);
wp_enqueue_script(
'wp-domain-mapping-admin',
WP_DOMAIN_MAPPING_DIR_URL . 'assets/js/admin.js',
array( 'jquery' ),
WP_DOMAIN_MAPPING_VERSION,
true
);
wp_localize_script( 'wp-domain-mapping-admin', 'wpDomainMapping', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'domain_mapping' ),
'messages' => array(
'domainRequired' => __( 'Domain is required.', 'wp-domain-mapping' ),
'siteRequired' => __( 'Site ID is required.', 'wp-domain-mapping' ),
'saving' => __( 'Saving...', 'wp-domain-mapping' ),
'processing' => __( 'Processing...', 'wp-domain-mapping' ),
'error' => __( 'An error occurred.', 'wp-domain-mapping' ),
'noSelection' => __( 'Please select at least one domain.', 'wp-domain-mapping' ),
'invalidDomain' => __( 'Invalid domain format.', 'wp-domain-mapping' ),
'checking' => __( 'Checking...', 'wp-domain-mapping' ), // 新增
)
));
}
/**
* Check if sunrise.php is properly configured
*
* @param bool $die Whether to die with an error message
* @return bool True if there's a problem, false if everything is okay
*/
public function sunrise_warning( $die = true ) {
if ( ! file_exists( WP_CONTENT_DIR . '/sunrise.php' ) ) {
if ( ! $die ) return true;
if ( dm_is_site_admin() ) {
wp_die(
sprintf(
/* translators: %1$s: Content directory, %2$s: WordPress install path */
esc_html__( 'Please copy sunrise.php to %1$s/sunrise.php and ensure the SUNRISE definition is in %2$swp-config.php', 'wp-domain-mapping' ),
esc_html( WP_CONTENT_DIR ),
esc_html( ABSPATH )
)
);
} else {
wp_die( esc_html__( "This plugin has not been configured correctly yet.", 'wp-domain-mapping' ) );
}
} elseif ( ! defined( 'SUNRISE' ) ) {
if ( ! $die ) return true;
if ( dm_is_site_admin() ) {
wp_die(
sprintf(
/* translators: %s: WordPress install path */
esc_html__( 'Please uncomment the line <em>define( \'SUNRISE\', \'on\' );</em> or add it to your %swp-config.php', 'wp-domain-mapping' ),
esc_html( ABSPATH )
)
);
} else {
wp_die( esc_html__( "This plugin has not been configured correctly yet.", 'wp-domain-mapping' ) );
}
} elseif ( ! defined( 'SUNRISE_LOADED' ) ) {
if ( ! $die ) return true;
if ( dm_is_site_admin() ) {
wp_die(
sprintf(
/* translators: %s: WordPress install path */
esc_html__( 'Please edit your %swp-config.php and move the line <em>define( \'SUNRISE\', \'on\' );</em> above the last require_once() in that file or make sure you updated sunrise.php.', 'wp-domain-mapping' ),
esc_html( ABSPATH )
)
);
} else {
wp_die( esc_html__( "This plugin has not been configured correctly yet.", 'wp-domain-mapping' ) );
}
}
return false;
}
/**
* Display warning message for network configuration
*/
public function domain_mapping_warning() {
echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'Domain Mapping Disabled.', 'wp-domain-mapping' ) . '</strong> ' .
sprintf(
/* translators: %s: URL to WordPress network creation documentation */
wp_kses(
__( 'You must <a href="%1$s">create a network</a> for it to work.', 'wp-domain-mapping' ),
array( 'a' => array( 'href' => array() ) )
),
'http://codex.wordpress.org/Create_A_Network'
) . '</p></div>';
}
/**
* AJAX handler for domain actions - IMPROVED SECURITY
*/
public function ajax_handle_actions() {
// Verify nonce first
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'domain_mapping' ) ) {
wp_send_json_error( __( 'Security check failed.', 'wp-domain-mapping' ) );
}
// Check permissions
if ( ! current_user_can( 'manage_network' ) ) {
wp_send_json_error( __( 'Permission denied.', 'wp-domain-mapping' ) );
}
global $wpdb;
// Sanitize and validate inputs
$action = isset( $_POST['action_type'] ) ? sanitize_key( $_POST['action_type'] ) : '';
$domain = isset( $_POST['domain'] ) ? dm_clean_domain( sanitize_text_field( $_POST['domain'] ) ) : '';
$blog_id = isset( $_POST['blog_id'] ) ? absint( $_POST['blog_id'] ) : 0;
$active = isset( $_POST['active'] ) ? absint( $_POST['active'] ) : 0;
$orig_domain = isset( $_POST['orig_domain'] ) ? dm_clean_domain( sanitize_text_field( $_POST['orig_domain'] ) ) : '';
$current_user_id = get_current_user_id();
// Validate action type
$allowed_actions = array( 'save', 'delete' );
if ( ! in_array( $action, $allowed_actions ) ) {
wp_send_json_error( __( 'Invalid action.', 'wp-domain-mapping' ) );
}
switch ( $action ) {
case 'save':
// Enhanced validation for save action
if ( $blog_id <= 0 || $blog_id === 1 ) {
wp_send_json_error( __( 'Invalid site ID.', 'wp-domain-mapping' ) );
}
// Validate domain format
if ( empty( $domain ) || ! dm_validate_domain( $domain ) ) {
wp_send_json_error( __( 'Invalid domain format.', 'wp-domain-mapping' ) );
}
// Check if blog exists
if ( ! get_blog_details( $blog_id ) ) {
wp_send_json_error( __( 'Site does not exist.', 'wp-domain-mapping' ) );
}
// For editing, check if domain changed
$domain_changed = ! empty( $orig_domain ) && $orig_domain !== $domain;
if ( $domain_changed || empty( $orig_domain ) ) {
// Check if domain exists for another blog
$exists = dm_domain_exists_for_another_blog( $domain, $blog_id );
if ( $exists ) {
wp_send_json_error( sprintf(
__( 'Domain %s is already mapped to site ID %d.', 'wp-domain-mapping' ),
esc_html( $domain ),
intval( $exists->blog_id )
));
}
}
$wpdb->query( 'START TRANSACTION' );
try {
if ( empty( $orig_domain ) ) {
// Insert new domain
$success = $wpdb->insert(
$this->tables['domains'],
array(
'blog_id' => $blog_id,
'domain' => $domain,
'active' => $active
),
array( '%d', '%s', '%d' )
);
if ( $success ) {
// If setting as primary, reset other domains
if ( $active ) {
$wpdb->update(
$this->tables['domains'],
array( 'active' => 0 ),
array(
'blog_id' => $blog_id,
'domain' => array( 'NOT LIKE', $domain )
),
array( '%d' ),
array( '%d' )
);
}
// Log the action
dm_log_action( 'add', $domain, $blog_id, $current_user_id );
$wpdb->query( 'COMMIT' );
wp_send_json_success( __( 'Domain added successfully.', 'wp-domain-mapping' ) );
} else {
$wpdb->query( 'ROLLBACK' );
wp_send_json_error( __( 'Failed to add domain.', 'wp-domain-mapping' ) );
}
} else {
// Validate original domain exists
$orig_exists = dm_get_domain_by_name( $orig_domain );
if ( ! $orig_exists || $orig_exists->blog_id != $blog_id ) {
$wpdb->query( 'ROLLBACK' );
wp_send_json_error( __( 'Original domain not found or access denied.', 'wp-domain-mapping' ) );
}
// Update existing domain
$update_data = array(
'blog_id' => $blog_id,
'active' => $active
);
$update_format = array( '%d', '%d' );
// If domain changed, update it
if ( $domain_changed ) {
$update_data['domain'] = $domain;
$update_format[] = '%s';
}
// If setting as primary, reset other domains first
if ( $active ) {
$wpdb->update(
$this->tables['domains'],
array( 'active' => 0 ),
array( 'blog_id' => $blog_id ),
array( '%d' ),
array( '%d' )
);
}
$success = $wpdb->update(
$this->tables['domains'],
$update_data,
array( 'domain' => $orig_domain ),
$update_format,
array( '%s' )
);
if ( $success !== false ) {
// Log the action
dm_log_action( 'edit', $domain, $blog_id, $current_user_id );
$wpdb->query( 'COMMIT' );
wp_send_json_success( __( 'Domain updated successfully.', 'wp-domain-mapping' ) );
} else {
$wpdb->query( 'ROLLBACK' );
wp_send_json_error( __( 'No changes were made or update failed.', 'wp-domain-mapping' ) );
}
}
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
error_log( 'Domain mapping error: ' . $e->getMessage() );
wp_send_json_error( __( 'An error occurred while saving domain.', 'wp-domain-mapping' ) );
}
break;
case 'delete':
$domains = isset( $_POST['domains'] ) ? array_map( 'sanitize_text_field', (array) $_POST['domains'] ) : array( $domain );
// Validate domains array
$domains = array_filter( $domains, function( $d ) {
return ! empty( $d ) && dm_validate_domain( $d );
});
if ( empty( $domains ) ) {
wp_send_json_error( __( 'No valid domains provided for deletion.', 'wp-domain-mapping' ) );
}
$wpdb->query( 'START TRANSACTION' );
$deleted = 0;
try {
foreach ( $domains as $del_domain ) {
// Get domain info before deletion for logging and validation
$domain_info = dm_get_domain_by_name( $del_domain );
if ( ! $domain_info ) {
continue; // Skip non-existent domains
}
// Check if user has permission to delete this domain
if ( ! current_user_can( 'manage_network' ) ) {
continue; // Skip if no permission
}
// Delete the domain
$result = $wpdb->delete(
$this->tables['domains'],
array( 'domain' => $del_domain ),
array( '%s' )
);
if ( $result ) {
$deleted++;
// Log the action
dm_log_action( 'delete', $del_domain, $domain_info->blog_id, $current_user_id );
}
}
if ( $deleted > 0 ) {
$wpdb->query( 'COMMIT' );
$message = sprintf(
_n(
'Domain deleted successfully.',
'%d domains deleted successfully.',
$deleted,
'wp-domain-mapping'
),
$deleted
);
wp_send_json_success( $message );
} else {
$wpdb->query( 'ROLLBACK' );
wp_send_json_error( __( 'No domains were deleted.', 'wp-domain-mapping' ) );
}
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
error_log( 'Domain mapping deletion error: ' . $e->getMessage() );
wp_send_json_error( __( 'An error occurred while deleting domains.', 'wp-domain-mapping' ) );
}
break;
default:
wp_send_json_error( __( 'Invalid action.', 'wp-domain-mapping' ) );
}
}
/**
* Handle user domain mapping actions
* UPDATED: Support www prefix and better domain validation
*/
public function handle_user_domain_actions() {
global $wpdb, $parent_file;
$url = add_query_arg( array( 'page' => 'domainmapping' ), admin_url( $parent_file ) );
// 处理 POST 请求(添加域名)
if ( ! empty( $_POST['action'] ) ) {
$domain = isset( $_POST['domain'] ) ? dm_clean_domain( sanitize_text_field( $_POST['domain'] ) ) : '';
if ( empty( $domain ) ) {
wp_die( esc_html__( "You must enter a domain", 'wp-domain-mapping' ) );
}
check_admin_referer( 'domain_mapping' );
do_action( 'dm_handle_actions_init', $domain );
switch ( $_POST['action'] ) {
case "add":
do_action( 'dm_handle_actions_add', $domain );
// Validate domain format
if ( ! dm_validate_domain( $domain ) ) {
wp_die( esc_html__( "Invalid domain format", 'wp-domain-mapping' ) );
}
// Check if domain already exists for any blog
$domain_exists = dm_get_domain_by_name( $domain );
// Also check if it exists in the blogs table
$blog_exists = $wpdb->get_row( $wpdb->prepare(
"SELECT blog_id FROM {$wpdb->blogs} WHERE domain = %s",
$domain
));
if ( $domain_exists || $blog_exists ) {
// Check if it's for the current blog
if ( $domain_exists && $domain_exists->blog_id == $wpdb->blogid ) {
wp_redirect( add_query_arg( array( 'updated' => 'exists' ), $url ) );
exit;
} else {
wp_die( sprintf(
esc_html__( "Domain '%s' is already mapped to another site.", 'wp-domain-mapping' ),
esc_html( $domain )
));
}
}
// If primary, reset other domains to not primary
if ( isset( $_POST['primary'] ) && $_POST['primary'] ) {
$wpdb->update(
$this->tables['domains'],
array( 'active' => 0 ),
array( 'blog_id' => $wpdb->blogid ),
array( '%d' ),
array( '%d' )
);
}
// Insert new domain
$wpdb->insert(
$this->tables['domains'],
array(
'blog_id' => $wpdb->blogid,
'domain' => $domain,
'active' => isset( $_POST['primary'] ) ? 1 : 0
),
array( '%d', '%s', '%d' )
);
// 记录日志
dm_log_action( 'add', $domain, $wpdb->blogid );
// 清除缓存
dm_clear_domain_cache( $wpdb->blogid );
wp_redirect( add_query_arg( array( 'updated' => 'add' ), $url ) );
exit;
break;
}
}
// 处理 GET 请求(删除和设为主域名)
elseif ( isset( $_GET['action'] ) ) {
$action = sanitize_text_field( $_GET['action'] );
$domain = isset( $_GET['domain'] ) ? sanitize_text_field( $_GET['domain'] ) : '';
if ( empty( $domain ) ) {
wp_die( esc_html__( "You must enter a domain", 'wp-domain-mapping' ) );
}
switch ( $action ) {
case 'primary':
check_admin_referer( 'domain_mapping' );
do_action( 'dm_handle_actions_primary', $domain );
// Verify domain belongs to current blog
$domain_info = dm_get_domain_by_name( $domain );
if ( ! $domain_info || $domain_info->blog_id != $wpdb->blogid ) {
wp_die( esc_html__( "Domain not found or does not belong to this site.", 'wp-domain-mapping' ) );
}
// Reset all domains to not primary
$wpdb->update(
$this->tables['domains'],
array( 'active' => 0 ),
array( 'blog_id' => $wpdb->blogid ),
array( '%d' ),
array( '%d' )
);
// Check if domain is not the original domain
$core = WP_Domain_Mapping_Core::get_instance();
$orig_url = parse_url( $core->get_original_url( 'siteurl' ) );
if ( $domain != $orig_url['host'] ) {
// Set the selected domain as primary
$wpdb->update(
$this->tables['domains'],
array( 'active' => 1 ),
array(
'domain' => $domain,
'blog_id' => $wpdb->blogid
),
array( '%d' ),
array( '%s', '%d' )
);
}
// 记录日志
dm_log_action( 'edit', $domain, $wpdb->blogid );
// 清除缓存
dm_clear_domain_cache( $wpdb->blogid );
wp_redirect( add_query_arg( array( 'updated' => 'primary' ), $url ) );
exit;
break;
case 'delete':
check_admin_referer( "delete" . $domain );
do_action( 'dm_handle_actions_del', $domain );
// Verify domain belongs to current blog
$domain_info = dm_get_domain_by_name( $domain );
if ( ! $domain_info || $domain_info->blog_id != $wpdb->blogid ) {
wp_die( esc_html__( "Domain not found or does not belong to this site.", 'wp-domain-mapping' ) );
}
// Don't allow deleting primary domain
if ( $domain_info->active == 1 ) {
wp_die( esc_html__( "Cannot delete the primary domain. Please set another domain as primary first.", 'wp-domain-mapping' ) );
}
// Delete the domain
$wpdb->delete(
$this->tables['domains'],
array(
'domain' => $domain,
'blog_id' => $wpdb->blogid
),
array( '%s', '%d' )
);
// 记录日志
dm_log_action( 'delete', $domain, $wpdb->blogid );
// 清除缓存
dm_clear_domain_cache( $wpdb->blogid );
wp_redirect( add_query_arg( array( 'updated' => 'del' ), $url ) );
exit;
break;
}
}
}
/**
* Render domains admin page
*/
public function render_domains_admin() {
if ( ! dm_is_site_admin() ) {
return false;
}
$this->sunrise_warning();
// Include the admin page template
dm_render_domains_page();
}
/**
* Render admin configuration page
*/
public function render_admin_page() {
if ( ! dm_is_site_admin() ) {
return false;
}
$this->sunrise_warning();
global $current_site;
if ( isset($current_site->path) && $current_site->path != "/" ) {
wp_die( sprintf(
esc_html__( "<strong>Warning!</strong> This plugin will only work if WordPress is installed in the root directory of your webserver. It is currently installed in '%s'.", "wp-domain-mapping" ),
esc_html( $current_site->path )
));
}
// Initialize options if needed
if ( get_site_option( 'dm_remote_login', 'NA' ) == 'NA' ) {
add_site_option( 'dm_remote_login', 1 );
}
if ( get_site_option( 'dm_redirect_admin', 'NA' ) == 'NA' ) {
add_site_option( 'dm_redirect_admin', 1 );
}
if ( get_site_option( 'dm_user_settings', 'NA' ) == 'NA' ) {
add_site_option( 'dm_user_settings', 1 );
}
// Handle form submission
if ( ! empty( $_POST['action'] ) && $_POST['action'] == 'update' ) {
check_admin_referer( 'domain_mapping' );
// Validate and save IP addresses
$ipok = true;
$ipaddresses = explode( ',', sanitize_text_field( $_POST['ipaddress'] ) );
foreach ( $ipaddresses as $address ) {
if ( ( $ip = trim( $address ) ) && ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
$ipok = false;
break;
}
}
if ( $ipok ) {
update_site_option( 'dm_ipaddress', sanitize_text_field( $_POST['ipaddress'] ) );
}
// Save remote login option
if ( intval( $_POST['always_redirect_admin'] ) == 0 ) {
$_POST['dm_remote_login'] = 0;
}
update_site_option( 'dm_remote_login', intval( $_POST['dm_remote_login'] ) );
// Validate and save CNAME
if ( ! preg_match( '/(--|\.\.)/', $_POST['cname'] ) && preg_match( '|^([a-zA-Z0-9-\.])+$|', $_POST['cname'] ) ) {
update_site_option( 'dm_cname', sanitize_text_field( $_POST['cname'] ) );
} else {
update_site_option( 'dm_cname', '' );
}
// Save other options
update_site_option( 'dm_301_redirect', isset( $_POST['permanent_redirect'] ) ? intval( $_POST['permanent_redirect'] ) : 0 );
update_site_option( 'dm_redirect_admin', isset( $_POST['always_redirect_admin'] ) ? intval( $_POST['always_redirect_admin'] ) : 0 );
update_site_option( 'dm_user_settings', isset( $_POST['dm_user_settings'] ) ? intval( $_POST['dm_user_settings'] ) : 0 );
update_site_option( 'dm_no_primary_domain', isset( $_POST['dm_no_primary_domain'] ) ? intval( $_POST['dm_no_primary_domain'] ) : 0 );
// Add settings saved notice
add_settings_error(
'dm_settings',
'settings_updated',
__( 'Settings saved.', 'wp-domain-mapping' ),
'updated'
);
}
// Include the admin page template
dm_render_admin_page();
}
/**
* Render user management page
*/
public function render_manage_page() {
global $wpdb, $parent_file;
if ( isset( $_GET['updated'] ) ) {
do_action( 'dm_echo_updated_msg' );
}
$this->sunrise_warning();
if ( ! get_site_option( 'dm_ipaddress' ) && ! get_site_option( 'dm_cname' ) ) {
if ( dm_is_site_admin() ) {
echo wp_kses(
__( "Please set the IP address or CNAME of your server in the <a href='wp-admin/network/settings.php?page=domain-mapping'>site admin page</a>.", 'wp-domain-mapping' ),
array( 'a' => array( 'href' => array() ) )
);
} else {
esc_html_e( "This plugin has not been configured correctly yet.", 'wp-domain-mapping' );
}
return false;
}
$protocol = is_ssl() ? 'https://' : 'http://';
$domains = dm_get_domains_by_blog_id( $wpdb->blogid );
// Include the user page template
dm_render_user_page( $protocol, $domains );
}
/**
* Add "Domains" link to sites list
*
* @param array $actions The row actions
* @param int $blog_id The blog ID
* @return array Modified actions
*/
public function add_domain_link_to_sites( $actions, $blog_id ) {
$domains_url = add_query_arg(
array( 'page' => 'domains', 'blog_id' => $blog_id ),
network_admin_url( 'sites.php' )
);
$actions['domains'] = '<a href="' . esc_url( $domains_url ) . '">' .
esc_html__( 'Domains', 'wp-domain-mapping' ) . '</a>';
return $actions;
}
/**
* Add domain mapping columns to sites list
*
* @param array $columns The columns
* @return array Modified columns
*/
public function add_domain_mapping_columns( $columns ) {
$columns['map'] = __( 'Mapping', 'wp-domain-mapping' );
return $columns;
}
/**
* Display domain mapping column content
*
* @param string $column The column name
* @param int $blog_id The blog ID
*/
public function display_domain_mapping_column( $column, $blog_id ) {
global $wpdb;
static $maps = false;
if ( $column == 'map' ) {
if ( $maps === false ) {
$work = $wpdb->get_results( "SELECT blog_id, domain FROM {$this->tables['domains']} ORDER BY blog_id" );
$maps = array();
if ( $work ) {
foreach ( $work as $blog ) {
$maps[$blog->blog_id][] = $blog->domain;
}
}
}
if ( ! empty( $maps[$blog_id] ) && is_array( $maps[$blog_id] ) ) {
foreach ( $maps[$blog_id] as $blog ) {
echo esc_html( $blog ) . '<br />';
}
}
}
}
/**
* Default updated messages
*/
public static function echo_default_updated_msg() {
if ( ! isset( $_GET['updated'] ) ) {
return;
}
switch ( $_GET['updated'] ) {
case "add":
$msg = __( 'New domain added.', 'wp-domain-mapping' );
break;
case "exists":
$msg = __( 'New domain already exists.', 'wp-domain-mapping' );
break;
case "primary":
$msg = __( 'New primary domain.', 'wp-domain-mapping' );
break;
case "del":
$msg = __( 'Domain deleted.', 'wp-domain-mapping' );
break;
default:
return;
}
echo "<div class='notice notice-success'><p>" . esc_html( $msg ) . "</p></div>";
}
}
// Add default updated messages action
add_action( 'dm_echo_updated_msg', array( WP_Domain_Mapping_Admin::class, 'echo_default_updated_msg' ) );