*: remove newly superfluous static types on attributes
[nit.git] / lib / curl / curl.nit
index 323e660..57a2423 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Network functionnalities based on Curl_c module.
+# Data transfer with URL syntax
+#
+# Download or upload over HTTP with `CurlHTTPRequest` and send emails with `CurlMail`.
 module curl
 
-import curl_c
+import native_curl
+
+redef class Sys
+       # Shared Curl library handle
+       #
+       # Usually, you do not have to use this attribute, it instancied by `CurlHTTPRequest` and `CurlMail`.
+       # But in some cases you may want to finalize it to free some small resources.
+       # However, if Curl services are needed once again, this attribute must be manually set.
+       var curl: Curl = new Curl is lazy, writable
+end
 
-# Top level of Curl
+# Curl library handle, it is initialized and released with this class
 class Curl
-       protected var prim_curl: CCurl
+       super FinalizableOnce
 
-       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
-       end
+       private var native = new NativeCurl.easy_init
 
        # 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
+       fun is_ok: Bool do return self.native.is_init
 
-               return new CurlHTTPRequest(url, self)
-       end
-
-       # Release Curl instance
-       fun destroy do self.prim_curl.easy_clean
+       redef fun finalize_once do if is_ok then native.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
+       private var curl: Curl = sys.curl
 
-       # Launch request method
-       fun execute: CurlResponse is abstract
+       # Shall this request be verbose?
+       var verbose: Bool = false is writable
 
        # 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)
+               err = self.curl.native.easy_setopt(new CURLOption.verbose, self.verbose)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
-               err = self.curl.prim_curl.easy_perform
+               err = self.curl.native.easy_perform
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
                return null
        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
 end
 
-# CURL HTTP Request
+# HTTP request builder
+#
+# The request itself is sent by either `execute` or `download_to_file`.
+# The attributes of this class must be set before calling either of these two methods.
+#
+# ## Minimal usage example
+#
+# ~~~
+# var request = new CurlHTTPRequest("http://example.org/")
+# var response = request.execute
+# if response isa CurlResponseSuccess then
+#     print "Response status code: {response.status_code}"
+#     print response.body_str
+# else if response isa CurlResponseFailed then
+#     print_error response.error_msg
+# end
+# ~~~
 class CurlHTTPRequest
        super CurlRequest
+       super NativeCurlCallbacks
+
+       # Address of the remote resource to request
+       var url: String
+
+       # Data for the body of a POST request
+       var data: nullable HeaderMap is writable
+
+       # Raw body string
+       #
+       # Set this value to send raw data instead of the POST formatted `data`.
+       #
+       # If `data` is set, the body will not be sent.
+       var body: nullable String is writable
+
+       # Header content of the request
+       var headers: nullable HeaderMap is writable
+
+       # Delegates to customize the behavior when running `execute`
+       var delegate: nullable CurlCallbacks is writable
 
-       # Execute HTTP request with settings configured through attribute
-       redef fun execute: CurlResponse
+       # Set the user agent for all following HTTP requests
+       var user_agent: nullable String is writable
+
+       # Execute HTTP request
+       #
+       # By default, the response body is returned in an instance of `CurlResponse`.
+       # This behavior can be customized by setting a custom `delegate`.
+       fun execute: CurlResponse
        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.native.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.native.easy_setopt(new CURLOption.url, url)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               var user_agent = user_agent
+               if user_agent != null then
+                       err = curl.native.easy_setopt(new CURLOption.user_agent, user_agent)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
 
-               err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
+               # Callbacks
+               err = self.curl.native.register_callback_header(callback_receiver)
                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)
+               err = self.curl.native.register_callback_body(callback_receiver)
                if not err.is_ok then return answer_failure(err.to_i, err.to_s)
 
+               # HTTP Header
+               var headers = self.headers
+               if headers != null then
+                       var headers_joined = headers.join_pairs(": ")
+                       err = self.curl.native.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
+               var data = self.data
+               if data != null then
+                       var postdatas = data.to_url_encoded(self.curl)
+                       err = self.curl.native.easy_setopt(new CURLOption.postfields, postdatas)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               else if body != null then
+                       err = self.curl.native.easy_setopt(new CURLOption.postfields, body.as(not null))
+                       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
 
-               var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
-               if not st_code == null then success_response.status_code = st_code.response
+               var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
+               if not st_code == null then success_response.status_code = st_code
 
                return success_response
        end
@@ -134,57 +180,253 @@ 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
-               err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
+               var err
+
+               err = self.curl.native.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.native.easy_setopt(new CURLOption.url, url)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               err = self.curl.native.register_callback_header(callback_receiver)
                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)
