子比主题开发文档
使用指南Codestar Framework主题扩展在线部署AI 功能推荐插件赞助打赏

外链重定向与网址导航

解读子比主题 go.php 外链中转、go_link 生成函数、nonce 鉴权、排除域名、文章内容外链、评论链接、用户网址和友情链接模块。

模块定位

子比主题的外链重定向不是独立插件,而是主题 SEO 和链接展示体系的一部分。它由三层组成:

层级入口
配置项inc/options/admin-options.phpgo_link_sgo_link_nonce_sgo_link_postgo_link_exclude_domain
链接生成inc/functions/zib-theme.phpzib_get_gourl()go_link()zib_is_go_link()
跳转模板主题根目录 go.php

典型链路是:

外部链接
  -> go_link($url, true)
  -> /?golink={base64_url}&nonce={nonce}
  -> template_redirect 加载 go.php
  -> go.php 校验来源、nonce、协议
  -> 延迟跳转到目标 URL

后台配置

外链重定向配置位于「SEO 优化」相关设置:

配置 key作用
go_link_s是否启用外链重定向
go_link_nonce_s是否给中转链接加 nonce 鉴权
go_link_post是否处理文章正文里的外链
go_link_exclude_domain排除域名列表,逗号、中文逗号、空格、换行都可分隔

主题说明里有一个非常重要的缓存边界:开启 go_link_nonce_s 后,转换后的链接会定期变化,有这些链接的位置不能被长期静态缓存。尤其是文章正文、网址导航页、友情链接小工具和用户资料外链,要么排除缓存,要么关闭 nonce 鉴权。

路由与模板加载

主题先把 golink 加入公开 query var:

function zib_add_gophp_query_vars($public_query_vars)
{
    if (!is_admin()) {
        $public_query_vars[] = 'golink';
    }
    return $public_query_vars;
}
add_filter('query_vars', 'zib_add_gophp_query_vars');

然后在 template_redirect 中捕获 golink,加载主题根目录的 go.php

function zib_gophp_template()
{
    $golink = get_query_var('golink');
    if ($golink) {
        global $wp_query;
        $wp_query->is_home = false;
        $wp_query->is_page = true;
        $template          = get_theme_file_path('/go.php');

        @session_start();
        $_SESSION['GOLINK'] = $golink;
        load_template($template);
        exit;
    }
}
add_action('template_redirect', 'zib_gophp_template', 5);

这里把 golink 放进 $_SESSION['GOLINK'],是为了让 go.php 可以用更兼容的方式读取完整参数。开发时不要在这个流程前输出内容,否则 go.php 的 header、HTML 和跳转脚本可能失效。

生成中转链接

主题使用 zib_get_gourl() 生成中转 URL:

function zib_get_gourl($url)
{
    $url   = base64_encode($url);
    $nonce = '';
    if (_pz('go_link_nonce_s')) {
        $nonce = '&nonce=' . wp_create_nonce('go_link_nonce');
    }

    return esc_url(home_url('?golink=' . $url . $nonce));
}

普通开发里更常用的是 go_link()

$url = go_link($url, true);

第二个参数为 true 时,表示传入的是一个 URL 字符串。主题会先判断是否应该中转,只有外链才会转换:

function go_link($text = '', $link = false)
{
    if (!$text || !_pz('go_link_s')) {
        return $text;
    }

    if ($link) {
        if (zib_is_go_link($text)) {
            $text = zib_get_gourl($text);
        }
        return $text;
    }

    return $text;
}

如果传入的是一段 HTML,主题会扫描其中的 <a> 标签,把外链替换为中转链接,并追加 target="_blank"

$content = go_link($content);

主题已经把评论作者链接和评论正文接入了这条链路:

add_filter('get_comment_author_link', 'add_redirect_comment_link', 5);
add_filter('comment_text', 'add_redirect_comment_link', 99);

function add_redirect_comment_link($text = '')
{
    return go_link($text);
}

判断是否外链

zib_is_go_link() 是外链判断的核心:

