diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d00636d4c7..64385359b7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -54,7 +54,8 @@ module ApplicationHelper name = h(user.name(options[:format])) if user.active? || (User.current.admin? && user.logged?) only_path = options[:only_path].nil? ? true : options[:only_path] - link_to name, user_url(user, :only_path => only_path), :class => user.css_classes + css_classes = options[:class] ? "#{user.css_classes} #{options[:class]}" : user.css_classes + link_to name, user_url(user, :only_path => only_path), :class => css_classes else name end @@ -990,9 +991,6 @@ module ApplicationHelper if p = Project.visible.find_by_id(oid) link = link_to_project(p, {:only_path => only_path}, :class => 'project') end - when 'user' - u = User.visible.find_by(:id => oid, :type => 'User') - link = link_to_user(u, :only_path => only_path) if u end elsif sep == ':' name = remove_double_quotes(identifier) @@ -1051,14 +1049,14 @@ module ApplicationHelper if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first link = link_to_project(p, {:only_path => only_path}, :class => 'project') end - when 'user' - u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase) - link = link_to_user(u, :only_path => only_path) if u end - elsif sep == "@" - name = remove_double_quotes(identifier) - u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase) - link = link_to_user(u, :only_path => only_path) if u + end + if link.nil? && $~ + user = User.mentioned_user($~.named_captures.symbolize_keys) + if user + css_classes = (user.notify_mentioned_user?(obj) ? 'notified' : nil) + link = link_to_user(user, :only_path => only_path, :class => css_classes) + end end end (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) diff --git a/app/models/document.rb b/app/models/document.rb index 5be0a61c7c..2ec1535b78 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -63,7 +63,7 @@ class Document < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end private diff --git a/app/models/issue.rb b/app/models/issue.rb index cd13fd3c55..f8ed3223d1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1040,8 +1040,7 @@ class Issue < ActiveRecord::Base notified += project.notified_users notified.uniq! # Remove users that can not view the issue - notified.reject! {|user| !visible?(user)} - notified + notified.select {|user| user.allowed_to_view_notify_target?(self)} end # Returns the email addresses that should be notified diff --git a/app/models/journal.rb b/app/models/journal.rb index 0077b548dc..acfe97c9c4 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -143,10 +143,7 @@ class Journal < ActiveRecord::Base def notified_users notified = journalized.notified_users - if private_notes? - notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} - end - notified + notified.select{ |u| u.allowed_to_view_notify_target?(self) } end def recipients diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 5ef10c1d7e..fd9955f69c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -92,6 +92,7 @@ class Mailer < ActionMailer::Base # Mailer.deliver_issue_add(issue) def self.deliver_issue_add(issue) users = issue.notified_users | issue.notified_watchers + users |= mentioned_users(issue.description, issue) users.each do |user| issue_add(user, issue).deliver_later end @@ -126,6 +127,7 @@ class Mailer < ActionMailer::Base # Mailer.deliver_issue_edit(journal) def self.deliver_issue_edit(journal) users = journal.notified_users | journal.notified_watchers + users |= mentioned_users(journal.notes, journal) users.select! do |user| journal.notes? || journal.visible_details(user).any? end @@ -151,6 +153,7 @@ class Mailer < ActionMailer::Base # Mailer.deliver_document_added(document, author) def self.deliver_document_added(document, author) users = document.notified_users + users |= mentioned_users(document.description, document) users.each do |user| document_added(user, document, author).deliver_later end @@ -219,6 +222,7 @@ class Mailer < ActionMailer::Base # Mailer.deliver_news_added(news) def self.deliver_news_added(news) users = news.notified_users | news.notified_watchers_for_added_news + users |= mentioned_users(news.description, news) users.each do |user| news_added(user, news).deliver_later end @@ -246,6 +250,7 @@ class Mailer < ActionMailer::Base def self.deliver_news_comment_added(comment) news = comment.commented users = news.notified_users | news.notified_watchers + users |= mentioned_users(comment.content, news) users.each do |user| news_comment_added(user, comment).deliver_later end @@ -273,6 +278,7 @@ class Mailer < ActionMailer::Base users = message.notified_users users |= message.root.notified_watchers users |= message.board.notified_watchers + users |= mentioned_users(message.content, message) users.each do |user| message_posted(user, message).deliver_later @@ -300,6 +306,7 @@ class Mailer < ActionMailer::Base # Mailer.deliver_wiki_content_added(wiki_content) def self.deliver_wiki_content_added(wiki_content) users = wiki_content.notified_users | wiki_content.page.wiki.notified_watchers + users |= mentioned_users(wiki_content.text, wiki_content) users.each do |user| wiki_content_added(user, wiki_content).deliver_later end @@ -331,6 +338,7 @@ class Mailer < ActionMailer::Base users = wiki_content.notified_users users |= wiki_content.page.notified_watchers users |= wiki_content.page.wiki.notified_watchers + users |= mentioned_users(wiki_content.text, wiki_content) users.each do |user| wiki_content_updated(user, wiki_content).deliver_later @@ -760,5 +768,16 @@ class Mailer < ActionMailer::Base @references_objects ||= [] @references_objects << object end + + def self.mentioned_users(text, obj) + users = [] + return users if text.blank? + text.scan(ApplicationHelper::LINKS_RE) do |_| + target = User.mentioned_user($~.named_captures.symbolize_keys) + next if target.blank? || users.include?(target) + users << target if target.notify_mentioned_user?(obj) + end + users + end end diff --git a/app/models/message.rb b/app/models/message.rb index c466f66be6..4baaf1f9f9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -105,7 +105,7 @@ class Message < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end private diff --git a/app/models/news.rb b/app/models/news.rb index 02418107c9..913d3d1159 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -56,7 +56,7 @@ class News < ActiveRecord::Base end def notified_users - project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)} + project.users.select {|user| user.notify_about?(self) && user.allowed_to_view_notify_target?(self)} end def recipients diff --git a/app/models/user.rb b/app/models/user.rb index 698b1bac9b..ca3af2ada2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -797,6 +797,47 @@ class User < Principal RequestStore.store[:current_user] ||= User.anonymous end + # Return the mentioned user to based on the match data + # of ApplicationHelper::LINKS_RE. + # user:jsmith -> Link to user with login jsmith + # @jsmith -> Link to user with login jsmith + # user#2 -> Link to user with id 2 + def self.mentioned_user(match_data) + return nil if match_data[:esc] + sep = match_data[:sep1] || match_data[:sep2] || match_data[:sep3] || match_data[:sep4] + identifier = match_data[:identifier1] || match_data[:identifier2] || match_data[:identifier3] + prefix = match_data[:prefix] + if (sep == '#' || sep == '##') && prefix == 'user' + User.visible.find_by(:id => identifier.to_i, :type => 'User') + elsif sep == '@' || (sep == ':' && prefix == 'user') + name = identifier.gsub(%r{^"(.*)"$}, "\\1") + User.find_by_login(CGI.unescapeHTML(name).downcase) + end + end + + # Return true if notify the mentioned user. + def notify_mentioned_user?(object) + self.active? && + self.mail.present? && + self.mail_notification.present? && self.mail_notification != 'none' && + self.allowed_to_view_notify_target?(object) + end + + # Return true if the user is allowed to view the notify target. + def allowed_to_view_notify_target?(object) + case object + when Journal + self.allowed_to_view_notify_target?(object.journalized) && + (!object.private_notes? || self.allowed_to?(:view_private_notes, object.journalized.project)) + when Comment + self.allowed_to_view_notify_target?(object.commented) + when nil + false + else + object.visible?(self) + end + end + # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only # one anonymous user per database. def self.anonymous diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 7bda26009c..01d95aaf35 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -53,7 +53,7 @@ class WikiContent < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end # Returns the mail addresses of users that should be notified diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index a524c91b82..91463ebc13 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1562,6 +1562,12 @@ RAW assert_equal ::I18n.t(:label_user_anonymous), t end + def test_link_to_user_with_class_name_should_include_class + user = User.find(2) + result = link_to("John Smith", "/users/2", :class => "user active test") + assert_equal result, link_to_user(user, :class => 'test') + end + def test_link_to_attachment a = Attachment.find(3) assert_equal 'logo.gif', diff --git a/test/unit/mailer_test.rb b/test/unit/mailer_test.rb index 51fc001f9d..59758df8a4 100644 --- a/test/unit/mailer_test.rb +++ b/test/unit/mailer_test.rb @@ -365,6 +365,30 @@ class MailerTest < ActiveSupport::TestCase assert_include 'dlopper@somenet.foo', recipients end + def test_issue_added_should_notify_mentioned_users + # Non-public project issue + issue = Issue.find(4) + # Developer + user = User.find(8) + assert issue.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |description| + issue.update(:description => description) + ActionMailer::Base.deliveries.clear + Mailer.deliver_issue_add(issue) + assert_include user.mail, recipients + end + + # Do not notify unauthorized mentioned users. + Role.find(2).remove_permission!(:view_issues) + assert_not issue.visible?(user.reload) + issue.update(:description => "user##{user.id}") + ActionMailer::Base.deliveries.clear + Mailer.deliver_issue_add(issue) + assert_not_include user.mail, recipients + end + def test_issue_add_should_send_mail_to_all_user_email_address EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo') issue = Issue.find(1) @@ -534,6 +558,29 @@ class MailerTest < ActiveSupport::TestCase end end + def test_issue_edit_should_notify_mentioned_users + # Non-public project journal + issue = Issue.find(4) + journal = Journal.generate!(:notes => '', :details => [JournalDetail.new], :journalized => issue) + # Developer + user = User.find(8) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |note| + journal.update(:notes => note) + Mailer.deliver_issue_edit(journal) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify mentioned users without view_private_notes permission and journal is private. + Role.find(2).remove_permission!(:view_private_notes) + journal.update(:notes => "user##{user.id}", :private_notes => true) + Mailer.deliver_issue_edit(journal) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + def test_version_file_added attachements = [ Attachment.find_by_container_type('Version') ] assert Mailer.deliver_attachments_added(attachements) @@ -565,6 +612,53 @@ class MailerTest < ActiveSupport::TestCase assert_not_include user2.mail, recipients end + def test_news_added_should_notify_mentioned_users + project = Project.find(1) + news = project.news.first + user = User.find(8) # non member + assert news.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |description| + news.update(:description => description) + Mailer.deliver_news_added(news) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify unauthorized mentioned users. + Role.non_member.remove_permission!(:view_news) + assert_not news.visible?(user.reload) + news.update(:description => "user##{user.id}") + Mailer.deliver_news_added(news) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + def test_news_comment_added_should_notify_mentioned_users + project = Project.find(1) # public project + news = project.news.first + comment = news.comments.first + user = User.find(8) # non member + assert news.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |text| + comment.update(:content => text) + Mailer.deliver_news_comment_added(comment) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify unauthorized mentioned users. + Role.non_member.remove_permission!(:view_news) + assert_not news.visible?(user.reload) + comment.update(:content => "user##{user.id}") + Mailer.deliver_news_comment_added(comment) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + def test_wiki_content_added content = WikiContent.find(1) assert_difference 'ActionMailer::Base.deliveries.size', 2 do @@ -577,6 +671,30 @@ class MailerTest < ActiveSupport::TestCase end end + def test_wiki_content_added_should_notify_mentioned_users + # Non-public project wiki_content + project = Project.find(2) + wiki_content = project.wiki.pages.first.content + user = User.find(8) + assert wiki_content.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |text| + wiki_content.update(:text => text) + Mailer.deliver_wiki_content_added(wiki_content) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify unauthorized mentioned users. + Role.find(2).remove_permission!(:view_wiki_pages) + assert_not wiki_content.visible?(user.reload) + wiki_content.update(:text => "user##{user.id}") + Mailer.deliver_wiki_content_added(wiki_content) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + def test_wiki_content_updated content = WikiContent.find(1) assert Mailer.deliver_wiki_content_updated(content) @@ -946,6 +1064,56 @@ class MailerTest < ActiveSupport::TestCase Mailer.email_addresses(User.find(2)).sort end + def test_document_added_should_notify_mentioned_users + # Non-public project document + project = Project.find(2) + project.enable_module!(:documents) + document = Document.generate!(:project => project) + # Developer + user = User.find(8) + assert document.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |description| + document.update(:description => description) + Mailer.deliver_document_added(document, User.first) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify unauthorized mentioned users. + Role.find(2).remove_permission!(:view_documents) + assert_not document.visible?(user.reload) + document.update(:description => "user##{user.id}") + Mailer.deliver_document_added(document, User.first) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + def test_message_posted_should_notify_mentioned_users + # Non-public project message + project = Project.find(2) + message = project.boards.first.messages.first + user = User.find(8) + assert message.visible?(user) + + # Notify mentioned users. + ["user##{user.id}", "@#{user.login}", "user:#{user.login}"].each do |text| + message.update(:content => text) + Mailer.deliver_message_posted(message) + assert_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + + # Do not notify unauthorized mentioned users. + Role.find(2).remove_permission!(:view_messages) + assert_not message.visible?(user.reload) + message.update(:content => "user##{user.id}") + Mailer.deliver_message_posted(message) + assert_not_include user.mail, recipients + ActionMailer::Base.deliveries.clear + end + private # Returns an array of email addresses to which emails were sent diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 5ae17508f0..9065c76e50 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -461,6 +461,69 @@ class UserTest < ActiveSupport::TestCase assert_nil changeset.reload.user_id end + def test_mentioned_user + # user#id + match_data = { :esc => nil, :prefix => 'user', :sep1 => '#', :identifier1 => '1' } + assert_equal User.mentioned_user(match_data), User.find(1) + + # user:login + match_data = { :esc => nil, :prefix => 'user', :sep3 => ':', :identifier2 => 'admin' } + assert_equal User.mentioned_user(match_data), User.find_by_login('admin') + + # @login + match_data = { :esc => nil, :sep4 => '@', :identifier3 => 'admin' } + assert_equal User.mentioned_user(match_data), User.find_by_login('admin') + end + + def test_notify_mentioned_user? + issue = Issue.first + user = User.find(2) + assert user.notify_mentioned_user?(issue) + + user.roles_for_project(issue.project).first.remove_permission!(:view_issues) + assert_not user.reload.notify_mentioned_user?(issue) + end + + def test_allowed_to_view_notify_target + issue = Issue.first + user = User.find(2) + + assert issue.visible?(user) + assert user.allowed_to_view_notify_target?(issue) + + user.roles_for_project(issue.project).first.remove_permission!(:view_issues) + user.reload + assert_not issue.visible?(user) + assert_not user.allowed_to_view_notify_target?(issue) + end + + def test_allowed_to_view_notify_target_with_journal + journal = Journal.first + user = User.find(3) + + assert journal.issue.visible?(user) + assert user.allowed_to_view_notify_target?(journal) + + journal.update(:private_notes => true) + Role.find(2).remove_permission!(:view_private_note) + assert_not user.reload.allowed_to_view_notify_target?(journal) + + journal.update(:private_notes => false) + Role.find(2).remove_permission!(:view_issues) + assert_not user.reload.allowed_to_view_notify_target?(journal) + end + + def test_allowed_to_view_notify_target_with_comment + comment = Comment.first + user = User.find(3) + + assert comment.commented.visible?(user) + assert user.allowed_to_view_notify_target?(comment) + + Role.find(2).remove_permission!(:view_news) + assert_not user.reload.allowed_to_view_notify_target?(comment) + end + def test_anonymous_user_should_not_be_destroyable assert_no_difference 'User.count' do assert_equal false, User.anonymous.destroy