benches/strings: add .gitignore and `make clean`
[nit.git] / lib / curl / curl.nit
index 05dfe92..c087642 100644 (file)
 module curl
 
 import curl_c
-import mail
 
 # Top level of Curl
 class Curl
-       protected var prim_curl: CCurl
+       protected var prim_curl = new CCurl.easy_init
 
        init
        do
-               self.prim_curl = new CCurl.easy_init
                assert curlInstance:self.prim_curl.is_init else
                        print "Curl must be instancied to be used"
                end
@@ -35,55 +33,25 @@ class Curl
        # Check for correct initialization
        fun is_ok: Bool do return self.prim_curl.is_init
 
-       # Get an HTTP Request object to perform your own
-       fun http_request(url: String): nullable CurlRequest
-       do
-               var err: CURLCode
-               err = self.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
-               if not err.is_ok then return null
-
-               err = self.prim_curl.easy_setopt(new CURLOption.url, url)
-               if not err.is_ok then return null
-
-               return new CurlHTTPRequest(url, self)
-       end
-
        # Release Curl instance
        fun destroy do self.prim_curl.easy_clean
 end
 
 # CURL Request
 class CurlRequest
-       super CCurlCallbacks
 
-       var url: String
-       var headers: nullable HeaderMap writable = null
-       var datas: nullable HeaderMap writable = null
-       var delegate: nullable CurlCallbacks writable = null
-       var verbose: Bool writable = false
-       private var curl: nullable Curl
+       var verbose: Bool = false is writable
+       private var curl: nullable Curl = null
 
        # Launch request method
        fun execute: CurlResponse is abstract
 
        # Intern perform method, lowest level of request launching
-       private fun perform: nullable CurlResponse
+       private fun perform: nullable CurlResponseFailed
        do
                if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
 
-               var err: CURLCode
-
-               if self.datas != null then
-                       var postdatas = self.datas.to_url_encoded(self.curl.prim_curl)
-                       err = self.curl.prim_curl.easy_setopt(new CURLOption.postfields, postdatas)
-                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
-               end
-
-               if self.headers != null then
-                       var headers_joined = self.headers.join_pairs(": ")
-                       err = self.curl.prim_curl.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
-                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
-               end
+               var err
 
                err = self.curl.prim_curl.easy_setopt(new CURLOption.verbose, self.verbose)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
@@ -95,7 +63,7 @@ class CurlRequest
        end
 
        # Intern method with return a failed answer with given code and message
-       private fun answer_failure(error_code: Int, error_msg: String): CurlResponse
+       private fun answer_failure(error_code: Int, error_msg: String): CurlResponseFailed
        do
                return new CurlResponseFailed(error_code, error_msg)
        end
@@ -104,25 +72,62 @@ end
 # CURL HTTP Request
 class CurlHTTPRequest
        super CurlRequest
+       super CCurlCallbacks
+       super CurlCallbacksRegisterIntern
+
+       var url: String
+       var datas: nullable HeaderMap = null is writable
+       var headers: nullable HeaderMap = null is writable
+
+       # Set the user agent for all following HTTP requests
+       fun user_agent=(name: String)
+       do
+               curl.prim_curl.easy_setopt(new CURLOption.user_agent, name)
+       end
+
+       init (url: String, curl: nullable Curl) is old_style_init do
+               self.url = url
+               self.curl = curl
+       end
 
        # Execute HTTP request with settings configured through attribute
-       redef fun execute: CurlResponse
+       redef fun execute
        do
                if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
 
-               var success_response: CurlResponseSuccess = new CurlResponseSuccess
-
+               var success_response = new CurlResponseSuccess
                var callback_receiver: CurlCallbacks = success_response
                if self.delegate != null then callback_receiver = self.delegate.as(not null)
 
-               var err: CURLCode
+               var err
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
+               # Callbacks
                err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
                err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.body)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
+               # HTTP Header
+               if self.headers != null then
+                       var headers_joined = self.headers.join_pairs(": ")
+                       err = self.curl.prim_curl.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
+
+               # Datas
+               if self.datas != null then
+                       var postdatas = self.datas.to_url_encoded(self.curl.prim_curl)
+                       err = self.curl.prim_curl.easy_setopt(new CURLOption.postfields, postdatas)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
+
                var err_resp = perform
                if err_resp != null then return err_resp
 
