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 +5 -5
- data/lib/xcapclient.rb +17 -24
- data/lib/xcapclient/application.rb +60 -60
- data/lib/xcapclient/client.rb +719 -698
- data/lib/xcapclient/document.rb +51 -38
- data/lib/xcapclient/errors.rb +21 -21
- data/lib/xcapclient/version.rb +17 -0
- data/test/PRES_RULES_EXAMPLE.xml +46 -46
- data/test/test_unit_01.rb +322 -322
- metadata +4 -5
data/README.rdoc
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
= XCAPClient (version 1.
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
27
|
-
require File.join(
|
28
|
-
require File.join(
|
29
|
-
require File.join(
|
30
|
-
require File.join(
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
data/lib/xcapclient/client.rb
CHANGED
@@ -1,700 +1,721 @@
|
|
1
1
|
module XCAPClient
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
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
|