diff --git a/app/models/messenger.rb b/app/models/messenger.rb index 7683021..cdefda6 100644 --- a/app/models/messenger.rb +++ b/app/models/messenger.rb @@ -3,251 +3,261 @@ require 'net/http' class Messenger include Redmine::I18n - def self.markup_format(text) - # TODO: output format should be markdown, but at the moment there is no - # solution without using pandoc (http://pandoc.org/), which requires - # packages on os level - # - # Redmine::WikiFormatting.html_parser.to_text(text) - ERB::Util.html_escape(text) - end + class << self + def markup_format(text) + # TODO: output format should be markdown, but at the moment there is no + # solution without using pandoc (http://pandoc.org/), which requires + # packages on os level + # + # Redmine::WikiFormatting.html_parser.to_text(text) - def self.default_url_options - { only_path: true, script_name: Redmine::Utils.relative_url_root } - end + text = text.to_s + text.gsub!('&', '&') + text.gsub!('<', '<') + text.gsub!('>', '>') - def self.speak(msg, channels, url, options) - url ||= RedmineMessenger.settings[:messenger_url] - - return if url.blank? - return if channels.blank? - - params = { - text: msg, - link_names: 1 - } - - username = Messenger.textfield_for_project(options[:project], :messenger_username) - params[:username] = username if username.present? - params[:attachments] = [options[:attachment]] if options[:attachment]&.any? - - icon = Messenger.textfield_for_project(options[:project], :messenger_icon) - if icon.present? - if icon.start_with? ':' - params[:icon_emoji] = icon - else - params[:icon_url] = icon - end + text end - channels.each do |channel| - uri = URI(url) - params[:channel] = channel - http_options = { use_ssl: uri.scheme == 'https' } - http_options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE unless RedmineMessenger.setting?(:messenger_verify_ssl) + def default_url_options + { only_path: true, script_name: Redmine::Utils.relative_url_root } + end - begin - req = Net::HTTP::Post.new(uri) - req.set_form_data(payload: params.to_json) - Net::HTTP.start(uri.hostname, uri.port, http_options) do |http| - response = http.request(req) - Rails.logger.warn(response) unless [Net::HTTPSuccess, Net::HTTPRedirection, Net::HTTPOK].include? response + def speak(msg, channels, url, options) + url ||= RedmineMessenger.settings[:messenger_url] + + return if url.blank? + return if channels.blank? + + params = { + text: msg, + link_names: 1 + } + + username = Messenger.textfield_for_project(options[:project], :messenger_username) + params[:username] = username if username.present? + params[:attachments] = [options[:attachment]] if options[:attachment]&.any? + + icon = Messenger.textfield_for_project(options[:project], :messenger_icon) + if icon.present? + if icon.start_with? ':' + params[:icon_emoji] = icon + else + params[:icon_url] = icon + end + end + + channels.each do |channel| + uri = URI(url) + params[:channel] = channel + http_options = { use_ssl: uri.scheme == 'https' } + http_options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE unless RedmineMessenger.setting?(:messenger_verify_ssl) + + begin + req = Net::HTTP::Post.new(uri) + req.set_form_data(payload: params.to_json) + Net::HTTP.start(uri.hostname, uri.port, http_options) do |http| + response = http.request(req) + Rails.logger.warn(response) unless [Net::HTTPSuccess, Net::HTTPRedirection, Net::HTTPOK].include? response + end + rescue StandardError => e + Rails.logger.warn("cannot connect to #{url}") + Rails.logger.warn(e) end - rescue StandardError => e - Rails.logger.warn("cannot connect to #{url}") - Rails.logger.warn(e) end end - end - def self.object_url(obj) - if Setting.host_name.to_s =~ %r{\A(https?\://)?(.+?)(\:(\d+))?(/.+)?\z}i - host = Regexp.last_match(2) - port = Regexp.last_match(4) - prefix = Regexp.last_match(5) - Rails.application.routes.url_for(obj.event_url(host: host, protocol: Setting.protocol, port: port, script_name: prefix)) - else - Rails.application.routes.url_for(obj.event_url(host: Setting.host_name, protocol: Setting.protocol, script_name: '')) - end - end - - def self.url_for_project(proj) - return if proj.blank? - - # project based - pm = MessengerSetting.find_by(project_id: proj.id) - return pm.messenger_url if !pm.nil? && pm.messenger_url.present? - - # parent project based - parent_url = url_for_project(proj.parent) - return parent_url if parent_url.present? - # system based - return RedmineMessenger.settings[:messenger_url] if RedmineMessenger.settings[:messenger_url].present? - - nil - end - - def self.textfield_for_project(proj, config) - return if proj.blank? - - # project based - pm = MessengerSetting.find_by(project_id: proj.id) - return pm.send(config) if !pm.nil? && pm.send(config).present? - - default_textfield(proj, config) - end - - def self.default_textfield(proj, config) - # parent project based - parent_field = textfield_for_project(proj.parent, config) - return parent_field if parent_field.present? - return RedmineMessenger.settings[config] if RedmineMessenger.settings[config].present? - - '' - end - - def self.channels_for_project(proj) - return [] if proj.blank? - - # project based - pm = MessengerSetting.find_by(project_id: proj.id) - if !pm.nil? && pm.messenger_channel.present? - return [] if pm.messenger_channel == '-' - - return pm.messenger_channel.split(',').map!(&:strip).uniq - end - default_project_channels(proj) - end - - def self.default_project_channels(proj) - # parent project based - parent_channel = channels_for_project(proj.parent) - return parent_channel if parent_channel.present? - # system based - if RedmineMessenger.settings[:messenger_channel].present? && - RedmineMessenger.settings[:messenger_channel] != '-' - return RedmineMessenger.settings[:messenger_channel].split(',').map!(&:strip).uniq + def object_url(obj) + if Setting.host_name.to_s =~ %r{\A(https?\://)?(.+?)(\:(\d+))?(/.+)?\z}i + host = Regexp.last_match(2) + port = Regexp.last_match(4) + prefix = Regexp.last_match(5) + Rails.application.routes.url_for(obj.event_url(host: host, protocol: Setting.protocol, port: port, script_name: prefix)) + else + Rails.application.routes.url_for(obj.event_url(host: Setting.host_name, protocol: Setting.protocol, script_name: '')) + end end - [] - end + def url_for_project(proj) + return if proj.blank? - def self.setting_for_project(proj, config) - return false if proj.blank? + # project based + pm = MessengerSetting.find_by(project_id: proj.id) + return pm.messenger_url if !pm.nil? && pm.messenger_url.present? - @setting_found = 0 - # project based - pm = MessengerSetting.find_by(project_id: proj.id) - unless pm.nil? || pm.send(config).zero? - @setting_found = 1 - return false if pm.send(config) == 1 - return true if pm.send(config) == 2 - # 0 = use system based settings + # parent project based + parent_url = url_for_project(proj.parent) + return parent_url if parent_url.present? + # system based + return RedmineMessenger.settings[:messenger_url] if RedmineMessenger.settings[:messenger_url].present? + + nil end - default_project_setting(proj, config) - end - def self.default_project_setting(proj, config) - if proj.present? && proj.parent.present? - parent_setting = setting_for_project(proj.parent, config) - return parent_setting if @setting_found == 1 + def textfield_for_project(proj, config) + return if proj.blank? + + # project based + pm = MessengerSetting.find_by(project_id: proj.id) + return pm.send(config) if !pm.nil? && pm.send(config).present? + + default_textfield(proj, config) end - # system based - return true if RedmineMessenger.settings[config].present? && RedmineMessenger.setting?(config) - false - end + def default_textfield(proj, config) + # parent project based + parent_field = textfield_for_project(proj.parent, config) + return parent_field if parent_field.present? + return RedmineMessenger.settings[config] if RedmineMessenger.settings[config].present? - def self.detail_to_field(detail) - field_format = nil - key = nil - escape = true + '' + end - if detail.property == 'cf' - key = CustomField.find(detail.prop_key).name rescue nil - title = key - field_format = CustomField.find(detail.prop_key).field_format rescue nil - elsif detail.property == 'attachment' - key = 'attachment' - title = I18n.t :label_attachment - else - key = detail.prop_key.to_s.sub('_id', '') - title = if key == 'parent' - I18n.t "field_#{key}_issue" + def channels_for_project(proj) + return [] if proj.blank? + + # project based + pm = MessengerSetting.find_by(project_id: proj.id) + if !pm.nil? && pm.messenger_channel.present? + return [] if pm.messenger_channel == '-' + + return pm.messenger_channel.split(',').map!(&:strip).uniq + end + default_project_channels(proj) + end + + def setting_for_project(proj, config) + return false if proj.blank? + + @setting_found = 0 + # project based + pm = MessengerSetting.find_by(project_id: proj.id) + unless pm.nil? || pm.send(config).zero? + @setting_found = 1 + return false if pm.send(config) == 1 + return true if pm.send(config) == 2 + # 0 = use system based settings + end + default_project_setting(proj, config) + end + + def default_project_setting(proj, config) + if proj.present? && proj.parent.present? + parent_setting = setting_for_project(proj.parent, config) + return parent_setting if @setting_found == 1 + end + # system based + return true if RedmineMessenger.settings[config].present? && RedmineMessenger.setting?(config) + + false + end + + def detail_to_field(detail) + field_format = nil + key = nil + escape = true + + if detail.property == 'cf' + key = CustomField.find(detail.prop_key).name rescue nil + title = key + field_format = CustomField.find(detail.prop_key).field_format rescue nil + elsif detail.property == 'attachment' + key = 'attachment' + title = I18n.t :label_attachment + else + key = detail.prop_key.to_s.sub('_id', '') + title = if key == 'parent' + I18n.t "field_#{key}_issue" + else + I18n.t "field_#{key}" + end + end + + short = true + value = detail.value.to_s + + case key + when 'title', 'subject', 'description' + short = false + when 'tracker' + tracker = Tracker.find(detail.value) + value = tracker.to_s if tracker.present? + when 'project' + project = Project.find(detail.value) + value = project.to_s if project.present? + when 'status' + status = IssueStatus.find(detail.value) + value = status.to_s if status.present? + when 'priority' + priority = IssuePriority.find(detail.value) + value = priority.to_s if priority.present? + when 'category' + category = IssueCategory.find(detail.value) + value = category.to_s if category.present? + when 'assigned_to' + user = User.find(detail.value) + value = user.to_s if user.present? + when 'fixed_version' + fixed_version = Version.find(detail.value) + value = fixed_version.to_s if fixed_version.present? + when 'attachment' + attachment = Attachment.find(detail.prop_key) + value = "<#{Messenger.object_url attachment}|#{markup_format(attachment.filename)}>" if attachment.present? + escape = false + when 'parent' + issue = Issue.find(detail.value) + value = "<#{Messenger.object_url issue}|#{markup_format(issue)}>" if issue.present? + escape = false + end + + if detail.property == 'cf' && field_format == 'version' + version = Version.find(detail.value) + value = version.to_s if version.present? + end + + value = if value.present? + if escape + markup_format(value) + else + value + end else - I18n.t "field_#{key}" + '-' end + + result = { title: title, value: value } + result[:short] = true if short + result end - short = true - value = detail.value.to_s - - case key - when 'title', 'subject', 'description' - short = false - when 'tracker' - tracker = Tracker.find(detail.value) - value = tracker.to_s if tracker.present? - when 'project' - project = Project.find(detail.value) - value = project.to_s if project.present? - when 'status' - status = IssueStatus.find(detail.value) - value = status.to_s if status.present? - when 'priority' - priority = IssuePriority.find(detail.value) - value = priority.to_s if priority.present? - when 'category' - category = IssueCategory.find(detail.value) - value = category.to_s if category.present? - when 'assigned_to' - user = User.find(detail.value) - value = user.to_s if user.present? - when 'fixed_version' - fixed_version = Version.find(detail.value) - value = fixed_version.to_s if fixed_version.present? - when 'attachment' - attachment = Attachment.find(detail.prop_key) - value = "<#{Messenger.object_url attachment}|#{ERB::Util.html_escape(attachment.filename)}>" if attachment.present? - escape = false - when 'parent' - issue = Issue.find(detail.value) - value = "<#{Messenger.object_url issue}|#{ERB::Util.html_escape(issue)}>" if issue.present? - escape = false + def mentions(project, text) + names = [] + Messenger.textfield_for_project(project, :default_mentions) + .split(',').each { |m| names.push m.strip } + names += extract_usernames(text) unless text.nil? + names.present? ? ' To: ' + names.uniq.join(', ') : nil end - if detail.property == 'cf' && field_format == 'version' - version = Version.find(detail.value) - value = version.to_s if version.present? + private + + def extract_usernames(text) + text = '' if text.nil? + # messenger usernames may only contain lowercase letters, numbers, + # dashes, dots and underscores and must start with a letter or number. + text.scan(/@[a-z0-9][a-z0-9_\-.]*/).uniq end - value = if value.present? - if escape - ERB::Util.html_escape(value) - else - value - end - else - '-' - end + def default_project_channels(proj) + # parent project based + parent_channel = channels_for_project(proj.parent) + return parent_channel if parent_channel.present? + # system based + if RedmineMessenger.settings[:messenger_channel].present? && + RedmineMessenger.settings[:messenger_channel] != '-' + return RedmineMessenger.settings[:messenger_channel].split(',').map!(&:strip).uniq + end - result = { title: title, value: value } - result[:short] = true if short - result - end - - def self.mentions(project, text) - names = [] - Messenger.textfield_for_project(project, :default_mentions) - .split(',').each { |m| names.push m.strip } - names += extract_usernames(text) unless text.nil? - names.present? ? ' To: ' + names.uniq.join(', ') : nil - end - - def self.extract_usernames(text) - text = '' if text.nil? - # messenger usernames may only contain lowercase letters, numbers, - # dashes, dots and underscores and must start with a letter or number. - text.scan(/@[a-z0-9][a-z0-9_\-.]*/).uniq + [] + end end end diff --git a/lib/redmine_messenger/hooks.rb b/lib/redmine_messenger/hooks.rb index c651366..bc191eb 100644 --- a/lib/redmine_messenger/hooks.rb +++ b/lib/redmine_messenger/hooks.rb @@ -11,7 +11,7 @@ module RedmineMessenger return unless channels.present? && url && issue.changes.any? && Messenger.setting_for_project(issue.project, :post_updates) return if issue.is_private? && !Messenger.setting_for_project(issue.project, :post_private_issues) - msg = "[#{ERB::Util.html_escape(issue.project)}] #{ERB::Util.html_escape(journal.user.to_s)} updated <#{Messenger.object_url issue}|#{ERB::Util.html_escape(issue)}>" + msg = "[#{Messenger.markup_format(issue.project)}] #{Messenger.markup_format(journal.user.to_s)} updated <#{Messenger.object_url issue}|#{Messenger.markup_format(issue)}>" repository = changeset.repository @@ -43,7 +43,7 @@ module RedmineMessenger end attachment = {} - attachment[:text] = ll(Setting.default_language, :text_status_changed_by_changeset, "<#{revision_url}|#{ERB::Util.html_escape(changeset.comments)}>") + attachment[:text] = ll(Setting.default_language, :text_status_changed_by_changeset, "<#{revision_url}|#{Messenger.markup_format(changeset.comments)}>") attachment[:fields] = journal.details.map { |d| Messenger.detail_to_field d } Messenger.speak(msg, channels, url, attachment: attachment, project: repository.project) diff --git a/lib/redmine_messenger/patches/contact_patch.rb b/lib/redmine_messenger/patches/contact_patch.rb index eb874d3..f7aaa92 100644 --- a/lib/redmine_messenger/patches/contact_patch.rb +++ b/lib/redmine_messenger/patches/contact_patch.rb @@ -22,7 +22,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_contact_created, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) @@ -40,7 +40,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_contact_updated, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) diff --git a/lib/redmine_messenger/patches/db_entry_patch.rb b/lib/redmine_messenger/patches/db_entry_patch.rb index 6f55fae..813c083 100644 --- a/lib/redmine_messenger/patches/db_entry_patch.rb +++ b/lib/redmine_messenger/patches/db_entry_patch.rb @@ -22,7 +22,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_db_entry_created, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) @@ -40,7 +40,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_db_entry_updated, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) diff --git a/lib/redmine_messenger/patches/issue_patch.rb b/lib/redmine_messenger/patches/issue_patch.rb index a0df5b1..ae62779 100644 --- a/lib/redmine_messenger/patches/issue_patch.rb +++ b/lib/redmine_messenger/patches/issue_patch.rb @@ -24,27 +24,27 @@ module RedmineMessenger attachment[:text] = Messenger.markup_format(description) end attachment[:fields] = [{ title: I18n.t(:field_status), - value: ERB::Util.html_escape(status.to_s), + value: Messenger.markup_format(status.to_s), short: true }, { title: I18n.t(:field_priority), - value: ERB::Util.html_escape(priority.to_s), + value: Messenger.markup_format(priority.to_s), short: true }] if assigned_to.present? attachment[:fields] << { title: I18n.t(:field_assigned_to), - value: ERB::Util.html_escape(assigned_to.to_s), + value: Messenger.markup_format(assigned_to.to_s), short: true } end if RedmineMessenger.setting?(:display_watchers) && watcher_users.count.positive? attachment[:fields] << { title: I18n.t(:field_watcher), - value: ERB::Util.html_escape(watcher_users.join(', ')), + value: Messenger.markup_format(watcher_users.join(', ')), short: true } end Messenger.speak(l(:label_messenger_issue_created, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: send_messenger_mention_url(project, description), user: author), channels, url, attachment: attachment, project: project) @@ -70,24 +70,24 @@ module RedmineMessenger fields = current_journal.details.map { |d| Messenger.detail_to_field d } if saved_change_to_status_id? fields << { title: I18n.t(:field_status), - value: ERB::Util.html_escape(status.to_s), + value: Messenger.markup_format(status.to_s), short: true } end if saved_change_to_priority_id? fields << { title: I18n.t(:field_priority), - value: ERB::Util.html_escape(priority.to_s), + value: Messenger.markup_format(priority.to_s), short: true } end if assigned_to.present? fields << { title: I18n.t(:field_assigned_to), - value: ERB::Util.html_escape(assigned_to.to_s), + value: Messenger.markup_format(assigned_to.to_s), short: true } end attachment[:fields] = fields if fields.any? Messenger.speak(l(:label_messenger_issue_updated, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: send_messenger_mention_url(project, description), user: current_journal.user), channels, url, attachment: attachment, project: project) @@ -101,7 +101,7 @@ module RedmineMessenger Messenger.textfield_for_project(project, :default_mentions).present? mention_to = Messenger.mentions(project, text) end - "<#{Messenger.object_url(self)}|#{ERB::Util.html_escape(self)}>#{mention_to}" + "<#{Messenger.object_url(self)}|#{Messenger.markup_format(self)}>#{mention_to}" end end end diff --git a/lib/redmine_messenger/patches/password_patch.rb b/lib/redmine_messenger/patches/password_patch.rb index 31c855e..6dea40a 100644 --- a/lib/redmine_messenger/patches/password_patch.rb +++ b/lib/redmine_messenger/patches/password_patch.rb @@ -22,7 +22,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_password_created, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) @@ -40,7 +40,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_password_updated, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{name}>", user: User.current), channels, url, project: project) diff --git a/lib/redmine_messenger/patches/wiki_page_patch.rb b/lib/redmine_messenger/patches/wiki_page_patch.rb index 5288bac..d67d8eb 100644 --- a/lib/redmine_messenger/patches/wiki_page_patch.rb +++ b/lib/redmine_messenger/patches/wiki_page_patch.rb @@ -21,7 +21,7 @@ module RedmineMessenger return unless channels.present? && url Messenger.speak(l(:label_messenger_wiki_created, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{title}>", user: User.current), channels, url, project: project) @@ -44,7 +44,7 @@ module RedmineMessenger end Messenger.speak(l(:label_messenger_wiki_updated, - project_url: "<#{Messenger.object_url project}|#{ERB::Util.html_escape(project)}>", + project_url: "<#{Messenger.object_url project}|#{Messenger.markup_format(project)}>", url: "<#{Messenger.object_url self}|#{title}>", user: content.author), channels, url, project: project, attachment: attachment)