@@ -135,19 +140,26 @@ class CurlHTTPRequest
        # Download to file given resource
        fun download_to_file(output_file_name: nullable String): CurlResponse
        do
-               var success_response: CurlFileResponseSuccess = new CurlFileResponseSuccess
+               var success_response = new CurlFileResponseSuccess
 
                var callback_receiver: CurlCallbacks = success_response
                if self.delegate != null then callback_receiver = self.delegate.as(not null)
 
-               var err: CURLCode
+               var err
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
                err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
                err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.stream)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
-               var opt_name:nullable String
+               var opt_name
                if not output_file_name == null then
                        opt_name = output_file_name
                else if not self.url.substring(self.url.length-1, self.url.length) == "/" then
@@ -183,11 +195,150 @@ class CurlHTTPRequest
        end
 end
 
+# CURL Mail Request
+class CurlMailRequest
+       super CurlRequest
+       super CCurlCallbacks
+
+       var headers: nullable HeaderMap = null is writable
+       var headers_body: nullable HeaderMap = null is writable
+       var from: nullable String = null is writable
+       var to: nullable Array[String] = null is writable
+       var cc: nullable Array[String] = null is writable
+       var bcc: nullable Array[String] = null is writable
+       var subject: nullable String = "" is writable
+       var body: nullable String = "" is writable
+       private var supported_outgoing_protocol: Array[String] = ["smtp", "smtps"]
+
+       init (curl: nullable Curl) is old_style_init do
+               self.curl = curl
+       end
+
+       # Helper method to add conventional space while building entire mail
+       private fun add_conventional_space(str: String):String do return "{str}\n" end
+
+       # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
+       private fun add_pair_to_content(str: String, att: String, val: nullable String):String
+       do
+               if val != null then return "{str}{att}{val}\n"
+               return "{str}{att}\n"
+       end
+
+       # Helper method to add entire list of pairs to mail content
+       private fun add_pairs_to_content(content: String, pairs: HeaderMap):String
+       do
+               for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
+               return content
+       end
+
+       # Check for host and protocol availability
+       private fun is_supported_outgoing_protocol(host: String):CURLCode
+       do
+               var host_reach = host.split_with("://")
+               if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
+               return once new CURLCode.unsupported_protocol
+       end
+
+       # Configure server host and user credentials if needed.
+       fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String): nullable CurlResponseFailed
+       do
+               # Check Curl initialisation
+               if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
+
+               var err
+
+               # Host & Protocol
+               err = is_supported_outgoing_protocol(host)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.url, host)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               # Credentials
+               if not user == null and not pwd == null then
+                       err = self.curl.prim_curl.easy_setopt(new CURLOption.username, user)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+                       err = self.curl.prim_curl.easy_setopt(new CURLOption.password, pwd)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
+
+               return null
+       end
+
+       # Execute Mail request with settings configured through attribute
+       redef fun execute
+       do
+               if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
+
+               var success_response = new CurlMailResponseSuccess
+               var content = ""
+               # Headers
+               if self.headers != null then
+                       content = add_pairs_to_content(content, self.headers.as(not null))
+               end
+
+               # Recipients
+               var g_rec = new Array[String]
+               if self.to != null and self.to.length > 0 then
+                       content = add_pair_to_content(content, "To:", self.to.join(","))
+                       g_rec.append(self.to.as(not null))
+               end
+               if self.cc != null and self.cc.length > 0 then
+                       content = add_pair_to_content(content, "Cc:", self.cc.join(","))
+                       g_rec.append(self.cc.as(not null))
+               end
+               if self.bcc != null and self.bcc.length > 0 then g_rec.append(self.bcc.as(not null))
+
+               if g_rec.length < 1 then return answer_failure(0, "The mail recipients can not be empty")
+
+               var err
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_rcpt, g_rec.to_curlslist)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               # From
+               if not self.from == null then
+                       content = add_pair_to_content(content, "From:", self.from)
+                       err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_from, self.from.as(not null))
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
+
+               # Subject
+               content = add_pair_to_content(content, "Subject:", self.subject)
+
+               # Headers body
+               if self.headers_body != null then
+                       content = add_pairs_to_content(content, self.headers_body.as(not null))
+               end
+
+               # Body
+               content = add_conventional_space(content)
+               content = add_pair_to_content(content, "", self.body)
+               content = add_conventional_space(content)
+               err = self.curl.prim_curl.register_callback(self, once new CURLCallbackType.read)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               err = self.curl.prim_curl.register_read_datas_callback(self, content)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               var err_resp = perform
+               if err_resp != null then return err_resp
+
+               return success_response
+       end
+end
+
 # Callbacks Interface, allow you to manage in your way the different streams
 interface CurlCallbacks
        super CCurlCallbacks
 end
 
