bridgetown-webfinger 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # Wraps a [link][1] object within a {JRD}
6
+ #
7
+ # Links represent links to resources external to the entity. They are
8
+ # _optional_ within a JRD so may not appear in your Webfinger setup.
9
+ #
10
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4
11
+ class Link
12
+ extend Logging
13
+
14
+ # Parses and maybe-returns a {Link} when the value is one
15
+ #
16
+ # [Links][1] within the {JRD} are member objects representing a link to
17
+ # another resource.
18
+ #
19
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4
20
+ #
21
+ # @since 0.1.0
22
+ # @api private
23
+ #
24
+ # @param data [Hash] the data to parse as a link object
25
+ # @return [Link, nil] the link parsed from the data
26
+ def self.parse(data)
27
+ unless (rel = LinkRelationType.parse(data[:rel]))
28
+ return warn(
29
+ "Webfinger link rel is missing or malformed: #{data.inspect}, ignoring"
30
+ )
31
+ end
32
+
33
+ new(
34
+ rel: rel,
35
+ href: Href.parse(data[:href]),
36
+ properties: Properties.parse(data[:properties]),
37
+ titles: Titles.parse(data[:titles]),
38
+ type: data[:type]
39
+ )
40
+ end
41
+
42
+ # Creates a new {Link}
43
+ #
44
+ # @since 0.1.0
45
+ # @api private
46
+ #
47
+ # @param rel [LinkRelationType] the type of relation the {Link} represents
48
+ # @param href [Href, nil] the optional {Href} URI of the link
49
+ # @param properties [Properties, nil] the optional list of {Properties}
50
+ # describing the link
51
+ # @param titles [Titles, nil] the optional list of {Titles} naming the link
52
+ # @param type [String, nil] the optional media type of the target resource
53
+ def initialize(rel:, href: nil, properties: nil, titles: nil, type: nil)
54
+ @href = href
55
+ @properties = properties
56
+ @rel = rel
57
+ @titles = titles
58
+ @type = type
59
+ end
60
+
61
+ # The optional hypertext reference to a URI for the {Link}
62
+ #
63
+ # @since 0.1.0
64
+ # @api private
65
+ #
66
+ # @return [Href, nil] the optional {Href} URI of the link
67
+ attr_reader :href
68
+
69
+ # The optional {Properties} characterizing the {Link}
70
+ #
71
+ # @since 0.1.0
72
+ # @api private
73
+ #
74
+ # @return [Properties, nil] the optional list of {Properties} describing
75
+ # the link
76
+ attr_reader :properties
77
+
78
+ # The {LinkRelationType} describing what the {Link} links to
79
+ #
80
+ # @since 0.1.0
81
+ # @api private
82
+ #
83
+ # @return [LinkRelationType] the type of relation the {Link} represents
84
+ attr_reader :rel
85
+
86
+ # The optional {Titles} describing the {Link}
87
+ #
88
+ # @since 0.1.0
89
+ # @api private
90
+ #
91
+ # @return [Titles, nil] the optional list of {Titles} naming the link
92
+ attr_reader :titles
93
+
94
+ # The optional media type of the {#href} for the {Link}
95
+ #
96
+ # @since 0.1.0
97
+ # @api private
98
+ #
99
+ # @return [String, nil] the optional media type of the target resource
100
+ attr_reader :type
101
+
102
+ # Converts the {Link} into a JSON-serializable Hash
103
+ #
104
+ # @since 0.1.0
105
+ # @api private
106
+ #
107
+ # @return [Hash] the Link as a JSON-compatible Hash
108
+ def to_h
109
+ result = {rel: rel}
110
+ result[:href] = href if href
111
+ result[:properties] = properties if properties
112
+ result[:titles] = titles if titles
113
+ result[:type] = type if type
114
+ result
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # Wraps the type of a {Link} relation
6
+ class LinkRelationType < Model
7
+ # The list of relation types, as [registered with IANA][1]
8
+ #
9
+ # This constant can be regenerated with the `data:link_relations` Rake
10
+ # task.
11
+ #
12
+ # [1]: https://www.iana.org/assignments/link-relations
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ REGISTERED = Set.new(%w[
17
+ about
18
+ acl
19
+ alternate
20
+ amphtml
21
+ appendix
22
+ apple-touch-icon
23
+ apple-touch-startup-image
24
+ archives
25
+ author
26
+ blocked-by
27
+ bookmark
28
+ canonical
29
+ chapter
30
+ cite-as
31
+ collection
32
+ contents
33
+ convertedfrom
34
+ copyright
35
+ create-form
36
+ current
37
+ describedby
38
+ describes
39
+ disclosure
40
+ dns-prefetch
41
+ duplicate
42
+ edit
43
+ edit-form
44
+ edit-media
45
+ enclosure
46
+ external
47
+ first
48
+ glossary
49
+ help
50
+ hosts
51
+ hub
52
+ icon
53
+ index
54
+ intervalafter
55
+ intervalbefore
56
+ intervalcontains
57
+ intervaldisjoint
58
+ intervalduring
59
+ intervalequals
60
+ intervalfinishedby
61
+ intervalfinishes
62
+ intervalin
63
+ intervalmeets
64
+ intervalmetby
65
+ intervaloverlappedby
66
+ intervaloverlaps
67
+ intervalstartedby
68
+ intervalstarts
69
+ item
70
+ last
71
+ latest-version
72
+ license
73
+ linkset
74
+ lrdd
75
+ manifest
76
+ mask-icon
77
+ media-feed
78
+ memento
79
+ micropub
80
+ modulepreload
81
+ monitor
82
+ monitor-group
83
+ next
84
+ next-archive
85
+ nofollow
86
+ noopener
87
+ noreferrer
88
+ opener
89
+ openid2.local_id
90
+ openid2.provider
91
+ original
92
+ p3pv1
93
+ payment
94
+ pingback
95
+ preconnect
96
+ predecessor-version
97
+ prefetch
98
+ preload
99
+ prerender
100
+ prev
101
+ preview
102
+ previous
103
+ prev-archive
104
+ privacy-policy
105
+ profile
106
+ publication
107
+ related
108
+ restconf
109
+ replies
110
+ ruleinput
111
+ search
112
+ section
113
+ self
114
+ service
115
+ service-desc
116
+ service-doc
117
+ service-meta
118
+ sip-trunking-capability
119
+ sponsored
120
+ start
121
+ status
122
+ stylesheet
123
+ subsection
124
+ successor-version
125
+ sunset
126
+ tag
127
+ terms-of-service
128
+ timegate
129
+ timemap
130
+ type
131
+ ugc
132
+ up
133
+ version-history
134
+ via
135
+ webmention
136
+ working-copy
137
+ working-copy-of
138
+ ]).freeze
139
+
140
+ # Parses a maybe-returns a {LinkRelationType} when the value is one
141
+ #
142
+ # Link relation types may either be a URI to a relation type description
143
+ # or a value [registered with IANA][1].
144
+ #
145
+ # [1]: https://www.iana.org/assignments/link-relations
146
+ #
147
+ # @since 0.1.0
148
+ # @api private
149
+ #
150
+ # @param rel [String] the rel to parse and validate
151
+ # @return [LinkRelationType, nil] the link relation type parsed from the rel
152
+ def self.parse(rel)
153
+ return unless Webfinger.uri?(rel) || REGISTERED.include?(rel)
154
+
155
+ new(rel)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # Helper methods for logging purposes
6
+ module Logging
7
+ # When including the module, also extend it as well
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ # @private
12
+ #
13
+ # @param base [Module] the base module or class including the module
14
+ # @return [void]
15
+ def self.included(base)
16
+ super
17
+ base.extend(self)
18
+ end
19
+
20
+ # Sends a warning message through the Bridgetown logger
21
+ #
22
+ # @since 0.1.0
23
+ # @api public
24
+ #
25
+ # @example Print a warning in the logger
26
+ #
27
+ # extend Bridgetown::Webfinger::Logging
28
+ # warn "I like some of you less than you deserve"
29
+ #
30
+ # @param msg [String] the message to log
31
+ # @return [void]
32
+ def warn(msg)
33
+ Bridgetown.logger.warn(msg)
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # A wrapper for wrapping models within a {JRD}
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ class Model < SimpleDelegator
10
+ include Logging
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Bridgetown
6
+ module Webfinger
7
+ # Parses URI parameters in the Webfinger style
8
+ #
9
+ # This is necessary because Rack handles parameters differently than the
10
+ # Webfinger specification. In Rack, repeated parameters compete in a
11
+ # last-one-wins scheme. For example:
12
+ #
13
+ # https://example.com?foo=1&foo=2&bar=3
14
+ #
15
+ # parses into:
16
+ #
17
+ # {"foo" => 2, "bar" => 3}
18
+ #
19
+ # whereas Webfinger's processing [wants this][1]:
20
+ #
21
+ # {"foo" => [1, 2], "bar" => 3}
22
+ #
23
+ # per the phrasing “the "rel" parameter MAY be included multiple times in
24
+ # order to request multiple link relation types.”
25
+ #
26
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.3
27
+ #
28
+ # @since 0.1.0
29
+ # @api private
30
+ class Parameters
31
+ # Splits query parameters at the ampersand and optional spaces
32
+ #
33
+ # This was borrowed from [Rack::QueryParser][2] and simplified.
34
+ #
35
+ # [2]: https://rubydoc.info/gems/rack/Rack/QueryParser
36
+ SPLITTER = %r{& *}n
37
+
38
+ # Parses a query string for a webfinger request into a hash
39
+ #
40
+ # This method cleans any unrelated parameters from the result and combines
41
+ # multiple requests for `rel`s into an array. It also handles unescaping
42
+ # terms.
43
+ #
44
+ # @since 0.1.0
45
+ # @api private
46
+ #
47
+ # @param [String] query_string the query string for a webfinger request
48
+ # @return [Parameters] the cleaned param values
49
+ def self.from_query_string(query_string)
50
+ query_string
51
+ .split(SPLITTER)
52
+ .map { |pair| pair.split("=", 2).map! { |component| decode(component) } }
53
+ .each_with_object(Parameters.new) do |(param, value), result|
54
+ case param
55
+ when "resource" then result.resource = value
56
+ when "rel" then result.add_rel(value)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Decodes a URI component; an alias to the URI method, for brevity
62
+ #
63
+ # @since 0.1.0
64
+ # @api private
65
+ # @private
66
+ #
67
+ # @param component [String] the component from the URI
68
+ # @return [String] the decoded component
69
+ private_class_method def self.decode(component)
70
+ URI.decode_uri_component(component)
71
+ end
72
+
73
+ # Creates a new {Parameters} from an optional resource and rel
74
+ #
75
+ #
76
+ #
77
+ # @since 0.1.0
78
+ # @api public
79
+ #
80
+ # @example Create an empty
81
+ #
82
+ # @param resource [String, nil] the resource for the request
83
+ # @param rel [Array<String>, nil] the types of relation to scope the links to
84
+ # @return [void]
85
+ def initialize(resource: nil, rel: nil)
86
+ @resource = resource
87
+ @rel = rel
88
+ end
89
+
90
+ # The list of link relations requested within the {Parameters}
91
+ #
92
+ # @since 0.1.0
93
+ # @api public
94
+ #
95
+ # @example Reading the link relations requested as a URL parameter
96
+ #
97
+ # params = Bridgetown::Webfinger::Parameters.from_query_string(
98
+ # "resource=acct%3Abilbo%40bagend.com&" \
99
+ # "rel=http%3A%2F%2Fwebfinger.net%2Frel%2Favatar&" \
100
+ # "rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page"
101
+ # )
102
+ # params.rel
103
+ # #=> ["https://webfinger.net/rel/avatar", "https://webfinger.net/rel/profile-page"]
104
+ # @return [Array<String>, nil] the types of relation to scope the links to
105
+ attr_reader :rel
106
+
107
+ # The decoded resource requested within the {Parameters}
108
+ #
109
+ # @since 0.1.0
110
+ # @api public
111
+ #
112
+ # @example Reading the resource requested as a URL parameter
113
+ #
114
+ # params = Bridgetown::Webfinger::Parameters.from_query_string(
115
+ # "resource=acct%3Abilbo%40bagend.com"
116
+ # )
117
+ # params.resource #=> "acct:bilbo@bagend.com"
118
+ #
119
+ # @return [String, nil] the resource for the request
120
+ attr_accessor :resource
121
+
122
+ # Checks for value object equality
123
+ #
124
+ # @since 0.1.0
125
+ # @api private
126
+ #
127
+ # @param other [Parameters] the other parameter set to compare against
128
+ # @return [Boolean] true when they are equal, false otherwise
129
+ def ==(other)
130
+ other.instance_of?(Parameters) &&
131
+ resource == other.resource &&
132
+ rel == other.rel
133
+ end
134
+ alias_method :eql?, :==
135
+
136
+ # Adds a link relation, or "rel," to the {Parameters}
137
+ #
138
+ # @since 0.1.0
139
+ # @api public
140
+ #
141
+ # @example Adding a link relation to a new {Parameters} instance
142
+ #
143
+ # params = Bridgetown::Webfinger::Parameters.new
144
+ # params.add_rel("http://webfinger.net/rel/avatar")
145
+ # params.rel #=> ["http://webfinger.net/rel/avatar"]
146
+ #
147
+ # @param rel [String] a type of relation to add to the scope request
148
+ # @return [void]
149
+ def add_rel(rel)
150
+ return unless rel
151
+
152
+ (@rel ||= []).push(rel)
153
+ end
154
+
155
+ # Allows the {Parameters} to be used in a Hash key; part of value object semantics
156
+ #
157
+ # @since 0.1.0
158
+ # @api private
159
+ #
160
+ # @return [Integer]
161
+ def hash
162
+ [resource, *rel].hash
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # Wraps a `properties` member within a {Link} or {JRD}
6
+ class Properties < Model
7
+ # Parses and maybe-returns {Properties} when the value is one
8
+ #
9
+ # Properties within the {JRD} are name/value pairs [with URIs for names
10
+ # and strings or nulls for values][1].
11
+ #
12
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ #
17
+ # @param data [Hash] the data to parse as a properties object
18
+ # @return [Properties, nil] the properties parsed from the data
19
+ def self.parse(data)
20
+ return unless data
21
+
22
+ unless data.is_a?(Hash)
23
+ return warn("Webfinger link properties are malformed: #{data.inspect}, ignoring")
24
+ end
25
+
26
+ properties = data.select do |name, value|
27
+ if !Webfinger.uri?(name)
28
+ next warn("Webfinger property name is not a URI: #{name}, ignoring")
29
+ elsif !value.is_a?(String) || value.nil?
30
+ next warn("Webfinger property value is not a nullable string: #{value}, ignoring")
31
+ else
32
+ true
33
+ end
34
+ end
35
+
36
+ if !properties.empty?
37
+ new(properties)
38
+ else
39
+ warn("All Webfinger link properties pruned: #{data.inspect}, ignoring")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # Wraps a `titles` member within a {Link}
6
+ class Titles < Model
7
+ # Parses and maybe-returns {Titles} when the value is one
8
+ #
9
+ # Titles within the {JRD} are name/value pairs [with language tag names
10
+ # and string values][2]. When the language is indeterminate, use `"und"`.
11
+ #
12
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ #
17
+ # @param data [Hash] the data to parse as a titles object
18
+ # @return [Titles, nil] the titles parsed from the data
19
+ def self.parse(data)
20
+ return unless data.is_a?(Hash)
21
+
22
+ data = data.select do |key, value|
23
+ if !key.is_a?(String)
24
+ next warn("Webfinger title key is not a string: #{key}, ignoring")
25
+ elsif !value.is_a?(String)
26
+ next warn("Webfinger title value for #{key} is not a string: #{value}, ignoring")
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ if !data.empty?
33
+ new(data)
34
+ else
35
+ warn("All Webfinger link titles pruned: #{data.inspect}, ignoring")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end