class EL_HTTP_CONNECTION_IMPLEMENTATION

(source code)

description

Implementation routines for class EL_HTTP_CONNECTION

note
	description: "Implementation routines for class ${EL_HTTP_CONNECTION}"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2024-11-05 15:10:40 GMT (Tuesday 5th November 2024)"
	revision: "20"

deferred class
	EL_HTTP_CONNECTION_IMPLEMENTATION

inherit
	EL_MEMORY_ROUTINES

	EL_CURL_OPTION_CONSTANTS
		export
			{NONE} all
		end

	EL_CURL_SSL_CONSTANTS
		export
			{NONE} all
		end

	EL_CURL_INFO_CONSTANTS
		export
			{NONE} all
		end

	CURL_FORM_CONSTANTS
		rename
			is_valid as is_valid_form_constant
		export
			{NONE} all
		end

	EL_STRING_HANDLER

	EL_MODULE_TUPLE; EL_MODULE_URI

	EL_SHARED_CURL_API; EL_SHARED_HTTP_STATUS

	EL_SHARED_PROGRESS_LISTENER
		rename
			progress_listener as close_listener,
			is_progress_tracking as is_close_tracking,
			Progress_listener_cell as Close_listener_cell
		end

feature {NONE} -- Initialization

	make
		do
			create last_string.make_empty
			create http_response.make_empty
			create request_headers.make_equal (0)
			create post_data.make (0)
		end

feature -- Access

	cookie_load_path: detachable FILE_PATH

	cookie_store_path: detachable FILE_PATH

	error_code: INTEGER
		-- curl error code

	last_headers: EL_HTTP_HEADERS
		do
			create Result.make (last_string)
		end

	last_string: STRING

