ruby-openid2 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- metadata.gz.sig +0 -0
data/lib/openid/util.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "uri"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
# See OpenID::Consumer or OpenID::Server modules, as well as the store classes
|
6
|
+
module OpenID
|
7
|
+
class AssertionError < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
# Exceptions that are raised by the library are subclasses of this
|
11
|
+
# exception type, so if you want to catch all exceptions raised by
|
12
|
+
# the library, you can catch OpenIDError
|
13
|
+
class OpenIDError < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
module Util
|
17
|
+
BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
|
18
|
+
"abcdefghijklmnopqrstuvwxyz0123456789+/"
|
19
|
+
BASE64_RE = Regexp.compile(
|
20
|
+
"
|
21
|
+
\\A
|
22
|
+
([#{BASE64_CHARS}]{4})*
|
23
|
+
([#{BASE64_CHARS}]{2}==|
|
24
|
+
[#{BASE64_CHARS}]{3}=)?
|
25
|
+
\\Z",
|
26
|
+
Regexp::EXTENDED,
|
27
|
+
)
|
28
|
+
|
29
|
+
def self.truthy_assert(value, message = nil)
|
30
|
+
return if value
|
31
|
+
|
32
|
+
raise AssertionError, message or value
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.to_base64(s)
|
36
|
+
[s].pack("m").delete("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.from_base64(s)
|
40
|
+
without_newlines = s.gsub(/[\r\n]+/, "")
|
41
|
+
raise ArgumentError, "Malformed input: #{s.inspect}" unless BASE64_RE.match(without_newlines)
|
42
|
+
|
43
|
+
without_newlines.unpack1("m")
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.urlencode(args)
|
47
|
+
a = []
|
48
|
+
args.each do |key, val|
|
49
|
+
if val.nil?
|
50
|
+
val = ""
|
51
|
+
elsif !!val == val
|
52
|
+
# it's boolean let's convert it to string representation
|
53
|
+
# or else CGI::escape won't like it
|
54
|
+
val = val.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
a << (CGI.escape(key) + "=" + CGI.escape(val))
|
58
|
+
end
|
59
|
+
a.join("&")
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.parse_query(qs)
|
63
|
+
query = {}
|
64
|
+
CGI.parse(qs).each { |k, v| query[k] = v[0] }
|
65
|
+
query
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.append_args(url, args)
|
69
|
+
url = url.dup
|
70
|
+
return url if args.length == 0
|
71
|
+
|
72
|
+
args = args.sort if args.respond_to?(:each_pair)
|
73
|
+
|
74
|
+
url << (url.include?("?") ? "&" : "?")
|
75
|
+
url << Util.urlencode(args)
|
76
|
+
end
|
77
|
+
|
78
|
+
@@logger = Logger.new(STDERR)
|
79
|
+
@@logger.progname = "OpenID"
|
80
|
+
|
81
|
+
def self.logger=(logger)
|
82
|
+
@@logger = logger
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.logger
|
86
|
+
@@logger
|
87
|
+
end
|
88
|
+
|
89
|
+
# change the message below to do whatever you like for logging
|
90
|
+
def self.log(message)
|
91
|
+
logger.info(message)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.auto_submit_html(form, title = "OpenID transaction in progress")
|
95
|
+
"
|
96
|
+
<html>
|
97
|
+
<head>
|
98
|
+
<title>#{title}</title>
|
99
|
+
</head>
|
100
|
+
<body onload='document.forms[0].submit();'>
|
101
|
+
#{form}
|
102
|
+
<script>
|
103
|
+
var elements = document.forms[0].elements;
|
104
|
+
for (var i = 0; i < elements.length; i++) {
|
105
|
+
elements[i].style.display = \"none\";
|
106
|
+
}
|
107
|
+
</script>
|
108
|
+
</body>
|
109
|
+
</html>
|
110
|
+
"
|
111
|
+
end
|
112
|
+
|
113
|
+
ESCAPE_TABLE = {"&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'"}
|
114
|
+
# Modified from ERb's html_encode
|
115
|
+
def self.html_encode(str)
|
116
|
+
str.to_s.gsub(/[&<>"']/) { |s| ESCAPE_TABLE[s] }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module OpenID
|
2
|
+
module Yadis
|
3
|
+
# Generate an accept header value
|
4
|
+
#
|
5
|
+
# [str or (str, float)] -> str
|
6
|
+
def self.generate_accept_header(*elements)
|
7
|
+
parts = []
|
8
|
+
elements.each do |element|
|
9
|
+
if element.is_a?(String)
|
10
|
+
qs = "1.0"
|
11
|
+
mtype = element
|
12
|
+
else
|
13
|
+
mtype, q = element
|
14
|
+
q = q.to_f
|
15
|
+
raise ArgumentError.new("Invalid preference factor: #{q}") if q > 1 or q <= 0
|
16
|
+
|
17
|
+
qs = format("%0.1f", q)
|
18
|
+
end
|
19
|
+
|
20
|
+
parts << [qs, mtype]
|
21
|
+
end
|
22
|
+
|
23
|
+
parts.sort!
|
24
|
+
chunks = []
|
25
|
+
parts.each do |q, mtype|
|
26
|
+
chunks << if q == "1.0"
|
27
|
+
mtype
|
28
|
+
else
|
29
|
+
format("%s; q=%s", mtype, q)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
chunks.join(", ")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.parse_accept_header(value)
|
37
|
+
# Parse an accept header, ignoring any accept-extensions
|
38
|
+
#
|
39
|
+
# returns a list of tuples containing main MIME type, MIME
|
40
|
+
# subtype, and quality markdown.
|
41
|
+
#
|
42
|
+
# str -> [(str, str, float)]
|
43
|
+
chunks = value.split(",", -1).collect { |v| v.strip }
|
44
|
+
accept = []
|
45
|
+
chunks.each do |chunk|
|
46
|
+
parts = chunk.split(";", -1).collect { |s| s.strip }
|
47
|
+
|
48
|
+
mtype = parts.shift
|
49
|
+
if mtype.index("/").nil?
|
50
|
+
# This is not a MIME type, so ignore the bad data
|
51
|
+
next
|
52
|
+
end
|
53
|
+
|
54
|
+
main, sub = mtype.split("/", 2)
|
55
|
+
|
56
|
+
q = nil
|
57
|
+
parts.each do |ext|
|
58
|
+
unless ext.index("=").nil?
|
59
|
+
k, v = ext.split("=", 2)
|
60
|
+
q = v.to_f if k == "q"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
q = 1.0 if q.nil?
|
65
|
+
|
66
|
+
accept << [q, main, sub]
|
67
|
+
end
|
68
|
+
|
69
|
+
accept.sort!
|
70
|
+
accept.reverse!
|
71
|
+
|
72
|
+
accept.collect { |q, main, sub| [main, sub, q] }
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.match_types(accept_types, have_types)
|
76
|
+
# Given the result of parsing an Accept: header, and the
|
77
|
+
# available MIME types, return the acceptable types with their
|
78
|
+
# quality markdowns.
|
79
|
+
#
|
80
|
+
# For example:
|
81
|
+
#
|
82
|
+
# >>> acceptable = parse_accept_header('text/html, text/plain; q=0.5')
|
83
|
+
# >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg'])
|
84
|
+
# [('text/html', 1.0), ('text/plain', 0.5)]
|
85
|
+
#
|
86
|
+
# Type signature: ([(str, str, float)], [str]) -> [(str, float)]
|
87
|
+
default = if accept_types.nil? or accept_types == []
|
88
|
+
# Accept all of them
|
89
|
+
1
|
90
|
+
else
|
91
|
+
0
|
92
|
+
end
|
93
|
+
|
94
|
+
match_main = {}
|
95
|
+
match_sub = {}
|
96
|
+
accept_types.each do |main, sub, q|
|
97
|
+
if main == "*"
|
98
|
+
default = [default, q].max
|
99
|
+
next
|
100
|
+
elsif sub == "*"
|
101
|
+
match_main[main] = [match_main.fetch(main, 0), q].max
|
102
|
+
else
|
103
|
+
match_sub[[main, sub]] = [match_sub.fetch([main, sub], 0), q].max
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
accepted_list = []
|
108
|
+
order_maintainer = 0
|
109
|
+
have_types.each do |mtype|
|
110
|
+
main, sub = mtype.split("/", 2)
|
111
|
+
q = if match_sub.member?([main, sub])
|
112
|
+
match_sub[[main, sub]]
|
113
|
+
else
|
114
|
+
match_main.fetch(main, default)
|
115
|
+
end
|
116
|
+
|
117
|
+
if q != 0
|
118
|
+
accepted_list << [1 - q, order_maintainer, q, mtype]
|
119
|
+
order_maintainer += 1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
accepted_list.sort!
|
124
|
+
accepted_list.collect { |_, _, q, mtype| [mtype, q] }
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.get_acceptable(accept_header, have_types)
|
128
|
+
# Parse the accept header and return a list of available types
|
129
|
+
# in preferred order. If a type is unacceptable, it will not be
|
130
|
+
# in the resulting list.
|
131
|
+
#
|
132
|
+
# This is a convenience wrapper around matchTypes and
|
133
|
+
# parse_accept_header
|
134
|
+
#
|
135
|
+
# (str, [str]) -> [str]
|
136
|
+
accepted = parse_accept_header(accept_header)
|
137
|
+
preferred = match_types(accepted, have_types)
|
138
|
+
preferred.collect { |mtype, _| mtype }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative "accept"
|
2
|
+
|
3
|
+
module OpenID
|
4
|
+
module Yadis
|
5
|
+
YADIS_HEADER_NAME = "X-XRDS-Location"
|
6
|
+
YADIS_CONTENT_TYPE = "application/xrds+xml"
|
7
|
+
|
8
|
+
# A value suitable for using as an accept header when performing
|
9
|
+
# YADIS discovery, unless the application has special requirements
|
10
|
+
YADIS_ACCEPT_HEADER = generate_accept_header(
|
11
|
+
["text/html", 0.3],
|
12
|
+
["application/xhtml+xml", 0.5],
|
13
|
+
[YADIS_CONTENT_TYPE, 1.0],
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require_relative "../util"
|
2
|
+
require_relative "../fetchers"
|
3
|
+
require_relative "constants"
|
4
|
+
require_relative "parsehtml"
|
5
|
+
|
6
|
+
module OpenID
|
7
|
+
# Raised when a error occurs in the discovery process
|
8
|
+
class DiscoveryFailure < OpenIDError
|
9
|
+
attr_accessor :identity_url, :http_response
|
10
|
+
|
11
|
+
def initialize(message, http_response)
|
12
|
+
super(message)
|
13
|
+
@identity_url = nil
|
14
|
+
@http_response = http_response
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Yadis
|
19
|
+
# Contains the result of performing Yadis discovery on a URI
|
20
|
+
class DiscoveryResult
|
21
|
+
# The result of following redirects from the request_uri
|
22
|
+
attr_accessor :normalize_uri
|
23
|
+
|
24
|
+
# The URI from which the response text was returned (set to
|
25
|
+
# nil if there was no XRDS document found)
|
26
|
+
attr_accessor :xrds_uri
|
27
|
+
|
28
|
+
# The content-type returned with the response_text
|
29
|
+
attr_accessor :content_type
|
30
|
+
|
31
|
+
# The document returned from the xrds_uri
|
32
|
+
attr_accessor :response_text
|
33
|
+
|
34
|
+
attr_accessor :request_uri, :normalized_uri
|
35
|
+
|
36
|
+
def initialize(request_uri)
|
37
|
+
# Initialize the state of the object
|
38
|
+
#
|
39
|
+
# sets all attributes to None except the request_uri
|
40
|
+
@request_uri = request_uri
|
41
|
+
@normalized_uri = nil
|
42
|
+
@xrds_uri = nil
|
43
|
+
@content_type = nil
|
44
|
+
@response_text = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Was the Yadis protocol's indirection used?
|
48
|
+
def used_yadis_location?
|
49
|
+
@normalized_uri != @xrds_uri
|
50
|
+
end
|
51
|
+
|
52
|
+
# Is the response text supposed to be an XRDS document?
|
53
|
+
def is_xrds
|
54
|
+
(used_yadis_location? or
|
55
|
+
@content_type == YADIS_CONTENT_TYPE)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Discover services for a given URI.
|
60
|
+
#
|
61
|
+
# uri: The identity URI as a well-formed http or https URI. The
|
62
|
+
# well-formedness and the protocol are not checked, but the
|
63
|
+
# results of this function are undefined if those properties do
|
64
|
+
# not hold.
|
65
|
+
#
|
66
|
+
# returns a DiscoveryResult object
|
67
|
+
#
|
68
|
+
# Raises DiscoveryFailure when the HTTP response does not have
|
69
|
+
# a 200 code.
|
70
|
+
def self.discover(uri)
|
71
|
+
result = DiscoveryResult.new(uri)
|
72
|
+
begin
|
73
|
+
resp = OpenID.fetch(uri, nil, {"Accept" => YADIS_ACCEPT_HEADER})
|
74
|
+
rescue Exception
|
75
|
+
raise DiscoveryFailure.new("Failed to fetch identity URL #{uri} : #{$!}", $!)
|
76
|
+
end
|
77
|
+
if resp.code != "200" and resp.code != "206"
|
78
|
+
raise DiscoveryFailure.new(
|
79
|
+
'HTTP Response status from identity URL host is not "200".' \
|
80
|
+
"Got status #{resp.code.inspect} for #{resp.final_url}",
|
81
|
+
resp,
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Note the URL after following redirects
|
86
|
+
result.normalized_uri = resp.final_url
|
87
|
+
|
88
|
+
# Attempt to find out where to go to discover the document or if
|
89
|
+
# we already have it
|
90
|
+
result.content_type = resp["content-type"]
|
91
|
+
|
92
|
+
result.xrds_uri = where_is_yadis?(resp)
|
93
|
+
|
94
|
+
if result.xrds_uri and result.used_yadis_location?
|
95
|
+
begin
|
96
|
+
resp = OpenID.fetch(result.xrds_uri)
|
97
|
+
rescue StandardError
|
98
|
+
raise DiscoveryFailure.new("Failed to fetch Yadis URL #{result.xrds_uri} : #{$!}", $!)
|
99
|
+
end
|
100
|
+
if resp.code != "200" and resp.code != "206"
|
101
|
+
exc = DiscoveryFailure.new(
|
102
|
+
'HTTP Response status from Yadis host is not "200". ' +
|
103
|
+
"Got status #{resp.code.inspect} for #{resp.final_url}",
|
104
|
+
resp,
|
105
|
+
)
|
106
|
+
exc.identity_url = result.normalized_uri
|
107
|
+
raise exc
|
108
|
+
end
|
109
|
+
|
110
|
+
result.content_type = resp["content-type"]
|
111
|
+
end
|
112
|
+
|
113
|
+
result.response_text = resp.body
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Given a HTTPResponse, return the location of the Yadis
|
118
|
+
# document.
|
119
|
+
#
|
120
|
+
# May be the URL just retrieved, another URL, or None, if I
|
121
|
+
# can't find any.
|
122
|
+
#
|
123
|
+
# [non-blocking]
|
124
|
+
def self.where_is_yadis?(resp)
|
125
|
+
# Attempt to find out where to go to discover the document or if
|
126
|
+
# we already have it
|
127
|
+
content_type = resp["content-type"]
|
128
|
+
|
129
|
+
# According to the spec, the content-type header must be an
|
130
|
+
# exact match, or else we have to look for an indirection.
|
131
|
+
if !content_type.nil? and !content_type.to_s.empty? and
|
132
|
+
content_type.split(";", 2)[0].downcase == YADIS_CONTENT_TYPE
|
133
|
+
return resp.final_url
|
134
|
+
else
|
135
|
+
# Try the header
|
136
|
+
yadis_loc = resp[YADIS_HEADER_NAME.downcase]
|
137
|
+
|
138
|
+
if yadis_loc.nil?
|
139
|
+
# Parse as HTML if the header is missing.
|
140
|
+
#
|
141
|
+
# XXX: do we want to do something with content-type, like
|
142
|
+
# have a whitelist or a blacklist (for detecting that it's
|
143
|
+
# HTML)?
|
144
|
+
yadis_loc = Yadis.html_yadis_location(resp.body)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
yadis_loc
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# This file contains functions and classes used for extracting
|
2
|
+
# endpoint information out of a Yadis XRD file using the REXML
|
3
|
+
# XML parser.
|
4
|
+
|
5
|
+
module OpenID
|
6
|
+
module Yadis
|
7
|
+
class BasicServiceEndpoint
|
8
|
+
attr_reader :type_uris, :yadis_url, :uri, :service_element
|
9
|
+
|
10
|
+
# Generic endpoint object that contains parsed service
|
11
|
+
# information, as well as a reference to the service element
|
12
|
+
# from which it was generated. If there is more than one
|
13
|
+
# xrd:Type or xrd:URI in the xrd:Service, this object represents
|
14
|
+
# just one of those pairs.
|
15
|
+
#
|
16
|
+
# This object can be used as a filter, because it implements
|
17
|
+
# fromBasicServiceEndpoint.
|
18
|
+
#
|
19
|
+
# The simplest kind of filter you can write implements
|
20
|
+
# fromBasicServiceEndpoint, which takes one of these objects.
|
21
|
+
def initialize(yadis_url, type_uris, uri, service_element)
|
22
|
+
@type_uris = type_uris
|
23
|
+
@yadis_url = yadis_url
|
24
|
+
@uri = uri
|
25
|
+
@service_element = service_element
|
26
|
+
end
|
27
|
+
|
28
|
+
# Query this endpoint to see if it has any of the given type
|
29
|
+
# URIs. This is useful for implementing other endpoint classes
|
30
|
+
# that e.g. need to check for the presence of multiple
|
31
|
+
# versions of a single protocol.
|
32
|
+
def match_types(type_uris)
|
33
|
+
@type_uris & type_uris
|
34
|
+
end
|
35
|
+
|
36
|
+
# Trivial transform from a basic endpoint to itself. This
|
37
|
+
# method exists to allow BasicServiceEndpoint to be used as a
|
38
|
+
# filter.
|
39
|
+
#
|
40
|
+
# If you are subclassing this object, re-implement this function.
|
41
|
+
def self.from_basic_service_endpoint(endpoint)
|
42
|
+
endpoint
|
43
|
+
end
|
44
|
+
|
45
|
+
# A hack to make both this class and its instances respond to
|
46
|
+
# this message since Ruby doesn't support static methods.
|
47
|
+
def from_basic_service_endpoint(endpoint)
|
48
|
+
self.class.from_basic_service_endpoint(endpoint)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Take a list of basic filters and makes a filter that
|
53
|
+
# transforms the basic filter into a top-level filter. This is
|
54
|
+
# mostly useful for the implementation of make_filter, which
|
55
|
+
# should only be needed for special cases or internal use by
|
56
|
+
# this library.
|
57
|
+
#
|
58
|
+
# This object is useful for creating simple filters for services
|
59
|
+
# that use one URI and are specified by one Type (we expect most
|
60
|
+
# Types will fit this paradigm).
|
61
|
+
#
|
62
|
+
# Creates a BasicServiceEndpoint object and apply the filter
|
63
|
+
# functions to it until one of them returns a value.
|
64
|
+
class TransformFilterMaker
|
65
|
+
attr_reader :filter_procs
|
66
|
+
|
67
|
+
# Initialize the filter maker's state
|
68
|
+
#
|
69
|
+
# filter_functions are the endpoint transformer
|
70
|
+
# Procs to apply to the basic endpoint. These are called in
|
71
|
+
# turn until one of them does not return nil, and the result
|
72
|
+
# of that transformer is returned.
|
73
|
+
def initialize(filter_procs)
|
74
|
+
@filter_procs = filter_procs
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns an array of endpoint objects produced by the
|
78
|
+
# filter procs.
|
79
|
+
def get_service_endpoints(yadis_url, service_element)
|
80
|
+
endpoints = []
|
81
|
+
|
82
|
+
# Do an expansion of the service element by xrd:Type and
|
83
|
+
# xrd:URI
|
84
|
+
Yadis.expand_service(service_element).each do |type_uris, uri, _|
|
85
|
+
# Create a basic endpoint object to represent this
|
86
|
+
# yadis_url, Service, Type, URI combination
|
87
|
+
endpoint = BasicServiceEndpoint.new(
|
88
|
+
yadis_url, type_uris, uri, service_element
|
89
|
+
)
|
90
|
+
|
91
|
+
e = apply_filters(endpoint)
|
92
|
+
endpoints << e unless e.nil?
|
93
|
+
end
|
94
|
+
endpoints
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_filters(endpoint)
|
98
|
+
# Apply filter procs to an endpoint until one of them returns
|
99
|
+
# non-nil.
|
100
|
+
@filter_procs.each do |filter_proc|
|
101
|
+
e = filter_proc.call(endpoint)
|
102
|
+
unless e.nil?
|
103
|
+
# Once one of the filters has returned an endpoint, do not
|
104
|
+
# apply any more.
|
105
|
+
return e
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class CompoundFilter
|
114
|
+
attr_reader :subfilters
|
115
|
+
|
116
|
+
# Create a new filter that applies a set of filters to an
|
117
|
+
# endpoint and collects their results.
|
118
|
+
def initialize(subfilters)
|
119
|
+
@subfilters = subfilters
|
120
|
+
end
|
121
|
+
|
122
|
+
# Generate all endpoint objects for all of the subfilters of
|
123
|
+
# this filter and return their concatenation.
|
124
|
+
def get_service_endpoints(yadis_url, service_element)
|
125
|
+
endpoints = []
|
126
|
+
@subfilters.each do |subfilter|
|
127
|
+
endpoints += subfilter.get_service_endpoints(yadis_url, service_element)
|
128
|
+
end
|
129
|
+
endpoints
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Exception raised when something is not able to be turned into a
|
134
|
+
# filter
|
135
|
+
@@filter_type_error = TypeError.new(
|
136
|
+
"Expected a filter, an endpoint, a callable or a list of any of these.",
|
137
|
+
)
|
138
|
+
|
139
|
+
# Convert a filter-convertable thing into a filter
|
140
|
+
#
|
141
|
+
# parts should be a filter, an endpoint, a callable, or a list of
|
142
|
+
# any of these.
|
143
|
+
def self.make_filter(parts)
|
144
|
+
# Convert the parts into a list, and pass to mk_compound_filter
|
145
|
+
parts = [BasicServiceEndpoint] if parts.nil?
|
146
|
+
|
147
|
+
return mk_compound_filter(parts) if parts.is_a?(Array)
|
148
|
+
|
149
|
+
mk_compound_filter([parts])
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create a filter out of a list of filter-like things
|
153
|
+
#
|
154
|
+
# Used by make_filter
|
155
|
+
#
|
156
|
+
# parts should be a list of things that can be passed to make_filter
|
157
|
+
def self.mk_compound_filter(parts)
|
158
|
+
raise TypeError, "#{parts.inspect} is not iterable" unless parts.respond_to?(:each)
|
159
|
+
|
160
|
+
# Separate into a list of callables and a list of filter objects
|
161
|
+
transformers = []
|
162
|
+
filters = []
|
163
|
+
parts.each do |subfilter|
|
164
|
+
if !subfilter.is_a?(Array)
|
165
|
+
# If it's not an iterable
|
166
|
+
if subfilter.respond_to?(:get_service_endpoints)
|
167
|
+
# It's a full filter
|
168
|
+
filters << subfilter
|
169
|
+
elsif subfilter.respond_to?(:from_basic_service_endpoint)
|
170
|
+
# It's an endpoint object, so put its endpoint conversion
|
171
|
+
# attribute into the list of endpoint transformers
|
172
|
+
transformers << subfilter.method(:from_basic_service_endpoint)
|
173
|
+
elsif subfilter.respond_to?(:call)
|
174
|
+
# It's a proc, so add it to the list of endpoint
|
175
|
+
# transformers
|
176
|
+
transformers << subfilter
|
177
|
+
else
|
178
|
+
raise @@filter_type_error
|
179
|
+
end
|
180
|
+
else
|
181
|
+
filters << mk_compound_filter(subfilter)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
filters << TransformFilterMaker.new(transformers) if transformers.length > 0
|
186
|
+
|
187
|
+
return filters[0] if filters.length == 1
|
188
|
+
|
189
|
+
CompoundFilter.new(filters)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|