forget-passwords 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,269 @@
1
+ require 'nokogiri'
2
+ require 'stringio'
3
+ require 'xml-mixup'
4
+ require 'http-negotiate'
5
+ require 'forget-passwords/types'
6
+ require 'uri'
7
+
8
+ module ForgetPasswords
9
+
10
+ class Template
11
+
12
+ # XXX do we even need this?
13
+ class Mapper
14
+
15
+ private
16
+
17
+ # XXX i can't remember what the nice way to get the gem root is lol
18
+ DEFAULT_PATH = (
19
+ Pathname(__FILE__).parent + '../../content').expand_path.freeze
20
+
21
+
22
+ # Normalize the input to symbol `:like_this`.
23
+ #
24
+ # @param key [#to_s, #to_sym] the input to normalize
25
+ #
26
+ # @return [Symbol] the normalized key
27
+ #
28
+ def normalize key
29
+ key.to_s.strip.downcase.gsub(/[[:space:]-]/, ?_).tr_s(?_, ?_).to_sym
30
+ end
31
+
32
+ TType = ForgetPasswords::Types.Instance(ForgetPasswords::Template)
33
+
34
+ THash = ForgetPasswords::Types::Hash.map(ForgetPasswords::Types::NormSym,
35
+ ForgetPasswords::Types::String).default({}.freeze)
36
+
37
+ RawParams = ForgetPasswords::Types::SymbolHash.schema(
38
+ path: ForgetPasswords::Types::ExtantPathname.default(DEFAULT_PATH),
39
+ mapping: THash,
40
+ base?: ForgetPasswords::Types::URI,
41
+ transform?: ForgetPasswords::Types::URI,
42
+ ).hash_default
43
+
44
+ public
45
+
46
+ Type = ForgetPasswords::Types.Constructor(self) do |input|
47
+ # what we're gonna do is validate the input as a hash, then use it
48
+ input = RawParams.(input)
49
+ path = input.delete :path
50
+ self.new(path, **input)
51
+ end
52
+
53
+ attr_reader :path, :base, :transform
54
+
55
+ def initialize path = DEFAULT_PATH, base: nil,
56
+ transform: nil, mapping: {}
57
+ @path = Pathname(path).expand_path
58
+ @base = base
59
+ @transform = transform
60
+ @mapping = mapping.map do |k, v|
61
+ name = normalize k
62
+ template = v.is_a?(ForgetPasswords::Template) ? v :
63
+ ForgetPasswords::Template.new(self, k, @path + v)
64
+ [name, template]
65
+ end.to_h
66
+ end
67
+
68
+ def [] key
69
+ @mapping[normalize key]
70
+ end
71
+
72
+ def []= key, path
73
+ name = normalize key
74
+ @mapping[name] = path.is_a?(ForgetPasswords::Template) ? path :
75
+ ForgetPasswords::Template.new(self, key, @path + path)
76
+ end
77
+
78
+ def manifest
79
+ @mapping.keys
80
+ end
81
+
82
+ # Ensure that the mapper contains templates with the given names.
83
+ #
84
+ # @param *names [Array<#to_s, #to_sym>] the template names
85
+ #
86
+ # @return [true, false]
87
+ #
88
+ def verify *names
89
+ names = names.first if names.first.is_a? Array
90
+ # i dunno, is there a better way to do this?
91
+ (names.map { |k| normalize k } - manifest).empty?
92
+ end
93
+
94
+ # Ensure that the mapper contains templates with the given names
95
+ # and raise an exception if it doesn't.
96
+ #
97
+ # @param *names [Array<#to_sym>] the template names
98
+ #
99
+ # @return [true, false]
100
+ #
101
+ def verify! *names
102
+ verify(*names) or raise "Could not verify names: #{names.join ?,}"
103
+ end
104
+ end
105
+
106
+ include XML::Mixup
107
+
108
+ private
109
+
110
+ TEXT_TEMPLATE = Nokogiri::XSLT.parse(
111
+ (Mapper::DEFAULT_PATH + '../etc/text-only.xsl').expand_path.read)
112
+
113
+ # this is gonna be run in the context of the document
114
+ TO_XML = -> { to_xml }
115
+ TO_HTML = -> { to_html }
116
+ TO_TEXT = -> {
117
+ TEXT_TEMPLATE.apply_to(self).to_s
118
+ }
119
+
120
+ ATTRS = %w[
121
+ about typeof rel rev property resource href src
122
+ action data id class name value
123
+ ].freeze
124
+
125
+ ATTRS_XPATH = ('//*[%s]/@*' % ATTRS.map { |a| "@#{a}" }.join(?|)).freeze
126
+
127
+ XPATHNS = {
128
+ html: 'http://www.w3.org/1999/xhtml',
129
+ svg: 'http://www.w3.org/2000/svg',
130
+ xsl: 'http://www.w3.org/1999/XSL/Transform',
131
+ }.freeze
132
+
133
+ public
134
+
135
+ attr_reader :name, :doc, :mapper
136
+
137
+ def initialize mapper, name, content
138
+ # boring members
139
+ @mapper = mapper
140
+ @name = name
141
+
142
+ # resolve content
143
+ @doc = case content
144
+ when Nokogiri::XML::Document then content
145
+ when IO, Pathname
146
+ content = mapper.path + content
147
+ fh = content.respond_to?(:open) ? content.open : content
148
+ Nokogiri::XML.parse fh
149
+ when String
150
+ Nokogiri::XML.parse content
151
+ else
152
+ raise ArgumentError, "Not sure what to do with #{content.class}"
153
+ end
154
+ end
155
+
156
+ # Perform the variable substitution on the associated document and
157
+ # return it.
158
+ #
159
+ # @param vars [#to_h] a hash-like object of variables.
160
+ #
161
+ # @return [Nokogiri::XML::Document] the altered document
162
+ #
163
+ def process vars: {}, base: nil, transform: nil
164
+ # sub all the placeholders for variables
165
+ doc = @doc.dup
166
+
167
+ # add doctype if missing
168
+ doc.create_internal_subset('html', nil, nil) unless doc.internal_subset
169
+
170
+ # set the base URI
171
+ if base ||= mapper.base
172
+ if b = doc.at_xpath('(/html:html/html:head/html:base)[1]', XPATHNS)
173
+ # check for a <base href="..."/> already
174
+ b['href'] = base.to_s
175
+ elsif t = doc.at_xpath('(/html:html/html:head/html:title)[1]', XPATHNS)
176
+ # otherwise check for a <title>, after which we'll plunk it
177
+ markup spec: { nil => :base, href: base.to_s }, after: t
178
+ elsif h = doc.at_xpath('/html:html/html:head[1]', XPATHNS)
179
+ # otherwise check for <head>, to which we will prepend
180
+ markup spec: { nil => :base, href: base.to_s }, parent: h
181
+ end
182
+ end
183
+
184
+ # add xsl transform if present
185
+ if transform ||= mapper.transform
186
+ pi = { '#pi' => 'xml-stylesheet',
187
+ type: 'text/xsl', href: transform.to_s }
188
+ if t = doc.at_xpath("/processing-instruction('xml-stylesheet')[1]")
189
+ t = markup spec: pi, replace: t
190
+ else
191
+ t = markup spec: pi, before: doc.children.first
192
+ end
193
+ end
194
+
195
+ # do the processing instructions
196
+ doc.xpath("/*//processing-instruction('var')").each do |pi|
197
+ key = pi.content.delete_prefix(?$).delete_suffix(??).to_sym
198
+ if vars[key]
199
+ text = pi.document.create_text_node vars[key].to_s
200
+ pi.replace text
201
+ end
202
+ end
203
+
204
+ # do the attributes
205
+ doc.xpath(ATTRS_XPATH).each do |attr|
206
+ attr.content = attr.content.gsub(/\$([A-Z_][0-9A-Z_]*)/) do |key|
207
+ key = key.delete_prefix ?$
208
+ vars[key.to_sym] || "$#{key}"
209
+ end
210
+ end
211
+
212
+ doc
213
+ end
214
+
215
+ # Given a document, perform rudimentary content negotiation.
216
+ # Return the resulting string, or nil if no variant was chosen.
217
+ #
218
+ # @param doc [Nokogiri::XML::Document] the document
219
+ # @param headers [#to_h] the header set for content negotiation
220
+ # @param full [false, true] whether to return a content-header pair
221
+ #
222
+ # @return [String, Array<(String, String)>, nil] the serialized
223
+ # document (maybe, or maybe the Content-Type header too).
224
+ #
225
+ def serialize doc, headers = {}, full: false
226
+ # XXX TODO go back and make it possible for this method to
227
+ # return a hash with all the headers etc so i don't have to do
228
+ # this dumb hack
229
+ method, type = HTTP::Negotiate.negotiate(headers, {
230
+ [TO_XML, 'application/xhtml+xml'] => {
231
+ weight: 1.0, type: 'application/xhtml+xml' },
232
+ [TO_HTML, 'text/html'] => { weight: 0.8, type: 'text/html' },
233
+ [TO_TEXT, 'text/plain'] => { weight: 0.5, type: 'text/plain' },
234
+ })
235
+
236
+ # no type selected
237
+ return unless method
238
+
239
+ # warn method.inspect
240
+
241
+ out = [doc.instance_exec(&method), type]
242
+
243
+ full ? out : out.first
244
+ end
245
+
246
+ # Give us the Rack::Response object and we'll populate the headers
247
+ # and body for you.
248
+ #
249
+ # @param resp [Rack::Response] the response to populate
250
+ # @param headers [#to_h] the header set
251
+ # @param vars [#to_h] the variable bindings
252
+ #
253
+ # @return [Rack::Response] the response object, updated in place
254
+ #
255
+ def populate resp, headers = {}, vars = {}, base: nil
256
+ if (body, type = serialize(
257
+ process(vars: vars, base: base), headers, full: true))
258
+ #resp.length = body.bytesize # not sure if necessary
259
+ resp.write body
260
+ resp.content_type = type
261
+ else
262
+ # otherwise 406 lol, the client didn't like any of our responses
263
+ resp.status = 406
264
+ end
265
+
266
+ resp
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,118 @@
1
+ require 'pathname'
2
+ require 'iso8601'
3
+ require 'uri'
4
+ require 'dry-schema'
5
+
6
+ module ForgetPasswords
7
+ module Types
8
+ include Dry::Types()
9
+
10
+ private
11
+
12
+ # ascii token
13
+ ASCII = /^[A-Za-z_][0-9A-Za-z_.-]*$/
14
+
15
+ # hostname
16
+ HN = /^(?:[0-9a-z-]+(?:\.[0-9a-z-]+)*|[0-9a-f]{,4}(?::[0-9a-f]{,4}){,7})$/i
17
+
18
+ public
19
+
20
+ # XXX THIS IS A BAD SOLUTION TO THE URI PROBLEM
21
+ Dry::Schema::PredicateInferrer::Compiler.infer_predicate_by_class_name false
22
+
23
+ # config primitives
24
+
25
+ ASCIIToken = Strict::String.constrained(format: ASCII).constructor(&:strip)
26
+
27
+ # actually pretty sure i can define constraints for this type, oh well
28
+
29
+ Hostname = String.constructor(&:strip).constrained(format: HN)
30
+
31
+ Duration = Types.Constructor(ISO8601::Duration) do |x|
32
+ begin
33
+ out = ISO8601::Duration.new x.to_s.strip.upcase
34
+ rescue ISO8601::Errors::UnknownPattern => e
35
+ raise Dry::Types::CoercionError.new e
36
+ end
37
+
38
+ out
39
+ end
40
+
41
+ # okay so this shit doesn't seem to work (2022-04-12 huh? what doesn't?)
42
+
43
+ # XXX note this is a fail in dry-types
44
+ URI = Types.Constructor(::URI) do |x|
45
+ begin
46
+ out = ::URI.parse(x)
47
+ rescue ::URI::InvalidURIError => e
48
+ raise Dry::Types::CoercionError, e
49
+ end
50
+
51
+ out
52
+ end
53
+
54
+ RelativePathname = Types.Constructor(::Pathname) { |x| Pathname(x) }
55
+
56
+ ExtantPathname = Types.Constructor(::Pathname) do |x|
57
+ out = Pathname(x).expand_path
58
+ dir = out.dirname
59
+ raise Dry::Types::CoercionError, "#{dir} does not exist" unless
60
+ out.exist? || dir.exist?
61
+
62
+ out
63
+ end
64
+
65
+
66
+ # should be WritablePathname but whatever
67
+ WritablePathname = Types.Constructor(::Pathname) do |x|
68
+ out = Pathname(x)
69
+ dir = out.expand_path.dirname
70
+ raise Dry::Types::CoercionError, "#{dir} is not writable" unless
71
+ dir.writable?
72
+ raise Dry::Types::CoercionError, "#{out} can't be overwritten" if
73
+ out.exist? and !out.writable?
74
+ out
75
+ end
76
+
77
+ NormSym = Symbol.constructor do |k|
78
+ k.to_s.strip.downcase.tr_s(' _-', ?_).to_sym
79
+ end
80
+
81
+ # symbol hash
82
+ SymbolHash = Hash.schema({}).with_key_transform do |k|
83
+ NormSym.call k
84
+ end
85
+
86
+ # apparently you can't go from schema to map
87
+ SymbolMap = Hash.map NormSym, Any
88
+
89
+ # this is a generic type for stuff that comes off the command line
90
+ # or out of a config file that we don't want to explicitly define
91
+ # but nevertheless needs to be coerced (in particular integers,
92
+ # floats) so it can be passed into eg a constructor
93
+ Atomic = Coercible::Integer | Coercible::Float | Coercible::String
94
+ end
95
+
96
+ end
97
+
98
+ module Dry::Types::Builder
99
+ def hash_default
100
+ # obtain all the required keys from the spec
101
+ reqd = keys.select(&:required?)
102
+
103
+ if reqd.empty?
104
+ # there aren't any requireed keys, but we'll set the empty hash
105
+ # as a default if there exist optional keys, otherwise any
106
+ # default will interfere with input from upstream.
107
+ return default({}.freeze) unless keys.empty?
108
+ else
109
+ # similarly, we only set a default if all the required keys have them.
110
+ return default(reqd.map { |k| [k.name, k.value] }.to_h.freeze) if
111
+ reqd.all?(&:default?)
112
+ # XXX THIS WILL FAIL IF THE DEFAULT IS A PROC; THE FAIL IS IN DRY-RB
113
+ end
114
+
115
+ # otherwise just return self
116
+ self
117
+ end
118
+ end
@@ -0,0 +1,3 @@
1
+ module ForgetPasswords
2
+ VERSION = '0.2.9'
3
+ end