xcapclient 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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