function zib_is_go_link($url)
{
    if (strpos($url, '://') == false) {
        return false;
    }

    if (strpos($url, zib_get_url_top_host())) {
        return false;
    }

    $go_link_exclude_domain = _pz('go_link_exclude_domain');
    if ($go_link_exclude_domain) {
        $exclude_domain = preg_split("/,|,|\s|\n/", $go_link_exclude_domain);
        if (in_array(zib_get_url_top_host($url), $exclude_domain)) {
            return false;
        }
    }

    return true;
}

注意它判断的是顶级域名,不是完整 host。zib_get_url_top_host() 会处理常见二级后缀:

function zib_get_url_top_host($url = '')
{
    $url   = $url ? $url : home_url();
    $url   = strtolower($url);
    $hosts = parse_url($url);
    $host  = !empty($hosts['host']) ? $hosts['host'] : $url;

    $data = explode('.', $host);
    $n    = count($data);
    $preg = '/[\w].+\.(com|net|org|gov|edu)\.cn$/';

    if (($n > 2) && preg_match($preg, $host)) {
        $host = $data[$n - 3] . '.' . $data[$n - 2] . '.' . $data[$n - 1];
    } elseif (($n > 1)) {
        $host = $data[$n - 2] . '.' . $data[$n - 1];
    }

    return $host;
}

所以排除域名应该填顶级域名,例如:

example.com
baidu.com
qq.com

不要填 https://www.example.com/path。填完整 URL 时可能无法命中排除规则。

go.php 安全边界

go.php 会先做请求长度和危险片段过滤:

if (
    strlen($_SERVER['REQUEST_URI']) > 384 ||
    strpos($_SERVER['REQUEST_URI'], 'eval(') ||
    strpos($_SERVER['REQUEST_URI'], 'base64')
) {
    @header('HTTP/1.1 414 Request-URI Too Long');
    @exit;
}

然后读取 $_SESSION['GOLINK'] 或 query string 中 url= 后面的值。目标可以是 base64 编码后的 URL,也可以是明文 URL:

$t_url = !empty($_SESSION['GOLINK']) ? $_SESSION['GOLINK'] : preg_replace('/^url=(.*)$/i', '$1', $_SERVER['QUERY_STRING']);

if ($t_url == base64_encode(base64_decode($t_url))) {
    $t_url = base64_decode($t_url);
}

接着对输出值做 XSS 处理,并只允许这些协议:

preg_match('/^(http|https|thunder|qqdl|ed2k|Flashget|qbrowser):\/\//i', $t_url, $matches);

如果没有协议但看起来像域名,会补 http://;如果参数错误,则回到首页。

go.php 还会检查来源站点:

$host    = zib_get_url_top_host($_SERVER['HTTP_HOST']);
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
if (!empty($referer) && !preg_match('/' . preg_quote($host, '/') . '/i', $referer)) {
    $url   = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
    $title = '非法请求,正在返回首页...';
}

开启 go_link_nonce_s 后,还会验证 nonce:

if (_pz('go_link_nonce_s')) {
    $nonce = isset($_GET['nonce']) ? $_GET['nonce'] : '';
    if (empty($nonce) || !wp_verify_nonce($nonce, 'go_link_nonce')) {
        $url   = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
        $title = '非法请求,正在返回首页...';
    }
}

最后页面用 setTimeout 延迟跳转:

setTimeout(link_jump, 1500);
setTimeout(function() {
    window.opener = null;
    window.close();
}, 15000);

如果你改跳转页样式,可以改 go.php 的 HTML/CSS,但不要删除 noindex,nofollow、nonce 校验、referer 校验和协议判断。

文章内容外链

开启 go_link_sgo_link_post 后,主题会过滤文章正文:

if (_pz('go_link_s') && _pz('go_link_post')) {
    add_filter('the_content', 'the_content_nofollow', 999);
}

过滤器只改外链 href

function the_content_nofollow($content)
{
    $pattern = '/<a(.*?)href="(.*?)"(.*?)>/';
    preg_match_all($pattern, $content, $matches);
    if ($matches) {
        foreach ($matches[2] as $val) {
            if (zib_is_go_link($val)) {
                $content = str_replace("href=\"$val\"", 'href="' . zib_get_gourl($val) . '" ', $content);
            }
        }
    }
    return $content;
}

