附件缺少链接计数器

我已经调试了这个问题,并找到了一个潜在的修复方案(在 Gemini 的帮助下)。

我在版本 3.4.5(我知道该版本可以正常工作)和最新版本上分别创建了这条消息(拖拽文件的内容并不重要):

然后我使用了另外两个账户下载了这两个文件,并刷新了页面。

对于“without-attachement”(无附件),我没有看到下载计数器(尽管当我使用源版本 v3.4.5 时它是有效的)。“with-attachment”(有附件)从未显示下载计数器,我只是为了“完整性”而添加了它。

当在调试器中执行以下代码时:

post = Post.last
TopicLink.counts_for(Guardian.new(post.user), post.topic, [post])

我得到了:

=> {8 =>
  [{url: "/uploads/short-url/iMUBjeLNs0eRWZhdYnh24XGXY0f.funscript", clicks: 2, title: nil, internal: true, reflection: false},
   {url: "/uploads/short-url/rgnPszmhmEizcRVBobULTkMP1UI.funscript", clicks: 0, title: nil, internal: true, reflection: false}]}

所以点击次数是存在的,但 UI 没有考虑它们,因为它们被用于防止用户查看指向私有/隐藏主题的链接点击次数的规则过滤掉了。问题在于,新规则没有考虑到内部链接可能是一个主题,也可能是一个内部文件。目前,任何 internal 且 not a topic 的链接都会被丢弃。

我通过修改 topic_link.rb 中的代码解决了这个问题:

def self.apply_link_visibility_filters(builder, link:, target_topic:, target_posts:)
    builder.where(<<\~SQL)
      #{target_topic}.deleted_at IS NULL
      AND (#{target_topic}.id IS NULL OR #{target_topic}.visible = true)
      AND (#{link}.internal = false OR #{target_topic}.id IS NOT NULL)
      AND (#{link}.link_post_id IS NULL OR (#{link_posts}.id IS NOT NULL AND #{link_posts}.deleted_at IS NULL))
    SQL
  end

改为:

def self.apply_link_visibility_filters(builder, link:, target_topic:, target_posts:)
    builder.where(<<\~SQL)
      #{target_topic}.deleted_at IS NULL
      AND (#{target_topic}.id IS NULL OR #{target_topic}.visible = true)
      AND (#{link}.internal = false OR #{link}.link_topic_id IS NULL OR #{target_topic}.id IS NOT NULL)
      AND (#{link}.link_post_id IS NULL OR (#{link_posts}.id IS NOT NULL AND #{link_posts}.deleted_at IS NULL))
    SQL
  end
Gemini 完整解释

你找到了确凿的证据!通过提供 topic_link.rb 的两个版本,现在非常清楚是什么破坏了 Discourse 最新版本中的此功能。

这个问题完全是由添加到 counts_for 数据库查询中的新安全过滤器引起的。

罪魁祸首:新的 apply_link_visibility_filters 方法

如果你查看新的 topic_link.rb 文件底部附近,你会发现这个新方法:

codeRuby

def self.apply_link_visibility_filters(builder, link:, target_topic:, target_posts:)
    builder.where(<<~SQL)
      #{target_topic}.deleted_at IS NULL
      AND (#{target_topic}.id IS NULL OR #{target_topic}.visible = true)
      AND (#{link}.internal = false OR #{target_topic}.id IS NOT NULL)  <--- 这是 BUG
      AND (#{link}.link_post_id IS NULL OR (#{link_posts}.id IS NOT NULL AND #{link_posts}.deleted_at IS NULL))
    SQL
  end

为什么这会破坏标准的文件上传

Discourse 开发者添加这条 SQL 规则是为了防止用户查看指向他们没有权限查看的私有/隐藏主题的链接的点击次数。然而,他们意外地波及了文件上传。

以下是 SQL 引擎如何评估你的 without-attachment.funscript 上传链接的:

  1. #{link}.internal = false:当保存上传文件时,Discourse 将其标记为 internal = true(因为文件托管在你的论坛上)。所以这评估为 FALSE。
  2. #{target_topic}.id IS NOT NULL:因为这是一个文件上传,而不是指向另一个论坛帖子的链接,所以它没有目标主题。主题 ID 为 NULL。所以这也评估为 FALSE。
  3. FALSE OR FALSE = FALSE。

由于这两个条件都失败,SQL 查询完全过滤掉了你的文件上传,并且 counts_for 方法向序列化器返回了一个空数组。

(注意:|attachment 仍然有效的原因是,前端通过一个完全独立的 API 端点/机制处理 .attachment 类以进行下载跟踪,从而完全绕过了 TopicLink!)

如何修复代码

要在你的本地环境中修复此 bug,你需要告诉 SQL 查询:“如果内部链接没有目标主题是可以的,只要它一开始就不应该有目标主题(例如文件上传)。"

在你的 Dev Container 中打开 app/models/topic_link.rb,找到 apply_link_visibility_filters 方法(大约在第 185 行),并将损坏的行更改为:

将此更改:

AND (#{link}.internal = false OR #{target_topic}.id IS NOT NULL)

改为:

AND (#{link}.internal = false OR #{link}.link_topic_id IS NULL OR #{target_topic}.id IS NOT NULL)

为什么这能修复它:
通过添加 #{link}.link_topic_id IS NULL,你是在告诉数据库允许纯粹是文件/上传的内部链接。它仍然会成功阻止那些 确实 有 link_topic_id 但 target_topic 缺失/隐藏的情况(这满足了 Discourse 开发者试图实现的安全要求)。