*: remove newly superfluous static types on attributes
[nit.git] / lib / curl / curl.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Matthieu Lucas <lucasmatthieu@gmail.com>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Data transfer with URL syntax
18 #
19 # Download or upload over HTTP with `CurlHTTPRequest` and send emails with `CurlMail`.
20 module curl
21
22 import native_curl
23
24 redef class Sys
25 # Shared Curl library handle
26 #
27 # Usually, you do not have to use this attribute, it instancied by `CurlHTTPRequest` and `CurlMail`.
28 # But in some cases you may want to finalize it to free some small resources.
29 # However, if Curl services are needed once again, this attribute must be manually set.
30 var curl: Curl = new Curl is lazy, writable
31 end
32
33 # Curl library handle, it is initialized and released with this class
34 class Curl
35 super FinalizableOnce
36
37 private var native = new NativeCurl.easy_init
38
39 # Check for correct initialization
40 fun is_ok: Bool do return self.native.is_init
41
42 redef fun finalize_once do if is_ok then native.easy_clean
43 end
44
45 # CURL Request
46 class CurlRequest
47
48 private var curl: Curl = sys.curl
49
50 # Shall this request be verbose?
51 var verbose: Bool = false is writable
52
53 # Intern perform method, lowest level of request launching
54 private fun perform: nullable CurlResponseFailed
55 do
56 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
57
58 var err
59
60 err = self.curl.native.easy_setopt(new CURLOption.verbose, self.verbose)
61 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
62
63 err = self.curl.native.easy_perform
64 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
65
66 return null
67 end
68
69 # Intern method with return a failed answer with given code and message
70 private fun answer_failure(error_code: Int, error_msg: String): CurlResponseFailed
71 do
72 return new CurlResponseFailed(error_code, error_msg)
73 end
74 end
75
76 # HTTP request builder
77 #
78 # The request itself is sent by either `execute` or `download_to_file`.
79 # The attributes of this class must be set before calling either of these two methods.
80 #
81 # ## Minimal usage example
82 #
83 # ~~~
84 # var request = new CurlHTTPRequest("http://example.org/")
85 # var response = request.execute
86 # if response isa CurlResponseSuccess then
87 # print "Response status code: {response.status_code}"
88 # print response.body_str
89 # else if response isa CurlResponseFailed then
90 # print_error response.error_msg
91 # end
92 # ~~~
93 class CurlHTTPRequest
94 super CurlRequest
95 super NativeCurlCallbacks
96
97 # Address of the remote resource to request
98 var url: String
99
100 # Data for the body of a POST request
101 var data: nullable HeaderMap is writable
102
103 # Raw body string
104 #
105 # Set this value to send raw data instead of the POST formatted `data`.
106 #
107 # If `data` is set, the body will not be sent.
108 var body: nullable String is writable
109
110 # Header content of the request
111 var headers: nullable HeaderMap is writable
112
113 # Delegates to customize the behavior when running `execute`
114 var delegate: nullable CurlCallbacks is writable
115
116 # Set the user agent for all following HTTP requests
117 var user_agent: nullable String is writable
118
119 # Execute HTTP request
120 #
121 # By default, the response body is returned in an instance of `CurlResponse`.
122 # This behavior can be customized by setting a custom `delegate`.
123 fun execute: CurlResponse
124 do
125 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
126
127 var success_response = new CurlResponseSuccess
128 var callback_receiver: CurlCallbacks = success_response
129 if self.delegate != null then callback_receiver = self.delegate.as(not null)
130
131 var err
132
133 err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
134 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
135
136 err = self.curl.native.easy_setopt(new CURLOption.url, url)
137 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
138
139 var user_agent = user_agent
140 if user_agent != null then
141 err = curl.native.easy_setopt(new CURLOption.user_agent, user_agent)
142 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
143 end
144
145 # Callbacks
146 err = self.curl.native.register_callback_header(callback_receiver)
147 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
148
149 err = self.curl.native.register_callback_body(callback_receiver)
150 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
151
152 # HTTP Header
153 var headers = self.headers
154 if headers != null then
155 var headers_joined = headers.join_pairs(": ")
156 err = self.curl.native.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
157 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
158 end
159
160 # Datas
161 var data = self.data
162 if data != null then
163 var postdatas = data.to_url_encoded(self.curl)
164 err = self.curl.native.easy_setopt(new CURLOption.postfields, postdatas)
165 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
166 else if body != null then
167 err = self.curl.native.easy_setopt(new CURLOption.postfields, body.as(not null))
168 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
169 end
170
171 var err_resp = perform
172 if err_resp != null then return err_resp
173
174 var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
175 if not st_code == null then success_response.status_code = st_code
176
177 return success_response
178 end
179
180 # Download to file given resource
181 fun download_to_file(output_file_name: nullable String): CurlResponse
182 do
183 var success_response = new CurlFileResponseSuccess
184
185 var callback_receiver: CurlCallbacks = success_response
186 if self.delegate != null then callback_receiver = self.delegate.as(not null)
187
188 var err
189
190 err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
191 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
192
193 err = self.curl.native.easy_setopt(new CURLOption.url, url)
194 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
195
196 err = self.curl.native.register_callback_header(callback_receiver)
197 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
198
199 err = self.curl.native.register_callback_stream(callback_receiver)
200 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
201
202 var opt_name
203 if not output_file_name == null then
204 opt_name = output_file_name
205 else if not self.url.substring(self.url.length-1, self.url.length) == "/" then
206 opt_name = self.url.basename
207 else
208 return answer_failure(0, "Unable to extract file name, please specify one")
209 end
210
211 success_response.file = new FileWriter.open(opt_name)
212 if not success_response.file.is_writable then
213 return answer_failure(0, "Unable to create associated file")
214 end
215
216 var err_resp = perform
217 if err_resp != null then return err_resp
218
219 var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
220 if not st_code == null then success_response.status_code = st_code
221
222 var speed = self.curl.native.easy_getinfo_double(new CURLInfoDouble.speed_download)
223 if not speed == null then success_response.speed_download = speed
224
225 var size = self.curl.native.easy_getinfo_double(new CURLInfoDouble.size_download)
226 if not size == null then success_response.size_download = size
227
228 var time = self.curl.native.easy_getinfo_double(new CURLInfoDouble.total_time)
229 if not time == null then success_response.total_time = time
230
231 success_response.file.close
232
233 return success_response
234 end
235 end
236
237 # CURL Mail Request
238 #
239 # ~~~
240 # # Craft mail
241 # var mail = new CurlMail("sender@example.org",
242 # to=["to@example.org"], cc=["bob@example.org"])
243 #
244 # mail.headers_body["Content-Type:"] = """text/html; charset="UTF-8""""
245 # mail.headers_body["Content-Transfer-Encoding:"] = "quoted-printable"
246 #
247 # mail.body = "<h1>Here you can write HTML stuff.</h1>"
248 # mail.subject = "Hello From My Nit Program"
249 #
250 # # Set mail server
251 # var error = mail.set_outgoing_server("smtps://smtp.example.org:465",
252 # "user@example.org", "mypassword")
253 # if error != null then
254 # print "Mail Server Error: {error}"
255 # exit 0
256 # end
257 #
258 # # Send
259 # error = mail.execute
260 # if error != null then
261 # print "Transfer Error: {error}"
262 # exit 0
263 # end
264 # ~~~
265 class CurlMail
266 super CurlRequest
267 super NativeCurlCallbacks
268
269 # Address of the sender
270 var from: nullable String is writable
271
272 # Main recipients
273 var to: nullable Array[String] is writable
274
275 # Subject line
276 var subject: nullable String is writable
277
278 # Text content
279 var body: nullable String is writable
280
281 # CC recipients
282 var cc: nullable Array[String] is writable
283
284 # BCC recipients (hidden from other recipients)
285 var bcc: nullable Array[String] is writable
286
287 # HTTP header
288 var headers = new HeaderMap is lazy, writable
289
290 # Content header
291 var headers_body = new HeaderMap is lazy, writable
292
293 # Protocols supported to send mail to a server
294 #
295 # Default value at `["smtp", "smtps"]`
296 var supported_outgoing_protocol = ["smtp", "smtps"]
297
298 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
299 private fun add_pair_to_content(str: String, att: String, val: nullable String): String
300 do
301 if val != null then return "{str}{att}{val}\n"
302 return "{str}{att}\n"
303 end
304
305 # Helper method to add entire list of pairs to mail content
306 private fun add_pairs_to_content(content: String, pairs: HeaderMap): String
307 do
308 for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
309 return content
310 end
311
312 # Check for host and protocol availability
313 private fun is_supported_outgoing_protocol(host: String): CURLCode
314 do
315 var host_reach = host.split_with("://")
316 if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
317 return once new CURLCode.unsupported_protocol
318 end
319
320 # Configure server host and user credentials if needed.
321 fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String): nullable CurlResponseFailed
322 do
323 # Check Curl initialisation
324 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
325
326 var err
327
328 # Host & Protocol
329 err = is_supported_outgoing_protocol(host)
330 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
331
332 err = self.curl.native.easy_setopt(new CURLOption.url, host)
333 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
334
335 # Credentials
336 if not user == null and not pwd == null then
337 err = self.curl.native.easy_setopt(new CURLOption.username, user)
338 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
339
340 err = self.curl.native.easy_setopt(new CURLOption.password, pwd)
341 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
342 end
343
344 return null
345 end
346
347 # Execute Mail request with settings configured through attribute
348 fun execute: nullable CurlResponseFailed
349 do
350 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
351
352 var lines = new Array[String]
353
354 # Headers
355 var headers = self.headers
356 if not headers.is_empty then
357 for k, v in headers do lines.add "{k}{v}"
358 end
359
360 # Recipients
361 var all_recipients = new Array[String]
362 var to = self.to
363 if to != null and to.length > 0 then
364 lines.add "To:{to.join(",")}"
365 all_recipients.append to
366 end
367
368 var cc = self.cc
369 if cc != null and cc.length > 0 then
370 lines.add "Cc:{cc.join(",")}"
371 all_recipients.append cc
372 end
373
374 var bcc = self.bcc
375 if bcc != null and bcc.length > 0 then all_recipients.append bcc
376
377 if all_recipients.is_empty then return answer_failure(0, "There must be at lease one recipient")
378
379 var err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
380 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
381
382 err = self.curl.native.easy_setopt(new CURLOption.mail_rcpt, all_recipients.to_curlslist)
383 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
384
385 # From
386 var from = self.from
387 if not from == null then
388 lines.add "From:{from}"
389
390 err = self.curl.native.easy_setopt(new CURLOption.mail_from, from)
391 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
392 end
393
394 # Subject
395 var subject = self.subject
396 if subject == null then subject = "" # Default
397 lines.add "Subject: {subject}"
398
399 # Headers body
400 var headers_body = self.headers_body
401 if not headers_body.is_empty then
402 for k, v in headers_body do lines.add "{k}{v}"
403 end
404
405 # Body
406 var body = self.body
407 if body == null then body = "" # Default
408
409 lines.add ""
410 lines.add body
411 lines.add ""
412
413 err = self.curl.native.register_callback_read(self)
414 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
415
416 var content = lines.join("\n")
417 err = self.curl.native.register_read_datas_callback(self, content)
418 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
419
420 var err_resp = perform
421 if err_resp != null then return err_resp
422
423 return null
424 end
425 end
426
427 # Callbacks Interface, allow you to manage in your way the different streams
428 interface CurlCallbacks
429 super NativeCurlCallbacks
430 end
431
432 # Abstract Curl request response
433 abstract class CurlResponse
434 end
435
436 # Failed Response Class returned when errors during configuration are raised
437 class CurlResponseFailed
438 super CurlResponse
439
440 var error_code: Int
441 var error_msg: String
442
443 redef fun to_s do return "{error_msg} ({error_code})"
444 end
445
446 # Success Abstract Response Success Class
447 abstract class CurlResponseSuccessIntern
448 super CurlCallbacks
449 super CurlResponse
450
451 var headers = new HashMap[String, String]
452
453 # Receive headers from request due to headers callback registering
454 redef fun header_callback(line)
455 do
456 var splitted = line.split_with(':')
457 if splitted.length > 1 then
458 var key = splitted.shift
459 self.headers[key] = splitted.to_s
460 end
461 end
462 end
463
464 # Success Response Class of a basic response
465 class CurlResponseSuccess
466 super CurlResponseSuccessIntern
467
468 var body_str = ""
469 var status_code = 0
470
471 # Receive body from request due to body callback registering
472 redef fun body_callback(line) do
473 self.body_str = "{self.body_str}{line}"
474 end
475 end
476
477 # Success Response Class of a downloaded File
478 class CurlFileResponseSuccess
479 super CurlResponseSuccessIntern
480
481 var status_code = 0
482 var speed_download = 0.0
483 var size_download = 0.0
484 var total_time = 0.0
485 private var file: nullable FileWriter = null
486
487 # Receive bytes stream from request due to stream callback registering
488 redef fun stream_callback(buffer)
489 do
490 file.write buffer
491 end
492 end
493
494 # Pseudo map associating `String` to `String` for HTTP exchanges
495 #
496 # This structure differs from `Map` as each key can have multiple associations
497 # and the order of insertion is important to some services.
498 class HeaderMap
499 private var array = new Array[Couple[String, String]]
500
501 # Add a `value` associated to `key`
502 fun []=(key, value: String)
503 do
504 array.add new Couple[String, String](key, value)
505 end
506
507 # Get a list of the keys associated to `key`
508 fun [](k: String): Array[String]
509 do
510 var res = new Array[String]
511 for c in array do if c.first == k then res.add c.second
512 return res
513 end
514
515 # Iterate over all the associations in `self`
516 fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
517
518 # Get `self` as a single string for HTTP POST
519 #
520 # Require: `curl.is_ok`
521 fun to_url_encoded(curl: Curl): String
522 do
523 assert curl.is_ok
524
525 var lines = new Array[String]
526 for k, v in self do
527 if k.length == 0 then continue
528
529 k = curl.native.escape(k)
530 v = curl.native.escape(v)
531 lines.add "{k}={v}"
532 end
533 return lines.join("&")
534 end
535
536 # Concatenate couple of 'key value' separated by 'sep' in Array
537 fun join_pairs(sep: String): Array[String]
538 do
539 var col = new Array[String]
540 for k, v in self do col.add("{k}{sep}{v}")
541 return col
542 end
543
544 # Number of values in `self`
545 fun length: Int do return array.length
546
547 # Is this map empty?
548 fun is_empty: Bool do return array.is_empty
549 end
550
551 private class HeaderMapIterator
552 super MapIterator[String, String]
553
554 var map: HeaderMap
555 var iterator: Iterator[Couple[String, String]] = map.array.iterator is lazy
556
557 redef fun is_ok do return self.iterator.is_ok
558 redef fun next do self.iterator.next
559 redef fun item do return self.iterator.item.second
560 redef fun key do return self.iterator.item.first
561 end