xcapclient 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ERRORS.rdoc +123 -0
- data/LICENSE.txt +674 -0
- data/README.rdoc +172 -0
- data/lib/xcapclient/application.rb +62 -0
- data/lib/xcapclient/client.rb +584 -0
- data/lib/xcapclient/document.rb +40 -0
- data/lib/xcapclient/errors.rb +21 -0
- data/lib/xcapclient.rb +30 -0
- data/test/PRES_RULES_EXAMPLE.xml +49 -0
- data/test/test_unit_01.rb +352 -0
- metadata +74 -0
@@ -0,0 +1,584 @@
|
|
1
|
+
module XCAPClient
|
2
|
+
|
3
|
+
|
4
|
+
# The base class of the library. A program using this library must instantiate it.
|
5
|
+
#
|
6
|
+
# === Common notes for methods <tt>get*</tt>, <tt>put*</tt> and <tt>delete*</tt>
|
7
|
+
#
|
8
|
+
# ==== Shared parameters
|
9
|
+
#
|
10
|
+
# *_auid_*: String, the auid of the application. Example:"pres-rules".
|
11
|
+
#
|
12
|
+
# *_document_*: nil by default. It can be:
|
13
|
+
# * String, so the application must contain a document called as it.
|
14
|
+
# * XCAPClient::Document object.
|
15
|
+
# * nil, so the application default document is selected.
|
16
|
+
#
|
17
|
+
# *_check_etag_*: true by default. If true the client adds the header "If-None-Match" or "If-Match" to the HTTP request containing the last ETag received.
|
18
|
+
#
|
19
|
+
# *_selector_*: Document/node selector. The path that identifies the XML document or node within the XCAP root URL. It's automatically converted to ASCII and percent encoded if needed. Example:
|
20
|
+
#
|
21
|
+
# '/cp:ruleset/cp:rule/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]'
|
22
|
+
#
|
23
|
+
# *_xml_namespaces_*: nil by default. It's a hash containing the prefixes and namespaces used in the query. Example:
|
24
|
+
#
|
25
|
+
# {"pr"=>"urn:ietf:params:xml:ns:pres-rules", "cp"=>"urn:ietf:params:xml:ns:common-policy"}
|
26
|
+
#
|
27
|
+
# ==== Exceptions
|
28
|
+
#
|
29
|
+
# If these methods receive a non HTTP 2XX response they generate an exception. Check ERRORS.rdoc for detailed info.
|
30
|
+
#
|
31
|
+
# ==== Access to the full response
|
32
|
+
#
|
33
|
+
# In any case, the HTTP response is stored in the document _last_response_ attribute as an HTTP::Message[http://dev.ctor.org/doc/httpclient/] object. To get it:
|
34
|
+
#
|
35
|
+
# @xcapclient.application("pres-rules").document.last_response
|
36
|
+
#
|
37
|
+
class Client
|
38
|
+
|
39
|
+
|
40
|
+
USER_AGENT = "Ruby-XCAPClient"
|
41
|
+
COMMON_HEADERS = {
|
42
|
+
"User-Agent" => "#{USER_AGENT}/#{VERSION}",
|
43
|
+
"Connection" => "close"
|
44
|
+
}
|
45
|
+
GLOBAL_XUI = "global"
|
46
|
+
XCAP_CAPS_XMLNS = "urn:ietf:params:xml:ns:xcap-caps"
|
47
|
+
HTTP_TIMEOUT = 6
|
48
|
+
CONF_PARAMETERS = [:xcap_root, :user, :auth_user, :password, :ssl_verify_cert]
|
49
|
+
|
50
|
+
|
51
|
+
attr_reader :xcap_root, :user, :auth_user, :password, :ssl_verify_cert
|
52
|
+
|
53
|
+
|
54
|
+
# Create a new XCAP client. It requires two parameters:
|
55
|
+
#
|
56
|
+
# * _conf_: A hash containing settings related to the server:
|
57
|
+
# * _xcap_root_: The URL where the documents hold.
|
58
|
+
# * _user_: The client username. Depending on the server it could look like "sip:alice@domain.org", "alice@domain.org" or "alice".
|
59
|
+
# * _auth_user_: Username for authentication (if required).
|
60
|
+
# * _ssl_verify_cert_: If true and the server uses SSL, the certificate is inspected (expiration time, signature...).
|
61
|
+
#
|
62
|
+
# * _applications_: A hash of hashes containing each XCAP application available for the client. Each application is an entry of the hash containing a key whose value is the "auid" of the application and whose value is a hast with the following fields:
|
63
|
+
# * _xmlns_: The XML namespace uri of the application.
|
64
|
+
# * _document_name_: The name of the default document for this application ("index" if not set).
|
65
|
+
# * _scope_: Can be :user or :global (:user if not set).
|
66
|
+
# * _:user_: Each user has his own document(s) for this application.
|
67
|
+
# * _:global_: The document(s) is shared for all the users.
|
68
|
+
#
|
69
|
+
# Example:
|
70
|
+
# xcap_conf = {
|
71
|
+
# :xcap_root => "https://xcap.domain.org/xcap-root",
|
72
|
+
# :user => "sip:alice@domain.org",
|
73
|
+
# :auth_user => "alice",
|
74
|
+
# :password => "1234",
|
75
|
+
# :ssl_verify_cert => false
|
76
|
+
# }
|
77
|
+
# xcap_apps = {
|
78
|
+
# "pres-rules" => {
|
79
|
+
# :xmlns => "urn:ietf:params:xml:ns:pres-rules",
|
80
|
+
# :mime_type => "application/auth-policy+xml",
|
81
|
+
# :document_name => "index",
|
82
|
+
# :scope => :user
|
83
|
+
# },
|
84
|
+
# "rls-services" => {
|
85
|
+
# :xmlns => "urn:ietf:params:xml:ns:rls-services",
|
86
|
+
# :mime_type => "application/rls-services+xml",
|
87
|
+
# :document_name => "index",
|
88
|
+
# :scope => :user
|
89
|
+
# }
|
90
|
+
# }
|
91
|
+
#
|
92
|
+
# @client = Client.new(xcap_conf, xcap_apps)
|
93
|
+
#
|
94
|
+
# A XCAP application called "xcap-caps" is automatically added to the list of applications of the new client. This application is defined in the {RFC 4825}[http://tools.ietf.org/html/rfc4825].
|
95
|
+
#
|
96
|
+
def initialize(conf={}, applications={})
|
97
|
+
|
98
|
+
# Check conf hash.
|
99
|
+
raise ConfigError, "`conf' must be a hash" unless (Hash === conf)
|
100
|
+
|
101
|
+
# Check non existing parameter names.
|
102
|
+
conf.each_key do |key|
|
103
|
+
raise ConfigError, "Uknown parameter name '#{key}' in `conf' hash" unless CONF_PARAMETERS.include?(key)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check xcap_root parameter.
|
107
|
+
@xcap_root = URI.parse(conf[:xcap_root].to_s)
|
108
|
+
raise ConfigError, "`xcap_root' must be http or https URI" unless [URI::HTTP, URI::HTTPS].include?(@xcap_root.class)
|
109
|
+
|
110
|
+
# Check user.
|
111
|
+
@user = conf[:user].freeze
|
112
|
+
raise ConfigError, "`user' must be a non empty string" unless (String === @user && ! @user.empty?)
|
113
|
+
|
114
|
+
@auth_user = conf[:auth_user].freeze || @user
|
115
|
+
@password = conf[:password].freeze
|
116
|
+
|
117
|
+
# Initialize the HTTP client.
|
118
|
+
@http_client = HTTPClient.new
|
119
|
+
@http_client.set_auth(@xcap_root, @auth_user, @password)
|
120
|
+
@http_client.protocol_retry_count = 3 ### TODO: Set an appropiate value (min 2 for 401).
|
121
|
+
@http_client.connect_timeout = HTTP_TIMEOUT
|
122
|
+
@http_client.send_timeout = HTTP_TIMEOUT
|
123
|
+
@http_client.receive_timeout = HTTP_TIMEOUT
|
124
|
+
|
125
|
+
@xcap_root.freeze # Freeze now as it has been modified in @http_client.set_auth.
|
126
|
+
|
127
|
+
# Check ssl_verify_cert parameter.
|
128
|
+
if URI::HTTPS === @xcap_root
|
129
|
+
@ssl_verify_cert = conf[:ssl_verify_cert] || false
|
130
|
+
raise ConfigError, "`ssl_verify_cert' must be true or false" unless [TrueClass, FalseClass].include?(@ssl_verify_cert.class)
|
131
|
+
@http_client.ssl_config.verify_mode = ( @ssl_verify_cert ? 3 : 0 )
|
132
|
+
end
|
133
|
+
|
134
|
+
# Generate applications.
|
135
|
+
@applications = {}
|
136
|
+
|
137
|
+
# Add the "xcap-caps" application.
|
138
|
+
@applications["xcap-caps"] = Application.new("xcap-caps", {
|
139
|
+
:xmlns => "urn:ietf:params:xml:ns:xcap-caps",
|
140
|
+
:mime_type => "application/xcap-caps+xml",
|
141
|
+
:scope => :global,
|
142
|
+
:document_name => "index"
|
143
|
+
})
|
144
|
+
|
145
|
+
# Add custom applications.
|
146
|
+
applications.each do |auid, data|
|
147
|
+
@applications[auid] = Application.new(auid, data)
|
148
|
+
end
|
149
|
+
|
150
|
+
@applications.freeze
|
151
|
+
|
152
|
+
end # def initialize
|
153
|
+
|
154
|
+
|
155
|
+
# Returns the XCAPClient::Application whose auid mathes the _auid_ parameter.
|
156
|
+
#
|
157
|
+
# Example:
|
158
|
+
#
|
159
|
+
# @xcapclient.application("pres-rules")
|
160
|
+
#
|
161
|
+
def application(auid)
|
162
|
+
@applications[auid]
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
# Returns an Array with all the applications configured in the client.
|
167
|
+
def applications
|
168
|
+
@applications
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# Fetch a document from the server.
|
173
|
+
#
|
174
|
+
# Example:
|
175
|
+
#
|
176
|
+
# @xcapclient.get("pres-rules")
|
177
|
+
#
|
178
|
+
# If success:
|
179
|
+
# * The method returns true.
|
180
|
+
# * Received XML plain document is stored in <tt>@xcapclient.application("pres-rules").document.plain</tt> (the default document).
|
181
|
+
# * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
|
182
|
+
#
|
183
|
+
def get(auid, document=nil, check_etag=true)
|
184
|
+
|
185
|
+
application, document = get_app_doc(auid, document)
|
186
|
+
response = send_request(:get, application, document, nil, nil, nil, nil, check_etag)
|
187
|
+
|
188
|
+
# Check Content-Type.
|
189
|
+
check_content_type(response, application.mime_type)
|
190
|
+
|
191
|
+
# Store the plain document.
|
192
|
+
document.plain = response.body.content
|
193
|
+
|
194
|
+
# Update ETag.
|
195
|
+
document.etag = response.header["ETag"].first
|
196
|
+
|
197
|
+
return true
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
# Create/replace a document in the server.
|
203
|
+
#
|
204
|
+
# Example:
|
205
|
+
#
|
206
|
+
# @xcapclient.put("pres-rules")
|
207
|
+
#
|
208
|
+
# If success:
|
209
|
+
# * The method returns true.
|
210
|
+
# * Local plain document in <tt>@xcapclient.application("pres-rules").document.plain</tt> is uploaded to the server.
|
211
|
+
# * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
|
212
|
+
#
|
213
|
+
def put(auid, document=nil, check_etag=true)
|
214
|
+
|
215
|
+
application, document = get_app_doc(auid, document)
|
216
|
+
response = send_request(:put, application, document, nil, nil, nil, application.mime_type, check_etag)
|
217
|
+
|
218
|
+
# Update ETag.
|
219
|
+
document.etag = response.header["ETag"].first
|
220
|
+
|
221
|
+
return true
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
# Delete a document in the server.
|
227
|
+
#
|
228
|
+
# Example:
|
229
|
+
#
|
230
|
+
# @xcapclient.delete("pres-rules")
|
231
|
+
#
|
232
|
+
# If success:
|
233
|
+
# * The method returns true.
|
234
|
+
# * Local plain document and ETag are deleted.
|
235
|
+
#
|
236
|
+
def delete(auid, document=nil, check_etag=true)
|
237
|
+
|
238
|
+
application, document = get_app_doc(auid, document)
|
239
|
+
response = send_request(:delete, application, document, nil, nil, nil, nil, check_etag)
|
240
|
+
|
241
|
+
# Reset the local document.
|
242
|
+
document.plain = nil
|
243
|
+
document.etag = nil
|
244
|
+
|
245
|
+
return true
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
# Fetch an element from the document stored in the server.
|
251
|
+
#
|
252
|
+
# Example:
|
253
|
+
#
|
254
|
+
# @xcapclient.get_element("pres-rules", nil,
|
255
|
+
# '/cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
|
256
|
+
# {"cp" => "urn:ietf:params:xml:ns:common-policy"})
|
257
|
+
#
|
258
|
+
# If success:
|
259
|
+
# * The method returns the response body as a String.
|
260
|
+
#
|
261
|
+
def get_element(auid, document, selector, xml_namespaces=nil, check_etag=true)
|
262
|
+
|
263
|
+
application, document = get_app_doc(auid, document)
|
264
|
+
response = send_request(:get, application, document, selector, nil, xml_namespaces, nil, check_etag)
|
265
|
+
|
266
|
+
# Check Content-Type.
|
267
|
+
check_content_type(response, "application/xcap-el+xml")
|
268
|
+
|
269
|
+
return response.body.content
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
# Create/replace an element in the document stored in the server.
|
274
|
+
#
|
275
|
+
# Example:
|
276
|
+
#
|
277
|
+
# @xcapclient.put_element("pres-rules", nil,
|
278
|
+
# '/cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:bob@example.org"]',
|
279
|
+
# '<cp:one id="sip:bob@example.org"/>',
|
280
|
+
# {"cp"=>"urn:ietf:params:xml:ns:common-policy"})
|
281
|
+
#
|
282
|
+
# If success:
|
283
|
+
# * The method returns true.
|
284
|
+
# * Local plain document and ETag are deleted (as the document has changed).
|
285
|
+
#
|
286
|
+
def put_element(auid, document, selector, selector_body, xml_namespaces=nil, check_etag=true)
|
287
|
+
|
288
|
+
application, document = get_app_doc(auid, document)
|
289
|
+
response = send_request(:put, application, document, selector, selector_body, xml_namespaces, "application/xcap-el+xml", check_etag)
|
290
|
+
|
291
|
+
# Reset local plain document and ETag as we have modified it.
|
292
|
+
document.plain = nil
|
293
|
+
document.etag = nil
|
294
|
+
|
295
|
+
return true
|
296
|
+
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
# Delete an element in the document stored in the server.
|
301
|
+
#
|
302
|
+
# Example:
|
303
|
+
#
|
304
|
+
# @xcapclient.delete_element("pres-rules", nil,
|
305
|
+
# '/cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
|
306
|
+
# {"cp" => "urn:ietf:params:xml:ns:common-policy"})
|
307
|
+
#
|
308
|
+
# If success:
|
309
|
+
# * The method returns true.
|
310
|
+
# * Local plain document and ETag are deleted (as the document has changed).
|
311
|
+
#
|
312
|
+
def delete_element(auid, document, selector, xml_namespaces=nil, check_etag=true)
|
313
|
+
|
314
|
+
application, document = get_app_doc(auid, document)
|
315
|
+
response = send_request(:delete, application, document, selector, nil, xml_namespaces, nil, check_etag)
|
316
|
+
|
317
|
+
# Reset local plain document and ETag as we have modified it
|
318
|
+
document.plain = nil
|
319
|
+
document.etag = nil
|
320
|
+
|
321
|
+
return true
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
# Fetch the namespace prefixes of an element.
|
327
|
+
#
|
328
|
+
# If the client wants to create/replace an element, the body of the PUT request must use the same namespaces and prefixes as those used in the document stored in the server. This methods allows the client to fetch these namespaces and prefixes.
|
329
|
+
#
|
330
|
+
# Related documentation: {RFC 4825 section 7.10}[http://tools.ietf.org/html/rfc4825#section-7.10], {RFC 4825 section 10}[http://tools.ietf.org/html/rfc4825#section-10]
|
331
|
+
#
|
332
|
+
# Example:
|
333
|
+
#
|
334
|
+
# @xcapclient.get_element_namespaces("pres-rules", nil,
|
335
|
+
# '/ccpp:ruleset/ccpp:rule[@id="pres_whitelist"]',
|
336
|
+
# { "ccpp" => "urn:ietf:params:xml:ns:common-policy" })
|
337
|
+
#
|
338
|
+
# Assuming the server uses "cp" for that namespace, it would reply:
|
339
|
+
#
|
340
|
+
# HTTP/1.1 200 OK
|
341
|
+
# Content-Type: application/xcap-ns+xml
|
342
|
+
#
|
343
|
+
# <cp:identity xmlns:pr="urn:ietf:params:xml:ns:pres-rules"
|
344
|
+
# xmlns:cp="urn:ietf:params:xml:ns:common-policy" />
|
345
|
+
#
|
346
|
+
# If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns a hash:
|
347
|
+
# {"pr"=>"urn:ietf:params:xml:ns:pres-rules", "cp"=>"urn:ietf:params:xml:ns:common-policy"}
|
348
|
+
# If not, the method returns the response body as a String and the application must parse it.
|
349
|
+
#
|
350
|
+
def get_element_namespaces(auid, document, selector, xml_namespaces=nil, check_etag=true)
|
351
|
+
|
352
|
+
application, document = get_app_doc(auid, document)
|
353
|
+
response = send_request(:get, application, document, selector + "/namespace::*", nil, xml_namespaces, nil, check_etag)
|
354
|
+
|
355
|
+
# Check Content-Type.
|
356
|
+
check_content_type(response, "application/xcap-ns+xml")
|
357
|
+
|
358
|
+
return case NOKOGIRI_INSTALLED
|
359
|
+
when true
|
360
|
+
Nokogiri::XML.parse(response.body.content).namespaces
|
361
|
+
when false
|
362
|
+
response.body.content
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
# Fetch the XCAP applications (auids) supported by the server.
|
369
|
+
#
|
370
|
+
# Related documentation: {RFC 4825 section 12}[http://tools.ietf.org/html/rfc4825#section-12]
|
371
|
+
#
|
372
|
+
# If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns an Array containing the auids. If not, the response body is returned as a String.
|
373
|
+
#
|
374
|
+
def get_xcap_auids
|
375
|
+
|
376
|
+
body = get_element("xcap-caps", nil, '/xcap-caps/auids')
|
377
|
+
|
378
|
+
return case NOKOGIRI_INSTALLED
|
379
|
+
when true
|
380
|
+
parse(body).xpath("/auids/auid", {"xmlns" => XCAP_CAPS_XMLNS}).map {|auid| auid.content}
|
381
|
+
when false
|
382
|
+
body
|
383
|
+
end
|
384
|
+
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
# Fetch the XCAP extensions supported by the server.
|
389
|
+
#
|
390
|
+
# Same as XCAPClient::Client::get_xcap_auids but fetching the supported extensions.
|
391
|
+
#
|
392
|
+
def get_xcap_extensions
|
393
|
+
|
394
|
+
body = get_element("xcap-caps", nil, '/xcap-caps/extensions')
|
395
|
+
|
396
|
+
return case NOKOGIRI_INSTALLED
|
397
|
+
when true
|
398
|
+
parse(body).xpath("/extensions/extension", {"xmlns" => XCAP_CAPS_XMLNS}).map {|extension| extension.content}
|
399
|
+
when false
|
400
|
+
body
|
401
|
+
end
|
402
|
+
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
# Fetch the XCAP namespaces supported by the server.
|
407
|
+
#
|
408
|
+
# Same as XCAPClient::Client::get_xcap_auids but fetching the supported namespaces.
|
409
|
+
#
|
410
|
+
def get_xcap_namespaces
|
411
|
+
|
412
|
+
body = get_element("xcap-caps", nil, '/xcap-caps/namespaces')
|
413
|
+
|
414
|
+
return case NOKOGIRI_INSTALLED
|
415
|
+
when true
|
416
|
+
parse(body).xpath("/namespaces/namespace", {"xmlns" => XCAP_CAPS_XMLNS}).map {|namespace| namespace.content}
|
417
|
+
when false
|
418
|
+
body
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
|
427
|
+
def get_app_doc(auid, document=nil)
|
428
|
+
|
429
|
+
# Get the application.
|
430
|
+
application = @applications[auid]
|
431
|
+
raise WrongAUID, "There is no application with auid '#{auid}'" unless application
|
432
|
+
|
433
|
+
# Get the document.
|
434
|
+
case document
|
435
|
+
when Document
|
436
|
+
when String
|
437
|
+
document_name = document
|
438
|
+
document = application.document(document_name)
|
439
|
+
raise DocumentError, "document '#{document_name}' doesn't exist in application '#{auid}'" unless document
|
440
|
+
when NilClass
|
441
|
+
document = application.document # Default document.
|
442
|
+
else
|
443
|
+
raise ArgumentError, "`document' must be Document, String or nil"
|
444
|
+
end
|
445
|
+
|
446
|
+
return [application, document]
|
447
|
+
|
448
|
+
end
|
449
|
+
|
450
|
+
|
451
|
+
# Converts the hash namespaces hash into a HTTP query.
|
452
|
+
#
|
453
|
+
# get_xmlns_query( { "a"=>"urn:test:default-namespace", "b"=>"urn:test:namespace1-uri" } )
|
454
|
+
# => "?xmlns(a=urn:test:default-namespace)xmlns(b=urn:test:namespace1-uri)"
|
455
|
+
#
|
456
|
+
def get_xmlns_query(xml_namespaces)
|
457
|
+
|
458
|
+
return "" unless xml_namespaces
|
459
|
+
|
460
|
+
xmlns_query="?"
|
461
|
+
xml_namespaces.each do |prefix, uri|
|
462
|
+
xmlns_query += "xmlns(#{prefix}=#{uri})"
|
463
|
+
end
|
464
|
+
|
465
|
+
return xmlns_query
|
466
|
+
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
def send_request(method, application, document, selector, selector_body, xml_namespaces, content_type, check_etag)
|
471
|
+
|
472
|
+
# Set extra headers.
|
473
|
+
extra_headers = {}.merge(COMMON_HEADERS)
|
474
|
+
if check_etag && document.etag
|
475
|
+
case method
|
476
|
+
when :get
|
477
|
+
extra_headers["If-None-Match"] = document.etag
|
478
|
+
when :put
|
479
|
+
extra_headers["If-Match"] = document.etag
|
480
|
+
when :delete
|
481
|
+
extra_headers["If-Match"] = document.etag
|
482
|
+
end
|
483
|
+
end
|
484
|
+
extra_headers["Content-Type"] = content_type if content_type
|
485
|
+
|
486
|
+
# XUI.
|
487
|
+
xui = case application.scope
|
488
|
+
when :user
|
489
|
+
"users/#{user_encode(@user)}"
|
490
|
+
when :global
|
491
|
+
GLOBAL_XUI
|
492
|
+
end
|
493
|
+
|
494
|
+
# URI.
|
495
|
+
uri = "#{@xcap_root}/#{application.auid}/#{xui}/#{document.name}"
|
496
|
+
uri += "/~~#{percent_encode(selector)}" if selector
|
497
|
+
uri += get_xmlns_query(xml_namespaces) if xml_namespaces
|
498
|
+
|
499
|
+
# Body (just in case of PUT).
|
500
|
+
body = ( selector_body || document.plain || nil ) if method == :put
|
501
|
+
raise ArgumentError, "PUT body empty" unless String === body if method == :put
|
502
|
+
|
503
|
+
begin
|
504
|
+
response = @http_client.request(method, uri, nil, body, extra_headers)
|
505
|
+
rescue => e
|
506
|
+
raise ConnectionError, "Error contacting the server <#{e.class}: #{e.message}>"
|
507
|
+
end
|
508
|
+
|
509
|
+
document.last_response = response
|
510
|
+
|
511
|
+
# Process the response.
|
512
|
+
case response.status.to_s
|
513
|
+
|
514
|
+
when /^2[0-9]{2}$/
|
515
|
+
return response
|
516
|
+
|
517
|
+
when "304"
|
518
|
+
raise HTTPDocumentNotModified
|
519
|
+
|
520
|
+
when "400"
|
521
|
+
raise HTTPBadRequest
|
522
|
+
|
523
|
+
when "404"
|
524
|
+
raise HTTPDocumentNotFound
|
525
|
+
|
526
|
+
when /^(401|407)$/
|
527
|
+
raise HTTPAuthenticationError, "Couldn't authenticate for URI '#{uri}' [#{response.status} #{response.reason}]"
|
528
|
+
|
529
|
+
when "409"
|
530
|
+
raise HTTPConflictError
|
531
|
+
|
532
|
+
when "412"
|
533
|
+
raise HTTPNoMatchingETag
|
534
|
+
|
535
|
+
when "500"
|
536
|
+
raise HTTPServerError
|
537
|
+
|
538
|
+
else
|
539
|
+
raise HTTPUnknownError, "Unknown error for URI '#{uri}' [#{response.status} #{response.reason}]"
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
end # def send_request
|
544
|
+
|
545
|
+
|
546
|
+
def percent_encode(str)
|
547
|
+
return case RUBY_VERSION_CORE
|
548
|
+
when :RUBY_1_9
|
549
|
+
str.dup.force_encoding('ASCII-8BIT').gsub(/[^a-zA-Z0-9\.,:;\-_?!@$&=+*\/~'()]/) { '%%%02x' % $&.ord }
|
550
|
+
when :RUBY_1_8
|
551
|
+
str.gsub(/[^a-zA-Z0-9\.,:;\-_?!@$&=+*\/~'()]/n) {|s| sprintf('%%%02x', s[0]) }
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
|
556
|
+
def user_encode(str)
|
557
|
+
return case RUBY_VERSION_CORE
|
558
|
+
when :RUBY_1_9
|
559
|
+
str.gsub(/[?\/]/) { '%%%02x' % $&.ord }
|
560
|
+
when :RUBY_1_8
|
561
|
+
str.gsub(/[?\/]/) {|s| sprintf('%%%02x', s[0]) }
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
|
566
|
+
def check_content_type(response, valid_content_type)
|
567
|
+
content_type = response.header["Content-Type"].first
|
568
|
+
raise HTTPWrongContentType, "Wrong Content-Type ('#{content_type})" unless content_type =~ /^#{Regexp.escape(valid_content_type)};?/i
|
569
|
+
end
|
570
|
+
|
571
|
+
|
572
|
+
def parse(str)
|
573
|
+
begin
|
574
|
+
Nokogiri::XML.parse(str, nil, nil, PARSE_OPTIONS)
|
575
|
+
rescue Nokogiri::SyntaxError => e
|
576
|
+
raise XMLParsingError, "Couldn't parse the XML file <#{e.class}: #{e.message}>"
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
|
581
|
+
end # class Client
|
582
|
+
|
583
|
+
|
584
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module XCAPClient
|
2
|
+
|
3
|
+
class Document
|
4
|
+
|
5
|
+
# The name of the document as it exists in the server.
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
# Contains the plain document fetched from the server or manually set.
|
9
|
+
attr_accessor :plain
|
10
|
+
|
11
|
+
# The last received ETag value in a response from the server. It can be set manually.
|
12
|
+
attr_accessor :etag
|
13
|
+
|
14
|
+
# This attribute could contain the parsed instance of the XML document. It's not required by XCAPClient itself, but could be useful for the program in top of it.
|
15
|
+
attr_accessor :parsed
|
16
|
+
|
17
|
+
# The last response received from the server. It's a HTTP::Message[http://dev.ctor.org/doc/httpclient/] object.
|
18
|
+
attr_accessor :last_response
|
19
|
+
|
20
|
+
# Create a new instance. _name_, _plain_ and _etag_ are String.
|
21
|
+
def initialize(name, plain=nil, etag=nil, parsed=nil)
|
22
|
+
@name = name
|
23
|
+
@plain = plain
|
24
|
+
@parsed = parsed
|
25
|
+
@etag = etag
|
26
|
+
|
27
|
+
# Check name.
|
28
|
+
raise ConfigError, "Document `name' must be a non empty string" unless String === @name && ! @name.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Delete the local plain and parsed document and the ETag.
|
32
|
+
def reset
|
33
|
+
@plain = nil
|
34
|
+
@parsed = nil
|
35
|
+
@etag = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module XCAPClient
|
2
|
+
|
3
|
+
class XCAPClientError < StandardError ; end
|
4
|
+
class ConfigError < XCAPClientError ; end
|
5
|
+
class ArgumentError < XCAPClientError ; end
|
6
|
+
class ConnectionError < XCAPClientError ; end
|
7
|
+
class WrongAUID < XCAPClientError ; end
|
8
|
+
class DocumentError < XCAPClientError ; end
|
9
|
+
class HTTPError < XCAPClientError ; end
|
10
|
+
class HTTPAuthenticationError < HTTPError ; end
|
11
|
+
class HTTPNoMatchingETag < HTTPError ; end
|
12
|
+
class HTTPConflictError < HTTPError ; end
|
13
|
+
class HTTPDocumentNotModified < HTTPError ; end
|
14
|
+
class HTTPDocumentNotFound < HTTPError ; end
|
15
|
+
class HTTPWrongContentType < HTTPError ; end
|
16
|
+
class HTTPBadRequest < HTTPError ; end
|
17
|
+
class HTTPServerError < HTTPError ; end
|
18
|
+
class HTTPUnknownError < HTTPError ; end
|
19
|
+
class XMLParsingError < XCAPClientError ; end
|
20
|
+
|
21
|
+
end
|
data/lib/xcapclient.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module XCAPClient
|
2
|
+
|
3
|
+
VERSION = "1.0"
|
4
|
+
|
5
|
+
RUBY_VERSION_CORE = case RUBY_VERSION
|
6
|
+
when /^1\.9\./
|
7
|
+
:RUBY_1_9
|
8
|
+
when /^1\.8\./
|
9
|
+
:RUBY_1_8
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
require "httpclient"
|
16
|
+
begin
|
17
|
+
require "nokogiri"
|
18
|
+
XCAPClient::NOKOGIRI_INSTALLED = true
|
19
|
+
XCAPClient::PARSE_OPTIONS = Nokogiri::XML::ParseOptions::NONET + Nokogiri::XML::ParseOptions::NOERROR
|
20
|
+
rescue LoadError
|
21
|
+
STDERR.puts "WARNING: Nokogiri XML parser is not installed. Some non vital features are disabled."
|
22
|
+
XCAPClient::NOKOGIRI_INSTALLED = false
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
xcapclient_root = File.join(File.dirname(__FILE__), 'xcapclient')
|
27
|
+
require File.join(xcapclient_root, "client")
|
28
|
+
require File.join(xcapclient_root, "errors")
|
29
|
+
require File.join(xcapclient_root, "application")
|
30
|
+
require File.join(xcapclient_root, "document")
|