xcapclient 1.2.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,4 +1,4 @@
1
- = XCAPClient (version 1.2.2)
1
+ = XCAPClient (version 1.3.1)
2
2
 
3
3
  * http://dev.sipdoc.net/projects/ruby-xcapclient/wiki/
4
4
  * http://rubyforge.org/projects/xcapclient/
@@ -66,7 +66,7 @@ A developer interested in this library should study the following documents:
66
66
  :password => "xxxxxx",
67
67
  :ssl_verify_cert => true
68
68
  }
69
-
69
+
70
70
  xcap_apps = {
71
71
  "pres-rules" => {
72
72
  :xmlns => "urn:ietf:params:xml:ns:pres-rules",
@@ -75,14 +75,14 @@ A developer interested in this library should study the following documents:
75
75
  :document_name => "index"
76
76
  }
77
77
  }
78
-
78
+
79
79
  @xcapclient = Client.new(xcap_conf, xcap_apps)
80
80
 
81
81
 
82
82
  ==== Fetch the "pres-rules" document from the server
83
83
 
84
84
  @xcapclient.get("pres-rules")
85
-
85
+
86
86
 
87
87
  ==== Fetch again the "pres-rules" document (now including the stored ETag)
88
88
 
@@ -121,7 +121,7 @@ By default, the methods accesing the XCAP server include the ETag if it's availa
121
121
  @xcapclient.get_node("pres-rules", nil,
122
122
  'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
123
123
  {"cp" => "urn:ietf:params:xml:ns:common-policy"})
124
-
124
+
125
125
 
126
126
  ==== Add a new node (a new allowed user in "pres-rules" document)
127
127
 
data/lib/xcapclient.rb CHANGED
@@ -1,30 +1,23 @@
1
- module XCAPClient
2
-
3
- VERSION = "1.2.2"
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
-
1
+ module XCAPClient ; end
14
2
 
15
3
  require "httpclient"
4
+ require "timeout"
16
5
  begin
17
- require "nokogiri"
18
- XCAPClient::NOKOGIRI_INSTALLED = true
19
- XCAPClient::PARSE_OPTIONS = Nokogiri::XML::ParseOptions::NONET + Nokogiri::XML::ParseOptions::NOERROR
6
+ require "nokogiri"
7
+ XCAPClient::NOKOGIRI_INSTALLED = true
8
+ XCAPClient::PARSE_OPTIONS = \
9
+ ::Nokogiri::XML::ParseOptions::STRICT + \
10
+ ::Nokogiri::XML::ParseOptions::NONET + \
11
+ ::Nokogiri::XML::ParseOptions::NOBLANKS + \
12
+ ::Nokogiri::XML::ParseOptions::PEDANTIC
20
13
  rescue LoadError
21
- STDERR.puts "WARNING: Nokogiri XML parser is not installed. Some non vital features are disabled."
22
- XCAPClient::NOKOGIRI_INSTALLED = false
14
+ STDERR.puts "WARNING: Nokogiri XML parser is not installed. Some non vital features are disabled."
15
+ XCAPClient::NOKOGIRI_INSTALLED = false
23
16
  end
24
17
 
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")
18
+ XCAPClient::DIR_LIB = File.join(File.dirname(__FILE__), 'xcapclient')
19
+ require File.join(XCAPClient::DIR_LIB, "version")
20
+ require File.join(XCAPClient::DIR_LIB, "client")
21
+ require File.join(XCAPClient::DIR_LIB, "errors")
22
+ require File.join(XCAPClient::DIR_LIB, "application")
23
+ require File.join(XCAPClient::DIR_LIB, "document")
@@ -1,62 +1,62 @@
1
1
  module XCAPClient
2
-
3
- class Application
4
-
5
- attr_reader :auid, :xmlns, :mime_type, :document_name, :scope
6
-
7
- def initialize(auid, data={}) #:nodoc:
8
-
9
- @auid = auid
10
-
11
- # Check application data.
12
- raise ConfigError, "Application `data' must be a hash ('#{@auid}')" unless (Hash === data)
13
-
14
- @xmlns = data[:xmlns].freeze
15
- @mime_type = data[:mime_type].freeze
16
- @document_name = data[:document_name] || "index"
17
- @scope = data[:scope] || :user
18
- @scope.freeze
19
-
20
- # Check auid.
21
- raise ConfigError, "Application `auid' must be a non empty string ('#{@auid}')" unless String === @auid && ! @auid.empty?
22
-
23
- # Check xmlns.
24
- raise ConfigError, "Application `xmlns' must be a non empty string ('#{@auid}')" unless String === @xmlns && ! @xmlns.empty?
25
-
26
- # Check mime-type.
27
- raise ConfigError, "Application `mime_type' must be a non empty string ('#{@auid}')" unless String === @mime_type && ! @mime_type.empty?
28
-
29
- # Check document_name
30
- raise ConfigError, "Application `document_name' must be a non empty string ('#{@auid}')" unless String === @document_name && ! @document_name.empty?
31
-
32
- # Check scope.
33
- raise ConfigError, "Application `scope' must be :user or :global ('#{@auid}')" unless [:user, :global].include?(@scope)
34
-
35
- # Create first document.
36
- @documents = {}
37
- @documents[@document_name] = Document.new(@document_name)
38
-
39
- end
40
-
41
- # Get the XCAPClient::Document with name _document_name_ within this application. If the parameter is not set, the default document is got.
42
- #
43
- def document(document_name=nil)
44
- @documents[document_name || @document_name]
45
- end
46
-
47
- # Get an Array containing all the documents created for this application.
48
- def documents
49
- @documents
50
- end
51
-
52
- # Creates a new XCAPClient::Document for this application with name _document_name_.
53
- def add_document(document_name)
54
- raise DocumentError, "document '#{document_name}' already exists" if @documents[document_name]
55
- @documents[document_name] = Document.new(document_name)
56
-
57
- return @documents[document_name]
58
- end
59
-
60
- end
61
-
2
+
3
+ class Application
4
+
5
+ attr_reader :auid, :xmlns, :mime_type, :document_name, :scope
6
+
7
+ def initialize(auid, data={}) #:nodoc:
8
+
9
+ @auid = auid
10
+
11
+ # Check application data.
12
+ raise ConfigError, "Application `data' must be a hash ('#{@auid}')" unless (Hash === data)
13
+
14
+ @xmlns = data[:xmlns].freeze
15
+ @mime_type = data[:mime_type].freeze
16
+ @document_name = data[:document_name] || "index"
17
+ @scope = data[:scope] || :user
18
+ @scope.freeze
19
+
20
+ # Check auid.
21
+ raise ConfigError, "Application `auid' must be a non empty string ('#{@auid}')" unless String === @auid && ! @auid.empty?
22
+
23
+ # Check xmlns.
24
+ raise ConfigError, "Application `xmlns' must be a non empty string ('#{@auid}')" unless String === @xmlns && ! @xmlns.empty?
25
+
26
+ # Check mime-type.
27
+ raise ConfigError, "Application `mime_type' must be a non empty string ('#{@auid}')" unless String === @mime_type && ! @mime_type.empty?
28
+
29
+ # Check document_name
30
+ raise ConfigError, "Application `document_name' must be a non empty string ('#{@auid}')" unless String === @document_name && ! @document_name.empty?
31
+
32
+ # Check scope.
33
+ raise ConfigError, "Application `scope' must be :user or :global ('#{@auid}')" unless [:user, :global].include?(@scope)
34
+
35
+ # Create first document.
36
+ @documents = {}
37
+ @documents[@document_name] = Document.new(@document_name)
38
+
39
+ end
40
+
41
+ # Get the XCAPClient::Document with name _document_name_ within this application. If the parameter is not set, the default document is got.
42
+ #
43
+ def document(document_name=nil)
44
+ @documents[document_name || @document_name]
45
+ end
46
+
47
+ # Get an Array containing all the documents created for this application.
48
+ def documents
49
+ @documents
50
+ end
51
+
52
+ # Creates a new XCAPClient::Document for this application with name _document_name_.
53
+ def add_document(document_name)
54
+ raise DocumentError, "document '#{document_name}' already exists" if @documents[document_name]
55
+ @documents[document_name] = Document.new(document_name)
56
+
57
+ return @documents[document_name]
58
+ end
59
+
60
+ end
61
+
62
62
  end
