첨부 파일 링크 카운터가 없습니다

문제를 디버깅했고 잠재적인 수정 방법을 찾았습니다 (Gemini의 도움으로).

저는 버전 3.4.5(정상적으로 작동하는 것으로 알려진 버전)와 최신 버전에서 이 메시지(드래그한 파일의 내용은 중요하지 않음)를 생성했습니다:

그 후 다른 2개의 계정을 사용하여 두 파일을 다운로드하고 페이지를 새로고침했습니다.

"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이면서 토픽이 아닌 모든 항목은 폐기됩니다.

저는 (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 (#{target_posts}.id IS NOT NULL AND #{target_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 (#{target_posts}.id IS NOT NULL AND #{target_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)  <--- 이것이 버그입니다
      AND (#{link}.link_post_id IS NULL OR (#{target_posts}.id IS NOT NULL AND #{target_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가 여전히 작동하는 이유는 프론트엔드가 .attachment 클래스를 다운로드 추적을 위한 완전히 다른 API 엔드포인트/메커니즘을 통해 처리하여 TopicLink를 우회하기 때문입니다!)

코드 수정 방법

로컬 환경에서 이 버그를 수정하려면 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 개발자들이 달성하려 했던 보안 요구 사항을 충족합니다).