+               err = self.curl.native.register_callback_stream(callback_receiver)
                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
-                       opt_name = self.url.basename("")
+                       opt_name = self.url.basename
                else
                        return answer_failure(0, "Unable to extract file name, please specify one")
                end
 
-               success_response.i_file = new OFile.open(opt_name.to_cstring)
-               if not success_response.i_file.is_valid then
-                       success_response.i_file.close
+               success_response.file = new FileWriter.open(opt_name)
+               if not success_response.file.is_writable then
                        return answer_failure(0, "Unable to create associated file")
                end
 
                var err_resp = perform
                if err_resp != null then return err_resp
 
-               var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
-               if not st_code == null then success_response.status_code = st_code.response
+               var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
+               if not st_code == null then success_response.status_code = st_code
 
-               var speed = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.speed_download)
-               if not speed == null then success_response.speed_download = speed.response
+               var speed = self.curl.native.easy_getinfo_double(new CURLInfoDouble.speed_download)
+               if not speed == null then success_response.speed_download = speed
 
-               var size = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.size_download)
-               if not size == null then success_response.size_download = size.response
+               var size = self.curl.native.easy_getinfo_double(new CURLInfoDouble.size_download)
+               if not size == null then success_response.size_download = size
 
-               var time = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.total_time)
-               if not time == null then success_response.total_time = time.response
+               var time = self.curl.native.easy_getinfo_double(new CURLInfoDouble.total_time)
+               if not time == null then success_response.total_time = time
 
-               success_response.i_file.close
+               success_response.file.close
 
                return success_response
        end
 end
 
+# CURL Mail Request
+#
+# ~~~
+# # Craft mail
+# var mail = new CurlMail("sender@example.org",
+#                            to=["to@example.org"], cc=["bob@example.org"])
+#
+# mail.headers_body["Content-Type:"] = """text/html; charset="UTF-8""""
+# mail.headers_body["Content-Transfer-Encoding:"] = "quoted-printable"
+#
+# mail.body = "<h1>Here you can write HTML stuff.</h1>"
+# mail.subject = "Hello From My Nit Program"
+#
+# # Set mail server
+# var error = mail.set_outgoing_server("smtps://smtp.example.org:465",
+#                                      "user@example.org", "mypassword")
+# if error != null then
+#     print "Mail Server Error: {error}"
+#     exit 0
+# end
+#
+# # Send
+# error = mail.execute
+# if error != null then
+#     print "Transfer Error: {error}"
+#     exit 0
+# end
+# ~~~
+class CurlMail
+       super CurlRequest
+       super NativeCurlCallbacks
+
+       # Address of the sender
+       var from: nullable String is writable
+
+       # Main recipients
+       var to: nullable Array[String] is writable
+
+       # Subject line
+       var subject: nullable String is writable
+
+       # Text content
+       var body: nullable String is writable
+
+       # CC recipients
+       var cc: nullable Array[String] is writable
+
+       # BCC recipients (hidden from other recipients)
+       var bcc: nullable Array[String] is writable
+
+       # HTTP header
+       var headers = new HeaderMap is lazy, writable
+
+       # Content header
+       var headers_body = new HeaderMap is lazy, writable
+
+       # Protocols supported to send mail to a server
+       #
+       # Default value at `["smtp", "smtps"]`
+       var supported_outgoing_protocol = ["smtp", "smtps"]
+
+       # 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.native.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.native.easy_setopt(new CURLOption.username, user)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+                       err = self.curl.native.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
+       fun execute: nullable CurlResponseFailed
+       do
+               if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
+
+               var lines = new Array[String]
+
+               # Headers
+               var headers = self.headers
+               if not headers.is_empty then
+                       for k, v in headers do lines.add "{k}{v}"
+               end
+
+               # Recipients
+               var all_recipients = new Array[String]
+               var to = self.to
+               if to != null and to.length > 0 then
+                       lines.add "To:{to.join(",")}"
+                       all_recipients.append to
+               end
+
+               var cc = self.cc
+               if cc != null and cc.length > 0 then
+                       lines.add "Cc:{cc.join(",")}"
+                       all_recipients.append cc
+               end
+
+               var bcc = self.bcc
+               if bcc != null and bcc.length > 0 then all_recipients.append bcc
+
+               if all_recipients.is_empty then return answer_failure(0, "There must be at lease one recipient")
+
+               var err = self.curl.native.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.native.easy_setopt(new CURLOption.mail_rcpt, all_recipients.to_curlslist)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               # From
+               var from = self.from
+               if not from == null then
+                       lines.add "From:{from}"
+
+                       err = self.curl.native.easy_setopt(new CURLOption.mail_from, from)
+                       if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+               end
+
+               # Subject
+               var subject = self.subject
+               if subject == null then subject = "" # Default
+               lines.add "Subject: {subject}"
+
+               # Headers body
+               var headers_body = self.headers_body
+               if not headers_body.is_empty then
+                       for k, v in headers_body do lines.add "{k}{v}"
+               end
+
+               # Body
+               var body = self.body
+               if body == null then body = "" # Default
+
+               lines.add ""
+               lines.add body
+               lines.add ""
+
+               err = self.curl.native.register_callback_read(self)
+               if not err.is_ok then return answer_failure(err.to_i, err.to_s)
+
+               var content = lines.join("\n")
+               err = self.curl.native.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 null
+       end
+end
+
 # Callbacks Interface, allow you to manage in your way the different streams
 interface CurlCallbacks
