note
	description: "[
			Light implementation of {OPENID} consumer.
			
			Sign-on with OpenID is a two step process:
			]"
	author: ""
	date: "$Date$"
	revision: "$Revision$"

class
	OPENID_CONSUMER

create
	make

feature {NONE} -- Initialization

	make (a_server: READABLE_STRING_8)
		do
			trusted_root := a_server
			return_url := a_server
			create required_info.make (0)
			create optional_info.make (0)
		end

feature -- Access

	trusted_root: READABLE_STRING_8

	return_url: READABLE_STRING_8

	required_info: ARRAYED_LIST [READABLE_STRING_8]
	optional_info: ARRAYED_LIST [READABLE_STRING_8]

	error: detachable READABLE_STRING_8

	has_error: BOOLEAN
		do
			Result := error /= Void
		end

feature -- Change

	ask_all_info (is_required: BOOLEAN)
		do
			across
				ax_to_sreg_map as c
			loop
				ask_info (c.key, is_required)
			end
		end

	ask_required_info (s: READABLE_STRING_8)
		do
			required_info.force (s)
		end

	ask_optional_info (s: READABLE_STRING_8)
		do
			optional_info.force (s)
		end

	ask_info (s: READABLE_STRING_8; is_required: BOOLEAN)
		do
			if is_required then
				ask_required_info (s)
			else
				ask_optional_info (s)
			end
		end

	ask_nickname (is_required: BOOLEAN)
		do
			ask_info ("namePerson/friendly", is_required)
		end

	ask_email (is_required: BOOLEAN)
		do
			ask_info ("contact/email", is_required)
		end

	ask_fullname (is_required: BOOLEAN)
		do
			ask_info ("namePerson", is_required)
		end

	ask_birthdate (is_required: BOOLEAN)
		do
			ask_info ("birthDate", is_required)
		end

	ask_gender (is_required: BOOLEAN)
		do
			ask_info ("person/gender", is_required)
		end

	ask_postcode (is_required: BOOLEAN)
		do
			ask_info ("contact/postalCode/home", is_required)
		end

	ask_country (is_required: BOOLEAN)
		do
			ask_info ("contact/country/home", is_required)
		end

	ask_language (is_required: BOOLEAN)
		do
			ask_info ("pref/language", is_required)
		end

	ask_timezone (is_required: BOOLEAN)
		do
			ask_info ("pref/timezone", is_required)
		end

feature -- Query

	auth_url (a_identity: READABLE_STRING_8): detachable READABLE_STRING_8
		do
			error := Void
			if attached discovering_info (a_identity) as d_info then
				if d_info.version = 2 then
					Result := auth_url_v2 (a_identity, d_info)
				else
					Result := auth_url_v1 (a_identity, d_info) -- FIXME for claimed_id
				end
			end
		end

