openSUSE/open-build-service

View on GitHub
src/api/app/controllers/request_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
96%
include MaintenanceHelper

class RequestController < ApplicationController
  validate_action show: { method: :get, response: :request }
  validate_action request_create: { method: :post, response: :request }

  # TODO: allow PUT for non-admins
  before_action :require_admin, only: %i[update destroy]

  # GET /request
  def index
    # Do not allow a full collection to avoid server load
    return render_request_collection if params[:view] == 'collection'

    # directory list of all requests. not very useful but for backward compatibility...
    # OBS3: make this more useful
    @request_list = BsRequest.order(:number).pluck(:number)
  end

  class RequireFilter < APIError
    setup 404, 'This call requires at least one filter, either by user, project or package or states or types or reviewstates'
  end

  class SaveError < APIError
    setup 'request_save_error'
  end

  def render_request_collection
    # if all params areblank, something is wrong
    raise RequireFilter if %i[project user states types reviewstates ids].all? { |f| params[f].blank? }

    # convert comma separated values into arrays
    params[:roles] = params[:roles].split(',') if params[:roles]
    params[:types] = params[:types].split(',') if params[:types]
    params[:states] = params[:states].split(',') if params[:states]
    params[:review_states] = params[:reviewstates].split(',') if params[:reviewstates]
    params[:ids] = params[:ids].split(',').map(&:to_i) if params[:ids]

    rel = BsRequest.find_for(params)
    rel = BsRequest.where(id: rel.select(:id)).preload([{ bs_request_actions: :bs_request_action_accept_info, reviews: { history_elements: :user } }])
    rel = rel.limit(params[:limit].to_i) if params[:limit].to_i.positive?
    rel = rel.offset(params[:offset].to_i) if params[:offset].to_i.positive?

    xml = Nokogiri::XML('<collection/>', &:strict).root
    matches = 0
    rel.each do |r|
      matches += 1
      xml.add_child(r.to_axml(params))
    end
    xml['matches'] = matches.to_s
    render xml: xml.to_xml
  end

  # GET /request/:id
  def show
    required_parameters :id
    req = BsRequest.find_by(number: params[:id])
    raise ActiveRecord::RecordNotFound, "Couldn't find Request with number '#{params[:id]}'" if req.nil?

    render xml: req.render_xml(params)
  end

  # POST /request?cmd=create
  def global_command
    raise UnknownCommandError, "Unknown command '#{params[:cmd]}' for path #{request.path}" unless params[:cmd] == 'create'

    # refuse request creation for anonymous users
    require_login
    # no need for dispatch_command, there is only one command
    request_create
  end

  # POST /request/:id?cmd=$CMD
  def request_command
    return request_command_diff if params[:cmd] == 'diff'

    # refuse request manipulation for anonymous users
    require_login

    params[:user] = User.session!.login
    @req = BsRequest.find_by_number!(params[:id])

    # transform request body into query parameter 'comment'
    # the query parameter is preferred if both are set
    params[:comment] = request.raw_post if params[:comment].blank?

    # might raise an exception (which then renders an error)
    # FIXME: this should be moved into the model functions, doing
    #        these actions
    case params[:cmd]
    when 'create', 'changestate', 'addreview', 'setpriority', 'setincident', 'setacceptat', 'approve', 'cancelapproval'
      # create -> noop
      # permissions are checked by the model
      nil
    when 'changereviewstate', 'assignreview'
      @req.permission_check_change_review!(params)
    else
      raise UnknownCommandError, "Unknown command '#{params[:cmd]}' for path #{request.path}"
    end

    # permission granted for the request at this point

    # special command defining an incident to be merged
    params[:check_for_patchinfo] = false

    dispatch_command(:request_command, params[:cmd])
  end

  # PUT /request/:id
  def update
    body = request.raw_post

    Suse::Validator.validate(:request, body)

    BsRequest.transaction do
      oldrequest = BsRequest.find_by_number!(params[:id])
      notify = oldrequest.event_parameters
      oldrequest.destroy

      req = BsRequest.new_from_xml(body)
      req.number = params[:id]
      req.skip_sanitize
      req.save!

      notify[:who] = User.session!.login
      Event::RequestChange.create(notify)

      render xml: req.render_xml
    end
  end

  # DELETE /request/:id
  def destroy
    request = BsRequest.find_by_number!(params[:id])
    notify = request.event_parameters
    request.destroy # throws us out of here if failing
    notify[:who] = User.session!.login
    Event::RequestDelete.create(notify)
    render_ok
  end

  private

  # POST /request?cmd=create
  def request_create
    BsRequest.transaction do
      @req = BsRequest.new_from_xml(request.raw_post.to_s)
      authorize @req, :create?
      @req.set_add_revision       if params[:addrevision].present?
      @req.set_ignore_delegate    if params[:ignore_delegate].present?
      @req.set_ignore_build_state if params[:ignore_build_state].present?
      @req.save!
      Suse::Validator.validate(:request, @req.render_xml)
    end

    render xml: @req.render_xml
  end

  def request_command_diff
    req = BsRequest.find_by_number!(params[:id])
    superseded_request = req.superseding.find_by_number(params[:diff_to_superseded])
    if params[:diff_to_superseded].present? && superseded_request.blank?
      msg = "Request #{params[:diff_to_superseded]} does not exist or is not superseded by request #{req.number}."
      render_error(message: msg, status: 404)
      return
    end

    diff_text = ''
    xml_request = Nokogiri::XML("<request id='#{req.number}'/>", &:strict).root if params[:view] == 'xml'

    req.bs_request_actions.each do |action|
      withissues = params[:withissues].to_s.in?(%w[1 true])
      action_diff = action.sourcediff(
        view: params[:view],
        withissues: withissues,
        superseded_bs_request_action: action.find_action_with_same_target(superseded_request)
      )

      if xml_request
        # Inject backend-provided XML diff into action XML:
        builder = Nokogiri::XML::Builder.new
        action.render_xml(builder)
        xml_request.add_child(builder.doc.root.to_xml)
        xml_request.at_xpath('//request/action[last()]').add_child(action_diff)
      else
        diff_text += action_diff
      end
    end

    if params[:withdescriptionissues].present?
      begin
        data = Backend::Api::IssueTrackers.parse(req.description)
      rescue Backend::Error
        return
      end

      if xml_request
        xml_request.at_xpath('//request').add_child(Nokogiri::XML(data).root)
      else
        diff_text += "Request Description issues:#{data}\n"
      end
    end

    if xml_request
      xml_request['actions'] = '0'
      render xml: xml_request.to_xml
    else
      render plain: diff_text
    end
  end

  def request_command_setincident
    @req.setincident(params[:incident])
    render_ok
  end

  def request_command_setacceptat
    time = Time.parse(params[:time]) if params[:time].present?
    @req.set_accept_at!(time)
    render_ok
  end

  def request_command_addreview
    @req.addreview(params)
    render_ok
  end

  def request_command_setpriority
    @req.setpriority(params)
    render_ok
  end

  def request_command_assignreview
    @req.assignreview(params)
    render_ok
  end

  def request_command_approve
    @req.approve(params)
    render_ok
  end

  def request_command_cancelapproval
    @req.cancelapproval(params)
    render_ok
  end

  def request_command_changereviewstate
    @req.change_review_state(params[:newstate], params)
    render_ok
  end

  class MultipleMaintenanceIncidents < APIError
    setup 404
  end

  def request_command_changestate
    @req.change_state(params)
    render_ok
  end
end