feature -- Status query

	has_error: BOOLEAN
		-- `True' if CURL operation returned with an error
		do
			Result := error_code /= 0
		end

feature -- Basic operations

	put_error (log: EL_LOGGABLE)
		require
			has_error: has_error
		do
			log.put_labeled_substitution ("CURL ERROR", "%S %S", [error_code, error_string])
			log.put_new_line
		end

feature {EL_HTTP_COMMAND} -- Implementation

	enable_get_method
		do
			set_curl_boolean_option (CURLOPT_httpget, True)
			set_curl_boolean_option (CURLOPT_post, False)
		end

	enable_post_method
		do
			set_curl_boolean_option (CURLOPT_httpget, False)
			set_curl_boolean_option (CURLOPT_post, True)
			if post_data.count > 0 then
				set_curl_option_with_data (CURLOPT_postfields, post_data.item)
				set_curl_integer_option (CURLOPT_postfieldsize, post_data_count)
			end
		end

	set_curl_boolean_option (a_option: INTEGER; flag: BOOLEAN)
		do
			Curl.setopt_integer (self_ptr, a_option, flag.to_integer)
		end

	set_curl_integer_option (a_option: INTEGER; option: INTEGER)
		do
			Curl.setopt_integer (self_ptr, a_option, option)
		end

	set_curl_option_with_data (a_option: INTEGER; a_data_ptr: POINTER)
		do
			Curl.setopt_void_star (self_ptr, a_option, a_data_ptr)
		end

	set_curl_string_32_option (a_option: INTEGER; string: STRING_32)
		local
			buffer: EL_STRING_8_BUFFER_ROUTINES
		do
			Curl.setopt_string (self_ptr, a_option, Buffer.copied_general_as_utf_8 (string))
		end

	set_curl_string_8_option (a_option: INTEGER; string: STRING)
		do
			Curl.setopt_string (self_ptr, a_option, string)
		end

	set_curl_string_option (a_option: INTEGER; string: ZSTRING)
		local
			buffer: EL_STRING_8_BUFFER_ROUTINES
		do
			Curl.setopt_string (self_ptr, a_option, Buffer.copied_general_as_utf_8 (string))
		end

	set_header_function (callback, user_data: POINTER)
		do
			set_curl_option_with_data (CURLOPT_headerfunction, callback)
			set_curl_option_with_data (CURLOPT_headerdata, user_data)
		end

	set_nobody (flag: BOOLEAN)
		do
			set_curl_boolean_option (CURLOPT_nobody, flag)
		end

	set_write_function (callback, user_data: POINTER)
		do
			set_curl_option_with_data (CURLOPT_writefunction, callback)
			set_curl_option_with_data (CURLOPT_writedata, user_data)
		end

feature {NONE} -- Implementation

	content: ZSTRING
		do
			create Result.make_from_utf_8 (last_string)
		end

	new_parameter_table: detachable HASH_TABLE [READABLE_STRING_GENERAL, READABLE_STRING_GENERAL]
		do
			create {HASH_TABLE [STRING, STRING]} Result.make (0)
		end

feature {NONE} -- Experimental

	read_string_experiment
			-- Failed experiment. Might come back to it again
		local
			form_post, form_last: CURL_FORM
		do
			create form_post.make; create form_last.make
			set_form_parameters (form_post, form_last)

			create http_response.make_empty
--			set_write_function (self_ptr)
			set_curl_integer_option (CURLOPT_writedata, http_response.object_id)
			error_code := Curl.perform (self_ptr)
			last_string.share (http_response)
		end

	redirect_url: STRING
		-- Fails because Curlinfo_redirect_url will not satisfy contract CURL_INFO_CONSTANTS.is_valid
		-- For some reason Curlinfo_redirect_url is missing from CURL_INFO_CONSTANTS
		require
			no_error: not has_error
		local
			result_cell: CELL [STRING]
			status: INTEGER
		do
			create Result.make_empty
			create result_cell.put (Result)
			status := Curl.get_info (self_ptr, Curlinfo_redirect_url, result_cell)
			if status = 0 then
				Result := result_cell.item
			end
		end

	set_form_parameters (form_post, form_last: CURL_FORM)
			-- Haven't worked out how to use this yet
		do
--			across parameters as parameter loop
--				Curl.formadd_string_string (
--					form_post, form_last,
--					CURLFORM_COPYNAME, parameter.key,
--					CURLFORM_COPYCONTENTS, parameter.item,
--					CURLFORM_END
--				)
--			end
			Curl.setopt_form (self_ptr, CURLOPT_httppost, form_post)
		end

feature {EL_HTTP_COMMAND} -- Implementation

	do_command (command: EL_DOWNLOAD_HTTP_COMMAND)
		do
			lio.put_labeled_string ("Sending " + command.type + " request", url)
			lio.put_new_line
			command.execute
			lio.put_line ("Sent")
			if attached {EL_STRING_DOWNLOAD_HTTP_COMMAND} command as string_download then
				if has_error then
					last_string.wipe_out
				else
					last_string.share (string_download.string)
				end
			end
		end

	do_transfer
			-- do data transfer to/from host
		local
			string_list: POINTER
		do
			string_list := request_headers.to_curl_string_list
			if is_attached (string_list) then
				set_curl_option_with_data (CURLOPT_httpheader, string_list)
			end
			error_code := Curl.perform (self_ptr)
			if is_attached (string_list) then
				curl.free_string_list (string_list)
			end
			if has_error then
				put_error (lio)
			end
		end

	set_cookie_options
		do
			if attached cookie_store_path as store_path then
				set_curl_string_option (CURLOPT_cookiejar, store_path)
			end
			if attached cookie_load_path as load_path then
				set_curl_string_option (CURLOPT_cookiefile, load_path)
			end
		end

	url_extension (a_url: EL_URL): STRING
		local
			dot_index: INTEGER
		do
			dot_index := a_url.last_index_of ('.', a_url.count)
			if dot_index > 0 then
				create Result.make (a_url.count - dot_index)
				Result.append_substring (a_url, dot_index + 1, a_url.count)
				Result.to_lower
			else
				create Result.make_empty
			end
		end

	valid_mime_type (a_url: EL_URL; mime_type: STRING): BOOLEAN
		local
			extension: STRING
		do
			extension := url_extension (a_url)
			if extension ~ once "jpg" then
				extension.insert_character ('e', 3)
			end
			if Image_types.has (extension) then
				Result := mime_type ~ Mime.image + extension
			else
				Result := mime_type.starts_with (Mime.text)
			end
		end

feature {NONE} -- Implementation attributes

	lio: EL_LOGGABLE

	request_headers: EL_CURL_HEADER_TABLE
		-- request headers to send

	http_response: CURL_STRING

	post_data: MANAGED_POINTER

	post_data_count: INTEGER

feature {NONE} -- Deferred

	error_string: STRING
		deferred
		end

	is_html_response: BOOLEAN
		-- `True' if `last_string' is html
		deferred
		end

	self_ptr: POINTER
		deferred
		end

	url: EL_URL
		deferred
		end

feature {NONE} -- Constants

	Image_types: EL_STRING_8_LIST
		once
			Result := "gif, png, jpeg"
		end

	Mime: TUPLE [image, text: STRING]
		once
			create Result
			Tuple.fill (Result, "image/, text/")
		end

	Max_post_data_count: INTEGER = 1024

end