2025.1.3 更新
nodeloc support 安装(测试版),实现左侧最下方的友情赞助
composer require nodeloc/support-widget:dev-main
增加外链跳转确认,我在pay-to-see插件里直接加的。
https://sx.sd/d/305
具体代码为:加到vendor/ziiven/flarum-pay-to-see/src/ProcessContent.php下面,改后的完整代码为:
<?php
namespace Ziven\pay2see;
use Flarum\Locale\Translator;
use Ziven\pay2see\Model\PaidDiscussion;
class ProcessContent{
    protected $translator;
    public function __construct(Translator $translator){
        $this->translator = $translator;
    }
    public function __invoke($serializer, $model, $attributes){
        if (isset($attributes["contentHtml"])) {
            $newContent = $attributes['contentHtml'];
            $discussionID = $model->discussion_id;
            $actor = $serializer->getActor();
            $currentUserID = $actor->id;
            $discussionOwnerID = $model->discussion->user_id;
            $discussionCost = $model->discussion->pay2see_cost;
            $postOwnerID = $model->user_id;
            if($postOwnerID!==$discussionOwnerID || !isset($discussionCost)){
            }else{
                $isPaid = false;
                $paidContentText = $this->translator->trans('pay-to-see.forum.pay_to_see_content');
                $allowBypassPay2See = $actor->can('pay2see.allowBypassPay2See', $model->user);
                if($model->number==1){
                    $count = PaidDiscussion::where(['user_id'=>$currentUserID,'discussion_id'=>$discussionID])->count();
                    $serializer->is_paid = $isPaid = $count>0;
                }else{
                    $isPaid = property_exists($serializer, 'is_paid')?$serializer->is_paid:false;
                }
                
                if($isPaid===true || $currentUserID===$discussionOwnerID || $allowBypassPay2See){
                    $paidContentClassName = "pay2see_gray";
                    $paidHeaderClassName = "pay2see_header_gray";
                    $paidContentText = $this->translator->trans('pay-to-see.forum.pay_to_see_owner_bypass');
                    if($currentUserID!==$discussionOwnerID && $isPaid===true){
                        $paidContentText = $this->translator->trans('pay-to-see.forum.purchased_content');
                        $paidContentClassName = "pay2see_green";
                        $paidHeaderClassName = "pay2see_header_green";
                    }
                    if($isPaid===false && $allowBypassPay2See){
                        $paidContentText = $this->translator->trans('pay-to-see.forum.pay_to_see_content_bypass');
                    }
                    $attributes['contentHtml'] = $newContent = str_replace('[pay]','<div class="'.$paidContentClassName.'"><div class="'.$paidHeaderClassName.'">'.$paidContentText.'</div>',$newContent);
                    $attributes['contentHtml'] = $newContent = str_replace('[/pay]','</div>',$newContent);
                }else{
                    $replacement = '<blockquote style="text-align:center;padding:30px"><div><p>'.$this->translator->trans('pay-to-see.forum.pay_to_see_content').'</p></div></blockquote>';
                    $attributes['contentHtml'] = self::replaceContent($newContent,"[pay]","[/pay]",$replacement);
                }
            }
        }
        return $attributes;
    }
    public function replaceContent($str, $start, $end, $replacement) {
        $start = preg_quote($start, '/');
        $end = preg_quote($end, '/');
        $regex = "/$start(.*?)$end/is";
        return preg_replace($regex,$replacement,$str);
    }
    public function getContent($string, $start, $end){
        $start = preg_quote($start, '/');
        $end = preg_quote($end, '/');
        $regex = "/$start(.*?)$end/is";
        preg_match($regex, $string, $matches);
        
        return $matches[1];
    }
}
//*增加外链确认引用
class FormatContent
{
    public function __invoke($serializer, $model, $attributes)
    {
        if (isset($attributes["contentHtml"])) {
            $newHTML = $attributes["contentHtml"];
            // 检查 HTML 内容是否为空
            if (!is_null($newHTML)) {
                // 使用正则表达式匹配所有的链接
                $newHTML = preg_replace_callback(
                    '/<a\s+(?:[^>]*?\s+)?href=["\']([^"\']+)["\']([^>]*)>(.*?)<\/a>/is',
                    function ($matches) {
                        $url = $matches[1]; // 获取链接的 href 值
                        $attributes = $matches[2]; // 获取链接的其他属性
                        $text = $matches[3]; // 获取链接的文本
                        // 判断是否为站内链接或内部链接
                        if ($this->isExternalLink($url)) {
                            // 对非站内链接添加跳转提示
                            $redirectUrl = '/goto/' . urlencode($url);
                            return "<a href=\"{$redirectUrl}\" {$attributes} target=\"_blank\" rel=\"noopener noreferrer\">{$text}</a>";
                        }
                        // 保持原样处理站内链接
                        return $matches[0];
                    },
                    $newHTML
                );
            }
            $attributes['contentHtml'] = $newHTML;
        }
        return $attributes;
    }
	/**
	 * 检查是否为外部链接
	 *
	 * @param string $url URL地址
	 * @return bool 如果是外部链接返回 true,否则返回 false
	 */
	private function isExternalLink($url)
	{
		// 如果 URL 为空,返回 false
		if (empty($url)) {
			return false;
		}
		// 处理特殊协议
		if (preg_match('/^(javascript|mailto|tel|sms|file):/i', $url)) {
			return false;
		}
		// 如果是以 // 开头的协议相对 URL
		if (strpos($url, '//') === 0) {
			$url = 'http:' . $url;
		}
		// 获取当前请求的主机名
		$currentHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
		$currentHost = rtrim($currentHost, '/');
		// 解析 URL
		$urlParts = parse_url($url);
		// 如果 URL 无法解析或没有 host 部分(相对路径),认为是内部链接
		if ($urlParts === false || empty($urlParts['host'])) {
			return false;
		}
		// 标准化主机名(移除 www. 前缀并转为小写)
		$currentHost = strtolower(preg_replace('/^www\./i', '', $currentHost));
		$urlHost = strtolower(preg_replace('/^www\./i', '', $urlParts['host']));
		// 检查域名是否在允许的内部域名列表中
		$internalDomains = [
			$currentHost,
			// 可以添加其他允许的域名
			's.sx.sd',
			// 'otherdomain.com'
		];
		return !in_array($urlHost, $internalDomains, true);
	}
	//*外链确认引用完成
    
}
在 vendor/ziiven/flarum-pay-to-see/extend.php 里边Inject一下,改后完整代码为
<?php
use Flarum\Extend;
use Flarum\Api\Serializer\BasicPostSerializer;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Discussion\Event\Saving as DiscussionSaving;
use Flarum\Post\Event\Saving as PostSaving;
use Ziven\pay2see\ProcessContent;
use Ziven\pay2see\AddDiscussionPaidAttributes;
use Ziven\pay2see\Controller\PayToSeePurchaseController;
use Ziven\pay2see\Controller\ListPayToSeePurchasedUserController;
use Ziven\pay2see\Controller\PayToSeeSetController;
use Ziven\pay2see\Listeners\AddPayToSeeDiscussion;
use Ziven\pay2see\Listeners\AddPayToSeePost;
use Ziven\pay2see\Notification\PayToSeeBlueprint;
use Ziven\pay2see\FormatContent;           //*增加外链确认引用
$extend = [
    (new Extend\Frontend('admin'))->js(__DIR__.'/js/dist/admin.js')->css(__DIR__.'/less/admin.less'),
    (new Extend\Frontend('forum'))->js(__DIR__ . '/js/dist/forum.js')->css(__DIR__.'/less/forum.less'),
    (new Extend\Locales(__DIR__ . '/locale')),
    (new Extend\Routes('api'))
        ->post('/pay2seePurchase', 'pay2see.create', PayToSeePurchaseController::class)
        ->get('/pay2seePurchase', 'pay2see.purchased', ListPayToSeePurchasedUserController::class)
        ->post('/pay2seeSet', 'pay2see.set', PayToSeeSetController::class),
    (new Extend\Event())
        ->listen(DiscussionSaving::class, AddPayToSeeDiscussion::class)
        ->listen(PostSaving::class, AddPayToSeePost::class),
    (new Extend\ApiSerializer(DiscussionSerializer::class))
        ->attributes(AddDiscussionPaidAttributes::class)
        ->attribute('pay2seeCount', function (DiscussionSerializer $serializer, $discussion) {
            return $discussion->pay2see_count;
        }),
    (new Extend\ApiSerializer(BasicPostSerializer::class))
        ->attributes(ProcessContent::class),
    (new Extend\ApiSerializer(ForumSerializer::class))
        ->attribute('allowUsePay2See', function (ForumSerializer $serializer) {
            return $serializer->getActor()->hasPermission("pay2see.allowUsePay2See");
        })
        ->attribute('allowSetPay2See', function (ForumSerializer $serializer) {
            return $serializer->getActor()->hasPermission("pay2see.allowSetPay2See");
        })
        ->attribute('allowPurchasePay2See', function (ForumSerializer $serializer) {
            return $serializer->getActor()->hasPermission("pay2see.allowPurchasePay2See");
        }),
    (new Extend\Settings())
        ->default('pay2see.pay2seeAllowTags', [])
        ->serializeToForum('pay2seeAllowTags', 'pay2see.pay2seeAllowTags')
        ->serializeToForum('pay2seeContentBadge', 'pay2see.pay2seeContentBadge', 'strval'),
    (new Extend\Notification())
        ->type(PayToSeeBlueprint::class, DiscussionSerializer::class, ['alert']),
//*增加外链确认引用
    (new Extend\ApiSerializer(PostSerializer::class))
        ->attributes(FormatContent::class),
    (new Extend\ApiSerializer(BasicPostSerializer::class))
        ->attributes(FormatContent::class),
//*外链确认引用完成
];
return $extend;
public目录下建一个goto目录,新建一个index.php文件,内容为:
<?php
// 获取跳转目标 URL
$redirectUrl = $_SERVER['REQUEST_URI'];
$redirectUrl = str_replace('/goto/', '', $redirectUrl);
// 初始化错误消息和解码后的链接变量
$errorMessage = null;
$decodedLink = null;
// 检查 URL 是否为空
if (empty($redirectUrl)) {
    $errorMessage = "跳转链接无效或为空。请检查您的链接。";
    $redirectUrl = "#"; // 默认值
} else {
    // 对链接进行解码
    $decodedLink = urldecode($redirectUrl);
    // 验证解码后的链接是否为有效的 URL
    if (filter_var($decodedLink, FILTER_VALIDATE_URL)) {
        // 确保链接是外部链接
        $host = parse_url($decodedLink, PHP_URL_HOST);
        if ($host === $_SERVER['HTTP_HOST']) {
            $errorMessage = "不支持站内链接跳转。";
            $redirectUrl = "#"; // 不合法时默认值
        } else {
            $redirectUrl = $decodedLink; // 合法的外部链接
        }
    } else {
        $errorMessage = "无效的 URL,请检查链接格式。";
        $redirectUrl = "#"; // 无效链接时默认值
    }
}
// 对最终链接进行 HTML 特殊字符编码,确保安全
$redirectUrl = htmlspecialchars($redirectUrl, ENT_QUOTES, 'UTF-8');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>跳转提示</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #009966;
            background-image: linear-gradient(135deg, #009966, #006644);
            color: #ffffff;
            margin: 0;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
        }
        .container {
            text-align: center;
            background-color: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 20px;
            width: 90%;
            max-width: 480px;
            box-sizing: border-box;
        }
        .container h1 {
            font-size: 22px;
            margin-bottom: 10px;
        }
        .container p {
            font-size: 14px;
            margin-bottom: 20px;
            word-wrap: break-word;
            word-break: break-all;
        }
        .container a {
            display: inline-block;
            background-color: #FF9933;
            color: #ffffff;
            text-decoration: none;
            font-size: 14px;
            font-weight: bold;
            padding: 10px 20px;
            border-radius: 5px;
            transition: background-color 0.3s ease;
        }
        .container a:hover {
            background-color: #cc7a29;
        }
        .container .small-text {
            font-size: 12px;
            margin-top: 15px;
            color: rgba(255, 255, 255, 0.8);
        }
        .error-message {
            color: #FF0000;
            font-size: 16px;
            font-weight: bold;
            margin-bottom: 20px;
        }
        /* 支持老旧浏览器的渐变背景 */
        @supports not (background: linear-gradient(135deg, #009966, #006644)) {
            body {
                background-color: #009966;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>跳转提示</h1>
        
        <?php if (isset($errorMessage)): ?>
            <div class="error-message"><?= $errorMessage ?></div>
        <?php else: ?>
            <p>您正在离开本站并访问以下链接:</p>
            <p><strong><?= $redirectUrl ?></strong></p>
            <a href="<?= $redirectUrl ?>" rel="noopener noreferrer">继续访问</a>
        <?php endif; ?>
        <p class="small-text">如果您不想访问此链接,请关闭此页面。</p>
    </div>
</body>
</html>
最后,最重要的一步就是,伪静态
 location /goto/ {
    rewrite ^/goto/(https?.+)$ /goto/index.php?url=$1 last;
}