atom-tools 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -3
- data/Rakefile +1 -1
- data/bin/atom-client.rb +13 -10
- data/lib/atom/collection.rb +2 -2
- data/lib/atom/element.rb +5 -1
- data/lib/atom/entry.rb +9 -2
- data/lib/atom/feed.rb +11 -6
- data/lib/atom/http.rb +157 -38
- data/lib/atom/service.rb +170 -0
- data/lib/atom/text.rb +15 -2
- data/lib/atom/xml.rb +1 -1
- data/test/conformance/updated.rb +2 -1
- data/test/test_constructs.rb +45 -2
- data/test/test_feed.rb +27 -0
- data/test/test_http.rb +116 -20
- data/test/test_protocol.rb +77 -13
- data/test/test_xml.rb +15 -1
- metadata +3 -38
- data/bin/atom-server.rb~ +0 -71
- data/doc/classes/Atom/App.html +0 -217
- data/doc/classes/Atom/Author.html +0 -130
- data/doc/classes/Atom/Category.html +0 -128
- data/doc/classes/Atom/Collection.html +0 -322
- data/doc/classes/Atom/Content.html +0 -129
- data/doc/classes/Atom/Contributor.html +0 -119
- data/doc/classes/Atom/Element.html +0 -325
- data/doc/classes/Atom/Entry.html +0 -365
- data/doc/classes/Atom/Feed.html +0 -585
- data/doc/classes/Atom/HTTP.html +0 -374
- data/doc/classes/Atom/Link.html +0 -137
- data/doc/classes/Atom/Text.html +0 -229
- data/doc/classes/XHTML.html +0 -118
- data/doc/created.rid +0 -1
- data/doc/files/README.html +0 -213
- data/doc/files/lib/atom/app_rb.html +0 -110
- data/doc/files/lib/atom/collection_rb.html +0 -110
- data/doc/files/lib/atom/element_rb.html +0 -109
- data/doc/files/lib/atom/entry_rb.html +0 -111
- data/doc/files/lib/atom/feed_rb.html +0 -112
- data/doc/files/lib/atom/http_rb.html +0 -109
- data/doc/files/lib/atom/text_rb.html +0 -108
- data/doc/files/lib/atom/xml_rb.html +0 -110
- data/doc/files/lib/atom/yaml_rb.html +0 -109
- data/doc/fr_class_index.html +0 -39
- data/doc/fr_file_index.html +0 -36
- data/doc/fr_method_index.html +0 -62
- data/doc/index.html +0 -24
- data/doc/rdoc-style.css +0 -208
- data/lib/atom/app.rb +0 -87
data/lib/atom/service.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
require "atom/http"
|
4
|
+
require "atom/element"
|
5
|
+
require "atom/collection"
|
6
|
+
|
7
|
+
module Atom
|
8
|
+
PP_NS = "http://purl.org/atom/app#"
|
9
|
+
|
10
|
+
class WrongNamespace < RuntimeError #:nodoc:
|
11
|
+
end
|
12
|
+
class WrongMimetype < RuntimeError # :nodoc:
|
13
|
+
end
|
14
|
+
class WrongResponse < RuntimeError # :nodoc:
|
15
|
+
end
|
16
|
+
|
17
|
+
# an Atom::Workspace has a #title (Atom::Text) and #collections, an Array of Atom::Collection s
|
18
|
+
class Workspace < Atom::Element
|
19
|
+
element :collections, Atom::Multiple(Atom::Collection)
|
20
|
+
element :title, Atom::Text
|
21
|
+
|
22
|
+
def self.parse(xml, base = "", http = Atom::HTTP.new) # :nodoc:
|
23
|
+
ws = Atom::Workspace.new("workspace")
|
24
|
+
|
25
|
+
rxml = if xml.is_a? REXML::Document
|
26
|
+
xml.root
|
27
|
+
elsif xml.is_a? REXML::Element
|
28
|
+
xml
|
29
|
+
else
|
30
|
+
REXML::Document.new(xml)
|
31
|
+
end
|
32
|
+
|
33
|
+
xml.fill_text_construct(ws, "title")
|
34
|
+
|
35
|
+
REXML::XPath.match( rxml,
|
36
|
+
"./app:collection",
|
37
|
+
{"app" => Atom::PP_NS} ).each do |col_el|
|
38
|
+
# absolutize relative URLs
|
39
|
+
url = base.to_uri + col_el.attributes["href"].to_uri
|
40
|
+
|
41
|
+
coll = Atom::Collection.new(url, http)
|
42
|
+
|
43
|
+
# XXX this is a Text Construct, and should be parsed as such
|
44
|
+
col_el.fill_text_construct(coll, "title")
|
45
|
+
|
46
|
+
accepts = REXML::XPath.first( col_el,
|
47
|
+
"./app:accept",
|
48
|
+
{"app" => Atom::PP_NS} )
|
49
|
+
coll.accepts = (accepts ? accepts.text : "entry")
|
50
|
+
|
51
|
+
ws.collections << coll
|
52
|
+
end
|
53
|
+
|
54
|
+
ws
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_element # :nodoc:
|
58
|
+
root = REXML::Element.new "workspace"
|
59
|
+
|
60
|
+
# damn you, REXML. Damn you and you bizarre handling of namespaces
|
61
|
+
title = self.title.to_element
|
62
|
+
title.name = "atom:title"
|
63
|
+
root << title
|
64
|
+
|
65
|
+
self.collections.each do |coll|
|
66
|
+
el = REXML::Element.new "collection"
|
67
|
+
|
68
|
+
el.attributes["href"] = coll.uri
|
69
|
+
|
70
|
+
title = coll.title.to_element
|
71
|
+
title.name = "atom:title"
|
72
|
+
el << title
|
73
|
+
|
74
|
+
unless coll.accepts.nil?
|
75
|
+
accepts = REXML::Element.new "accepts"
|
76
|
+
accepts.text = coll.accepts
|
77
|
+
el << accepts
|
78
|
+
end
|
79
|
+
|
80
|
+
root << el
|
81
|
+
end
|
82
|
+
|
83
|
+
root
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Atom::Service represents an Atom Publishing Protocol service
|
88
|
+
# document. Its only child is #workspaces, which is an Array of
|
89
|
+
# Atom::Workspace s
|
90
|
+
class Service < Atom::Element
|
91
|
+
element :workspaces, Atom::Multiple(Atom::Workspace)
|
92
|
+
|
93
|
+
# retrieves and parses an Atom service document.
|
94
|
+
def initialize(service_url = "", http = Atom::HTTP.new)
|
95
|
+
super("service")
|
96
|
+
|
97
|
+
@http = http
|
98
|
+
|
99
|
+
return if service_url.empty?
|
100
|
+
|
101
|
+
base = URI.parse(service_url)
|
102
|
+
|
103
|
+
rxml = nil
|
104
|
+
|
105
|
+
res = @http.get(base)
|
106
|
+
|
107
|
+
unless res.code == "200" # XXX needs to handle redirects, &c.
|
108
|
+
raise WrongResponse, "service document URL responded with unexpected code #{res.code}"
|
109
|
+
end
|
110
|
+
|
111
|
+
unless res.content_type == "application/atomserv+xml"
|
112
|
+
raise WrongMimetype, "this isn't an atom service document!"
|
113
|
+
end
|
114
|
+
|
115
|
+
parse(res.body, base)
|
116
|
+
end
|
117
|
+
|
118
|
+
# parse a service document, adding its workspaces to this object
|
119
|
+
def parse xml, base = ""
|
120
|
+
rxml = if xml.is_a? REXML::Document
|
121
|
+
xml.root
|
122
|
+
elsif xml.is_a? REXML::Element
|
123
|
+
xml
|
124
|
+
else
|
125
|
+
REXML::Document.new(xml)
|
126
|
+
end
|
127
|
+
|
128
|
+
unless rxml.root.namespace == PP_NS
|
129
|
+
raise WrongNamespace, "this isn't an atom service document!"
|
130
|
+
end
|
131
|
+
|
132
|
+
REXML::XPath.match( rxml, "/app:service/app:workspace", {"app" => Atom::PP_NS} ).each do |ws_el|
|
133
|
+
self.workspaces << Atom::Workspace.parse(ws_el, base, @http)
|
134
|
+
end
|
135
|
+
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
# serialize to a (namespaced) REXML::Document
|
140
|
+
def to_xml
|
141
|
+
doc = REXML::Document.new
|
142
|
+
|
143
|
+
root = REXML::Element.new "service"
|
144
|
+
root.add_namespace Atom::PP_NS
|
145
|
+
root.add_namespace "atom", Atom::NS
|
146
|
+
|
147
|
+
self.workspaces.each do |ws|
|
148
|
+
root << ws.to_element
|
149
|
+
end
|
150
|
+
|
151
|
+
doc << root
|
152
|
+
doc
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Entry
|
157
|
+
# the @href of an entry's link[@rel="edit"]
|
158
|
+
def edit_url
|
159
|
+
begin
|
160
|
+
edit_link = self.links.find do |link|
|
161
|
+
link["rel"] == "edit"
|
162
|
+
end
|
163
|
+
|
164
|
+
edit_link["href"]
|
165
|
+
rescue
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/lib/atom/text.rb
CHANGED
@@ -37,14 +37,27 @@ module Atom
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
#
|
40
|
+
# attempts to parse the content of this element as XML and return it
|
41
|
+
# as an array of REXML::Elements.
|
42
|
+
#
|
43
|
+
# If this self["type"] is "html" and Hpricot is installed, it will
|
44
|
+
# be converted to XHTML first.
|
41
45
|
def xml
|
42
46
|
if self["type"] == "xhtml"
|
43
47
|
@content.children
|
44
48
|
elsif self["type"] == "text"
|
45
49
|
[self.to_s]
|
50
|
+
elsif self["type"] == "html"
|
51
|
+
begin
|
52
|
+
require "hpricot"
|
53
|
+
rescue
|
54
|
+
raise "Turning HTML content into XML requires Hpricot."
|
55
|
+
end
|
56
|
+
|
57
|
+
fixed = Hpricot(self.to_s, :xhtml_strict => true)
|
58
|
+
REXML::Document.new("<div>#{fixed}</div>").root.children
|
46
59
|
else
|
47
|
-
# XXX
|
60
|
+
# XXX check that @type is an XML mimetype and parse it
|
48
61
|
raise "I haven't implemented this yet"
|
49
62
|
end
|
50
63
|
end
|
data/lib/atom/xml.rb
CHANGED
@@ -105,7 +105,7 @@ module REXML # :nodoc: all
|
|
105
105
|
thing.class.attrs.each do |name,req|
|
106
106
|
value = elem.ns_attr name.to_s
|
107
107
|
if value and name == :href
|
108
|
-
thing[name.to_s] = (
|
108
|
+
thing[name.to_s] = (top.base.to_uri + value).to_s
|
109
109
|
elsif value
|
110
110
|
thing[name.to_s] = value
|
111
111
|
end
|
data/test/conformance/updated.rb
CHANGED
@@ -16,7 +16,8 @@ class TestUpdatedConformance < Test::Unit::TestCase
|
|
16
16
|
|
17
17
|
# this is an insignificant change,
|
18
18
|
# (ie. atom:updated_1 == atom:updated_2),
|
19
|
-
#
|
19
|
+
#
|
20
|
+
# the update is applied, your application can handle that however it wants.
|
20
21
|
feed.update!
|
21
22
|
assert_equal "12 of 13 miner<b>s</b> survive mine collapse", feed.entries.first.content.to_s.strip
|
22
23
|
|
data/test/test_constructs.rb
CHANGED
@@ -2,11 +2,54 @@ require "test/unit"
|
|
2
2
|
require "atom/entry"
|
3
3
|
|
4
4
|
class ConstructTest < Test::Unit::TestCase
|
5
|
+
def test_text_construct_html_to_xml
|
6
|
+
begin
|
7
|
+
require "hpricot"
|
8
|
+
rescue
|
9
|
+
# hpricot isn't installed, just skip this test
|
10
|
+
puts "skipping hpricot tests"
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
entry = Atom::Entry.new
|
15
|
+
|
16
|
+
html = <<END
|
17
|
+
<p>Paragraph 1 contains <a href=http://example.org/>a link
|
18
|
+
<p>This really is a horrendous mess.
|
19
|
+
END
|
20
|
+
|
21
|
+
entry.content = html
|
22
|
+
entry.content["type"] = "html"
|
23
|
+
|
24
|
+
xhtml = entry.content.xml
|
25
|
+
|
26
|
+
# Hpricot is imperfect; for now I'll just test that it's parseable
|
27
|
+
assert_instance_of Array, xhtml
|
28
|
+
assert_instance_of REXML::Element, xhtml.first
|
29
|
+
|
30
|
+
=begin
|
31
|
+
assert_equal 2, xhtml.length
|
32
|
+
|
33
|
+
first = xhtml.first
|
34
|
+
assert_equal "p", first.name
|
35
|
+
assert_equal 2, first.children.length
|
36
|
+
|
37
|
+
a = first.children.last
|
38
|
+
assert_equal "a", a.name
|
39
|
+
assert_equal "http://example.org/", a.attributes["href"]
|
40
|
+
assert_equal "a link", a.text
|
41
|
+
|
42
|
+
last = xhtml.last
|
43
|
+
assert_equal "p", last.name
|
44
|
+
assert_equal "This really is a horrendous mess.", last.text
|
45
|
+
=end
|
46
|
+
end
|
47
|
+
|
5
48
|
def test_text_construct_text
|
6
49
|
entry = Atom::Entry.new
|
7
50
|
|
8
|
-
assert_nil
|
9
|
-
assert_equal
|
51
|
+
assert_nil entry.title
|
52
|
+
assert_equal "", entry.title.to_s
|
10
53
|
|
11
54
|
entry.title = "<3"
|
12
55
|
|
data/test/test_feed.rb
CHANGED
@@ -81,6 +81,32 @@ END
|
|
81
81
|
assert_equal 1, feed.entries.length
|
82
82
|
end
|
83
83
|
|
84
|
+
def test_media_types
|
85
|
+
c = proc do |c_t|
|
86
|
+
@s.mount_proc("/") do |req,res|
|
87
|
+
res.content_type = c_t
|
88
|
+
res.body = @test_feed
|
89
|
+
|
90
|
+
@s.stop
|
91
|
+
end
|
92
|
+
one_shot
|
93
|
+
end
|
94
|
+
|
95
|
+
feed = Atom::Feed.new "http://localhost:#{@port}/"
|
96
|
+
|
97
|
+
# even if it looks like a feed, the server's word is law
|
98
|
+
c.call("text/plain")
|
99
|
+
assert_raise(Atom::HTTPException) { feed.update! }
|
100
|
+
|
101
|
+
# text/xml isn't the preferred mimetype, but we'll accept it
|
102
|
+
c.call("text/xml")
|
103
|
+
assert_nothing_raised { feed.update! }
|
104
|
+
|
105
|
+
# same goes for application/xml
|
106
|
+
# XXX c.call("application/xml")
|
107
|
+
# assert_nothing_raised { feed.update! }
|
108
|
+
end
|
109
|
+
|
84
110
|
def test_conditional_get
|
85
111
|
@s.mount_proc("/") do |req,res|
|
86
112
|
assert_nil req["If-None-Match"]
|
@@ -124,5 +150,6 @@ END
|
|
124
150
|
assert_equal 1, feed.entries.length
|
125
151
|
end
|
126
152
|
|
153
|
+
# prepares the server for a single request
|
127
154
|
def one_shot; Thread.new { @s.start }; end
|
128
155
|
end
|
data/test/test_http.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
|
1
3
|
require "atom/http"
|
2
4
|
require "webrick"
|
3
5
|
|
4
6
|
class AtomProtocolTest < Test::Unit::TestCase
|
7
|
+
REALM = "test authentication"
|
8
|
+
USER = "test_user"
|
9
|
+
PASS = "aoeuaoeu"
|
10
|
+
SECRET_DATA = "I kissed a boy once"
|
11
|
+
|
5
12
|
def setup
|
6
13
|
@http = Atom::HTTP.new
|
7
14
|
@port = rand(1024) + 1024
|
@@ -11,13 +18,21 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
11
18
|
end
|
12
19
|
|
13
20
|
def test_parse_wwwauth
|
14
|
-
header = '
|
21
|
+
header = 'realm="SokEvo"'
|
15
22
|
|
16
|
-
|
17
|
-
|
23
|
+
params = @http.send :parse_quoted_wwwauth, header
|
24
|
+
assert_equal "SokEvo", params[:realm]
|
25
|
+
|
26
|
+
header = 'opaque="07UrfUiCYac5BbWJ", algorithm=MD5-sess, qop="auth", stale=TRUE, nonce="MDAx0Mzk", realm="test authentication"'
|
18
27
|
|
19
|
-
|
20
|
-
|
28
|
+
params = @http.send :parse_wwwauth_digest, header
|
29
|
+
|
30
|
+
assert_equal "test authentication", params[:realm]
|
31
|
+
assert_equal "MDAx0Mzk", params[:nonce]
|
32
|
+
assert_equal true, params[:stale]
|
33
|
+
assert_equal "auth", params[:qop]
|
34
|
+
assert_equal "MD5-sess", params[:algorithm]
|
35
|
+
assert_equal "07UrfUiCYac5BbWJ", params[:opaque]
|
21
36
|
end
|
22
37
|
|
23
38
|
def test_GET
|
@@ -32,11 +47,11 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
32
47
|
|
33
48
|
one_shot
|
34
49
|
|
35
|
-
|
50
|
+
get_root
|
36
51
|
|
37
|
-
assert_equal("200", res.code)
|
38
|
-
assert_equal("text/plain", res.content_type)
|
39
|
-
assert_equal("just junk", res.body)
|
52
|
+
assert_equal("200", @res.code)
|
53
|
+
assert_equal("text/plain", @res.content_type)
|
54
|
+
assert_equal("just junk", @res.body)
|
40
55
|
end
|
41
56
|
|
42
57
|
def test_GET_headers
|
@@ -48,39 +63,120 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
48
63
|
|
49
64
|
one_shot
|
50
65
|
|
51
|
-
|
66
|
+
get_root("User-Agent" => "tester agent")
|
52
67
|
|
53
|
-
assert_equal("200", res.code)
|
68
|
+
assert_equal("200", @res.code)
|
54
69
|
end
|
55
70
|
|
56
71
|
def test_basic_auth
|
57
72
|
@s.mount_proc("/") do |req,res|
|
58
|
-
WEBrick::HTTPAuth.basic_auth(req, res,
|
59
|
-
u ==
|
73
|
+
WEBrick::HTTPAuth.basic_auth(req, res, REALM) do |u,p|
|
74
|
+
u == USER and p == PASS
|
60
75
|
end
|
61
76
|
|
62
|
-
res.body =
|
77
|
+
res.body = SECRET_DATA
|
63
78
|
@s.stop
|
64
79
|
end
|
65
80
|
|
66
81
|
one_shot
|
67
|
-
|
82
|
+
|
83
|
+
# with no credentials
|
84
|
+
assert_raises(Atom::Unauthorized) { get_root }
|
85
|
+
|
86
|
+
@http.user = USER
|
87
|
+
@http.pass = "incorrect_password"
|
88
|
+
|
89
|
+
# with incorrect credentials
|
68
90
|
assert_raises(Atom::Unauthorized) { get_root }
|
69
91
|
|
70
92
|
@http.when_auth do |abs_url,realm|
|
71
93
|
assert_equal "http://localhost:#{@port}/", abs_url
|
72
|
-
assert_equal
|
94
|
+
assert_equal REALM, realm
|
73
95
|
|
74
|
-
[
|
96
|
+
[USER, PASS]
|
75
97
|
end
|
76
98
|
|
77
99
|
one_shot
|
78
100
|
|
101
|
+
get_root
|
102
|
+
assert_equal "200", @res.code
|
103
|
+
assert_equal SECRET_DATA, @res.body
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_digest_auth
|
107
|
+
# a dummy userdb
|
108
|
+
userdb = {}
|
109
|
+
userdb[USER] = PASS
|
110
|
+
|
111
|
+
def userdb.get_passwd(realm, user, reload)
|
112
|
+
Digest::MD5::hexdigest([user, realm, self[user]].join(":"))
|
113
|
+
end
|
114
|
+
|
115
|
+
authenticator = WEBrick::HTTPAuth::DigestAuth.new(
|
116
|
+
:UserDB => userdb,
|
117
|
+
:Realm => REALM,
|
118
|
+
:Algorithm => "MD5"
|
119
|
+
)
|
120
|
+
|
121
|
+
@s.mount_proc("/") do |req,res|
|
122
|
+
authenticator.authenticate(req, res)
|
123
|
+
res.body = SECRET_DATA
|
124
|
+
end
|
125
|
+
|
126
|
+
one_shot
|
127
|
+
|
128
|
+
# no credentials
|
129
|
+
assert_raises(Atom::Unauthorized) { get_root }
|
130
|
+
|
131
|
+
@http.user = USER
|
132
|
+
@http.pass = PASS
|
133
|
+
|
134
|
+
# correct credentials
|
79
135
|
res = get_root
|
80
|
-
assert_equal
|
81
|
-
|
136
|
+
assert_equal SECRET_DATA, res.body
|
137
|
+
|
138
|
+
@s.stop
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_wsse_auth
|
142
|
+
@s.mount_proc("/") do |req,res|
|
143
|
+
assert_equal 'WSSE profile="UsernameToken"', req["Authorization"]
|
144
|
+
|
145
|
+
xwsse = req["X-WSSE"]
|
146
|
+
|
147
|
+
p = @http.send :parse_quoted_wwwauth, xwsse
|
148
|
+
|
149
|
+
assert_equal USER, p[:Username]
|
150
|
+
assert_match /^UsernameToken /, xwsse
|
151
|
+
|
152
|
+
# un-base64 in preparation for SHA1-ing
|
153
|
+
nonce = p[:Nonce].unpack("m").first
|
154
|
+
|
155
|
+
# Base64( SHA1( Nonce + CreationTimestamp + Password ) )
|
156
|
+
pd_string = nonce + p[:Created] + PASS
|
157
|
+
password_digest = [Digest::SHA1.digest(pd_string)].pack("m").chomp
|
158
|
+
|
159
|
+
assert_equal password_digest, p[:PasswordDigest]
|
160
|
+
|
161
|
+
res.body = SECRET_DATA
|
162
|
+
@s.stop
|
163
|
+
end
|
164
|
+
|
165
|
+
one_shot
|
166
|
+
|
167
|
+
@http.always_auth = :wsse
|
168
|
+
@http.user = USER
|
169
|
+
@http.pass = PASS
|
170
|
+
|
171
|
+
get_root
|
172
|
+
|
173
|
+
assert_equal "200", @res.code
|
174
|
+
assert_equal SECRET_DATA, @res.body
|
175
|
+
end
|
176
|
+
|
177
|
+
def get_root(*args)
|
178
|
+
@res = @http.get("http://localhost:#{@port}/", *args)
|
82
179
|
end
|
83
180
|
|
84
|
-
def get_root(*args); @http.get("http://localhost:#{@port}/", *args); end
|
85
181
|
def one_shot; Thread.new { @s.start }; end
|
86
182
|
end
|