summaryrefslogtreecommitdiff
path: root/app/services/jira/requests/base.rb
blob: a16f8bbd3674934f2dcbe05916c66d766f52844b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# frozen_string_literal: true

module Jira
  module Requests
    class Base
      include ProjectServicesLoggable

      JIRA_API_VERSION = 2
      # Limit the size of the JSON error message we will attempt to parse, as the JSON is external input.
      JIRA_ERROR_JSON_SIZE_LIMIT = 5_000

      ERRORS = {
        connection: [Errno::ECONNRESET, Errno::ECONNREFUSED],
        jira_ruby:  JIRA::HTTPError,
        ssl:        OpenSSL::SSL::SSLError,
        timeout:    [Timeout::Error, Errno::ETIMEDOUT],
        uri:        [URI::InvalidURIError, SocketError]
      }.freeze
      ALL_ERRORS = ERRORS.values.flatten.freeze

      def initialize(jira_integration, params = {})
        @project = jira_integration&.project
        @jira_integration = jira_integration
      end

      def execute
        return ServiceResponse.error(message: _('Jira service not configured.')) unless jira_integration&.active?

        request
      end

      private

      attr_reader :jira_integration, :project

      # We have to add the context_path here because the Jira client is not taking it into account
      def base_api_url
        "#{context_path}/rest/api/#{api_version}"
      end

      def context_path
        client.options[:context_path].to_s
      end

      # override this method in the specific request class implementation if a differnt API version is required
      def api_version
        JIRA_API_VERSION
      end

      def client
        @client ||= jira_integration.client
      end

      def request
        response = client.get(url)
        build_service_response(response)
      rescue *ALL_ERRORS => e
        log_error('Error sending message',
          client_url: client.options[:site],
          error: {
            exception_class: e.class.name,
            exception_message: e.message,
            exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace)
          }
        )

        ServiceResponse.error(message: error_message(e))
      end

      def auth_docs_link_start
        auth_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira', anchor: 'authentication-in-jira')
        '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auth_docs_link_url }
      end

      def config_docs_link_start
        config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
        '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url }
      end

      def error_message(error)
        reportable_error_message(error) ||
          s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
      end

      # Returns a user-facing error message if possible, otherwise `nil`.
      def reportable_error_message(error)
        case error
        when ERRORS[:jira_ruby]
          reportable_jira_ruby_error_message(error)
        when ERRORS[:ssl]
          s_('JiraRequest|An SSL error occurred while connecting to Jira: %{message}. Try your request again.') % { message: error.message }
        when *ERRORS[:uri]
          s_('JiraRequest|The Jira API URL for connecting to Jira is not valid. Check your Jira integration API URL and try again.')
        when *ERRORS[:timeout]
          s_('JiraRequest|A timeout error occurred while connecting to Jira. Try your request again.')
        when *ERRORS[:connection]
          s_('JiraRequest|A connection error occurred while connecting to Jira. Try your request again.')
        end
      end

      # Returns a user-facing error message for a `JIRA::HTTPError` if possible,
      # otherwise `nil`.
      def reportable_jira_ruby_error_message(error)
        case error.message
        when 'Unauthorized'
          s_('JiraRequest|The credentials for accessing Jira are not valid. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
        when 'Forbidden'
          s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
        when 'Bad Request'
          s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
        when /errorMessages/
          jira_ruby_json_error_message(error.message)
        end
      end

      def jira_ruby_json_error_message(error_message)
        return if error_message.length > JIRA_ERROR_JSON_SIZE_LIMIT

        begin
          messages = Gitlab::Json.parse(error_message)['errorMessages']&.to_sentence
          messages = Rails::Html::FullSanitizer.new.sanitize(messages).presence
          return unless messages

          s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { messages: messages, docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
        rescue JSON::ParserError
        end
      end

      def url
        raise NotImplementedError
      end

      def build_service_response(response)
        raise NotImplementedError
      end
    end
  end
end