+# Callbacks attributes
+abstract class CurlCallbacksRegisterIntern
+       var delegate: nullable CurlCallbacks = null is writable
+end
+
 # Abstract Curl request response
 abstract class CurlResponse
 end
@@ -198,12 +349,6 @@ class CurlResponseFailed
 
        var error_code: Int
        var error_msg: String
-
-       init (err_code: Int, err_msg: String)
-       do
-               self.error_code = err_code
-               self.error_msg = err_msg
-       end
 end
 
 # Success Abstract Response Success Class
@@ -211,11 +356,10 @@ abstract class CurlResponseSuccessIntern
        super CurlCallbacks
        super CurlResponse
 
-       var headers: HashMap[String, String] = new HashMap[String, String]
-       var status_code: Int = 0
+       var headers = new HashMap[String, String]
 
        # Receive headers from request due to headers callback registering
-       redef fun header_callback(line: String)
+       redef fun header_callback(line)
        do
                var splitted = line.split_with(':')
                if splitted.length > 1 then
@@ -229,7 +373,8 @@ end
 class CurlResponseSuccess
        super CurlResponseSuccessIntern
 
-       var body_str: String = ""
+       var body_str = ""
+       var status_code = 0
 
        # Receive body from request due to body callback registering
        redef fun body_callback(line: String)
@@ -238,17 +383,23 @@ class CurlResponseSuccess
        end
 end
 
+# Success Response Class of mail request
+class CurlMailResponseSuccess
+       super CurlResponseSuccessIntern
+end
+
 # Success Response Class of a downloaded File
 class CurlFileResponseSuccess
        super CurlResponseSuccessIntern
 
-       var speed_download: Int = 0
-       var size_download: Int = 0
-       var total_time: Int = 0
+       var status_code = 0
+       var speed_download = 0
+       var size_download = 0
+       var total_time = 0
        private var i_file: nullable OFile = null
 
        # Receive bytes stream from request due to stream callback registering
-       redef fun stream_callback(buffer: String, size: Int, count: Int)
+       redef fun stream_callback(buffer, size, count)
        do
                self.i_file.write(buffer, size, count)
        end
@@ -269,15 +420,7 @@ class HeaderMap
                return res
        end
 
-       fun iterate !each(k, v: String)
-       do
-               var i = arr.iterator
-               while i.is_ok do
-                       var item = i.item
-                       each(item.first, item.second)
-                       i.next
-               end
-       end
+       fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
 
        # Convert Self to a single string used to post http fields
        fun to_url_encoded(curl: CCurl): String
@@ -285,7 +428,7 @@ class HeaderMap
                assert curlNotInitialized: curl.is_init else
                        print "to_url_encoded required a valid instance of CCurl Object."
                end
-               var str: String = ""
+               var str = ""
                var length = self.length
                var i = 0
                for k, v in self do
@@ -311,3 +454,15 @@ class HeaderMap
        fun length: Int do return arr.length
        fun is_empty: Bool do return arr.is_empty
 end
+
+class HeaderMapIterator
+       super MapIterator[String, String]
+
+       private var iterator: Iterator[Couple[String, String]]
+       init(map: HeaderMap) is old_style_init do self.iterator = map.arr.iterator
+
+       redef fun is_ok do return self.iterator.is_ok
+       redef fun next do self.iterator.next
+       redef fun item do return self.iterator.item.second
+       redef fun key do return self.iterator.item.first
+end