ruby-openid2 3.0.0
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.
- 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
|