discodactyl 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+ require 'discodactyl/link_header'
4
+
5
+ module Discodactyl # :nodoc:
6
+ class ResourceDiscovery
7
+ class << self
8
+
9
+ # perform LRDD on the URI, returning all linked URIs which match
10
+ # the provided rel. Any URITemplates will be expanded with the
11
+ # params if they are provided
12
+ #--
13
+ # TODO: xri support: no host
14
+ # TODO: check if XRD/Property[@type=http://lrdd.net/priority/resource] to indicate resource-priority
15
+ # TODO: handle 3** status redirects
16
+ # TODO: maintain a security bit
17
+ # TODO: URIs for all discovery modes should be appended, not just returned
18
+ # TODO: rewrite this so it's just get links, which yields objects providing at least the xrd link interface (rel, href, type)
19
+ #++
20
+ def get_uris_by_rel(uri, rel, params = {})
21
+ begin
22
+ uri = URI.parse(uri.to_s) unless uri.respond_to?('open')
23
+ resource = uri.open
24
+ rescue OpenURI::HTTPError => e
25
+ status = e.io.status[0] # => 3xx, 4xx, or 5xx
26
+
27
+ # code = Net::HTTPResponse::CODE_TO_OBJ[status]
28
+ # if code == "303"
29
+ # 303 HTTPSeeOther
30
+ # if res.key? 'Link'
31
+ # links = res['Link']
32
+ # end
33
+ # elsif code == "401" # HTTPUnauthorized
34
+ # authenticate
35
+ # return get_uris_by_rel(uri)
36
+ # elsif code == "301" || status == "302"
37
+ # 300 HTTPMultipleChoice
38
+ # 301 HTTPMovedPermanently
39
+ # 302 HTTPFound
40
+ # 304 HTTPNotModified
41
+ # 305 HTTPUseProxy
42
+ # 307 HTTPTemporaryRedirect
43
+ # resource = URI.parse(resource['location']).open
44
+ # else
45
+ rescue NoMethodError
46
+ end
47
+ if resource
48
+ # check for link headers first
49
+ if resource.meta.key? 'Link'
50
+ uris = get_uris_by_rel_from_link_header(resource, rel)
51
+ end
52
+
53
+ unless uris
54
+ # then check for links in the document
55
+ if content_sniff(resource) == 'text/html'
56
+ uris = get_uris_by_rel_from_html(resource, rel)
57
+ # Atom
58
+ elsif content_sniff(resource) == 'application/atom+xml'
59
+ uris = get_uris_by_rel_from_atom(resource, rel)
60
+ end
61
+ end
62
+ end
63
+ unless uris
64
+ host_meta = Discodactyl::HostMeta.from_uri uri
65
+ uris = host_meta.uris_by_rel(rel, params)
66
+ end
67
+ uris
68
+ end
69
+
70
+ def lrdd_discovery(uri)
71
+ get_uris_by_rel(uri, 'describedby')
72
+ end
73
+
74
+ def content_sniff(resource)
75
+ if resource.content_type == 'text/html'
76
+ type = 'text/html'
77
+ elsif resource.content_type == 'application/atom+xml'
78
+ type = 'application/atom+xml'
79
+ end
80
+ type
81
+ end
82
+
83
+ # take an HTTP response with a content-type of HTML,
84
+ # find all links by rel, and return each href
85
+ def get_uris_by_rel_from_html(resource, rel)
86
+ doc = Nokogiri::HTML(resource)
87
+ links = doc.xpath("//*[contains(@rel, \"#{rel}\")]")
88
+ uris = links.map {|link| link['href'] }
89
+ end
90
+
91
+ # take an HTTP response, find a link header
92
+ # with rel, and return its href
93
+ def get_uris_by_rel_from_link_header(response, rel)
94
+ links = [response.meta['Link']].flatten.collect {|link|
95
+ LinkHeader.parse(link);
96
+ }
97
+ link = links.find {|l| l[:rel].include? rel }
98
+ xrd = link[:href]
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,32 @@
1
+ module Discodactyl # :nodoc:
2
+ # Basic URI templates as used in XRD. Not to be confused with
3
+ # http://tools.ietf.org/html/draft-gregorio-uritemplate
4
+ class URITemplate
5
+ attr_accessor :pattern
6
+ def initialize(pattern)
7
+ @pattern = pattern
8
+ end
9
+ def to_uri(params)
10
+ require 'cgi'
11
+ uri = @pattern
12
+ while /\{%([^}]*)\}/ =~ uri
13
+ uri.gsub!($~[0], params[$~[1]])
14
+ end
15
+ while /\{([^}]*)\}/ =~ uri
16
+ uri.gsub!($~[0], CGI::escape(params[$~[1]].to_s))
17
+ end
18
+ uri
19
+ end
20
+
21
+ def ==(other)
22
+ return false unless other.class == self.class
23
+ return false unless other.instance_variables == self.instance_variables
24
+ self.instance_variables.each do |var|
25
+ self_var = self.instance_variable_get(var)
26
+ other_var = other.instance_variable_get(var)
27
+ return false unless self_var.eql?(other_var)
28
+ end
29
+ true
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,2 @@
1
+ require 'discodactyl/xrd/document'
2
+ require 'discodactyl/xrd/link'
@@ -0,0 +1,75 @@
1
+ require 'nokogiri'
2
+ require 'discodactyl/xrd/link'
3
+
4
+ module Discodactyl # :nodoc:
5
+ module XRD # :nodoc:
6
+ XMLNS = {'xrd' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0'}
7
+ class Document
8
+ class << self
9
+ def parse(string)
10
+ raw = Nokogiri::XML(string)
11
+ doc = self.new
12
+
13
+ doc.raw = raw
14
+
15
+ doc
16
+ end
17
+ end
18
+
19
+ attr_accessor :raw
20
+
21
+ def escapeXPath(str)
22
+ inner = str.split('\'').join('\',"\'",\'')
23
+ outer = 'concat(\'\',\'%s\')' % inner
24
+ end
25
+
26
+ def linkelems_by_rel(rel)
27
+ path = '/xrd:XRD/xrd:Link[@rel=%s]'% escapeXPath(rel)
28
+ @raw.xpath path, XMLNS
29
+ end
30
+
31
+ def links_by_rel(rel)
32
+ linkelems_by_rel(rel).map {|e| Link.parse(e) }
33
+ end
34
+
35
+ def uris_by_rel(rel, params = {})
36
+ links_by_rel(rel).map {|l| l.to_uri(params) }
37
+ end
38
+
39
+ # take an XML fragment for a link and append it to the document
40
+ def append(link)
41
+ initial_ids = ids
42
+ raw.root.add_child(link)
43
+ elem = Link.parse(raw.root.last_element_child)
44
+ elem.id = generate_tag_uri if elem.id.nil? || initial_ids.include?(elem.id)
45
+ elem
46
+ end
47
+
48
+ def links
49
+ raw.xpath('/xrd:XRD/xrd:Link', XMLNS).collect {|elem|
50
+ Link.parse(elem)
51
+ }
52
+ end
53
+
54
+ def find_link_by_id(link_id)
55
+ links.find {|link| link.id == link_id}
56
+ end
57
+
58
+ def ids
59
+ links.map(&:id).reject(&:nil?)
60
+ end
61
+
62
+ def to_s
63
+ raw.to_s
64
+ end
65
+
66
+ def generate_tag_uri
67
+ scheme = 'tag'
68
+ authority = 'dactylo.us'
69
+ date = Date.today.to_s
70
+ specific = "/xrd/link/#{rand(2**10)}"
71
+ "#{scheme}:#{authority},#{date}:#{specific}"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ require 'nokogiri'
2
+ require 'active_support/core_ext/object/misc'
3
+ require 'discodactyl/uri_template'
4
+
5
+ module Discodactyl # :nodoc:
6
+ module XRD # :nodoc:
7
+ class Link
8
+ class << self
9
+ def parse(element)
10
+ returning(link = self.new) do
11
+ link.rel = element['rel']
12
+ link.type = element['type']
13
+ link.href = element['href']
14
+ link.template = URITemplate.new(element['template']) unless link.href
15
+ link.raw = element
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_accessor :href, :template, :rel, :type, :raw
21
+
22
+ def to_uri(params = {})
23
+ @href || @template.to_uri(params)
24
+ end
25
+
26
+ def id
27
+ begin
28
+ @raw.attribute_with_ns('id', 'http://www.w3.org/XML/1998/namespace').value
29
+ rescue
30
+ end
31
+ end
32
+
33
+ def id=(value)
34
+ @raw['xml:id'] = value
35
+ end
36
+
37
+ def to_s
38
+ @raw.to_s
39
+ end
40
+
41
+ def ==(other)
42
+ (other.respond_to?('raw') && (@raw == other.raw)) ||
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/acct_uri"
7
+
8
+ class TestURIAcct < Test::Unit::TestCase
9
+ def test_parse_without_scheme
10
+ a = URI::ACCT.parse('user@host.example')
11
+ assert_equal('user', a.local_part)
12
+ assert_equal('host.example', a.host)
13
+ end
14
+
15
+ def test_parse_with_scheme
16
+ a = URI::ACCT.parse('acct:user@host.example')
17
+ assert_equal('user', a.local_part)
18
+ assert_equal('host.example', a.host)
19
+ end
20
+
21
+ def test_parse_uri_by_scheme
22
+ a = URI.parse('acct:user@host.example')
23
+ assert_equal('user', a.local_part)
24
+ assert_equal('host.example', a.host)
25
+ end
26
+
27
+ def test_build_from_hash
28
+ a = URI::ACCT.build(:local_part => 'user',
29
+ :host => 'host.example')
30
+ assert_equal('user', a.local_part)
31
+ assert_equal('host.example', a.host)
32
+ end
33
+
34
+ def test_build_from_opaque
35
+ a = URI::ACCT.build(:opaque => 'user@host.example')
36
+ assert_equal('user', a.local_part)
37
+ assert_equal('host.example', a.host)
38
+ end
39
+
40
+ def test_build_from_array
41
+ a = URI::ACCT.build(['user','host.example'])
42
+ assert_equal('user', a.local_part)
43
+ assert_equal('host.example', a.host)
44
+ end
45
+
46
+
47
+ def test_to_s
48
+ a = URI::ACCT.build(:local_part => 'user',
49
+ :host => 'host.example')
50
+ assert_equal('acct:user@host.example', a.to_s)
51
+ end
52
+ def test_id
53
+ a = URI::ACCT.build(:local_part => 'user',
54
+ :host => 'host.example')
55
+ assert_equal('user@host.example', a.id)
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ require "test/unit"
2
+ module Test::Unit::Assertions
3
+ def assert_length(expected, enum, message = nil)
4
+ message = build_message message, '<?> is not length <?>', enum, expected
5
+ assert_equal expected, enum.length, message
6
+ end
7
+
8
+ def assert_include?(atom, enum, message = nil)
9
+ message = build_message message, '<?> does not include <?>.', enum, atom
10
+ assert enum.include?(atom), message
11
+ end
12
+ end
13
+
14
+ class Test::Unit::TestCase
15
+ require 'rr'
16
+ include RR::Adapters::TestUnit
17
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/host_meta"
7
+ require "discodactyl/acct_uri"
8
+
9
+ class TestHostMeta < Test::Unit::TestCase
10
+ def test_get_uri_from_host
11
+ uri = 'host.example'
12
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
13
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
14
+ end
15
+
16
+ def test_get_uri_from_http
17
+ uri = URI.parse 'http://host.example/some/path'
18
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
19
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
20
+ end
21
+
22
+ def test_get_uri_from_acct
23
+ uri = URI.parse 'acct:user@host.example'
24
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
25
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
26
+ end
27
+
28
+ # def test_raise_meaningful_exception
29
+ # stub(io).status { [ '404', 'Not Found'] }
30
+ # stub(uri).open { raise OpenURI::HTTPError.new('foo', io)}
31
+ # stub(URI).parse('http://example.com/.well-known/host-meta') { uri }
32
+ #
33
+ # assert_raise Discodactyl::HostMetaHTTPError, "404 Not Found" do
34
+ # Discodactyl::HostMeta.from_uri(uri)
35
+ # end
36
+ # end
37
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/link_header"
7
+
8
+ class TestLinkHeader < Test::Unit::TestCase
9
+ def test_parse_rel_and_title
10
+ link = '<http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"'
11
+ expected = {:href => 'http://example.com/TheBook/chapter2', :rel => ['previous'], :title => 'previous chapter'}
12
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
13
+ end
14
+
15
+ def test_parse_seperated_rels
16
+ link = '<http://example.org/>; rel=index; rel="start http://example.net/relation/other"'
17
+ expected = {:href => 'http://example.org/', :rel => ['index', 'start', 'http://example.net/relation/other']}
18
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
19
+ end
20
+
21
+ def test_parse_simple_link
22
+ link = '</bar>; rel="http://example.com/profile1/foo"'
23
+ expected = {:href => '/bar', :rel => ['http://example.com/profile1/foo']}
24
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
25
+ end
26
+
27
+ def test_parse_xrd_link
28
+ link = '<http://josephholsten.com/descriptor.xrd>; rel="describedby"; type="application/xrd+xml"'
29
+ expected = {:href => 'http://josephholsten.com/descriptor.xrd', :rel => ['describedby'], :type => 'application/xrd+xml' }
30
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+ testdir = File.expand_path('../../test', __FILE__)
5
+ $LOAD_PATH.unshift(testdir) unless $LOAD_PATH.include?(testdir)
6
+
7
+ require 'test_helper'
8
+ require "test/unit"
9
+ require "discodactyl/resource_discovery"
10
+
11
+ class TestResourceDiscovery < Test::Unit::TestCase
12
+ def test_get_uris_by_rel_from_html
13
+ raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
14
+
15
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
16
+
17
+ assert_include? 'http://host.example/description.xrd', uris
18
+ end
19
+
20
+ def test_get_uris_by_rel_from_html_with_multiple_rels
21
+ raw = '<html><head><title></title><link rel="also describedby" href="http://host.example/description.xrd"></head><body></body></html>'
22
+
23
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
24
+
25
+ assert_include? 'http://host.example/description.xrd', uris
26
+ end
27
+
28
+ def test_get_uris_by_rel_from_html_via_disco
29
+ require 'ostruct'
30
+ raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
31
+ response = FakeResp.new(raw)
32
+ response.meta = {}
33
+ response.content_type = 'text/html'
34
+ uri = OpenStruct.new(:open => response)
35
+
36
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
37
+
38
+ assert_include? 'http://host.example/description.xrd', uris
39
+ end
40
+
41
+ def test_get_uris_by_rel_from_link_header
42
+ require 'ostruct'
43
+ header = '<http://host.example/description.xrd>; rel="describedby"'
44
+ response = OpenStruct.new(:meta => {'Link' => header})
45
+
46
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_link_header(response, 'describedby')
47
+
48
+ assert_equal 'http://host.example/description.xrd', uris
49
+ end
50
+
51
+ def test_get_uris_by_rel_from_header
52
+ require 'ostruct'
53
+ header = '<http://host.example/description.xrd>; rel="describedby"'
54
+ response = OpenStruct.new(:meta => {'Link' => header})
55
+ uri = OpenStruct.new(:open => response)
56
+
57
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
58
+
59
+ assert_equal 'http://host.example/description.xrd', uris
60
+ end
61
+ end
62
+
63
+ class FakeResp < String
64
+ attr_accessor :meta, :content_type
65
+ end