Files
wordpress-theme-aksrt/inc/classes/class-aksrt-go-redirect.php
T

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
);
}
}