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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +55 -0
- data/LICENSE.md +22 -0
- data/README.md +125 -0
- data/bridgetown-webfinger.gemspec +35 -0
- data/lib/bridgetown/webfinger/alias.rb +29 -0
- data/lib/bridgetown/webfinger/builder.rb +133 -0
- data/lib/bridgetown/webfinger/href.rb +29 -0
- data/lib/bridgetown/webfinger/initializer.rb +23 -0
- data/lib/bridgetown/webfinger/jrd.rb +164 -0
- data/lib/bridgetown/webfinger/link.rb +118 -0
- data/lib/bridgetown/webfinger/link_relation_type.rb +159 -0
- data/lib/bridgetown/webfinger/logging.rb +38 -0
- data/lib/bridgetown/webfinger/model.rb +13 -0
- data/lib/bridgetown/webfinger/parameters.rb +166 -0
- data/lib/bridgetown/webfinger/properties.rb +44 -0
- data/lib/bridgetown/webfinger/titles.rb +40 -0
- data/lib/bridgetown/webfinger/uri/acct.rb +138 -0
- data/lib/bridgetown/webfinger/version.rb +8 -0
- data/lib/bridgetown-webfinger.rb +49 -0
- data/lib/roda/plugins/bridgetown_webfinger.rb +149 -0
- metadata +131 -0
@@ -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,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
|