feature {OPENID_CONSUMER_VALIDATION} -- Implementation				

	discovering_info (id: READABLE_STRING_8): detachable OPENID_DISCOVER
		local

			sess: HTTP_CLIENT_SESSION
			ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT
			xrds_location: detachable READABLE_STRING_8
			xml: XML_STANDARD_PARSER
			tree: XML_CALLBACKS_DOCUMENT
			xelt: detachable XML_ELEMENT
			s: READABLE_STRING_32
			r_uri: detachable READABLE_STRING_8
			r_err: BOOLEAN
			r_delegate: detachable READABLE_STRING_8
			r_sreg_supported, r_ax_supported, r_identifier_select: BOOLEAN
			r_version: INTEGER
			l_xrds_content: detachable READABLE_STRING_8
		do
			sess := new_session (id)
			if attached sess.head ("", ctx) as rep then
				if rep.error_occurred then
					report_error ("Unable get answer from openid provider at " + rep.url)
				else
					if
						attached rep.header ("Content-Type") as l_content_type and then
						l_content_type.has_substring ("application/xrds+xml") and then
						attached sess.get ("", ctx) as l_getres
					then
						l_xrds_content := l_getres.body
					elseif attached rep.header ("X-XRDS-Location") as loc then
						xrds_location := loc
					else
						report_error ("Failed (probably %""+ id +"%" is an invalid openid identifier).")
					end
				end
			end
			if l_xrds_content = Void and xrds_location /= Void then
				sess := new_session (xrds_location)
				if attached sess.get ("", ctx) as rep then
					if rep.error_occurred then
						r_err := True
						report_error ("Can not get " + rep.url)
					elseif attached rep.body as l_content then
						l_xrds_content := l_content
					else
						r_err := True
						report_error ("No content: " + rep.url)
					end
				end
			end
			if l_xrds_content = Void then
				r_err := True
				report_error ("Unable to get the XRDS message.")
			else
				create xml.make
				create tree.make_null
				xml.set_callbacks (tree)
				xml.parse_from_string (l_xrds_content)
				if attached tree.document as xrds then
					xelt := Void
					xelt := xrds.elements.first
					xelt := xelt.elements.first
					if attached xelt as l_xrd then
						if attached l_xrd.elements_by_name ("Service") as l_services then
							across
								l_services as c
							until
								r_uri /= Void
							loop
								if attached c.item.elements_by_name ("Type") as l_types then
									across
										l_types as t
									loop
										s := xml_content (t.item)
										if s.same_string_general ("http://openid.net/sreg/1.0") then
											r_sreg_supported := True
										elseif s.same_string_general ("http://openid.net/extensions/sreg/1.1") then
											r_sreg_supported := True
										elseif s.same_string_general ("http://openid.net/srv/ax/1.0") then
											r_ax_supported := True
										elseif s.same_string_general ("http://specs.openid.net/auth/2.0/signon") then
											r_version := 2
										elseif s.same_string_general ("http://specs.openid.net/auth/2.0/server") then
											r_version := 2
											r_identifier_select := True
										elseif s.same_string_general ("http://openid.net/signon/1.1") then
											r_version := 1
										end
									end
								end
								if attached c.item.element_by_name ("URI") as l_uri then
									r_uri := xml_content (l_uri)
								end
								if r_version = 1 then
									if attached c.item.element_by_name ("openid:Delegate") as l_id then
										r_delegate := xml_content (l_id)
									end
									if attached c.item.element_by_name ("LocalID") as l_id then
										r_delegate := xml_content (l_id)
									end
								else
									if attached c.item.element_by_name ("CanonicalID") as l_id then
										r_delegate := xml_content (l_id)
									end
									if attached c.item.element_by_name ("LocalID") as l_id then
										r_delegate := xml_content (l_id)
									end
								end
							end
						end
					end
				end

			end
			if r_uri /= Void then
				create Result.make (r_uri, r_version)
				if r_delegate = Void then
					r_delegate := id
				end
				Result.delegate := r_delegate
				Result.ax_supported := r_ax_supported
				Result.sreg_supported := r_sreg_supported
				Result.identifier_select := r_identifier_select
				Result.has_error := r_err
			end
		end

