module Scrivito # Provides a simple wrapper for the CMS Rest API. # # @example Request the published workspace: # Scrivito::CmsRestApi.get('workspaces/published') # # @example Create a new Obj: # Scrivito::CmsRestApi.post('workspaces/001384beff9e5845/objs', # {'obj' => {'_path' => '/new_obj', '_obj_class' => 'Publication'}}) # # @example Update an Obj: # Scrivito::CmsRestApi.put('workspaces/001384beff9e5845/objs/9e432077f0412a63', # {'obj' => {'title' => 'new title'}}) # # @example Delete an Obj: # Scrivito::CmsRestApi.delete('workspaces/001384beff9e5845/objs/f4123622ff07b70b') # # @example Specify a poll interval (in seconds; default: 2) to use in case the response # is a task reference response and the final response is polled for: # Scrivito::CmsRestApi.put('workspace/001384beff9e5845/publish', nil, :interval => 10) # # @example Return immediately with the first response (without polling in case it is a # task reference response): # Scrivito::CmsRestApi.task_unaware_request(:put, 'workspace/001384beff9e5845/publish', nil) # class CmsRestApi METHOD_TO_NET_HTTP_CLASS = { :get => Net::HTTP::Get, :put => Net::HTTP::Put, :post => Net::HTTP::Post, :delete => Net::HTTP::Delete, }.freeze MAX_RATE_LIMIT_RETRY_DURATION = 20.seconds.freeze def self.get(resource_path, payload = nil, options = nil) request_cms_api(:get, resource_path, payload, options) end def self.put(resource_path, payload, options = nil) request_cms_api(:put, resource_path, payload, options) end def self.post(resource_path, payload, options = nil) request_cms_api(:post, resource_path, payload, options) end def self.delete(resource_path, payload = nil, options = nil) request_cms_api(:delete, resource_path, payload, options) end def self.task_unaware_request(method, resource_path, payload = nil) raise "Unexpected method #{method}" unless [:delete, :get, :post, :put].include?(method) response_for_request_cms_api(method, resource_path, payload) end def self.count_requests(path) @count_requests = path @number_of_requests = 0 yield @count_requests = nil @number_of_requests end def self.upload_file(file) upload_permission = get('blobs/upload_permission') uri = URI.parse(upload_permission['url']) File.open(file) do |open_file| content_type = MIME::Types.type_for(file.path).first.content_type upload_io = UploadIO.new(open_file, content_type, File.basename(file)) params = upload_permission['fields'].merge('file' => upload_io) request = Net::HTTP::Post::Multipart.new(uri.path, params) response = ConnectionManager.request(uri, request) if response.code.starts_with?('2') upload_permission['blob'].merge('filename' => File.basename(file.path)) else raise ScrivitoError, "File upload failed with code #{response.code}" end end end class << self private def request_cms_api(action, resource_path, payload, options) @number_of_requests += 1 if resource_path == @count_requests decoded = response_for_request_cms_api(action, resource_path, payload) return decoded unless Hash === decoded return decoded unless decoded.keys == ["task"] task_data = decoded["task"] return decoded unless Hash === task_data task_path = "tasks/#{task_data["id"]}" final_response(task_path, options) end def response_for_request_cms_api(method, resource_path, payload = nil) request = method_to_net_http_class(method).new(path(resource_path)) set_headers(request) request.body = MultiJson.encode(payload) if payload.present? response = nil retried = false wait_until = Time.now + MAX_RATE_LIMIT_RETRY_DURATION begin response = CmsRestApi::RateLimit.retry_on_rate_limit(wait_until) do # lower timeout back to DEFAULT_TIMEOUT once the backend has been fixed connection_manager.request(request, 25) end rescue NetworkError => e if method == :post || retried raise e else retried = true retry end end handle_response(resource_path, response) end def handle_response(resource_path, response) http_code = response.code.to_i if response.code.start_with?('2') MultiJson.load(response.body) elsif response.code == '403' raise AccessDenied.new(response.body) else begin error_body = MultiJson.decode(response.body) specific_output = error_body['error'] if response.code.start_with?('4') backend_code = error_body['code'] message = "'#{specific_output}' on '#{resource_path}'" raise ClientError.new(specific_output, http_code, backend_code) elsif response.code == '500' && specific_output raise BackendError.new(specific_output, http_code) else # 3xx and >500 are treated as NetworkErrors raise NetworkError.new(response.body, http_code) end rescue MultiJson::DecodeError raise NetworkError.new(response.body, http_code) end end end def final_response(task_path, options) options ||= {} wait = options[:interval].presence.try(:to_f) || 2 task_data = response = nil loop do sleep wait task_data = response_for_request_cms_api(:get, task_path, nil) break unless task_data["status"] == "open" end return task_data["result"] if task_data["status"] == "success" message = task_data["message"] || "Missing error message in task response #{task_data}" backend_code = task_data['code'] raise ClientError.new(message, 400, backend_code) end def set_headers(request) request.basic_auth('api_token', Configuration.api_key) request['Content-type'] = 'application/json' request['Accept'] = 'application/json' end def path(path) "/#{Configuration.endpoint_uri.path}/#{path}".squeeze('/') end def connection_manager ConnectionManager.instance end def method_to_net_http_class(method) METHOD_TO_NET_HTTP_CLASS.fetch(method) end end end end