openjaf/cenit

View on GitHub
app/controllers/api/v2/api_controller.rb

Summary

Maintainability
F
1 wk
Test Coverage
module Api::V2
  class ApiController < ApplicationController

    before_action :authorize_account, except: [:new_user, :cors_check, :auth]
    before_action :save_request_data, except: [:cors_check, :auth]
    before_action :authorize_action, except: [:auth, :new_user, :cors_check, :push]
    before_action :find_item, only: [:update, :show, :destroy, :pull, :run, :retry, :authorize]

    rescue_from Exception, with: :exception_handler

    respond_to :json

    def cors_check
      self.cors_header
      render text: '', content_type: 'text/plain'
    end

    def index
      page = get_page
      res =
        if klass
          @render_options.delete(:inspecting)
          if (model_ignore = klass.index_ignore_properties).present?
            @render_options[:ignore] =
              if (ignore_option = @render_options[:ignore])
                unless ignore_option.is_a?(Array)
                  ignore_option = ignore_option.to_s.split(',').collect(&:strip)
                end
                ignore_option + model_ignore
              else
                model_ignore
              end
          end
          maximum_entries =
            if (account = Account.current)
              account.index_max_entries
            else
              Account::DEFAULT_INDEX_MAX_ENTRIES
            end
          @render_options[:max_entries] =
            if (max_entries = @render_options[:max_entries])
              max_entries = max_entries.to_i
              if max_entries.zero? || max_entries > maximum_entries
                maximum_entries
              else
                max_entries
              end
            else
              maximum_entries
            end
          items = select_items
          items_data = items.map do |item|
            hash = item.default_hash(@render_options)
            @view.nil? ? hash : hash[@view]
          end
          count = items.count
          {
            json: {
              total_pages: (count * 1.0 / get_limit).ceil,
              current_page: page,
              count: count,
              @model.pluralize => items_data
            }
          }
        else
          {
            json: { error: 'no model found' },
            status: :not_found
          }
        end
      render res
    end

    def show
      if @item.orm_model.data_type.is_a?(Setup::FileDataType)
        send_data @item.data, filename: @item[:filename], type: @item[:contentType]
      else
        render json: @view.nil? ? @item.to_hash(@render_options) : @item.to_hash(@render_options)[@view]
      end
    end

    def content
      render json: @view.nil? ? @item.to_hash : { @view => @item.to_hash[@view] }
    end

    def push
      response =
        {
          success: success_report = Hash.new { |h, k| h[k] = [] },
          errors: broken_report = Hash.new { |h, k| h[k] = [] }
        }
      @parser_options[:add_only] = true unless @parser_options.key?('add_only')
      @payload.each do |root, message|
        @model = root
        if authorized_action? && (data_type = @payload.data_type_for(root))
          message = [message] unless message.is_a?(Array)
          message.each do |item|
            options = @parser_options.merge(create_collector: Set.new).symbolize_keys
            model = data_type.records_model
            if model.is_a?(Class) && model < FieldsInspection
              options[:inspect_fields] = Account.current.nil? || !::User.super_access?
            end
            if (record = data_type.send(@payload.create_method,
                                        @payload.process_item(item, data_type, options),
                                        options)).errors.blank?
              success_report[root.pluralize] << record.inspect_json(include_id: true, inspect_scope: options[:create_collector])
            else
              broken_report[root] << { errors: record.errors.full_messages, item: item }
            end
          end
        else
          broken_report[root] = 'no model found'
        end
      end
      response.delete(:success) if success_report.blank?
      response.delete(:errors) if broken_report.blank?
      render json: response, status: 202
    end

    def data_type_digest
      execution = ::Setup::DataTypeDigest.process(
        data_type: klass.data_type,
        payload: @webhook_body,
        options: @parser_options
      )
      render json: execution.to_hash(include_id: true, include_blanks: false)
    end

    def new
      response = {}
      %w(success warnings errors).each { |key| response[key.to_sym] = Hash.new { |h, k| h[k] = [] } }
      @payload.each do |root, message|
        if (data_type = @payload.data_type_for(root))
          message = [message] unless message.is_a?(Array)
          message.each do |item|
            begin
              options = @parser_options.merge(create_collector: Set.new).symbolize_keys
              model = data_type.records_model
              if model.is_a?(Class) && model < FieldsInspection
                options[:inspect_fields] = Account.current.nil? || !::User.super_access?
              end
              if (record = data_type.send(@payload.create_method,
                                          @payload.process_item(item, data_type, options),
                                          options)).errors.blank?
                response[:success][root] << record.inspect_json(include_id: true, inspect_scope: options[:create_collector])
                if (warnings = record.try(:warnings))
                  warnings =
                    begin
                      warnings.to_json
                    rescue
                      nil
                    end
                  response[:warnings][root] << { record.id.to_s => JSON.parse(warnings) } if warnings
                end
              else
                response[:errors][root] << { errors: record.errors.full_messages, item: item }
              end
            rescue Exception => ex
              response[:errors][root] = { errors: ex.message, item: item }
            end
          end
        else
          response[:errors][root] = 'no model found'
        end
      end
      response.reject! { |_, v| v.blank? }
      render json: response
    end

    def update
      @payload.each do |_, message|
        message = message.first if message.is_a?(Array)
        @parser_options[:add_only] = true
        @item.send(@payload.update_method, message, @parser_options)
        save_options = {}
        if @item.class.is_a?(Class) && @item.class < FieldsInspection
          save_options[:inspect_fields] = Account.current.nil? || !::User.super_access?
        end
        if Cenit::Utility.save(@item, save_options)
          if (warnings = @item.try(:warnings))
            warnings =
              begin
                warnings.to_json
              rescue
                nil
              end
            response.headers['X-Warnings'] = warnings if warnings
          end
          find_item
          render json: @item.to_hash, status: :ok
        else
          render json: { errors: @item.errors.full_messages }, status: :not_acceptable
        end
        break
      end
    end

    def destroy
      if @item.destroy
        render json: { status: :ok }
      else
        render json: { errors: @item.errors.full_messages }, status: :not_acceptable
      end
    end

    def run
      if @item.is_a?(Setup::Algorithm)
        begin
          execution = Setup::AlgorithmExecution.process(algorithm_id: @item.id,
                                                        input: @webhook_body,
                                                        skip_notification_level: true)
          execution.reload
          render json: execution.to_hash(include_blanks: false)
        rescue Exception => ex
          render json: { error: ex.message }, status: 406
        end
      else
        render json: { status: :not_allowed }, status: 405
      end
    end

    def pull
      if @item.is_a?(Setup::CrossSharedCollection)
        begin
          pull_request = @webhook_body.present? ? JSON.parse(@webhook_body) : {}
          render json: @item.pull(pull_request).to_json
        rescue Exception => ex
          render json: { error: ex.message, status: :bad_request }
        end
      else
        render json: { status: :not_allowed }
      end
    end

    def retry
      if @item.is_a?(Setup::Task)
        if @item.can_retry?
          @item.retry
          render json: { status: :ok }
        else
          render json: { status: "Task #{@item.id} is #{task.sattus}" }, status: :not_acceptable
        end
      else
        render json: { status: :not_allowed }, status: :bad_request
      end
    end

    def authorize
      if (auth = @item).is_a?(Setup::Oauth2Authorization)
        if (redirect_uri = params[:redirect_uri])
          if auth.check
            cenit_token = CallbackAuthorizationToken.create(authorization: auth, data: { redirect_uri: redirect_uri })
            auth_url = auth.authorize_url(cenit_token: cenit_token)
            cenit_token.save
            render json: { authorize_url: auth_url }
          else
            render json: { error: "Unable to authorize #{auth.custom_title}: #{auth.errors.full_messages.to_sentence}" }, status: :unprocessable_entity
          end
        else
          render json: { error: 'Missing parameter redirect_uri' }, status: :bad_request
        end
      else
        render json: { status: :not_allowed }, status: :bad_request
      end
    end

    def auth
      authorize_account
      if Account.current
        self.cors_header
        render json: { status: 'Sucess Auth' }, status: 200
      else
        self.cors_header
        render json: { status: 'Error Auth' }, status: 401
      end
    end

    USER_MODEL_FIELDS = %w(name email password password_confirmation)
    USER_API_FIELDS = USER_MODEL_FIELDS + %w(token code)

    def new_user
      data = (JSON.parse(@webhook_body) rescue {}).keep_if { |key, _| USER_API_FIELDS.include?(key) }
      data = data.with_indifferent_access
      data.reverse_merge!(email: params[:email], password: pwd = params[:password], password_confirmation: params[:password_confirmation] || pwd)
      data.reject! { |_, value| value.nil? }
      status = :not_acceptable
      response =
        if (token = data[:token] || params[:token])
          if (captcha_token = CaptchaToken.where(token: token).first)
            if (code = data[:code] || params[:code])
              if code == captcha_token.code
                token_data = captcha_token.data || {}
                if !data.key?(:email) || data[:email] == token_data[:email]
                  data.merge!(captcha_token.data || {}) { |_, left, right| left || right }
                  captcha_token.destroy
                  _, status, response = create_user_with(data)
                  response
                else #email mismatch
                  { email: ['does not match the one previously requested'] }
                end
              else #invalid code
                { code: ['is not valid'] }
              end
            else #code missing
              { code: ['is missing'] }
            end
          else #invalid token
            { token: ['is not valid'] }
          end
        elsif data[:email]
          data[:password] = Devise.friendly_token unless data[:password]
          data[:password_confirmation] = data[:password] unless data[:password_confirmation]
          if (user = User.new(data)).valid?(context: :create)
            if (captcha_token = CaptchaToken.create(email: data[:email], data: data)).errors.blank?
              status = :ok
              { token: captcha_token.token }
            else
              captcha_token.errors.to_json
            end
          else
            user.errors.to_json
          end
        else #bad request
          status = :bad_request
          { token: ['is missing'], email: ['is missing'] }
        end
      render json: response, status: status
    end

    protected

    def create_user_with(data)
      status = :not_acceptable
      data[:password] ||= Devise.friendly_token
      data[:password_confirmation] ||= data[:password]
      data.reject! { |key, _| USER_MODEL_FIELDS.exclude?(key) }
      current_account = Account.current
      begin
        Account.current = nil
        (user = ::User.new(data)).save
      rescue
        user #TODO Handle sending confirmation email error
      ensure
        Account.current = current_account
      end
      response =
        if user.errors.blank?
          status = :ok
          { id: user.id.to_s, number: user.number, token: user.authentication_token }
        else
          user.errors.to_json
        end
      [user, status, response]
    end

    def get_limit
      unless @limit
        limit_option = @criteria_options.delete(:limit)
        limit = (@criteria.delete(:limit) || limit_option).to_i
        @limit =
          if limit < 1
            Kaminari.config.default_per_page
          else
            [Kaminari.config.default_per_page, limit].min
          end
      end
      @limit
    end

    def get_page
      @page ||=
        if (page = @criteria.delete(:page))
          page.to_i
        else
          1
        end
    end

    def select_items
      asc = true
      if (order = @criteria.delete(:order))
        order.strip!
        asc = !order.match(/^-.*/)
      end

      limit = get_limit
      page = get_page
      skip = page < 1 ? 0 : (page - 1) * limit

      # TODO: Include Kaminari methods on CrossOrigin::Criteria
      items = accessible_records.limit(limit).skip(skip).where(@criteria)

      if (sort = @criteria_options[:sort])
        sort.each do |field, sort_option|
          items =
            case sort_option
            when 1
              items.asc(field)
            when -1
              items.desc(field)
            else
              items
            end
        end
      end
      if order
        if asc
          items.ascending(*order.split(','))
        else
          items.descending(*order.slice(1..-1).split(','))
        end
      else
        items
      end
    end

    def authorize_account
      user = nil
      if (auth_header = request.headers['Authorization'])
        auth_header = auth_header.to_s.squeeze(' ').strip.split(' ')
        if auth_header.length == 2
          access_token = Cenit::OauthAccessToken.where(token_type: auth_header[0], token: auth_header[1]).first
          if access_token && access_token.alive?
            if access_token.set_current_tenant!
              access_grant = Cenit::OauthAccessGrant.where(application_id: access_token.application_id).first
              if access_grant
                @oauth_scope = access_grant.oauth_scope
              end
            end
          end
        end
      else

        # New key and token params.
        key = request.headers['X-Tenant-Access-Key'] || params.delete('X-Tenant-Access-Key')
        token = request.headers['X-Tenant-Access-Token'] || params.delete('X-Tenant-Access-Token')

        # Legacy key and token params.
        key ||= request.headers['X-User-Access-Key'] || params.delete('X-User-Access-Key')
        token ||= request.headers['X-User-Access-Token'] || params.delete('X-User-Access-Token')

        if key || token
          [
            User,
            Account
          ].each do |model|
            next if user
            record = model.where(key: key).first
            if record && Devise.secure_compare(record[:authentication_token] || record[:token], token)
              Account.current = record.api_account
              user = record.user
            end
          end
        end
        unless key || token
          key = request.headers['X-Hub-Store']
          token = request.headers['X-Hub-Access-Token']
          Account.set_current_with_connection(key, token) if key || token
        end
      end
      if user
        User.current = user
      else
        User.current ||= (Account.current ? Account.current.owner : nil)
      end
      @ability = Ability.new(User.current)
      true
    end

    def authorized_action?
      authorize_action(skip_response: true)
    end

    def authorize_action(options = {})
      success = true
      if klass
        action_symbol =
          case @_action_name
          when 'push'
            get_data_type(@model).is_a?(Setup::FileDataType) ? :upload_file : :new
          when 'update'
            :edit
          when 'retry'
            :retry_task
          else
            @_action_name.to_sym
          end
        unless @ability.can?(action_symbol, @item || klass) &&
               (@oauth_scope.nil? || @oauth_scope.can?(action_symbol, klass))
          success = false
          unless options[:skip_response]
            responder = Cenit::Responder.new(@request_id, :unauthorized)
            render json: responder, root: false, status: responder.code
          end
        end
      else
        success = false
        unless options[:skip_response]
          if Account.current
            render json: { error: 'no model found' }, status: :not_found
          else
            render json: { error: 'not unauthorized' }, status: :unauthorized
          end
        end
      end
      cors_header
      success
    end

    def cors_header
      headers['Access-Control-Allow-Origin'] = request.headers['Origin'] || ::Cenit.homepage
      headers['Access-Control-Allow-Credentials'] = 'false'
      headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Accept, Content-Type, X-Tenant-Access-Key, X-Tenant-Access-Token, X-User-Access-Key, X-User-Access-Token, Authorization, X-Query-Options, X-Query-Selector, X-Parser-Options'
      headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
      headers['Access-Control-Max-Age'] = '1728000'
    end

    def exception_handler(exception)
      responder = Cenit::Responder.new(@request_id, exception)
      render json: responder, root: false, status: responder.code
      false
    end

    def find_item
      if (@item = accessible_records.where(id: params[:id]).first)
        true
      else
        render json: { status: 'item not found' }, status: :not_found
        false
      end
    end

    def get_data_type_by_slug(slug)
      if slug
        @data_types[slug] ||=
          if @ns_slug == 'setup' || @ns_slug == 'cenit'
            build_in_name =
              if slug == 'trace'
                Mongoid::Tracer::Trace.to_s
              else
                "#{@ns_slug.camelize}::#{slug.camelize}"
              end
            Setup::BuildInDataType[build_in_name] || Setup::BuildInDataType[slug.camelize]
          else
            if @ns_name.nil?
              ns = Setup::Namespace.where(slug: @ns_slug).first
              @ns_name = (ns && ns.name) || ''
            end
            if @ns_name
              Setup::DataType.where(namespace: @ns_name, slug: slug).first ||
                Setup::DataType.where(namespace: @ns_name, slug: slug.singularize).first
            else
              nil
            end
          end
      else
        nil
      end
    end

    def get_data_type(root)
      get_data_type_by_slug(root) if root
    end

    def get_model(root)
      if (data_type = get_data_type(root))
        data_type.records_model
      else
        nil
      end
    end

    def klass
      @klass ||= get_model(@model)
    end

    def accessible_records
      (@ability && klass.accessible_by(@ability)) || klass.all
    end

    def save_request_data
      @data_types ||= {}
      @request_id = request.uuid
      @webhook_body = request.body.read
      @ns_slug = params[:ns]
      @ns_name = nil
      @model = params[:model]
      @view = params[:view]
      @format = params[:format]
      @path = "#{params[:path]}.#{params[:format]}" if params[:path] && params[:format]
      case @_action_name
      when 'new', 'push', 'update'
        unless (@parser_options = Cenit::Utility.json_value_of(request.headers['X-Parser-Options'])).is_a?(Hash)
          @parser_options = {}
        end
        params.each do |key, value|
          next if %w(controller action ns model format api).include?(key)
          @parser_options[key] = Cenit::Utility.json_value_of(value)
        end
        %w(primary_field primary_fields ignore reset).each do |option|
          unless (value = @parser_options.delete(option)).is_a?(Array)
            value = value.to_s.split(',').collect(&:strip)
          end
          @parser_options[option] = value
        end
        content_type = request.content_type
        if @_action_name == 'push' && %w(application/json application/xml).exclude?(content_type)
          content_type =
            begin
              JSON.parse(@webhook_body)
              'application/json'
            rescue Exception
              begin
                Nokogiri::XML(@webhook_body)
                'application/xml'
              rescue Exception
                nil
              end
            end
        end
        @payload =
          case content_type
          when 'application/json'
            JSONPayload
          when 'application/xml'
            XMLPayload
          else
            BasicPayload
          end.new(controller: self,
                  message: @webhook_body,
                  content_type: content_type)
      when 'index', 'show'
        unless (@criteria = Cenit::Utility.json_value_of(request.headers['X-Query-Selector'])).is_a?(Hash)
          @criteria = {}
        end
        unless (@criteria_options = Cenit::Utility.json_value_of(request.headers['X-Query-Options'])).is_a?(Hash)
          @criteria_options = {}
        end
        @criteria = @criteria.with_indifferent_access
        @criteria_options = @criteria_options.with_indifferent_access
        @criteria.merge!(params.permit!.reject { |key, _| %w(controller action ns model format api).include?(key) })
        @criteria.each { |key, value| @criteria[key] = Cenit::Utility.json_value_of(value) }
        unless (@render_options = Cenit::Utility.json_value_of(request.headers['X-Render-Options'])).is_a?(Hash)
          @render_options = {}
        end
        @render_options = @render_options.with_indifferent_access
        if @criteria && klass
          %w(only ignore embedding).each do |option|
            if @criteria.key?(option) && !klass.property?(option)
              unless (value = @criteria.delete(option)).is_a?(Array)
                value = value.to_s.split(',').collect(&:strip)
              end
              @render_options[option] = value
            end
          end
        end
        if (fields_option = @criteria_options.delete(:fields)) || !@render_options.key?(:only)
          fields_option =
            case fields_option
            when Array
              fields_option
            when Hash
              fields_option.collect { |field, presence| presence.to_b ? field : nil }.select(&:presence)
            else
              fields_option.to_s.split(',').collect(&:strip)
            end
          @render_options[:only] = fields_option
        end
        unless @render_options.key?(:include_id)
          @render_options[:include_id] = true
        end
      end
    end

    private

    attr_reader :webhook_body

    class BasicPayload

      attr_reader :config

      def initialize(config)
        @config =
          {
            method_suffix: case config[:content_type]
                           when 'application/json'
                             :from_json
                           when 'application/xml'
                             :from_xml
                           else
                             :from
                           end,
            message: ''
          }.merge(config || {})
        controller = config[:controller]
        @data_type =
          begin
            controller.send(:get_data_type, (@root = controller.request.params[:model] || controller.request.headers['data-type']))
          rescue Exception
            nil
          end
      end

      def create_method
        "create_#{config[:method_suffix]}"
      end

      def update_method
        suffix = config[:method_suffix]
        if suffix == :from
          'fill_from'
        else
          suffix
        end
      end

      def message
        config[:message]
      end

      def each_root(&block)
        block.call(@root, message) if block
      end

      def each(&block)
        if @data_type
          block.call(@data_type.slug, message)
        else
          each_root(&block)
        end
      end

      def process_item(item, data_type, options)
        item
      end

      def data_type_for(root)
        @data_type && @data_type.slug == root ? @data_type : config[:controller].send(:get_data_type, root)
      end
    end

    class JSONPayload < BasicPayload

      def message
        @message ||= JSON.parse(msg = super)
      end

      def each_root(&block)
        if message.is_a?(Hash)
          message.each { |root, message| block.call(root, message) }
        else
          fail "JSON object payload expected but #{message.class} found"
        end
      end

      def process_item(item, data_type, options)
        if data_type.is_a?(Setup::FileDataType) && !options[:data_type_parser] && !item.is_a?(String)
          item.to_json
        else
          item
        end
      end
    end

    class XMLPayload < BasicPayload

      def each_root(&block)
        if (roots = Nokogiri::XML::DocumentFragment.parse(message).element_children)
          roots.each do |root|
            if (elements = root.element_children)
              elements.each { |e| block.call(root.name, e) }
            end
          end
        end if block
      end

      def process_item(item, data_type, options)
        if data_type.is_a?(Setup::FileDataType) && !options[:data_type_parser] && !item.is_a?(String)
          item.to_xml
        else
          item
        end
      end
    end
  end
end