Skip to content
Permalink
Browse files Browse the repository at this point in the history
SECURITY: update old username when the user is anonymized. (#55)
Previously, when a user is anonymized their new username is not updated in the yearly review topic.
  • Loading branch information
vinothkannans committed Feb 14, 2023
1 parent cb9a2df commit b3ab33b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 5 deletions.
4 changes: 3 additions & 1 deletion app/helpers/yearly_review_helper.rb
Expand Up @@ -3,8 +3,10 @@
module YearlyReviewHelper
include ActionView::Helpers::NumberHelper

AVATAR_SIZE ||= "50"

def avatar_image(username, uploaded_avatar_id)
template = User.avatar_template(username, uploaded_avatar_id).gsub(/{size}/, "50")
template = User.avatar_template(username, uploaded_avatar_id).gsub(/{size}/, AVATAR_SIZE)
"![avatar\\|25x25](#{template})"
end

Expand Down
12 changes: 11 additions & 1 deletion app/jobs/yearly_review.rb
Expand Up @@ -37,6 +37,9 @@ def compiled_method_container
raw: raw,
category: SiteSetting.yearly_review_publish_category,
skip_validations: true,
custom_fields: {
::YearlyReview::POST_CUSTOM_FIELD => review_year,
},
}

post = PostCreator.create!(Discourse.system_user, topic_opts)
Expand Down Expand Up @@ -80,7 +83,14 @@ def create_category_posts(view, review_start, review_end, topic_id)
view.assign(category_topics: category_post_topics)
raw = view.render partial: "yearly_review_category", layout: false
unless raw.empty?
post_opts = { topic_id: topic_id, raw: raw, skip_validations: true }
post_opts = {
topic_id: topic_id,
raw: raw,
skip_validations: true,
custom_fields: {
::YearlyReview::POST_CUSTOM_FIELD => review_start.year,
},
}

PostCreator.create!(Discourse.system_user, post_opts)
end
Expand Down
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class BackfillYearlyReviewCustomFields < ActiveRecord::Migration[6.1]
def change
2017.upto(2022) do |year|
topic_title = I18n.t("yearly_review.topic_title", year: year)
DB.exec(
<<~SQL,
INSERT INTO post_custom_fields (post_id, name, value, created_at, updated_at)
SELECT posts.id, :name, :value, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM posts
INNER JOIN topics ON topics.id = posts.topic_id
WHERE posts.user_id = :user_id AND topics.title = :title
ON CONFLICT DO NOTHING
SQL
title: topic_title,
user_id: Discourse::SYSTEM_USER_ID,
name: YearlyReview::POST_CUSTOM_FIELD,
value: year.to_s,
)
end
end
end
16 changes: 15 additions & 1 deletion plugin.rb
Expand Up @@ -2,7 +2,7 @@

# name: discourse-yearly-review
# about: Creates an automated Year in Review summary topic
# version: 0.1
# version: 0.2
# author: Simon Cossar
# url: https://github.com/discourse/discourse-yearly-review

Expand All @@ -12,6 +12,7 @@
after_initialize do
module ::YearlyReview
PLUGIN_NAME = "yearly-review"
POST_CUSTOM_FIELD ||= "yearly_review"

def self.current_year
Time.now.year
Expand Down Expand Up @@ -78,4 +79,17 @@ def self.last_year
.css("[data-review-topic-users] td table td:nth-child(2)")
.each { |element| element["style"] = "padding-left: 4px;padding-bottom: 6px;" }
end

DiscourseEvent.on(:username_changed) do |old_username, new_username|
Post
.joins(:_custom_fields)
.where(
"post_custom_fields.name = ? AND posts.raw LIKE ?",
YearlyReview::POST_CUSTOM_FIELD,
"%/#{old_username}/%",
)
.update_all(
"raw = REPLACE(raw, '/#{old_username}/', '/#{new_username}/'), baked_version = NULL",
)
end
end
30 changes: 28 additions & 2 deletions spec/jobs/yearly_review_spec.rb
Expand Up @@ -25,6 +25,9 @@ class Helper
expect(topic.title).to eq(
I18n.t("yearly_review.topic_title", year: ::YearlyReview.last_year),
)
expect(topic.first_post.custom_fields).to eq(
YearlyReview::POST_CUSTOM_FIELD => ::YearlyReview.last_year.to_s,
)
end
end

Expand Down Expand Up @@ -80,6 +83,30 @@ class Helper
expect(raw).to have_tag("div.topics-created") { with_text(/\@top_review_user\|5/) }
expect(raw).to have_tag("div.topics-created") { with_text(/\@reviewed_user\|1/) }
end

it "updates username correctly after anonymizing the user" do
Jobs.run_immediately!
UserActionManager.enable
stub_image_size

upload = Fabricate(:upload, user: top_review_user)
top_review_user.user_avatar =
UserAvatar.new(user_id: top_review_user.id, custom_upload_id: upload.id)
top_review_user.uploaded_avatar_id = upload.id
top_review_user.save!

Jobs::YearlyReview.new.execute({})
post = Topic.last.first_post
raw = post.raw
expect(raw).to have_tag("div.topics-created") { with_text(/\@top_review_user\|5/) }
expect(raw).to have_tag("div.topics-created") { with_text(%r{/top_review_user/50/}) }

user = UserAnonymizer.new(top_review_user, Discourse.system_user, {}).make_anonymous
raw = post.reload.raw
expect(raw).to have_tag("div.topics-created") { with_text(/\@#{user.username}\|5/) }
expect(raw).to have_tag("div.topics-created") { with_text(%r{/#{user.username}/50/}) }
expect(post.baked_version).to be_nil
end
end

describe "most replies" do
Expand Down Expand Up @@ -198,8 +225,7 @@ class Helper

it "should rank likes given and received correctly" do
Jobs::YearlyReview.new.execute({})
topic = Topic.last
raw = Post.where(topic_id: topic.id).first.raw
raw = Topic.last.first_post.raw
expect(raw).to have_tag("div.likes-given") { with_text(/\@top_review_user\|11/) }
expect(raw).to have_tag("div.likes-given") { with_text(/\@reviewed_user\|10/) }

Expand Down

0 comments on commit b3ab33b

Please sign in to comment.