250 lines
9.5 KiB
PHP
250 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Go Redirect — External link redirect with auth confirmation
|
|
*
|
|
* Replaces external links in post content with /go/?url=... redirect links.
|
|
* Shows a confirmation page before redirecting (anti-phishing).
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Go_Redirect {
|
|
|
|
private static $instance = null;
|
|
|
|
private function __construct() {}
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
public function init(): void {
|
|
add_action('init', [$this, 'register_rewrite']);
|
|
add_action('template_redirect', [$this, 'handle_go_page']);
|
|
add_filter('the_content', [$this, 'replace_external_links'], 99);
|
|
}
|
|
|
|
/**
|
|
* Register rewrite rule for /go/ endpoint
|
|
*/
|
|
public function register_rewrite(): void {
|
|
add_rewrite_rule('^go/?$', 'index.php?aksrt_go_redirect=1', 'top');
|
|
add_rewrite_tag('%aksrt_go_redirect%', '([0-1])');
|
|
}
|
|
|
|
/**
|
|
* Check if go redirect feature is enabled
|
|
*/
|
|
private function is_enabled(): bool {
|
|
if (!class_exists('AKSRT_Settings')) return false;
|
|
// schema key = seo_go_link; runtime alias = go_redirect_enabled
|
|
return (bool) (
|
|
AKSRT_Settings::get_value('go_redirect_enabled', false)
|
|
?: AKSRT_Settings::get_value('seo_go_link', false)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get excluded domains list
|
|
*/
|
|
private function get_excluded_domains(): array {
|
|
if (!class_exists('AKSRT_Settings')) return [];
|
|
// schema key = seo_go_link_exclude; runtime alias = go_redirect_exclude_domains
|
|
$domains = AKSRT_Settings::get_value('go_redirect_exclude_domains', '')
|
|
?: AKSRT_Settings::get_value('seo_go_link_exclude', '');
|
|
if (empty($domains)) return [];
|
|
return array_filter(array_map('trim', explode("\n", $domains)));
|
|
}
|
|
|
|
/**
|
|
* Check if URL is external
|
|
*/
|
|
private function is_external(string $url): bool {
|
|
$site_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
|
|
$url_host = wp_parse_url($url, PHP_URL_HOST);
|
|
if (!$url_host || !$site_host) return false;
|
|
|
|
// Same domain
|
|
if ($url_host === $site_host) return false;
|
|
|
|
// Check excluded domains
|
|
$excluded = $this->get_excluded_domains();
|
|
foreach ($excluded as $domain) {
|
|
$domain = strtolower(trim($domain));
|
|
if ($domain && (strtolower($url_host) === $domain || substr(strtolower($url_host), -strlen($domain) - 1) === '.' . $domain)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate signed go link with time-limited nonce
|
|
*/
|
|
public function create_go_link(string $url): string {
|
|
$token = wp_create_nonce('aksrt_go_' . $url);
|
|
return add_query_arg([
|
|
'url' => rawurlencode($url),
|
|
'token' => $token,
|
|
], home_url('/go/'));
|
|
}
|
|
|
|
/**
|
|
* Handle the /go/ confirmation page
|
|
*/
|
|
public function handle_go_page(): void {
|
|
if (!get_query_var('aksrt_go_redirect')) return;
|
|
|
|
$url = isset($_GET['url']) ? wp_unslash(rawurldecode($_GET['url'])) : '';
|
|
$token = isset($_GET['token']) ? sanitize_text_field(wp_unslash($_GET['token'])) : '';
|
|
|
|
if (empty($url) || empty($token)) {
|
|
wp_die(
|
|
esc_html__('无效的跳转链接。', 'aksrt'),
|
|
esc_html__('跳转错误', 'aksrt'),
|
|
['response' => 400]
|
|
);
|
|
}
|
|
|
|
// Validate nonce (gated by seo_go_link_nonce schema setting)
|
|
$require_nonce = class_exists('AKSRT_Settings')
|
|
? !empty(AKSRT_Settings::get_value('seo_go_link_nonce', true))
|
|
: true;
|
|
if ($require_nonce && !wp_verify_nonce($token, 'aksrt_go_' . $url)) {
|
|
wp_die(
|
|
esc_html__('链接已过期或无效,请返回原页面重新点击。', 'aksrt'),
|
|
esc_html__('链接过期', 'aksrt'),
|
|
['response' => 403]
|
|
);
|
|
}
|
|
|
|
// Validate URL
|
|
$url = esc_url_raw($url);
|
|
if (empty($url) || !wp_http_validate_url($url)) {
|
|
wp_die(
|
|
esc_html__('无效的目标地址。', 'aksrt'),
|
|
esc_html__('跳转错误', 'aksrt'),
|
|
['response' => 400]
|
|
);
|
|
}
|
|
|
|
// Security: must be external
|
|
if (!$this->is_external($url)) {
|
|
wp_redirect($url);
|
|
exit;
|
|
}
|
|
|
|
// Render confirmation page
|
|
$this->render_go_page($url);
|
|
}
|
|
|
|
/**
|
|
* Render the go confirmation page
|
|
*/
|
|
private function render_go_page(string $url): void {
|
|
$domain = wp_parse_url($url, PHP_URL_HOST) ?: $url;
|
|
$protocol = wp_parse_url($url, PHP_URL_SCHEME) ?: 'https';
|
|
|
|
set_query_var('aksrt_go_target_url', esc_url($url));
|
|
set_query_var('aksrt_go_target_domain', (string) $domain);
|
|
set_query_var('aksrt_go_target_protocol', (string) $protocol);
|
|
|
|
$template = locate_template('go.php', false, false);
|
|
if ($template) {
|
|
status_header(200);
|
|
nocache_headers();
|
|
include $template;
|
|
exit;
|
|
}
|
|
|
|
$safe_url = esc_url($url);
|
|
$safe_domain = esc_html($domain);
|
|
$safe_protocol = esc_html($protocol);
|
|
|
|
wp_head();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html <?php language_attributes(); ?>>
|
|
<head>
|
|
<meta charset="<?php bloginfo('charset'); ?>">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?php esc_html_e('外部链接跳转', 'aksrt'); ?></title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f6f7; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
|
.go-card { background: #fff; border-radius: 12px; padding: 40px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.08); text-align: center; }
|
|
.go-card h1 { font-size: 20px; margin: 0 0 12px; color: #333; }
|
|
.go-card .go-domain { font-size: 16px; font-weight: 600; color: #f04494; margin-bottom: 8px; }
|
|
.go-card .go-url { font-size: 13px; color: #999; word-break: break-all; margin-bottom: 24px; }
|
|
.go-card .go-warn { font-size: 13px; color: #999; margin-bottom: 20px; }
|
|
.go-card .go-btns { display: flex; gap: 12px; justify-content: center; }
|
|
.go-card .go-btn { padding: 10px 28px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; text-decoration: none; }
|
|
.go-card .go-btn--primary { background: #f04494; color: #fff; }
|
|
.go-card .go-btn--primary:hover { background: #e0307e; }
|
|
.go-card .go-btn--ghost { background: #f5f6f7; color: #333; border: 1px solid #ddd; }
|
|
.go-card .go-btn--ghost:hover { background: #eee; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="go-card">
|
|
<h1><?php esc_html_e('即将离开本站', 'aksrt'); ?></h1>
|
|
<div class="go-domain"><?php echo $safe_protocol; ?>://<?php echo $safe_domain; ?></div>
|
|
<div class="go-url"><?php echo $safe_url; ?></div>
|
|
<div class="go-warn"><?php esc_html_e('请注意:外部链接内容不受本站控制,请谨慎访问。', 'aksrt'); ?></div>
|
|
<div class="go-btns">
|
|
<a href="<?php echo $safe_url; ?>" class="go-btn go-btn--primary" rel="nofollow noopener noreferrer" target="_blank"><?php esc_html_e('继续访问', 'aksrt'); ?></a>
|
|
<a href="javascript:history.back()" class="go-btn go-btn--ghost"><?php esc_html_e('返回', 'aksrt'); ?></a>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Replace external links in post content with go links
|
|
*/
|
|
public function replace_external_links(string $content): string {
|
|
if (!$this->is_enabled()) return $content;
|
|
if (is_admin()) return $content;
|
|
if (!is_singular()) return $content;
|
|
// seo_go_link_post: whether to replace links in post content (default true)
|
|
if (class_exists('AKSRT_Settings') && empty(AKSRT_Settings::get_value('seo_go_link_post', true))) {
|
|
return $content;
|
|
}
|
|
|
|
return preg_replace_callback(
|
|
'/<a\s+([^>]*?)href=["\']([^"\']+)["\']([^>]*?)>/i',
|
|
function ($matches) {
|
|
$before = $matches[1];
|
|
$url = $matches[2];
|
|
$after = $matches[3];
|
|
|
|
// Skip if already a go link, anchor, or relative
|
|
if (strpos($url, '/go/') !== false || strpos($url, '#') === 0 || strpos($url, '/') === 0) {
|
|
return $matches[0];
|
|
}
|
|
|
|
// Skip if already has nofollow or is javascript
|
|
if (stripos($before . $after, 'nofollow') !== false || strpos($url, 'javascript:') === 0) {
|
|
return $matches[0];
|
|
}
|
|
|
|
if (!$this->is_external($url)) {
|
|
return $matches[0];
|
|
}
|
|
|
|
$go_url = $this->create_go_link($url);
|
|
return '<a ' . $before . 'href="' . esc_url($go_url) . '"' . $after . ' rel="nofollow noopener noreferrer">';
|
|
},
|
|
$content
|
|
);
|
|
}
|
|
}
|