-       super CCurlCallbacks
+       super NativeCurlCallbacks
 end
 
 # Abstract Curl request response
@@ -198,11 +440,7 @@ 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
+       redef fun to_s do return "{error_msg} ({error_code})"
 end
 
 # Success Abstract Response Success Class
@@ -210,11 +448,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
@@ -228,11 +465,11 @@ 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)
-       do
+       redef fun body_callback(line) do
                self.body_str = "{self.body_str}{line}"
        end
 end
@@ -241,62 +478,59 @@ end
 class CurlFileResponseSuccess
        super CurlResponseSuccessIntern
 
-       var speed_download: Int = 0
-       var size_download: Int = 0
-       var total_time: Int = 0
-       private var i_file: nullable OFile = null
+       var status_code = 0
+       var speed_download = 0.0
+       var size_download = 0.0
+       var total_time = 0.0
+       private var file: nullable FileWriter = 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)
        do
-               self.i_file.write(buffer, size, count)
+               file.write buffer
        end
 end
 
-# Pseudo map associating Strings to Strings,
-# each key can have multiple associations
-# and the order of insertion is important.
+# Pseudo map associating `String` to `String` for HTTP exchanges
+#
+# This structure differs from `Map` as each key can have multiple associations
+# and the order of insertion is important to some services.
 class HeaderMap
-       private var arr = new Array[Couple[String, String]]
+       private var array = new Array[Couple[String, String]]
 
-       fun []=(k, v: String) do arr.add(new Couple[String, String](k, v))
+       # Add a `value` associated to `key`
+       fun []=(key, value: String)
+       do
+               array.add new Couple[String, String](key, value)
+       end
 
+       # Get a list of the keys associated to `key`
        fun [](k: String): Array[String]
        do
                var res = new Array[String]
-               for c in arr do if c.first == k then res.add(c.second)
+               for c in array do if c.first == k then res.add c.second
                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
+       # Iterate over all the associations in `self`
+       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
+       # Get `self` as a single string for HTTP POST
+       #
+       # Require: `curl.is_ok`
+       fun to_url_encoded(curl: Curl): String
        do
-               assert curlNotInitialized: curl.is_init else
-                       print "to_url_encoded required a valid instance of CCurl Object."
-               end
-               var str: String = ""
-               var length = self.length
-               var i = 0
+               assert curl.is_ok
+
+               var lines = new Array[String]
                for k, v in self do
-                       if k.length > 0 then
-                               k = curl.escape(k)
-                               v = curl.escape(v)
-                               str = "{str}{k}={v}"
-                               if i < length-1 then str = "{str}&"
-                       end
-                       i += 1
+                       if k.length == 0 then continue
+
+                       k = curl.native.escape(k)
+                       v = curl.native.escape(v)
+                       lines.add "{k}={v}"
                end
-               return str
+               return lines.join("&")
        end
 
        # Concatenate couple of 'key value' separated by 'sep' in Array
@@ -307,6 +541,21 @@ class HeaderMap
                return col
        end
 
-       fun length: Int do return arr.length
-       fun is_empty: Bool do return arr.is_empty
+       # Number of values in `self`
+       fun length: Int do return array.length
+
+       # Is this map empty?
+       fun is_empty: Bool do return array.is_empty
+end
+
+private class HeaderMapIterator
+       super MapIterator[String, String]
+
+       var map: HeaderMap
+       var iterator: Iterator[Couple[String, String]] = map.array.iterator is lazy
+
+       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