它不会检查文章权限,也不会理解业务语义。因此付费内容、隐藏内容、下载按钮等模块如果输出外链,应该在自己的输出阶段明确决定是否使用 go_link($url, true),不要只依赖正文过滤器。

友情链接和网址导航

网址导航模板 pages/links.php 会读取页面 Meta:

Meta key作用
page_links_category友情链接分类
page_links_orderby排序字段
page_links_order升序或降序
page_links_limit数量限制
page_links_style展示样式
page_links_go_s是否启用中转
page_links_blank_s是否新窗口打开
page_links_nofollow_s是否加 rel="nofollow"

最终通过 zib_links_box() 统一渲染:

zib_links_box(get_bookmarks($args), $page_links_style, $args_nofollow_s, $args_go_s, $args_blank_s);

zib_links_box() 会兼容 WordPress bookmark 字段和主题自己的数组字段:

if (empty($link['href']) && !empty($link['link_url'])) {
    $link['href'] = $link['link_url'];
}
if (empty($link['title']) && !empty($link['link_name'])) {
    $link['title'] = $link['link_name'];
}

开启中转时:

if (!empty($link['go_link']) || $go_link) {
    $href = go_link($href, true);
}

如果你新增一个链接列表模块,优先复用 zib_links_box(),这样 card、bigcard、image、simple 四种样式、nofollow、新窗口和 go_link 都能保持一致。

小工具和用户资料外链

部分小工具也提供 go_link 开关,例如合作伙伴滚动卡片、复合信息卡片、友情链接小工具:

if ($go_link && $url !== 'javascript:void(0)') {
    $url = go_link($url, true);
}

用户资料里的个人网址也会被中转:

function zib_get_url_link($user_id, $class = 'focus-color')
{
    $user_url = get_userdata($user_id)->user_url;
    $url_name = zib_get_user_meta($user_id, 'url_name', true) ?: $user_url;
    $user_url = go_link($user_url, true);
    return $user_url ? '<a class="' . $class . '" href="' . esc_url($user_url) . '" target="_blank">' . esc_attr($url_name) . '</a>' : 0;
}

这类用户可填写 URL 的地方,一定要用 esc_url() 输出,并让 go_link() 只处理 URL,不要把用户输入拼成 HTML 后再不加筛选地输出。

自定义链接输出

新增外链按钮时,按这个顺序处理:

function zib_docs_get_external_button($url, $text)
{
    if (!$url || !$text) {
        return '';
    }

    $url = go_link($url, true);

    return '<a class="but c-blue" href="' . esc_url($url) . '" target="_blank" rel="nofollow noopener noreferrer">' . esc_html($text) . '</a>';
}

如果这个链接是站内页面,不要强行中转:

if (zib_is_go_link($url)) {
    $url = zib_get_gourl($url);
}

需要跳过中转的第三方域名,优先让站长配置 go_link_exclude_domain,不要在业务代码里散落多个硬编码白名单。

缓存与 SEO 注意事项

  • 中转页应该保持 noindex,nofollow,不要让搜索引擎收录跳转页。
  • 开启 nonce 鉴权后,带中转链接的页面不能长期静态缓存。
  • CDN 缓存规则要排除 ?golink= 请求,避免不同目标链接返回同一个跳转页。
  • 不要把中转页当下载鉴权页。它只负责外链跳转,不负责验证用户是否购买、是否登录、是否有下载权限。
  • go.php 允许 thunderqqdled2k 等下载协议,前端按钮要清楚告诉用户即将离开本站或调用客户端协议。
  • 排除域名只填顶级域名,多个域名用逗号、空格或换行分隔。
  • 如果评论区允许访客填写网址,保留评论链接中转可以降低直接外链风险。

落地清单

目标首选做法
输出单个外链按钮go_link($url, true) 后再 esc_url()
处理一段 HTML 中的外链go_link($html)
判断是否外链zib_is_go_link($url)
生成中转 URLzib_get_gourl($url)
友情链接列表复用 zib_links_box()
排除可信域名配置 go_link_exclude_domain
开启 nonce 后缓存异常排除包含 ?golink= 或中转链接的页面缓存
修改跳转页视觉只改 go.php 的展示层,不删安全校验

On this page