tests: update sav/test_new_native_alt1.res because of changes in array.nit
[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 # Network functionnalities based on Curl_c module.
18 module curl
19
20 import curl_c
21
22 # Top level of Curl
23 class Curl
24 protected var prim_curl: CCurl
25
26 init
27 do
28 self.prim_curl = new CCurl.easy_init
29 assert curlInstance:self.prim_curl.is_init else
30 print "Curl must be instancied to be used"
31 end
32 end
33
34 # Check for correct initialization
35 fun is_ok: Bool do return self.prim_curl.is_init
36
37 # Release Curl instance
38 fun destroy do self.prim_curl.easy_clean
39 end
40
41 # CURL Request
42 class CurlRequest
43
44 var verbose: Bool writable = false
45 private var curl: nullable Curl = null
46
47 # Launch request method
48 fun execute: CurlResponse is abstract
49
50 # Intern perform method, lowest level of request launching
51 private fun perform: nullable CurlResponse
52 do
53 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
54
55 var err
56
57 err = self.curl.prim_curl.easy_setopt(new CURLOption.verbose, self.verbose)
58 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
59
60 err = self.curl.prim_curl.easy_perform
61 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
62
63 return null
64 end
65
66 # Intern method with return a failed answer with given code and message
67 private fun answer_failure(error_code: Int, error_msg: String): CurlResponse
68 do
69 return new CurlResponseFailed(error_code, error_msg)
70 end
71 end
72
73 # CURL HTTP Request
74 class CurlHTTPRequest
75 super CurlRequest
76 super CCurlCallbacks
77 super CurlCallbacksRegisterIntern
78
79 var url: String
80 var datas: nullable HeaderMap writable = null
81 var headers: nullable HeaderMap writable = null
82
83 # Set the user agent for all following HTTP requests
84 fun user_agent=(name: String)
85 do
86 curl.prim_curl.easy_setopt(new CURLOption.user_agent, name)
87 end
88
89 init (url: String, curl: nullable Curl)
90 do
91 self.url = url
92 self.curl = curl
93 end
94
95 # Execute HTTP request with settings configured through attribute
96 redef fun execute
97 do
98 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
99
100 var success_response = new CurlResponseSuccess
101 var callback_receiver: CurlCallbacks = success_response
102 if self.delegate != null then callback_receiver = self.delegate.as(not null)
103
104 var err
105
106 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
107 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
108
109 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
110 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
111
112 # Callbacks
113 err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
114 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
115
116 err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.body)
117 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
118
119 # HTTP Header
120 if self.headers != null then
121 var headers_joined = self.headers.join_pairs(": ")
122 err = self.curl.prim_curl.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
123 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
124 end
125
126 # Datas
127 if self.datas != null then
128 var postdatas = self.datas.to_url_encoded(self.curl.prim_curl)
129 err = self.curl.prim_curl.easy_setopt(new CURLOption.postfields, postdatas)
130 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
131 end
132
133 var err_resp = perform
134 if err_resp != null then return err_resp
135
136 var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
137 if not st_code == null then success_response.status_code = st_code.response
138
139 return success_response
140 end
141
142 # Download to file given resource
143 fun download_to_file(output_file_name: nullable String): CurlResponse
144 do
145 var success_response = new CurlFileResponseSuccess
146
147 var callback_receiver: CurlCallbacks = success_response
148 if self.delegate != null then callback_receiver = self.delegate.as(not null)
149
150 var err
151
152 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
153 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
154
155 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
156 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
157
158 err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.header)
159 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
160
161 err = self.curl.prim_curl.register_callback(callback_receiver, new CURLCallbackType.stream)
162 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
163
164 var opt_name
165 if not output_file_name == null then
166 opt_name = output_file_name
167 else if not self.url.substring(self.url.length-1, self.url.length) == "/" then
168 opt_name = self.url.basename("")
169 else
170 return answer_failure(0, "Unable to extract file name, please specify one")
171 end
172
173 success_response.i_file = new OFile.open(opt_name.to_cstring)
174 if not success_response.i_file.is_valid then
175 success_response.i_file.close
176 return answer_failure(0, "Unable to create associated file")
177 end
178
179 var err_resp = perform
180 if err_resp != null then return err_resp
181
182 var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
183 if not st_code == null then success_response.status_code = st_code.response
184
185 var speed = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.speed_download)
186 if not speed == null then success_response.speed_download = speed.response
187
188 var size = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.size_download)
189 if not size == null then success_response.size_download = size.response
190
191 var time = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.total_time)
192 if not time == null then success_response.total_time = time.response
193
194 success_response.i_file.close
195
196 return success_response
197 end
198 end
199
200 # CURL Mail Request
201 class CurlMailRequest
202 super CurlRequest
203 super CCurlCallbacks
204
205 var headers: nullable HeaderMap writable = null
206 var headers_body: nullable HeaderMap writable = null
207 var from: nullable String writable = null
208 var to: nullable Array[String] writable = null
209 var cc: nullable Array[String] writable = null
210 var bcc: nullable Array[String] writable = null
211 var subject: nullable String writable = ""
212 var body: nullable String writable = ""
213 private var supported_outgoing_protocol: Array[String] = ["smtp", "smtps"]
214
215 init (curl: nullable Curl)
216 do
217 self.curl = curl
218 end
219
220 # Helper method to add conventional space while building entire mail
221 private fun add_conventional_space(str: String):String do return "{str}\n" end
222
223 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
224 private fun add_pair_to_content(str: String, att: String, val: nullable String):String
225 do
226 if val != null then return "{str}{att}{val}\n"
227 return "{str}{att}\n"
228 end
229
230 # Helper method to add entire list of pairs to mail content
231 private fun add_pairs_to_content(content: String, pairs: HeaderMap):String
232 do
233 for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
234 return content
235 end
236
237 # Check for host and protocol availability
238 private fun is_supported_outgoing_protocol(host: String):CURLCode
239 do
240 var host_reach = host.split_with("://")
241 if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
242 return once new CURLCode.unsupported_protocol
243 end
244
245 # Configure server host and user credentials if needed.
246 fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String):nullable CurlResponse
247 do
248 # Check Curl initialisation
249 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
250
251 var err
252
253 # Host & Protocol
254 err = is_supported_outgoing_protocol(host)
255 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
256 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, host)
257 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
258
259 # Credentials
260 if not user == null and not pwd == null then
261 err = self.curl.prim_curl.easy_setopt(new CURLOption.username, user)
262 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
263 err = self.curl.prim_curl.easy_setopt(new CURLOption.password, pwd)
264 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
265 end
266
267 return null
268 end
269
270 # Execute Mail request with settings configured through attribute
271 redef fun execute
272 do
273 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
274
275 var success_response = new CurlMailResponseSuccess
276 var content = ""
277 # Headers
278 if self.headers != null then
279 content = add_pairs_to_content(content, self.headers.as(not null))
280 end
281
282 # Recipients
283 var g_rec = new Array[String]
284 if self.to != null and self.to.length > 0 then
285 content = add_pair_to_content(content, "To:", self.to.join(","))
286 g_rec.append(self.to.as(not null))
287 end
288 if self.cc != null and self.cc.length > 0 then
289 content = add_pair_to_content(content, "Cc:", self.cc.join(","))
290 g_rec.append(self.cc.as(not null))
291 end
292 if self.bcc != null and self.bcc.length > 0 then g_rec.append(self.bcc.as(not null))
293
294 if g_rec.length < 1 then return answer_failure(0, "The mail recipients can not be empty")
295
296 var err
297
298 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
299 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
300
301 err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_rcpt, g_rec.to_curlslist)
302 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
303
304 # From
305 if not self.from == null then
306 content = add_pair_to_content(content, "From:", self.from)
307 err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_from, self.from.as(not null))
308 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
309 end
310
311 # Subject
312 content = add_pair_to_content(content, "Subject:", self.subject)
313
314 # Headers body
315 if self.headers_body != null then
316 content = add_pairs_to_content(content, self.headers_body.as(not null))
317 end
318
319 # Body
320 content = add_conventional_space(content)
321 content = add_pair_to_content(content, "", self.body)
322 content = add_conventional_space(content)
323 err = self.curl.prim_curl.register_callback(self, once new CURLCallbackType.read)
324 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
325 err = self.curl.prim_curl.register_read_datas_callback(self, content)
326 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
327
328 var err_resp = perform
329 if err_resp != null then return err_resp
330
331 return success_response
332 end
333 end
334
335 # Callbacks Interface, allow you to manage in your way the different streams
336 interface CurlCallbacks
337 super CCurlCallbacks
338 end
339
340 # Callbacks attributes
341 abstract class CurlCallbacksRegisterIntern
342 var delegate: nullable CurlCallbacks writable = null
343 end
344
345 # Abstract Curl request response
346 abstract class CurlResponse
347 end
348
349 # Failed Response Class returned when errors during configuration are raised
350 class CurlResponseFailed
351 super CurlResponse
352
353 var error_code: Int
354 var error_msg: String
355
356 init (err_code: Int, err_msg: String)
357 do
358 self.error_code = err_code
359 self.error_msg = err_msg
360 end
361 end
362
363 # Success Abstract Response Success Class
364 abstract class CurlResponseSuccessIntern
365 super CurlCallbacks
366 super CurlResponse
367
368 var headers = new HashMap[String, String]
369
370 # Receive headers from request due to headers callback registering
371 redef fun header_callback(line)
372 do
373 var splitted = line.split_with(':')
374 if splitted.length > 1 then
375 var key = splitted.shift
376 self.headers[key] = splitted.to_s
377 end
378 end
379 end
380
381 # Success Response Class of a basic response
382 class CurlResponseSuccess
383 super CurlResponseSuccessIntern
384
385 var body_str = ""
386 var status_code = 0
387
388 # Receive body from request due to body callback registering
389 redef fun body_callback(line: String)
390 do
391 self.body_str = "{self.body_str}{line}"
392 end
393 end
394
395 # Success Response Class of mail request
396 class CurlMailResponseSuccess
397 super CurlResponseSuccessIntern
398 end
399
400 # Success Response Class of a downloaded File
401 class CurlFileResponseSuccess
402 super CurlResponseSuccessIntern
403
404 var status_code = 0
405 var speed_download = 0
406 var size_download = 0
407 var total_time = 0
408 private var i_file: nullable OFile = null
409
410 # Receive bytes stream from request due to stream callback registering
411 redef fun stream_callback(buffer, size, count)
412 do
413 self.i_file.write(buffer, size, count)
414 end
415 end
416
417 # Pseudo map associating Strings to Strings,
418 # each key can have multiple associations
419 # and the order of insertion is important.
420 class HeaderMap
421 private var arr = new Array[Couple[String, String]]
422
423 fun []=(k, v: String) do arr.add(new Couple[String, String](k, v))
424
425 fun [](k: String): Array[String]
426 do
427 var res = new Array[String]
428 for c in arr do if c.first == k then res.add(c.second)
429 return res
430 end
431
432 fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
433
434 # Convert Self to a single string used to post http fields
435 fun to_url_encoded(curl: CCurl): String
436 do
437 assert curlNotInitialized: curl.is_init else
438 print "to_url_encoded required a valid instance of CCurl Object."
439 end
440 var str = ""
441 var length = self.length
442 var i = 0
443 for k, v in self do
444 if k.length > 0 then
445 k = curl.escape(k)
446 v = curl.escape(v)
447 str = "{str}{k}={v}"
448 if i < length-1 then str = "{str}&"
449 end
450 i += 1
451 end
452 return str
453 end
454
455 # Concatenate couple of 'key value' separated by 'sep' in Array
456 fun join_pairs(sep: String): Array[String]
457 do
458 var col = new Array[String]
459 for k, v in self do col.add("{k}{sep}{v}")
460 return col
461 end
462
463 fun length: Int do return arr.length
464 fun is_empty: Bool do return arr.is_empty
465 end
466
467 class HeaderMapIterator
468 super MapIterator[String, String]
469
470 private var iterator: Iterator[Couple[String, String]]
471 init(map: HeaderMap) do self.iterator = map.arr.iterator
472
473 redef fun is_ok do return self.iterator.is_ok
474 redef fun next do self.iterator.next
475 redef fun item do return self.iterator.item.second
476 redef fun key do return self.iterator.item.first
477 end