
require 'openid/util'
require 'openid/fetchers'
require 'openid/yadis/constants'
require 'openid/yadis/parsehtml'

module OpenID

  # Raised when a error occurs in the discovery process
  class DiscoveryFailure < OpenIDError
    attr_accessor :identity_url, :http_response

    def initialize(message, http_response)
      super(message)
      @identity_url = nil
      @http_response = http_response
    end
  end

  module Yadis

    # Contains the result of performing Yadis discovery on a URI
    class DiscoveryResult

      # The result of following redirects from the request_uri
      attr_accessor :normalize_uri

      # The URI from which the response text was returned (set to
      # nil if there was no XRDS document found)
      attr_accessor :xrds_uri

      # The content-type returned with the response_text
      attr_accessor :content_type

      # The document returned from the xrds_uri
      attr_accessor :response_text

      attr_accessor :request_uri, :normalized_uri

      def initialize(request_uri)
        # Initialize the state of the object
        #
        # sets all attributes to None except the request_uri
        @request_uri = request_uri
        @normalized_uri = nil
        @xrds_uri = nil
        @content_type = nil
        @response_text = nil
      end

      # Was the Yadis protocol's indirection used?
      def used_yadis_location?
        return @normalized_uri != @xrds_uri
      end

      # Is the response text supposed to be an XRDS document?
      def is_xrds
        return (used_yadis_location?() or
                @content_type == YADIS_CONTENT_TYPE)
      end
    end

    # Discover services for a given URI.
    #
    # uri: The identity URI as a well-formed http or https URI. The
    # well-formedness and the protocol are not checked, but the
    # results of this function are undefined if those properties do
    # not hold.
    #
    # returns a DiscoveryResult object
    #
    # Raises DiscoveryFailure when the HTTP response does not have
    # a 200 code.
    def self.discover(uri)
      result = DiscoveryResult.new(uri)
      begin
        resp = OpenID.fetch(uri, nil, {'Accept' => YADIS_ACCEPT_HEADER})
      rescue Exception
        raise DiscoveryFailure.new("Failed to fetch identity URL #{uri} : #{$!}", $!)
      end
      if resp.code != "200"
        raise DiscoveryFailure.new(
                "HTTP Response status from identity URL host is not \"200\"."\
                "Got status #{resp.code.inspect} for #{resp.final_url}", resp)
      end

      # Note the URL after following redirects
      result.normalized_uri = resp.final_url

      # Attempt to find out where to go to discover the document or if
      # we already have it
      result.content_type = resp['content-type']

      result.xrds_uri = self.where_is_yadis?(resp)

      if result.xrds_uri and result.used_yadis_location?
        begin
          resp = OpenID.fetch(result.xrds_uri)
        rescue
          raise DiscoveryFailure.new("Failed to fetch Yadis URL #{result.xrds_uri} : #{$!}", $!)
        end
        if resp.code != "200"
            exc = DiscoveryFailure.new(
                    "HTTP Response status from Yadis host is not \"200\". " +
                                       "Got status #{resp.code.inspect} for #{resp.final_url}", resp)
            exc.identity_url = result.normalized_uri
            raise exc
        end

        result.content_type = resp['content-type']
      end

      result.response_text = resp.body
      return result
    end

    # Given a HTTPResponse, return the location of the Yadis
    # document.
    #
    # May be the URL just retrieved, another URL, or None, if I
    # can't find any.
    #
    # [non-blocking]
    def self.where_is_yadis?(resp)
      # Attempt to find out where to go to discover the document or if
      # we already have it
      content_type = resp['content-type']

      # According to the spec, the content-type header must be an
      # exact match, or else we have to look for an indirection.
      if (!content_type.nil? and
          content_type.split(';', 2)[0].downcase == YADIS_CONTENT_TYPE)
        return resp.final_url
      else
        # Try the header
        yadis_loc = resp[YADIS_HEADER_NAME.downcase]

        if yadis_loc.nil?
          # Parse as HTML if the header is missing.
          #
          # XXX: do we want to do something with content-type, like
          # have a whitelist or a blacklist (for detecting that it's
          # HTML)?
          yadis_loc = Yadis.html_yadis_location(resp.body)
        end
      end

      return yadis_loc
    end

  end

end
