mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
323 lines
9.5 KiB
Ruby
323 lines
9.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
module Import
|
|
class JiraClient
|
|
class Error < StandardError; end
|
|
|
|
class ConnectionError < Error; end
|
|
|
|
class SsrfError < ConnectionError; end
|
|
|
|
class ParseError < Error; end
|
|
|
|
class ApiError < Error
|
|
attr_reader :status, :response_body
|
|
|
|
def initialize(message, status: nil, response_body: nil)
|
|
super(message)
|
|
@status = status
|
|
@response_body = response_body
|
|
end
|
|
end
|
|
|
|
HTTP_OPTIONS = {
|
|
open_timeout: 30,
|
|
read_timeout: 30
|
|
}.freeze
|
|
|
|
def initialize(url:, personal_access_token:)
|
|
raise ApiError.new(I18n.t(:"admin.jira.test.token_error")) if personal_access_token.nil?
|
|
|
|
@url = url.chomp("/")
|
|
@headers = {
|
|
"Accept" => "application/json",
|
|
"Authorization" => "Bearer #{personal_access_token}"
|
|
}
|
|
end
|
|
|
|
def mypermissions
|
|
get("/rest/api/2/mypermissions")
|
|
end
|
|
|
|
def index_condition_summary
|
|
get("/rest/api/2/index/summary")
|
|
end
|
|
|
|
def server_info
|
|
get("/rest/api/2/serverInfo")
|
|
end
|
|
|
|
def applicationrole
|
|
get("/rest/api/2/applicationrole")
|
|
end
|
|
|
|
def all_cluster_nodes
|
|
get("/rest/api/2/cluster/nodes")
|
|
end
|
|
|
|
def issue(issue_id, fields: "*all", expand: nil)
|
|
get("/rest/api/2/issue/#{issue_id}", params: { fields:, expand: }.compact)
|
|
end
|
|
|
|
def issues(jql: nil, start_at: 0, max_results: 100, fields: "*all", expand: "changelog")
|
|
get("/rest/api/2/search", params:
|
|
{
|
|
jql:,
|
|
startAt: start_at,
|
|
maxResults: max_results,
|
|
fields:,
|
|
expand:
|
|
})
|
|
end
|
|
|
|
def issues_count(jql: nil)
|
|
issues(jql:, max_results: 0, fields: "id")["total"]
|
|
end
|
|
|
|
def projects(expand = "description,projectKeys")
|
|
get("/rest/api/2/project", params: { "expand" => expand })
|
|
end
|
|
|
|
def project_types
|
|
get("/rest/api/2/project/type")
|
|
end
|
|
|
|
def project_versions(project_id_or_key:,
|
|
start_at: 0,
|
|
max_results: 100)
|
|
get("/rest/api/2/project/#{project_id_or_key}/version",
|
|
params: {
|
|
startAt: start_at,
|
|
maxResults: max_results
|
|
})
|
|
end
|
|
|
|
def issue_types
|
|
get("/rest/api/2/issuetype")
|
|
end
|
|
|
|
def issue_types_count
|
|
response = get_response("/rest/api/2/issuetype/page", params: { maxResults: 0 })
|
|
if response.is_a?(Net::HTTPSuccess)
|
|
parse_json(response)["total"]
|
|
else
|
|
issue_types.count
|
|
end
|
|
end
|
|
|
|
def issue_types_schemes
|
|
get("/rest/api/2/issuetypescheme")
|
|
end
|
|
|
|
def workflows
|
|
get("/rest/api/2/workflow")
|
|
end
|
|
|
|
def workflowschemes
|
|
get("/rest/api/2/workflowscheme")
|
|
end
|
|
|
|
def statuses
|
|
get("/rest/api/2/status")
|
|
end
|
|
|
|
def statuses_count
|
|
response = get_response("/rest/api/2/status/search", params: { maxResults: 0 })
|
|
if response.is_a?(Net::HTTPSuccess)
|
|
parse_json(response)["total"]
|
|
else
|
|
statuses.count
|
|
end
|
|
end
|
|
|
|
def status_categories
|
|
get("/rest/api/2/statuscategory")
|
|
end
|
|
|
|
def permissions
|
|
get("/rest/api/2/permissions")
|
|
end
|
|
|
|
def permission_schemes
|
|
get("/rest/api/2/permissionschemes")
|
|
end
|
|
|
|
def priorities
|
|
get("/rest/api/2/priority")
|
|
end
|
|
|
|
def priority_schemes
|
|
get("/rest/api/2/priorityschemes")
|
|
end
|
|
|
|
def roles
|
|
get("/rest/api/2/role")
|
|
end
|
|
|
|
def fields
|
|
get("/rest/api/2/field")
|
|
end
|
|
|
|
def issue_createmeta(project_keys: nil, project_ids: nil, issuetype_ids: nil, expand: "projects.issuetypes.fields")
|
|
params = { expand: }
|
|
params[:projectKeys] = Array(project_keys).join(",") if project_keys.present?
|
|
params[:projectIds] = Array(project_ids).join(",") if project_ids.present?
|
|
params[:issuetypeIds] = Array(issuetype_ids).join(",") if issuetype_ids.present?
|
|
get("/rest/api/2/issue/createmeta", params:)
|
|
end
|
|
|
|
def issue_editmeta(issue_id_or_key)
|
|
get("/rest/api/2/issue/#{issue_id_or_key}/editmeta")
|
|
end
|
|
|
|
def users_search(username: ".", start_at: 0, max_results: 50)
|
|
get("/rest/api/2/user/search", params:
|
|
{
|
|
username:,
|
|
startAt: start_at,
|
|
maxResults: max_results,
|
|
includeActive: true,
|
|
includeInactive: true
|
|
})
|
|
end
|
|
|
|
def user_by_key(key:)
|
|
get("/rest/api/2/user", params: { key:, expand: "groups" })
|
|
end
|
|
|
|
def user_by_username(username:)
|
|
get("/rest/api/2/user", params: { username:, expand: "groups" })
|
|
end
|
|
|
|
def groups(query: "", max_results: 1000)
|
|
get("/rest/api/2/groups/picker", params: { query:, maxResults: max_results })
|
|
end
|
|
|
|
def group_members(group_name: "jira-software-users", start_at: 0, max_results: 500)
|
|
get("/rest/api/2/group/member", params: { groupname: group_name, startAt: start_at, maxResults: max_results })
|
|
end
|
|
|
|
def project_statuses(project_id_or_key)
|
|
get("/rest/api/2/project/#{project_id_or_key}/statuses")
|
|
end
|
|
|
|
def project(project_id_or_key, expand:, properties:)
|
|
get("/rest/api/2/project/#{project_id_or_key}", params:
|
|
{
|
|
expand:,
|
|
properties:
|
|
})
|
|
end
|
|
|
|
##
|
|
# Downloads a file from the given URL and saves it to a temporary file.
|
|
#
|
|
# The temporary file is automatically deleted after the block completes.
|
|
# Use the block to process or copy the file contents before it is removed.
|
|
#
|
|
# @param content_url [String] The URL to download the attachment from
|
|
# @param filename [String] The name to use for the temporary file
|
|
# @yield [File] The temporary file containing the downloaded content
|
|
# @return [nil]
|
|
# @raise [ConnectionError] If SSRF protection blocks the request or connection fails
|
|
# @raise [ApiError] If the server returns a non-success response
|
|
def download_attachment(content_url, filename) # rubocop:disable Metrics/AbcSize
|
|
tempfile = nil
|
|
OpenProject::SsrfProtection.get(content_url, headers: @headers, http_options: HTTP_OPTIONS, max_redirects: 1) do |response|
|
|
case response
|
|
when Net::HTTPSuccess
|
|
tempfile = Tempfile.create(filename, binmode: true)
|
|
response.read_body do |chunk|
|
|
tempfile.write chunk
|
|
end
|
|
yield tempfile
|
|
else
|
|
raise ApiError.new(I18n.t("admin.jira.client.api_error"), status: response.code.to_i, response_body: response.body)
|
|
end
|
|
end
|
|
nil
|
|
rescue SsrfFilter::PrivateIPAddress
|
|
raise SsrfError, I18n.t("admin.jira.client.ssrf_blocked")
|
|
rescue SsrfFilter::Error => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.connection_error", message: e.message)
|
|
rescue OpenSSL::SSL::SSLError => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.ssl_error", message: e.message)
|
|
rescue Timeout::Error => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.connection_timeout", message: e.message)
|
|
ensure
|
|
File.unlink(tempfile) if tempfile
|
|
end
|
|
|
|
private
|
|
|
|
def get(path, params: {})
|
|
response = get_response(path, params:)
|
|
handle_response(response)
|
|
end
|
|
|
|
def get_response(path, params: {})
|
|
OpenProject::SsrfProtection.get(
|
|
"#{@url}#{path}",
|
|
headers: @headers,
|
|
params:,
|
|
http_options: HTTP_OPTIONS
|
|
)
|
|
rescue SsrfFilter::PrivateIPAddress
|
|
raise SsrfError, I18n.t("admin.jira.client.ssrf_blocked")
|
|
rescue SsrfFilter::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.connection_error", message: e.message)
|
|
rescue OpenSSL::SSL::SSLError => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.ssl_error", message: e.message)
|
|
rescue Timeout::Error => e
|
|
raise ConnectionError, I18n.t("admin.jira.client.connection_timeout", message: e.message)
|
|
end
|
|
|
|
def handle_response(response)
|
|
status = response.code.to_i
|
|
if response.is_a?(Net::HTTPSuccess)
|
|
parse_json(response)
|
|
else
|
|
raise ApiError.new(
|
|
I18n.t("admin.jira.client.#{status}_error", status:, default: :"admin.jira.client.api_error"),
|
|
status:,
|
|
response_body: response.body.to_s
|
|
)
|
|
end
|
|
end
|
|
|
|
def parse_json(response)
|
|
JSON.parse(response.body)
|
|
rescue JSON::ParserError => e
|
|
raise ParseError, I18n.t("admin.jira.client.parse_error", message: e.message)
|
|
end
|
|
end
|
|
end
|