bridgetown-webfinger 0.1.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.
@@ -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