feature {NONE} -- Implementation		

	auth_url_v1 (a_id: READABLE_STRING_8; a_info: OPENID_DISCOVER): READABLE_STRING_8
		local
			u: URI
			ret: URI
		do
			create u.make_from_string (a_info.server_uri)
			create ret.make_from_string (return_url)
			if
				attached a_info.delegate as l_claimed_id and then
				not a_id.same_string (l_claimed_id)
			then
				ret.add_query_parameter ("openid.claimed_id", l_claimed_id)
			end

			u.add_query_parameter ("openid.return_to", ret.string)
			u.add_query_parameter ("openid.mode", "checkid_setup") -- or "checkid_immediate"
			u.add_query_parameter ("openid.identity", a_id)
			u.add_query_parameter ("openid.trust_root", trusted_root)

			if a_info.sreg_supported then
				add_sreg_parameters_to (u)
			end

			Result := u.string
		end

	auth_url_v2 (a_id: READABLE_STRING_8; a_info: OPENID_DISCOVER): READABLE_STRING_8
		local
			u: URI
		do
			create u.make_from_string (a_info.server_uri)
			u.add_query_parameter ("openid.ns", "http://specs.openid.net/auth/2.0")
			u.add_query_parameter ("openid.mode", "checkid_setup") -- or "checkid_immediate"
			u.add_query_parameter ("openid.return_to", return_url)
			u.add_query_parameter ("openid.realm", trusted_root)

			if a_info.ax_supported then
				add_ax_parameters_to (u)
			end
			if a_info.sreg_supported then
				add_sreg_parameters_to (u)
			end
			if a_info.identifier_select then
				u.add_query_parameter ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
				u.add_query_parameter ("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
			else
				u.add_query_parameter ("openid.identity", a_id)
				u.add_query_parameter ("openid.claimed_id", a_id) -- Fixme
			end

			Result := u.string
		end

	add_ax_parameters_to (a_uri: URI)
		local
			lst: ARRAYED_LIST [READABLE_STRING_8]
			l_aliases: HASH_TABLE [READABLE_STRING_8, STRING_8]
			l_counts: HASH_TABLE [INTEGER, STRING_8]
			l_alias: READABLE_STRING_8
			s: STRING
		do
			create lst.make (required_info.count + optional_info.count)
			lst.append (required_info)
			lst.append (optional_info)
			if lst.count > 0 then
				a_uri.add_query_parameter ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
				a_uri.add_query_parameter ("openid.ax.mode", "fetch_request");

				create l_aliases.make (lst.count)
				create l_counts.make (lst.count)
				across
					lst as c
				loop
					l_alias := ax_to_alias (c.item)
					if l_aliases.has (l_alias) then
						if attached l_counts.item (l_alias) as l_count then
							l_counts.replace (l_count + 1, l_alias)
						else
							check has_alias: False end
							l_counts.force (1, l_alias)
						end
					else
						l_aliases.force ("http://axschema.org/" + c.item, l_alias)
						l_counts.force (1, l_alias)
					end
				end
				across
					l_aliases as c
				loop
					a_uri.add_query_parameter ("openid.ax.type." + c.key, c.item)
				end
				across
					l_counts as c
				loop
					if c.item > 1 then
						a_uri.add_query_parameter ("openid.ax.count." + c.key, c.item.out)
					end
				end
					-- required
				create s.make_empty
				across
					required_info as c
				loop
					if not s.is_empty then
						s.append_character (',')
					end
					s.append (ax_to_alias (c.item))
				end
				if not s.is_empty then
					a_uri.add_query_parameter ("openid.ax.required", s)
				end
					-- optional
				create s.make_empty
				across
					optional_info as c
				loop
					if not s.is_empty then
						s.append_character (',')
					end
					s.append (ax_to_alias (c.item))
				end
				if not s.is_empty then
					a_uri.add_query_parameter ("openid.ax.if_available", s)
				end
			end
		end

	ax_to_alias (n: READABLE_STRING_8): STRING_8
		do
			if attached ax_to_sreg (n) as s then
				Result := s
			else
				Result := n.string
				Result.replace_substring_all ("/", "_")
				Result.replace_substring_all (".", "_")
			end
		end

	add_sreg_parameters_to (a_uri: URI)
		local
			s: STRING
		do
			-- We always use SREG 1.1, even if the server is advertising only support for 1.0.
			-- That's because it's fully backwards compatibile with 1.0, and some providers
			-- advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
			a_uri.add_query_parameter ("openid.ns.sreg", "http://openid.net/extensions/sreg/1.1")
			if not required_info.is_empty then
	            create s.make_empty
	            across
	            	required_info as c
	            loop
	            	if attached ax_to_sreg (c.item) as sreg then
	            		if not s.is_empty then
	            			s.append_character (',')
	            		end
	            		s.append (sreg)
	            	end
	            end
	            if not s.is_empty then
	            	a_uri.add_query_parameter ("openid.sreg.required", s)
	            end
			end

			if not optional_info.is_empty then
	            create s.make_empty
	            across
	            	optional_info as c
	            loop
	            	if attached ax_to_sreg (c.item) as sreg then
	            		if not s.is_empty then
	            			s.append_character (',')
	            		end
	            		s.append (sreg)
	            	end
	            end
	            if not s.is_empty then
	            	a_uri.add_query_parameter ("openid.sreg.optional", s)
	            end
			end
		end

	ax_to_sreg_map: HASH_TABLE [READABLE_STRING_8, STRING_8]
		once
			create Result.make (7)
			Result.compare_objects
			Result.force ("nickname", "namePerson/friendly")
			Result.force ("email", "contact/email")
			Result.force ("fullname", "namePerson")
			Result.force ("dob", "birthDate")
			Result.force ("gender", "person/gender")
			Result.force ("postcode", "contact/postalCode/home")
			Result.force ("country", "contact/country/home")
			Result.force ("language", "pref/language")
			Result.force ("timezone", "pref/timezone")

--			-- extension
--			Result.force ("firstname", "namePerson/first")
--			Result.force ("lastname", "namePerson/last")
		end

	ax_to_sreg (n: READABLE_STRING_8): detachable READABLE_STRING_8
		do
	        if attached ax_to_sreg_map.item (n) as v then
	        	Result := v
	        end
		end

	sreg_to_ax (n: READABLE_STRING_8): detachable READABLE_STRING_8
		do
			if ax_to_sreg_map.has_item (n) and then
				attached ax_to_sreg_map.found_item as v
			then
				Result := v
			end
		end

	report_error (m: READABLE_STRING_8)
		local
			err: like error
		do
			err := error
			if err = Void then
				error := m
			else
				error := err + "%N" + m
			end
			debug
				print (m)
			end
		ensure
			has_error
		end

feature -- Helper		

	xml_content (e: XML_ELEMENT): STRING_32
		do
			create Result.make_empty
			if attached e.contents as lst then
				across
					lst as c
				loop
					Result.append (c.item.content)
				end
			end
		end

	new_session (a_uri: READABLE_STRING_8): HTTP_CLIENT_SESSION
		local
			cl: DEFAULT_HTTP_CLIENT
		do
			create cl
			Result := cl.new_session (a_uri)
			Result.set_is_insecure (True)
			Result.set_max_redirects (5)
			Result.add_header ("Accept", "application/xrds+xml, */*")
		end

end
