# 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 error_message(error) reportable_error_message(error) || s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.') 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 Jira integration credentials and try again.') when 'Forbidden' s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your Jira integration credentials and try again.') when 'Bad Request' s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.') 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 Jira integration configuration and try again.') % { messages: messages } rescue JSON::ParserError end end def url raise NotImplementedError end def build_service_response(response) raise NotImplementedError end end end end