lib/omnicontacts/middleware/base_oauth.rb
# This class contains the common behavior for middlewares
# implementing either versions of OAuth.
#
# Extending classes are required to implement
# the following methods:
# * request_authorization_from_user
# * fetch_contatcs
module OmniContacts
  module Middleware
    class BaseOAuth

      attr_reader :ssl_ca_file

      def initialize app, options
        @app = app
        @listening_path = MOUNT_PATH + class_name
        @ssl_ca_file = options[:ssl_ca_file]
      end

      def class_name
        self.class.name.split('::').last.downcase
      end

      # Rack callback. It handles three cases:
      # * user visit middleware entry point.
      #   In this case request_authorization_from_user is called
      # * user is redirected back to the application
      #   from the authorization site. In this case the list
      #   of contacts is fetched and stored in the variables
      #   omnicontacts.contacts within the Rack env variable.
      #   Once that is done the next middleware component is called.
      # * user visits any other resource. In this case the request
      #   is simply forwarded to the next middleware component.
      def call env
        @env = env
        if env["PATH_INFO"] =~ /^#{@listening_path}\/?$/
          session['omnicontacts.params'] = Rack::Request.new(env).params
          handle_initial_request
        elsif env["PATH_INFO"] =~ /^#{redirect_path}/
          env['omnicontacts.params'] = session.delete('omnicontacts.params')
          handle_callback
        else
          @app.call(env)
        end
      end

      private

      def test_mode?
        IntegrationTest.instance.enabled
      end

      def handle_initial_request
        execute_and_rescue_exceptions do
          if test_mode?
            IntegrationTest.instance.mock_authorization_from_user(self)
          else
            request_authorization_from_user
          end
        end
      end

      def handle_callback
        execute_and_rescue_exceptions do
          @env["omnicontacts.contacts"] = if test_mode?
            IntegrationTest.instance.mock_fetch_contacts(self)
          else
            fetch_contacts
          end
          set_current_user IntegrationTest.instance.mock_fetch_user(self) if test_mode?
          @app.call(@env)
        end
      end

      def set_current_user user
        @env["omnicontacts.user"] = user
      end

      #  This method rescues executes a block of code and
      #  rescue all exceptions. In case of an exception the
      #  user is redirected to the failure endpoint.
      def execute_and_rescue_exceptions
        yield
      rescue AuthorizationError => e
        handle_error :not_authorized, e
      rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
        handle_error :timeout, e
      rescue ::RuntimeError => e
        handle_error :internal_error, e
      end

      def handle_error error_type, exception
        logger.puts("Error #{error_type} while processing #{@env["PATH_INFO"]}: #{exception.message}") if logger
        failure_url = "#{ MOUNT_PATH }failure?error_message=#{error_type}&importer=#{class_name}"
        params_url = append_request_params(failure_url)
        target_url = append_state_query(params_url)
        [302, {"Content-Type" => "text/html", "location" => target_url}, []]
      end

      def session
        raise "You must provide a session to use OmniContacts" unless @env["rack.session"]
        @env["rack.session"]
      end

      def logger
        @env["rack.errors"] if @env
      end

      def base_prop_name
        "omnicontacts." + class_name
      end

      def append_request_params(target_url)
        return target_url unless @env['omnicontacts.params']
        params = Rack::Utils.build_query(@env['omnicontacts.params'])
        unless params.nil? or params.empty?
          target_url = target_url + (target_url.include?("?")?"&":"?") + params
        end
        return target_url
      end

      def append_state_query(target_url)
        state = Rack::Utils.parse_query(@env['QUERY_STRING'])['state']
        unless state.nil?
          target_url = target_url + (target_url.include?("?")?"&":"?") + 'state=' + state
        end

        return target_url
      end
    end
  end
end