@@ -1,700 +1,721 @@
1
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, :identity_header, :identity_user, :ssl_verify_cert]
49
-
50
-
51
- attr_reader :xcap_root, :user, :auth_user, :password, :identity_header, :identity_user, :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", "alice", "tel:+12345678"...
59
- # * _auth_user_: Username, SIP URI or TEL URI for HTTP Digest authentication (if required). If not set it takes the value of _user_ field.
60
- # * _identity_header_: Header required in some XCAP networks containing the user identity (i.e. "X-XCAP-Preferred-Identity").
61
- # * _identity_user_: Value for the _identity_header_. It could be a SIP or TEL URI. If not set it takes teh value of _user_ field.
62
- # * _ssl_verify_cert_: If true and the server uses SSL, the certificate is inspected (expiration time, signature...).
63
- #
64
- # * _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:
65
- # * _xmlns_: The XML namespace uri of the application.
66
- # * _document_name_: The name of the default document for this application ("index" if not set).
67
- # * _scope_: Can be :user or :global (:user if not set).
68
- # * _:user_: Each user has his own document(s) for this application.
69
- # * _:global_: The document(s) is shared for all the users.
70
- #
71
- # Example:
72
- # xcap_conf = {
73
- # :xcap_root => "https://xcap.domain.org/xcap-root",
74
- # :user => "sip:alice@domain.org",
75
- # :auth_user => "alice",
76
- # :password => "1234",
77
- # :ssl_verify_cert => false
78
- # }
79
- # xcap_apps = {
80
- # "pres-rules" => {
81
- # :xmlns => "urn:ietf:params:xml:ns:pres-rules",
82
- # :mime_type => "application/auth-policy+xml",
83
- # :document_name => "index",
84
- # :scope => :user
85
- # },
86
- # "rls-services" => {
87
- # :xmlns => "urn:ietf:params:xml:ns:rls-services",
88
- # :mime_type => "application/rls-services+xml",
89
- # :document_name => "index",
90
- # :scope => :user
91
- # }
92
- # }
93
- #
94
- # @client = Client.new(xcap_conf, xcap_apps)
95
- #
96
- # Example:
97
- # xcap_conf = {
98
- # :xcap_root => "https://xcap.domain.net",
99
- # :user => "tel:+12345678",
100
- # :auth_user => "tel:+12345678",
101
- # :password => "1234",
102
- # :identity_header => "X-XCAP-Preferred-Identity",
103
- # :ssl_verify_cert => true
104
- # }
105
- #
106
- # 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].
107
- #
108
- def initialize(conf={}, applications={})
109
-
110
- # Check conf hash.
111
- raise ConfigError, "`conf' must be a hash" unless (Hash === conf)
112
-
113
- # Check non existing parameter names.
114
- conf.each_key do |key|
115
- raise ConfigError, "Uknown parameter name '#{key}' in `conf' hash" unless CONF_PARAMETERS.include?(key)
116
- end
117
-
118
- # Check xcap_root parameter.
119
- @xcap_root = ( conf[:xcap_root] =~ /\/$/ ) ? URI.parse(conf[:xcap_root][0..-2]) : URI.parse(conf[:xcap_root])
120
- raise ConfigError, "`xcap_root' must be http or https URI" unless [URI::HTTP, URI::HTTPS].include?(@xcap_root.class)
121
-
122
- # Check user.
123
- @user = conf[:user].freeze
124
- raise ConfigError, "`user' must be a non empty string" unless (String === @user && ! @user.empty?)
125
-
126
- @auth_user = conf[:auth_user].freeze || @user
127
- @password = conf[:password].freeze
128
-
129
- @identity_header = conf[:identity_header].freeze
130
- @identity_user = ( conf[:identity_user].freeze || @user ) if @identity_header
131
- COMMON_HEADERS[@identity_header] = '"' + @identity_user + '"' if @identity_header
132
-
133
- # Initialize the HTTP client.
134
- @http_client = HTTPClient.new
135
- @http_client.set_auth(@xcap_root, @auth_user, @password)
136
- @http_client.protocol_retry_count = 3 ### TODO: Set an appropiate value (min 2 for 401).
137
- @http_client.connect_timeout = HTTP_TIMEOUT
138
- @http_client.send_timeout = HTTP_TIMEOUT
139
- @http_client.receive_timeout = HTTP_TIMEOUT
140
-
141
- @xcap_root.freeze # Freeze now as it has been modified in @http_client.set_auth.
142
-
143
- # Check ssl_verify_cert parameter.
144
- if URI::HTTPS === @xcap_root
145
- @ssl_verify_cert = conf[:ssl_verify_cert] || false
146
- raise ConfigError, "`ssl_verify_cert' must be true or false" unless [TrueClass, FalseClass].include?(@ssl_verify_cert.class)
147
- @http_client.ssl_config.verify_mode = ( @ssl_verify_cert ? 3 : 0 )
148
- end
149
-
150
- # Generate applications.
151
- @applications = {}
152
-
153
- # Add the "xcap-caps" application.
154
- @applications["xcap-caps"] = Application.new("xcap-caps", {
155
- :xmlns => "urn:ietf:params:xml:ns:xcap-caps",
156
- :mime_type => "application/xcap-caps+xml",
157
- :scope => :global,
158
- :document_name => "index"
159
- })
160
-
161
- # Add custom applications.
162
- applications.each do |auid, data|
163
- @applications[auid] = Application.new(auid, data)
164
- end
165
-
166
- @applications.freeze
167
-
168
- end # def initialize
169
-
170
-
171
- # Returns the XCAPClient::Application whose auid mathes the _auid_ parameter.
172
- #
173
- # Example:
174
- #
175
- # @xcapclient.application("pres-rules")
176
- #
177
- def application(auid)
178
- @applications[auid]
179
- end
180
-
181
-
182
- # Returns an Array with all the applications configured in the client.
183
- def applications
184
- @applications
185
- end
186
-
187
-
188
- # Fetch a document from the server.
189
- #
190
- # Example:
191
- #
192
- # @xcapclient.get("pres-rules")
193
- #
194
- # If success:
195
- # * The method returns true.
196
- # * Received XML plain document is stored in <tt>@xcapclient.application("pres-rules").document.plain</tt> (the default document).
197
- # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
198
- #
199
- def get(auid, document=nil, check_etag=true)
200
-
201
- application, document = get_app_doc(auid, document)
202
- response = send_request(:get, application, document, nil, nil, nil, nil, check_etag)
203
-
204
- # Check Content-Type.
205
- check_content_type(response, application.mime_type)
206
-
207
- # Store the plain document.
208
- document.plain = response.body.content
209
-
210
- # Update ETag.
211
- document.etag = response.header["ETag"].first
212
-
213
- return true
214
-
215
- end
216
-
217
-
218
- # Create/replace a document in the server.
219
- #
220
- # Example:
221
- #
222
- # @xcapclient.put("pres-rules")
223
- #
224
- # If success:
225
- # * The method returns true.
226
- # * Local plain document in <tt>@xcapclient.application("pres-rules").document.plain</tt> is uploaded to the server.
227
- # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
228
- #
229
- def put(auid, document=nil, check_etag=true)
230
-
231
- application, document = get_app_doc(auid, document)
232
- response = send_request(:put, application, document, nil, nil, nil, application.mime_type, check_etag)
233
-
234
- # Update ETag.
235
- document.etag = response.header["ETag"].first
236
-
237
- return true
238
-
239
- end
240
-
241
-
242
- # Delete a document in the server.
243
- #
244
- # Example:
245
- #
246
- # @xcapclient.delete("pres-rules")
247
- #
248
- # If success:
249
- # * The method returns true.
250
- # * Local plain document and ETag are deleted.
251
- #
252
- def delete(auid, document=nil, check_etag=true)
253
-
254
- application, document = get_app_doc(auid, document)
255
- response = send_request(:delete, application, document, nil, nil, nil, nil, check_etag)
256
-
257
- # Reset the local document.
258
- document.plain = nil
259
- document.etag = nil
260
-
261
- return true
262
-
263
- end
264
-
265
-
266
- # Fetch a node from the document stored in the server.
267
- #
268
- # Example, fetching the node with "id" = "sip:alice@example.org":
269
- #
270
- # @xcapclient.get_node("pres-rules", nil,
271
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
272
- # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
273
- #
274
- # If success:
275
- # * The method returns the node as a String.
276
- #
277
- def get_node(auid, document, selector, xml_namespaces=nil, check_etag=true)
278
-
279
- application, document = get_app_doc(auid, document)
280
- response = send_request(:get, application, document, selector, nil, xml_namespaces, nil, check_etag)
281
-
282
- # Check Content-Type.
283
- check_content_type(response, "application/xcap-el+xml")
284
-
285
- return response.body.content
286
-
287
- end
288
-
289
- # Create/replace a node in the document stored in the server.
290
- #
291
- # Example, creating/replacing the node with "id" = "sip:alice@example.org":
292
- #
293
- # @xcapclient.put_node("pres-rules", nil,
294
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:bob@example.org"]',
295
- # '<cp:one id="sip:bob@example.org"/>',
296
- # {"cp"=>"urn:ietf:params:xml:ns:common-policy"})
297
- #
298
- # If success:
299
- # * The method returns true.
300
- # * Local plain document and ETag are deleted (as the document has changed).
301
- #
302
- def put_node(auid, document, selector, selector_body, xml_namespaces=nil, check_etag=true)
303
-
304
- application, document = get_app_doc(auid, document)
305
- response = send_request(:put, application, document, selector, selector_body, xml_namespaces, "application/xcap-el+xml", check_etag)
306
-
307
- # Reset local plain document and ETag as we have modified it.
308
- document.plain = nil
309
- document.etag = nil
310
-
311
- return true
312
-
313
- end
314
-
315
-
316
- # Delete a node in the document stored in the server.
317
- #
318
- # Example, deleting the node with "id" = "sip:alice@example.org":
319
- #
320
- # @xcapclient.delete_node("pres-rules", nil,
321
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
322
- # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
323
- #
324
- # If success:
325
- # * The method returns true.
326
- # * Local plain document and ETag are deleted (as the document has changed).
327
- #
328
- def delete_node(auid, document, selector, xml_namespaces=nil, check_etag=true)
329
-
330
- application, document = get_app_doc(auid, document)
331
- response = send_request(:delete, application, document, selector, nil, xml_namespaces, nil, check_etag)
332
-
333
- # Reset local plain document and ETag as we have modified it
334
- document.plain = nil
335
- document.etag = nil
336
-
337
- return true
338
-
339
- end
340
-
341
-
342
- # Fetch a node attribute from the document stored in the server.
343
- #
344
- # Example, fetching the "name" attribute of the node with
345
- # "id" = "sip:alice@example.org":
346
- #
347
- # @xcapclient.get_attribute("pres-rules", nil,
348
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
349
- # "name",
350
- # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
351
- #
352
- # If success:
353
- # * The method returns the attribute as a String.
354
- #
355
- def get_attribute(auid, document, selector, attribute_name, xml_namespaces=nil, check_etag=true)
356
-
357
- application, document = get_app_doc(auid, document)
358
- response = send_request(:get, application, document, selector + "/@#{attribute_name}", nil, xml_namespaces, nil, check_etag)
359
-
360
- # Check Content-Type.
361
- check_content_type(response, "application/xcap-att+xml")
362
-
363
- return response.body.content
364
-
365
- end
366
-
367
-
368
- # Create/replace a node attribute in the document stored in the server.
369
- #
370
- # Example, creating/replacing the "name" attribute of the node with
371
- # "id" = "sip:alice@example.org" with new value "Alice Yeah":
372
- #
373
- # @xcapclient.put_attribute("pres-rules", nil,
374
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
375
- # "name",
376
- # "Alice Yeah",
377
- # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
378
- #
379
- # If success:
380
- # * The method returns true.
381
- # * Local plain document and ETag are deleted (as the document has changed).
382
- #
383
- def put_attribute(auid, document, selector, attribute_name, attribute_value, xml_namespaces=nil, check_etag=true)
384
-
385
- application, document = get_app_doc(auid, document)
386
- response = send_request(:put, application, document, selector + "/@#{attribute_name}", attribute_value, xml_namespaces, "application/xcap-att+xml", check_etag)
387
-
388
- # Reset local plain document and ETag as we have modified it.
389
- document.plain = nil
390
- document.etag = nil
391
-
392
- return true
393
-
394
- end
395
-
396
-
397
- # Delete a node attribute in the document stored in the server.
398
- #
399
- # Example, deleting the "name" attribute of the node with
400
- # "id" = "sip:alice@example.org":
401
- #
402
- # @xcapclient.delete_attribute("pres-rules", nil,
403
- # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
404
- # "name",
405
- # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
406
- #
407
- # If success:
408
- # * The method returns true.
409
- # * Local plain document and ETag are deleted (as the document has changed).
410
- #
411
- def delete_attribute(auid, document, selector, attribute_name, xml_namespaces=nil, check_etag=true)
412
-
413
- application, document = get_app_doc(auid, document)
414
- response = send_request(:delete, application, document, selector + "/@#{attribute_name}", nil, xml_namespaces, nil, check_etag)
415
-
416
- # Reset local plain document and ETag as we have modified it
417
- document.plain = nil
418
- document.etag = nil
419
-
420
- return true
421
-
422
- end
423
-
424
-
425
- # Fetch the namespace prefixes of a node.
426
- #
427
- # If the client wants to create/replace a node, the body of the PUT
428
- # request must use the same namespaces and prefixes as those used in
429
- # the document stored in the server. This methods allows the client
430
- # to fetch these namespaces and prefixes.
431
- #
432
- # Related documentation: {RFC 4825 section 7.10}[http://tools.ietf.org/html/rfc4825#section-7.10],
433
- # {RFC 4825 section 10}[http://tools.ietf.org/html/rfc4825#section-10]
434
- #
435
- # Example:
436
- #
437
- # @xcapclient.get_node_namespaces("pres-rules", nil,
438
- # 'ccpp:ruleset/ccpp:rule[@id="pres_whitelist"]',
439
- # { "ccpp" => "urn:ietf:params:xml:ns:common-policy" })
440
- #
441
- # Assuming the server uses "cp" for that namespace, it would reply:
442
- #
443
- # HTTP/1.1 200 OK
444
- # Content-Type: application/xcap-ns+xml
445
- #
446
- # <cp:identity xmlns:pr="urn:ietf:params:xml:ns:pres-rules"
447
- # xmlns:cp="urn:ietf:params:xml:ns:common-policy" />
448
- #
449
- # If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns a hash:
450
- # {"pr"=>"urn:ietf:params:xml:ns:pres-rules", "cp"=>"urn:ietf:params:xml:ns:common-policy"}
451
- # If not, the method returns the response body as a String and the application must parse it.
452
- #
453
- def get_node_namespaces(auid, document, selector, xml_namespaces=nil, check_etag=true)
454
-
455
- application, document = get_app_doc(auid, document)
456
- response = send_request(:get, application, document, selector + "/namespace::*", nil, xml_namespaces, nil, check_etag)
457
-
458
- # Check Content-Type.
459
- check_content_type(response, "application/xcap-ns+xml")
460
-
461
- return case NOKOGIRI_INSTALLED
462
- when true
463
- Nokogiri::XML.parse(response.body.content).namespaces
464
- when false
465
- response.body.content
466
- end
467
-
468
- end
469
-
470
-
471
- # Fetch the XCAP applications (auids) supported by the server.
472
- #
473
- # Related documentation: {RFC 4825 section 12}[http://tools.ietf.org/html/rfc4825#section-12]
474
- #
475
- # If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns an
476
- # Array containing the auids. If not, the response body is returned as a String.
477
- #
478
- def get_xcap_auids
479
-
480
- body = get_node("xcap-caps", nil, 'xcap-caps/auids')
481
-
482
- return case NOKOGIRI_INSTALLED
483
- when true
484
- parse(body).xpath("auids/auid", {"xmlns" => XCAP_CAPS_XMLNS}).map {|auid| auid.content}
485
- when false
486
- body
487
- end
488
-
489
- end
490
-
491
-
492
- # Fetch the XCAP extensions supported by the server.
493
- #
494
- # Same as XCAPClient::Client::get_xcap_auids but fetching the supported extensions.
495
- #
496
- def get_xcap_extensions
497
-
498
- body = get_node("xcap-caps", nil, 'xcap-caps/extensions')
499
-
500
- return case NOKOGIRI_INSTALLED
501
- when true
502
- parse(body).xpath("extensions/extension", {"xmlns" => XCAP_CAPS_XMLNS}).map {|extension| extension.content}
503
- when false
504
- body
505
- end
506
-
507
- end
508
-
509
-
510
- # Fetch the XCAP namespaces supported by the server.
511
- #
512
- # Same as XCAPClient::Client::get_xcap_auids but fetching the supported namespaces.
513
- #
514
- def get_xcap_namespaces
515
-
516
- body = get_node("xcap-caps", nil, 'xcap-caps/namespaces')
517
-
518
- return case NOKOGIRI_INSTALLED
519
- when true
520
- parse(body).xpath("namespaces/namespace", {"xmlns" => XCAP_CAPS_XMLNS}).map {|namespace| namespace.content}
521
- when false
522
- body
523
- end
524
-
525
- end
526
-
527
-
528
- private
529
-
530
-
531
- def get_app_doc(auid, document=nil)
532
-
533
- # Get the application.
534
- application = @applications[auid]
535
- raise WrongAUID, "There is no application with auid '#{auid}'" unless application
536
-
537
- # Get the document.
538
- case document
539
- when Document
540
- when String
541
- document_name = document
542
- document = application.document(document_name)
543
- raise DocumentError, "document '#{document_name}' doesn't exist in application '#{auid}'" unless document
544
- when NilClass
545
- document = application.document # Default document.
546
- else
547
- raise ArgumentError, "`document' must be Document, String or nil"
548
- end
549
-
550
- return [application, document]
551
-
552
- end
553
-
554
-
555
- # Converts the hash namespaces hash into a HTTP query.
556
- #
557
- # get_xmlns_query( { "a"=>"urn:test:default-namespace", "b"=>"urn:test:namespace1-uri" } )
558
- # => "?xmlns(a=urn:test:default-namespace)xmlns(b=urn:test:namespace1-uri)"
559
- #
560
- def get_xmlns_query(xml_namespaces)
561
-
562
- return "" if ( ! xml_namespaces || xml_namespaces.empty? )
563
-
564
- xmlns_query="?"
565
- xml_namespaces.each do |prefix, uri|
566
- xmlns_query += "xmlns(#{prefix}=#{uri})"
567
- end
568
-
569
- return xmlns_query
570
-
571
- end
572
-
573
-
574
- def send_request(method, application, document, selector, selector_body, xml_namespaces, content_type, check_etag)
575
-
576
- # Set extra headers.
577
- extra_headers = {}.merge(COMMON_HEADERS)
578
- if check_etag && document.etag
579
- case method
580
- when :get
581
- extra_headers["If-None-Match"] = document.etag
582
- when :put
583
- extra_headers["If-Match"] = document.etag
584
- when :delete
585
- extra_headers["If-Match"] = document.etag
586
- end
587
- end
588
- extra_headers["Content-Type"] = content_type if content_type
589
-
590
- # XUI.
591
- xui = case application.scope
592
- when :user
593
- "users/#{user_encode(@user)}"
594
- when :global
595
- GLOBAL_XUI
596
- end
597
-
598
- # URI.
599
- uri = "#{@xcap_root}/#{application.auid}/#{xui}/#{percent_encode(document.name)}"
600
- uri += "/~~/#{percent_encode(selector)}" if selector
601
- uri += get_xmlns_query(xml_namespaces) if xml_namespaces
602
-
603
- # Body (just in case of PUT).
604
- body = ( selector_body || document.plain || nil ) if method == :put
605
- raise ArgumentError, "PUT body must be a String" unless String === body if method == :put
606
-
607
- begin
608
- response = @http_client.request(method, uri, nil, body, extra_headers)
609
- rescue => e
610
- raise ConnectionError, "Error contacting the server <#{e.class}: #{e.message}>"
611
- end
612
-
613
- document.last_response = response
614
-
615
- # Process the response.
616
- case response.status.to_s
617
-
618
- when /^2[0-9]{2}$/
619
- return response
620
-
621
- when "304"
622
- raise HTTPDocumentNotModified
623
-
624
- when "400"
625
- raise HTTPBadRequest
626
-
627
- when "403"
628
- raise HTTPForbidden
629
-
630
- when "404"
631
- raise HTTPDocumentNotFound
632
-
633
- when /^(401|407)$/
634
- raise HTTPAuthenticationError, "Couldn't authenticate for URI '#{uri}' [#{response.status} #{response.reason}]"
635
-
636
- when "409"
637
- raise HTTPConflictError
638
-
639
- when "412"
640
- raise HTTPNoMatchingETag
641
-
642
- when /^50[03]$/
643
- raise HTTPServerError
644
-
645
- whe "501"
646
- raise HTTPNotImplemented
647
-
648
- else
649
- raise HTTPUnknownError, "Unknown Error [#{response.status} #{response.reason}]"
650
-
651
- end
652
-
653
- end # def send_request
654
-
655
-
656
- # http://tools.ietf.org/html/rfc3986#section-3.3
657
- # I add "/" and "@" as they must not be escaped.
658
- # I remove "&", "?" so they must be escaped now.
659
- ESCAPE_CHARS = "[^a-zA-Z0-9\\-._~!$&'()*+,=:;/@]"
660
-
661
- def percent_encode(str)
662
- ### NOTE: I've removed "&", "?" so they must be escaped now.
663
- return case RUBY_VERSION_CORE
664
- when :RUBY_1_9
665
- str.dup.force_encoding('ASCII-8BIT').gsub(/#{ESCAPE_CHARS}/) { '%%%02x' % $&.ord }
666
- when :RUBY_1_8
667
- str.gsub(/#{ESCAPE_CHARS}/n) {|s| sprintf('%%%02x', s[0]) }
668
- end
669
- end
670
-
671
-
672
- def user_encode(str)
673
- return case RUBY_VERSION_CORE
674
- when :RUBY_1_9
675
- str.gsub(/[?\/]/) { '%%%02x' % $&.ord }
676
- when :RUBY_1_8
677
- str.gsub(/[?\/]/) {|s| sprintf('%%%02x', s[0]) }
678
- end
679
- end
680
-
681
-
682
- def check_content_type(response, valid_content_type)
683
- content_type = response.header["Content-Type"].first
684
- raise HTTPWrongContentType, "Wrong Content-Type ('#{content_type})" unless content_type =~ /^#{Regexp.escape(valid_content_type)};?/i
685
- end
686
-
687
-
688
- def parse(str)
689
- begin
690
- Nokogiri::XML.parse(str, nil, nil, PARSE_OPTIONS)
691
- rescue Nokogiri::SyntaxError => e
692
- raise XMLParsingError, "Couldn't parse the XML file <#{e.class}: #{e.message}>"
693
- end
694
- end
695
-
696
-
697
- end # class Client
698
-
699
-
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, :identity_header, :identity_user, :ssl_verify_cert]
49
+
50
+
51
+ attr_reader :xcap_root, :user, :auth_user, :password, :identity_header, :identity_user, :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", "alice", "tel:+12345678"...
59
+ # * _auth_user_: Username, SIP URI or TEL URI for HTTP Digest authentication (if required). If not set it takes the value of _user_ field.
60
+ # * _identity_header_: Header required in some XCAP networks containing the user identity (i.e. "X-XCAP-Preferred-Identity").
61
+ # * _identity_user_: Value for the _identity_header_. It could be a SIP or TEL URI. If not set it takes teh value of _user_ field.
62
+ # * _ssl_verify_cert_: If true and the server uses SSL, the certificate is inspected (expiration time, signature...).
63
+ #
64
+ # * _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:
65
+ # * _xmlns_: The XML namespace uri of the application.
66
+ # * _document_name_: The name of the default document for this application ("index" if not set).
67
+ # * _scope_: Can be :user or :global (:user if not set).
68
+ # * _:user_: Each user has his own document(s) for this application.
69
+ # * _:global_: The document(s) is shared for all the users.
70
+ #
71
+ # Example:
72
+ # xcap_conf = {
73
+ # :xcap_root => "https://xcap.domain.org/xcap-root",
74
+ # :user => "sip:alice@domain.org",
75
+ # :auth_user => "alice",
76
+ # :password => "1234",
77
+ # :ssl_verify_cert => false
78
+ # }
79
+ # xcap_apps = {
80
+ # "pres-rules" => {
81
+ # :xmlns => "urn:ietf:params:xml:ns:pres-rules",
82
+ # :mime_type => "application/auth-policy+xml",
83
+ # :document_name => "index",
84
+ # :scope => :user
85
+ # },
86
+ # "rls-services" => {
87
+ # :xmlns => "urn:ietf:params:xml:ns:rls-services",
88
+ # :mime_type => "application/rls-services+xml",
89
+ # :document_name => "index",
90
+ # :scope => :user
91
+ # }
92
+ # }
93
+ #
94
+ # @client = Client.new(xcap_conf, xcap_apps)
95
+ #
96
+ # Example:
97
+ # xcap_conf = {
98
+ # :xcap_root => "https://xcap.domain.net",
99
+ # :user => "tel:+12345678",
100
+ # :auth_user => "tel:+12345678",
101
+ # :password => "1234",
102
+ # :identity_header => "X-XCAP-Preferred-Identity",
103
+ # :ssl_verify_cert => true
104
+ # }
105
+ #
106
+ # 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].
107
+ #
108
+ def initialize(conf={}, applications={})
109
+
110
+ # Check conf hash.
111
+ raise ConfigError, "`conf' must be a hash" unless (Hash === conf)
112
+
113
+ # Check non existing parameter names.
114
+ conf.each_key do |key|
115
+ raise ConfigError, "Uknown parameter name '#{key}' in `conf' hash" unless CONF_PARAMETERS.include?(key)
116
+ end
117
+
118
+ # Check xcap_root parameter.
119
+ @xcap_root = ( conf[:xcap_root] =~ /\/$/ ) ? URI.parse(conf[:xcap_root][0..-2]) : URI.parse(conf[:xcap_root])
120
+ raise ConfigError, "`xcap_root' must be http or https URI" unless [URI::HTTP, URI::HTTPS].include?(@xcap_root.class)
121
+
122
+ # Check user.
123
+ @user = conf[:user].freeze
124
+ raise ConfigError, "`user' must be a non empty string" unless (String === @user && ! @user.empty?)
125
+
126
+ @auth_user = conf[:auth_user].freeze || @user
127
+ @password = conf[:password].freeze
128
+
129
+ @identity_header = conf[:identity_header].freeze
130
+ @identity_user = ( conf[:identity_user].freeze || @user ) if @identity_header
131
+ COMMON_HEADERS[@identity_header] = '"' + @identity_user + '"' if @identity_header
132
+
133
+ # Initialize the HTTP client.
134
+ @http_client = HTTPClient.new
135
+ @http_client.set_auth(@xcap_root, @auth_user, @password)
136
+ @http_client.protocol_retry_count = 3 ### TODO: Set an appropiate value (min 2 for 401).
137
+ @http_client.connect_timeout = HTTP_TIMEOUT
138
+ @http_client.send_timeout = HTTP_TIMEOUT
139
+ @http_client.receive_timeout = HTTP_TIMEOUT
140
+
141
+ @xcap_root.freeze # Freeze now as it has been modified in @http_client.set_auth.
142
+
143
+ # Check ssl_verify_cert parameter.
144
+ if URI::HTTPS === @xcap_root
145
+ @ssl_verify_cert = conf[:ssl_verify_cert] || false
146
+ raise ConfigError, "`ssl_verify_cert' must be true or false" unless [TrueClass, FalseClass].include?(@ssl_verify_cert.class)
147
+ @http_client.ssl_config.verify_mode = ( @ssl_verify_cert ? 3 : 0 )
148
+ end
149
+
150
+ # Generate applications.
151
+ @applications = {}
152
+
153
+ # Add the "xcap-caps" application.
154
+ @applications["xcap-caps"] = Application.new("xcap-caps", {
155
+ :xmlns => "urn:ietf:params:xml:ns:xcap-caps",
156
+ :mime_type => "application/xcap-caps+xml",
157
+ :scope => :global,
158
+ :document_name => "index"
159
+ })
160
+
161
+ # Add custom applications.
162
+ applications.each do |auid, data|
163
+ @applications[auid] = Application.new(auid, data)
164
+ end
165
+
166
+ @applications.freeze
167
+
168
+ end # def initialize
169
+
170
+
171
+ # Checks the TCP connection with the XCAP server.
172
+ #
173
+ def check_connection
174
+ begin
175
+ Timeout.timeout(HTTP_TIMEOUT) do
176
+ TCPSocket.open @xcap_root.host, @xcap_root.port
177
+ end
178
+ rescue Timeout::Error
179
+ raise Timeout::Error, "cannot connect the XCAP server within #{HTTP_TIMEOUT} seconds"
180
+ rescue => e
181
+ raise e.class, "cannot connect the XCAP server (#{e.message})"
182
+ end
183
+ end
184
+
185
+
186
+ # Returns the XCAPClient::Application whose auid mathes the _auid_ parameter.
187
+ #
188
+ # Example:
189
+ #
190
+ # @xcapclient.application("pres-rules")
191
+ #
192
+ def application(auid)
193
+ @applications[auid]
194
+ end
195
+
196
+
197
+ # Returns an Array with all the applications configured in the client.
198
+ def applications
199
+ @applications
200
+ end
201
+
202
+
203
+ # Fetch a document from the server.
204
+ #
205
+ # Example:
206
+ #
207
+ # @xcapclient.get("pres-rules")
208
+ #
209
+ # If success:
210
+ # * The method returns true.
211
+ # * Received XML plain document is stored in <tt>@xcapclient.application("pres-rules").document.plain</tt> (the default document).
212
+ # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
213
+ #
214
+ def get(auid, document=nil, check_etag=true)
215
+
216
+ application, document = get_app_doc(auid, document)
217
+ response = send_request(:get, application, document, nil, nil, nil, nil, check_etag)
218
+
219
+ # Check Content-Type.
220
+ check_content_type(response, application.mime_type)
221
+
222
+ # Store the plain document.
223
+ document.plain = response.body.content
224
+
225
+ # Update ETag.
226
+ document.etag = response.header["ETag"].first
227
+
228
+ return true
229
+
230
+ end
231
+
232
+
233
+ # Create/replace a document in the server.
234
+ #
235
+ # Example:
236
+ #
237
+ # @xcapclient.put("pres-rules")
238
+ #
239
+ # If success:
240
+ # * The method returns true.
241
+ # * Local plain document in <tt>@xcapclient.application("pres-rules").document.plain</tt> is uploaded to the server.
242
+ # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
243
+ #
244
+ def put(auid, document=nil, check_etag=true)
245
+
246
+ application, document = get_app_doc(auid, document)
247
+ response = send_request(:put, application, document, nil, nil, nil, application.mime_type, check_etag)
248
+
249
+ # Update ETag.
250
+ document.etag = response.header["ETag"].first
251
+
252
+ return true
253
+
254
+ end
255
+
256
+
257
+ # Delete a document in the server.
258
+ #
259
+ # Example:
260
+ #
261
+ # @xcapclient.delete("pres-rules")
262
+ #
263
+ # If success:
264
+ # * The method returns true.
265
+ # * Local plain document and ETag are deleted.
266
+ #
267
+ def delete(auid, document=nil, check_etag=true)
268
+
269
+ application, document = get_app_doc(auid, document)
270
+ response = send_request(:delete, application, document, nil, nil, nil, nil, check_etag)
271
+
272
+ # Reset the local document.
273
+ document.plain = nil
274
+ document.etag = nil
275
+
276
+ return true
277
+
278
+ end
279
+
280
+
281
+ # Fetch a node from the document stored in the server.
282
+ #
283
+ # Example, fetching the node with "id" = "sip:alice@example.org":
284
+ #
285
+ # @xcapclient.get_node("pres-rules", nil,
286
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
287
+ # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
288
+ #
289
+ # If success:
290
+ # * The method returns the node as a String.
291
+ #
292
+ def get_node(auid, document, selector, xml_namespaces=nil, check_etag=true)
293
+
294
+ application, document = get_app_doc(auid, document)
295
+ response = send_request(:get, application, document, selector, nil, xml_namespaces, nil, check_etag)
296
+
297
+ # Check Content-Type.
298
+ check_content_type(response, "application/xcap-el+xml")
299
+
300
+ return response.body.content
301
+
302
+ end
303
+
304
+ # Create/replace a node in the document stored in the server.
305
+ #
306
+ # Example, creating/replacing the node with "id" = "sip:alice@example.org":
307
+ #
308
+ # @xcapclient.put_node("pres-rules", nil,
309
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:bob@example.org"]',
310
+ # '<cp:one id="sip:bob@example.org"/>',
311
+ # {"cp"=>"urn:ietf:params:xml:ns:common-policy"})
312
+ #
313
+ # If success:
314
+ # * The method returns true.
315
+ # * Local plain document is deleted (as the document has changed).
316
+ # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
317
+ #
318
+ def put_node(auid, document, selector, selector_body, xml_namespaces=nil, check_etag=true)
319
+
320
+ application, document = get_app_doc(auid, document)
321
+ response = send_request(:put, application, document, selector, selector_body, xml_namespaces, "application/xcap-el+xml", check_etag)
322
+
323
+ # Reset local plain document as we have modified it.
324
+ document.plain = nil
325
+
326
+ # Update ETag.
327
+ document.etag = response.header["ETag"].first
328
+
329
+ return true
330
+
331
+ end
332
+
333
+
334
+ # Delete a node in the document stored in the server.
335
+ #
336
+ # Example, deleting the node with "id" = "sip:alice@example.org":
337
+ #
338
+ # @xcapclient.delete_node("pres-rules", nil,
339
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
340
+ # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
341
+ #
342
+ # If success:
343
+ # * The method returns true.
344
+ # * Local plain document is deleted (as the document has changed).
345
+ # * Received ETag is stored in <tt>@xcapclient.application("pres-rules").document.etag</tt>.
346
+ #
347
+ def delete_node(auid, document, selector, xml_namespaces=nil, check_etag=true)
348
+
349
+ application, document = get_app_doc(auid, document)
350
+ response = send_request(:delete, application, document, selector, nil, xml_namespaces, nil, check_etag)
351
+
352
+ # Reset local plain document as we have modified it.
353
+ document.plain = nil
354
+
355
+ # Update ETag.
356
+ document.etag = response.header["ETag"].first
357
+
358
+ return true
359
+
360
+ end
361
+
362
+
363
+ # Fetch a node attribute from the document stored in the server.
364
+ #
365
+ # Example, fetching the "name" attribute of the node with
366
+ # "id" = "sip:alice@example.org":
367
+ #
368
+ # @xcapclient.get_attribute("pres-rules", nil,
369
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
370
+ # "name",
371
+ # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
372
+ #
373
+ # If success:
374
+ # * The method returns the attribute as a String.
375
+ #
376
+ def get_attribute(auid, document, selector, attribute_name, xml_namespaces=nil, check_etag=true)
377
+
378
+ application, document = get_app_doc(auid, document)
379
+ response = send_request(:get, application, document, selector + "/@#{attribute_name}", nil, xml_namespaces, nil, check_etag)
380
+
381
+ # Check Content-Type.
382
+ check_content_type(response, "application/xcap-att+xml")
383
+
384
+ return response.body.content
385
+
386
+ end
387
+
388
+
389
+ # Create/replace a node attribute in the document stored in the server.
390
+ #
391
+ # Example, creating/replacing the "name" attribute of the node with
392
+ # "id" = "sip:alice@example.org" with new value "Alice Yeah":
393
+ #
394
+ # @xcapclient.put_attribute("pres-rules", nil,
395
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
396
+ # "name",
397
+ # "Alice Yeah",
398
+ # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
399
+ #
400
+ # If success:
401
+ # * The method returns true.
402
+ # * Local plain document and ETag are deleted (as the document has changed).
403
+ #
404
+ def put_attribute(auid, document, selector, attribute_name, attribute_value, xml_namespaces=nil, check_etag=true)
405
+
406
+ application, document = get_app_doc(auid, document)
407
+ response = send_request(:put, application, document, selector + "/@#{attribute_name}", attribute_value, xml_namespaces, "application/xcap-att+xml", check_etag)
408
+
409
+ # Reset local plain document and ETag as we have modified it.
410
+ document.plain = nil
411
+ document.etag = nil
412
+
413
+ return true
414
+
415
+ end
416
+
417
+
418
+ # Delete a node attribute in the document stored in the server.
419
+ #
420
+ # Example, deleting the "name" attribute of the node with
421
+ # "id" = "sip:alice@example.org":
422
+ #
423
+ # @xcapclient.delete_attribute("pres-rules", nil,
424
+ # 'cp:ruleset/cp:rule[@id="pres_whitelist"]/cp:conditions/cp:identity/cp:one[@id="sip:alice@example.org"]',
425
+ # "name",
426
+ # {"cp" => "urn:ietf:params:xml:ns:common-policy"})
427
+ #
428
+ # If success:
429
+ # * The method returns true.
430
+ # * Local plain document and ETag are deleted (as the document has changed).
431
+ #
432
+ def delete_attribute(auid, document, selector, attribute_name, xml_namespaces=nil, check_etag=true)
433
+
434
+ application, document = get_app_doc(auid, document)
435
+ response = send_request(:delete, application, document, selector + "/@#{attribute_name}", nil, xml_namespaces, nil, check_etag)
436
+
437
+ # Reset local plain document and ETag as we have modified it
438
+ document.plain = nil
439
+ document.etag = nil
440
+
441
+ return true
442
+
443
+ end
444
+
445
+
446
+ # Fetch the namespace prefixes of a node.
447
+ #
448
+ # If the client wants to create/replace a node, the body of the PUT
449
+ # request must use the same namespaces and prefixes as those used in
450
+ # the document stored in the server. This methods allows the client
451
+ # to fetch these namespaces and prefixes.
452
+ #
453
+ # Related documentation: {RFC 4825 section 7.10}[http://tools.ietf.org/html/rfc4825#section-7.10],
454
+ # {RFC 4825 section 10}[http://tools.ietf.org/html/rfc4825#section-10]
455
+ #
456
+ # Example:
457
+ #
458
+ # @xcapclient.get_node_namespaces("pres-rules", nil,
459
+ # 'ccpp:ruleset/ccpp:rule[@id="pres_whitelist"]',
460
+ # { "ccpp" => "urn:ietf:params:xml:ns:common-policy" })
461
+ #
462
+ # Assuming the server uses "cp" for that namespace, it would reply:
463
+ #
464
+ # HTTP/1.1 200 OK
465
+ # Content-Type: application/xcap-ns+xml
466
+ #
467
+ # <cp:identity xmlns:pr="urn:ietf:params:xml:ns:pres-rules"
468
+ # xmlns:cp="urn:ietf:params:xml:ns:common-policy" />
469
+ #
470
+ # If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns a hash:
471
+ # {"pr"=>"urn:ietf:params:xml:ns:pres-rules", "cp"=>"urn:ietf:params:xml:ns:common-policy"}
472
+ # If not, the method returns the response body as a String and the application must parse it.
473
+ #
474
+ def get_node_namespaces(auid, document, selector, xml_namespaces=nil, check_etag=true)
475
+
476
+ application, document = get_app_doc(auid, document)
477
+ response = send_request(:get, application, document, selector + "/namespace::*", nil, xml_namespaces, nil, check_etag)
478
+
479
+ # Check Content-Type.
480
+ check_content_type(response, "application/xcap-ns+xml")
481
+
482
+ return case NOKOGIRI_INSTALLED
483
+ when true
484
+ Nokogiri::XML.parse(response.body.content).namespaces
485
+ when false
486
+ response.body.content
487
+ end
488
+
489
+ end
490
+
491
+
492
+ # Fetch the XCAP applications (auids) supported by the server.
493
+ #
494
+ # Related documentation: {RFC 4825 section 12}[http://tools.ietf.org/html/rfc4825#section-12]
495
+ #
496
+ # If Nokogiri[http://wiki.github.com/tenderlove/nokogiri] is available the method returns an
497
+ # Array containing the auids. If not, the response body is returned as a String.
498
+ #
499
+ def get_xcap_auids
500
+
501
+ body = get_node("xcap-caps", nil, 'xcap-caps/auids')
502
+
503
+ return case NOKOGIRI_INSTALLED
504
+ when true
505
+ parse(body).xpath("auids/auid", {"xmlns" => XCAP_CAPS_XMLNS}).map {|auid| auid.content}
506
+ when false
507
+ body
508
+ end
509
+
510
+ end
511
+
512
+
513
+ # Fetch the XCAP extensions supported by the server.
514
+ #
515
+ # Same as XCAPClient::Client::get_xcap_auids but fetching the supported extensions.
516
+ #
517
+ def get_xcap_extensions
518
+
519
+ body = get_node("xcap-caps", nil, 'xcap-caps/extensions')
520
+
521
+ return case NOKOGIRI_INSTALLED
522
+ when true
523
+ parse(body).xpath("extensions/extension", {"xmlns" => XCAP_CAPS_XMLNS}).map {|extension| extension.content}
524
+ when false
525
+ body
526
+ end
527
+
528
+ end
529
+
530
+
531
+ # Fetch the XCAP namespaces supported by the server.
532
+ #
533
+ # Same as XCAPClient::Client::get_xcap_auids but fetching the supported namespaces.
534
+ #
535
+ def get_xcap_namespaces
536
+
537
+ body = get_node("xcap-caps", nil, 'xcap-caps/namespaces')
538
+
539
+ return case NOKOGIRI_INSTALLED
540
+ when true
541
+ parse(body).xpath("namespaces/namespace", {"xmlns" => XCAP_CAPS_XMLNS}).map {|namespace| namespace.content}
542
+ when false
543
+ body
544
+ end
545
+
546
+ end
547
+
548
+
549
+ private
550
+
551
+
552
+ def get_app_doc(auid, document=nil)
553
+
554
+ # Get the application.
555
+ application = @applications[auid]
556
+ raise WrongAUID, "There is no application with auid '#{auid}'" unless application
557
+
558
+ # Get the document.
559
+ case document
560
+ when Document
561
+ when String
562
+ document_name = document
563
+ document = application.document(document_name)
564
+ raise DocumentError, "document '#{document_name}' doesn't exist in application '#{auid}'" unless document
565
+ when NilClass
566
+ document = application.document # Default document.
567
+ else
568
+ raise ArgumentError, "`document' must be Document, String or nil"
569
+ end
570
+
571
+ return [application, document]
572
+
573
+ end
574
+
575
+
576
+ # Converts the hash namespaces hash into a HTTP query.
577
+ #
578
+ # get_xmlns_query( { "a"=>"urn:test:default-namespace", "b"=>"urn:test:namespace1-uri" } )
579
+ # => "?xmlns(a=urn:test:default-namespace)xmlns(b=urn:test:namespace1-uri)"
580
+ #
581
+ def get_xmlns_query(xml_namespaces)
582
+
583
+ return "" if ( ! xml_namespaces || xml_namespaces.empty? )
584
+
585
+ xmlns_query="?"
586
+ xml_namespaces.each do |prefix, uri|
587
+ xmlns_query += "xmlns(#{prefix}=#{uri})"
588
+ end
589
+
590
+ return xmlns_query
591
+
592
+ end
593
+
594
+
595
+ def send_request(method, application, document, selector, selector_body, xml_namespaces, content_type, check_etag)
596
+
597
+ # Set extra headers.
598
+ extra_headers = {}.merge(COMMON_HEADERS)
599
+ if check_etag && document.etag
600
+ case method
601
+ when :get
602
+ extra_headers["If-None-Match"] = document.etag
603
+ when :put
604
+ extra_headers["If-Match"] = document.etag
605
+ when :delete
606
+ extra_headers["If-Match"] = document.etag
607
+ end
608
+ end
609
+ extra_headers["Content-Type"] = content_type if content_type
610
+
611
+ # XUI.
612
+ xui = case application.scope
613
+ when :user
614
+ "users/#{user_encode(@user)}"
615
+ when :global
616
+ GLOBAL_XUI
617
+ end
618
+
619
+ # URI.
620
+ uri = "#{@xcap_root}/#{application.auid}/#{xui}/#{percent_encode(document.name)}"
621
+ uri += "/~~/#{percent_encode(selector)}" if selector
622
+ uri += get_xmlns_query(xml_namespaces) if xml_namespaces
623
+
624
+ # Body (just in case of PUT).
625
+ body = ( selector_body || document.plain || nil ) if method == :put
626
+ raise ArgumentError, "PUT body must be a String" unless String === body if method == :put
627
+
628
+ begin
629
+ response = @http_client.request(method, uri, nil, body, extra_headers)
630
+ rescue => e
631
+ raise ConnectionError, "Error contacting the server <#{e.class}: #{e.message}>"
632
+ end
633
+
634
+ document.last_response = response
635
+
636
+ # Process the response.
637
+ case response.status.to_s
638
+
639
+ when /^2[0-9]{2}$/
640
+ return response
641
+
642
+ when "304"
643
+ raise HTTPDocumentNotModified
644
+
645
+ when "400"
646
+ raise HTTPBadRequest
647
+
648
+ when "403"
649
+ raise HTTPForbidden
650
+
651
+ when "404"
652
+ raise HTTPDocumentNotFound
653
+
654
+ when /^(401|407)$/
655
+ raise HTTPAuthenticationError, "Couldn't authenticate for URI '#{uri}' [#{response.status} #{response.reason}]"
656
+
657
+ when "409"
658
+ raise HTTPConflictError
659
+
660
+ when "412"
661
+ raise HTTPNoMatchingETag
662
+
663
+ when /^50[03]$/
664
+ raise HTTPServerError
665
+
666
+ when "501"
667
+ raise HTTPNotImplemented
668
+
669
+ else
670
+ raise HTTPUnknownError, "Unknown Error [#{response.status} #{response.reason}]"
671
+
672
+ end
673
+
674
+ end # def send_request
675
+
676
+
677
+ # http://tools.ietf.org/html/rfc3986#section-3.3
678
+ # My changes:
679
+ # - Not escaped: "/", "@"
680
+ # - Escaped: ";", "?"
681
+ ESCAPE_CHARS = "[^a-zA-Z0-9\\-._~!$&'()*+,=:/@]"
682
+
683
+ if RUBY_VERSION >= "1.9"
684
+ def percent_encode(str)
685
+ str.dup.force_encoding('ASCII-8BIT').gsub(/#{ESCAPE_CHARS}/) { '%%%02x' % $&.ord }
686
+ end
687
+
688
+ def user_encode(str)
689
+ str.gsub(/[?\/]/) { '%%%02x' % $&.ord }
690
+ end
691
+
692
+ else
693
+ def percent_encode(str)
694
+ str.gsub(/#{ESCAPE_CHARS}/n) {|s| sprintf('%%%02x', s[0]) }
695
+ end
696
+
697
+ def user_encode(str)
698
+ str.gsub(/[?\/]/) {|s| sprintf('%%%02x', s[0]) }
699
+ end
700
+ end
701
+
702
+
703
+ def check_content_type(response, valid_content_type)
704
+ content_type = response.header["Content-Type"].first
705
+ raise HTTPWrongContentType, "Wrong Content-Type ('#{content_type})" unless content_type =~ /^#{Regexp.escape(valid_content_type)};?/i
706
+ end
707
+
708
+
709
+ def parse(str)
710
+ begin
711
+ ::Nokogiri::XML.parse(str, nil, "UTF-8", PARSE_OPTIONS)
712
+ rescue ::Nokogiri::SyntaxError => e
713
+ raise XMLParsingError, "Couldn't parse the XML file (#{e.class}: #{e.message})"
714
+ end
715
+ end
716
+
717
+
718
+ end # class Client
719
+
720
+
700
721
  end